diff --git a/.agents/skills/docs-writer b/.agents/skills/docs-writer new file mode 120000 index 00000000000..a0dab145f75 --- /dev/null +++ b/.agents/skills/docs-writer @@ -0,0 +1 @@ +../../.claude/skills/docs-writer \ No newline at end of file diff --git a/.bun-version b/.bun-version index 9728bd69ac8..17e63e7affd 100644 --- a/.bun-version +++ b/.bun-version @@ -1 +1 @@ -1.2.21 +1.3.11 diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000000..d5d401c5189 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,31 @@ +# Use the latest 2.1 version of CircleCI pipeline process engine. +# See: https://circleci.com/docs/configuration-reference +version: 2.1 + +# Define a job to be invoked later in a workflow. +# See: https://circleci.com/docs/jobs-steps/#jobs-overview & https://circleci.com/docs/configuration-reference/#jobs +jobs: + say-hello: + # Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub. + # See: https://circleci.com/docs/executor-intro/ & https://circleci.com/docs/configuration-reference/#executor-job + docker: + # Specify the version you desire here + # See: https://circleci.com/developer/images/image/cimg/base + - image: cimg/base:current + + # Add steps to the job + # See: https://circleci.com/docs/jobs-steps/#steps-overview & https://circleci.com/docs/configuration-reference/#steps + steps: + # Checkout the code as the first step. + - checkout + - run: + name: "Say hello" + command: "echo Hello, World!" + +# Orchestrate jobs using workflows +# See: https://circleci.com/docs/workflows/ & https://circleci.com/docs/configuration-reference/#workflows +workflows: + say-hello-workflow: # This is the name of the workflow, feel free to change it to better match your workflow. + # Inside the workflow, you define the jobs you want to run. + jobs: + - say-hello diff --git a/.claude/prompts/claude-pr-bot.md b/.claude/prompts/claude-pr-bot.md deleted file mode 100644 index f8b66d4433d..00000000000 --- a/.claude/prompts/claude-pr-bot.md +++ /dev/null @@ -1,413 +0,0 @@ -# Claude PR Review Assistant - -You are Claude, an AI assistant specialized in GitHub PR code reviews. You operate in REVIEW MODE, providing thorough feedback using GitHub MCP tools. - -## Your Mission - -Help developers ship better code by providing high-signal feedback that prevents bugs, improves maintainability, and teaches valuable principles. Every comment should make the codebase measurably better. - - -**CRITICAL OPERATING CONSTRAINTS:** -1. You can ONLY submit "COMMENT" reviews (technical limitation) -2. You MUST check existing comments first to avoid duplicates (respects reviewer time) -3. You MUST verify patch-id before reviewing (prevents wasted work) -4. You MUST update the sticky comment (your primary communication channel) - - -## Review Philosophy - -**Every PR tells a story.** Help make it clearer and more maintainable without rewriting it entirely. - -**Review the code, not the coder.** Focus on patterns and principles. - -**Teach through specifics.** Concrete examples stick better than abstract feedback. - -**Keep it high signal.** Every comment should prevent a bug, improve maintainability, or teach something valuable. - -**Engineering principles make daily work easier.** When you spot opportunities to apply separation of concerns, designing against contracts, or dependency injection, show how it makes testing and maintenance simpler. - - -Don't hold back on teaching opportunities. When you see code that could demonstrate better engineering principles, give it your all - provide concrete examples, show the transformation, explain the immediate benefits. Go above and beyond to help developers understand not just what to change, but why it makes their daily work easier. - - - -Repository: $REPOSITORY -PR Number: $PR_NUMBER -PR Title: $PR_TITLE -PR Body: $PR_BODY -Current Patch ID: $CURRENT_PATCH_ID -Existing Claude Comment ID: $CLAUDE_COMMENT_ID -Existing Comments: $PR_COMMENTS -Review Comments: $REVIEW_COMMENTS -Changed Files: $CHANGED_FILES -Trigger: $TRIGGER_COMMENT - - -## Review Workflow - -You will follow these steps sequentially, producing specific outputs at each stage. This ensures thoroughness and prevents common review mistakes like duplicate comments. - - - -### Step 1: Check Existing Comments (MANDATORY FIRST OUTPUT) - -**Why this matters:** Duplicate comments waste everyone's time and make reviews harder to follow. By checking first, you ensure every comment adds unique value. - -**Action:** -```bash -mcp__github__get_pull_request_comments -``` - -**Required output format:** -```xml - -Found X inline comments: -- path/to/file.ts:42 - "Missing null check for user object" -- path/to/other.ts:15 - "Console.log should be removed" -- path/to/component.tsx:88 - "Function doing too many things" -[List ALL existing comments with file:line and issue summary] - -Total existing inline comments: X -Proceeding to Step 2... - -``` - -**You cannot proceed without completing this output.** - -### Step 2: Verify Patch-ID - -**Why this matters:** PRs often get rebased or amended. If nothing actually changed, re-reviewing wastes time and creates noise. - -**Actions:** -1. Extract CURRENT_PATCH_ID from context -2. Check your existing sticky comment for previous patch-id -3. Compare the two - -**Required output format:** -```xml - -Current patch-id: ${CURRENT_PATCH_ID:0:12} -Previous patch-id: [from sticky comment or "none"] -Status: [CHANGED - proceeding with review | UNCHANGED - skipping review] - -``` - -**If UNCHANGED:** Update sticky comment with timestamp only and STOP. - -### Step 3: Analyze Changes - -**Why this matters:** Understanding the full context leads to better, more relevant feedback. - -**Actions to take:** -- `mcp__github__get_pull_request` - Full PR metadata -- `mcp__github__get_pull_request_status` - CI/CD status -- `Read`, `Grep`, `Glob` - Examine files directly -- Git commands for history analysis - -**While analyzing, reflect on:** -- Which functions are hardest to test due to mixed concerns? -- Where would dependency injection eliminate mocking complexity? -- What interfaces would enable parallel team development? -- Are there examples of these principles done well? - -**Useful git commands:** -```bash -git rev-parse HEAD # Current commit SHA -git log --oneline -10 # Recent commits -git diff ..HEAD --name-status # What changed -git log --since="4 hours ago" -p # Recent changes -``` - -**Required output format:** -```xml - -Files analyzed: X -Key changes identified: -- [Component/area]: [Type of change] -- [Component/area]: [Type of change] -Focus areas for review: [List 3-5 most important areas] -Engineering opportunities spotted: [Y opportunities for better patterns] - -``` - -### Step 4: Create Pending Review - -**Action:** -``` -mcp__github__create_pending_pull_request_review -``` - -**Required output:** -```xml - -Pending review created successfully - -``` - -### Step 5: Add Inline Comments - -**Why this matters:** Inline comments provide context-specific feedback exactly where it's needed, making it easier for developers to understand and fix issues. - - -For each potential comment, follow this decision tree: - -1. Check against existing comments from Step 1: - - Same file and line? → SKIP - - Same issue already mentioned? → SKIP - - Similar pattern already noted? → SKIP - -2. If checks pass, evaluate importance: - - Critical bug or security issue? → ADD COMMENT - - Clear improvement with obvious fix? → ADD COMMENT - - Engineering principle opportunity with clear benefit? → ADD COMMENT - - Minor style preference? → SKIP - - Already in sticky comment summary? → SKIP - -3. For comments you ADD: - ``` - mcp__github__add_comment_to_pending_review - Parameters: - - path: "src/file.ts" - - line: 42 (or startLine + line for multi-line) - - side: "RIGHT" - - subjectType: "line" - - body: "Issue description with suggestion" - ``` - -**GitHub suggestion block format:** -```suggestion -ONLY the replacement code for the commented lines -``` - -Remember: Suggestion blocks must contain ONLY the replacement lines, not surrounding context. - - -**Required output format:** -```xml - -Added X new inline comments: -- file.ts:42 - Security issue: SQL injection vulnerability -- other.ts:88 - Bug: Potential null reference -- service.ts:15 - Pattern: Function doing multiple jobs -Skipped Y duplicate issues already covered - -``` - -### Step 6: Update Sticky Comment - -**Why this matters:** The sticky comment provides a persistent, comprehensive overview of your review that doesn't get lost in the PR discussion. - -**Action:** -``` -mcp__github_comment__update_claude_comment -``` - -**Required format:** -```markdown -
-🤖 Claude's Code Review (click to expand) - -### Review Summary -- **Updated:** [timestamp] -- **Commit:** [SHA from git rev-parse HEAD] -- **Patch ID:** `${CURRENT_PATCH_ID:0:12}` -- **Review Stats:** Found X existing comments, added Y new comments - -### Changes Since Last Review -[Only if this is a re-review after rebase/changes] -- Previous commit: [SHA] -- Key changes: [What actually changed vs just moved] - -### Critical Issues 🔴 -[Must-fix problems that could break production] -- [Issue description and location] - -### Improvements Suggested 🟡 -[Patterns and maintainability enhancements] -- [Suggestion with rationale] - -[When teaching a principle, include a brief example:] -**Example: Simplifying Testing Through Separation** -```ts -// From: handleSwap() doing validation + fetching + building -// To: Three focused functions that test independently -validateSwapInputs(token, amount) // Test with just inputs -fetchTokenPrice(tokenId) // Test with mock response -buildSwapTransaction(token, price) // Test with fixed values -``` - -### Good Practices Observed ✅ -[Only if truly noteworthy - especially good applications of engineering principles] -- Clean separation of concerns in [specific function/module] -- Excellent use of dependency injection in [specific area] - -### Action Items -1. [Most important fix] -2. [Second priority] -3. [Third priority] - -
-``` - -### Step 7: Submit Review - -**Action:** -``` -mcp__github__submit_pending_pull_request_review -Parameters: -- event: "COMMENT" # ALWAYS -- body: "Review complete - see inline comments and summary above" -``` - -**Required output:** -```xml - -Review submitted successfully with X inline comments - -``` - -
- -## Review Priorities - - -### Phase 1: Critical Issues (Must Fix) -Focus on problems that would cause immediate harm: -- Bugs or logic errors -- Security vulnerabilities -- Performance problems impacting users -- Data corruption risks -- Race conditions - -### Phase 2: Patterns & Principles -Improve code maintainability and team velocity: -- **Functions doing too many things** → *Why it matters: Can't test pieces independently, changes ripple everywhere* -- **Hidden dependencies** → *Why it matters: Makes testing require complex mocking, creates surprising behaviors* -- **Missing abstractions/contracts** → *Why it matters: Couples code to specific implementations, blocks parallel development* -- **Missing error handling** → *Why it matters: Silent failures in production, hard to debug issues* -- **Direct imports instead of injection** → *Why it matters: Can't swap implementations, hard to test* - -### Phase 3: Polish (Only if valuable) -Nice-to-haves that make code better: -- Naming improvements -- Test coverage -- Documentation -- Refactoring opportunities - - - -### What These Patterns Look Like in Code - -**Spot this pattern (mixed concerns):** -```ts -async function handleUserAction(userId, action) { - // Validation - if (!userId) throw new Error('Invalid user'); - // Fetching - const user = await db.getUser(userId); - // Business logic - const result = processAction(user, action); - // Saving - await db.save(result); - return result; -} -``` - -**Teach this improvement:** -"This function has 4 separate responsibilities. Splitting them makes testing trivial: -- `validateInput(userId)` - test with simple inputs -- `fetchUser(userId, db)` - test with mock db: `{ getUser: () => mockUser }` -- `processAction(user, action)` - pure function test -- `saveResult(result, db)` - test save logic alone - -Each can be tested without mocking the others!" - -**Spot this pattern (hardcoded dependency):** -```javascript -import { stripeClient } from './stripe'; -async function chargeCard(amount) { - return stripeClient.charge(amount); -} -``` - -**Teach this improvement:** -"Accepting the payment client as a parameter would make this more flexible: -```javascript -async function chargeCard(amount, paymentClient) { - return paymentClient.charge(amount); -} -``` -Benefits: -- Test with: `chargeCard(100, { charge: async () => ({ success: true }) })` -- Swap providers without changing this code -- No vendor lock-in" - - - -Actively look for opportunities to recognize good patterns. When you see: -- Clean separation of concerns (functions doing one thing) -- Well-defined interfaces/contracts -- Proper dependency injection -- Good error handling patterns - -Call it out specifically and explain why it's excellent. This reinforces good practices. Example: -"Excellent separation here - `validateOrder()` only validates, making it a pure function that's trivial to test!" - - -## Communication Guidelines - - -### Tone Examples - -**For bugs:** -> "I found a potential issue here: accessing `user.preferences` could throw if user is null. Since this comes from an API response, we should add a safety check: -> -> ```suggestion -> const theme = user?.preferences?.theme || 'default'; -> ``` -> -> This prevents those frustrating 'cannot read property of undefined' production errors." - -**For patterns:** -> "This function handles validation, data fetching, and UI updates. Breaking these into separate functions would make testing much easier: -> - Test validation without any API mocking -> - Test data fetching with a simple mock response -> - Test UI updates with fixed data -> -> Each piece becomes independently testable, and changes stay contained to their specific function." - -**For enhancements:** -> "Consider extracting this price calculation logic into a utility function. Not required, but it would make this cleaner and easier to test." - -### What to avoid: -- Starting with "Great job!" or "Nice work!" -- Apologizing ("Sorry, but...") -- Hedging ("Maybe you could...") -- Nitpicking without value -- Abstract theory without concrete examples - - -## Quality Checklist - -Before submitting your review, verify: -- ✅ Completed Step 1 existing comments check? -- ✅ No duplicate comments added? -- ✅ All comments are actionable? -- ✅ Updated sticky comment with summary? -- ✅ Using "COMMENT" review type? -- ✅ Limited to 5-7 inline comments max? -- ✅ Included at least one teaching moment if opportunity existed? - -## Remember - -You're helping developers: -1. Ship working code safely -2. Learn better patterns through their actual code -3. Build maintainable systems that scale with the team -4. Make testing and debugging easier today, not someday - -Balance teaching with shipping. Balance idealism with pragmatism. When teaching principles, always connect to immediate, practical benefits. - ---- - -**BEGIN REVIEW:** Start with Step 1 - check existing comments and show what you found. diff --git a/.cursor/rules/mobile/styling.mdc b/.cursor/rules/mobile/styling.mdc deleted file mode 100644 index 8f8f08af0dc..00000000000 --- a/.cursor/rules/mobile/styling.mdc +++ /dev/null @@ -1,31 +0,0 @@ ---- -description: Mobile styling conventions -globs: apps/mobile/**/*.ts* -alwaysApply: false ---- -# Mobile Styling Conventions - -## Component Styling -- Prefer Tamagui inline props over other methods - -## Theme Usage -- Use theme tokens from the UI package instead of hardcoded values -- Reference color tokens like `$neutral1` instead of hex values -- Use spacing tokens like `$spacing16` instead of raw numbers - -## Layout -- Use `Flex` from `ui/src` instead of View when possible -- Avoid nested ScrollViews which can cause performance issues -- Minimize view hierarchy depth - -## Platform Specific Code -- Use `..tsx` extensions for platform-specific components -- The bundler will grab the appropriate file during the build and always fallback to `.tsx` -- The `platform` variable must be one of the following: ios, android, macos, windows, web, native -- Use the `Platform.select` API for inline platform-specific code. This method expects an object keyed by `platform`. -- Also consider using our custom platform variables like `isMobileApp`, `isInterface`, etc. for more specific platform detection needs. - -## Performance -- Memoize complex style calculations -- Avoid large inline styles -- Use hardware acceleration for animations when possible diff --git a/.cursor/rules/shared/components.mdc b/.cursor/rules/shared/components.mdc deleted file mode 100644 index 2706e96faad..00000000000 --- a/.cursor/rules/shared/components.mdc +++ /dev/null @@ -1,82 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -# Component Structure and Best Practices - -## Component Organization -- Place state and hooks at the top of the component -- Group related state variables together -- Define handlers after state declarations -- Place JSX return statement at the end of the component - -## Props -- Use interface for component props -- Place prop interface directly above component -- Complex or shared types can be moved to a types.ts file -- Use descriptive prop names -- Provide default props where appropriate - -## Performance Optimizations -- Memoize expensive calculations with useMemo -- Memoize event handlers with useCallback or use our custom useEvent hook -- Use React.memo for pure components that render often -- Avoid anonymous functions in render - -## Component Size -- Keep components focused on a single responsibility -- Extract complex components into smaller, reusable pieces -- Aim for less than 250 lines per component file -- Extract prop interfaces and types to separate files if they become complex - -## Component Structure Example - -```typescript -interface ExampleComponentProps { - prop1: string; - prop2: () => void; -} - -export function ExampleComponent({ prop1, prop2 }: ExampleComponentProps): JSX.Element { - // State declarations - const [state1, setState1] = useState(false) - const [state2, setState2] = useState('') - - // Queries and mutations - const { data, isPending } = useQuery(exampleQueries.getData(prop1)) - const mutation = useMutation({ - mutationFn: () => exampleService.submit(prop1), - onSuccess: prop2 - }) - - // Derived values - const derivedValue = useMemo(() => { - return someCalculation(state1, data) - }, [state1, data]) - - // Event handlers - const handleClick = useCallback(() => { - setState1(!state1) - mutation.mutate() - }, [state1, mutation]) - - // Side effects - useEffect(() => { - // Effect logic - }, [prop2]) - - // Conditional rendering logic - if (isPending) { - return - } - - // Component JSX - return ( - - {derivedValue} - + + ) +} 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 a0a8074467f..bb2d0e200dc 100644 --- a/apps/extension/src/app/components/tabs/ActivityTab.tsx +++ b/apps/extension/src/app/components/tabs/ActivityTab.tsx @@ -1,17 +1,25 @@ import { memo } from 'react' -import { ScrollView } from 'ui/src' +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, }: { address: Address skip?: boolean }): JSX.Element { - const { maybeEmptyComponent, renderActivityItem, sectionData } = useActivityDataWallet({ - evmOwner: address, - skip, + const { maybeEmptyComponent, renderActivityItem, sectionData, fetchNextPage, hasNextPage, isFetchingNextPage } = + useActivityDataWallet({ + evmOwner: address, + skip, + }) + + const { sentinelRef } = useInfiniteScroll({ + onLoadMore: fetchNextPage, + hasNextPage, + isFetching: isFetchingNextPage, }) if (maybeEmptyComponent) { @@ -22,6 +30,14 @@ export const ActivityTab = memo(function _ActivityTab({ {/* `sectionData` will be either an array of transactions or an array of loading skeletons */} {sectionData.map((item, index) => renderActivityItem({ item, index }))} + {/* Show skeleton loading indicator while fetching next page */} + {isFetchingNextPage && ( + + + + )} + {/* Intersection observer sentinel for infinite scroll */} + ) }) 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 09f9327325c..f2ff6aa9995 100644 --- a/apps/extension/src/app/core/BaseAppContainer.tsx +++ b/apps/extension/src/app/core/BaseAppContainer.tsx @@ -1,19 +1,131 @@ -import { PropsWithChildren } from 'react' +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, + createTurnstileMockSolver, + type SessionInitializationService, +} from '@universe/sessions' +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 => { + // 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({ + 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() + + return ( + + + + + + + + + + + {children} + + + + + + + ) +} + export function BaseAppContainer({ children, appName, @@ -21,25 +133,18 @@ export function BaseAppContainer({ return ( - - - - - - - - - - {children} - - - - - - - - + {children} ) } + +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 db280c45dfb..0f5f366c2d4 100644 --- a/apps/extension/src/app/core/SidebarApp.tsx +++ b/apps/extension/src/app/core/SidebarApp.tsx @@ -1,11 +1,11 @@ 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' import { createHashRouter, RouterProvider } from 'react-router' import { PersistGate } from 'redux-persist/integration/react' +import { AUTO_LOCK_ALARM_NAME } from 'src/app/components/AutoLockProvider' import { ErrorElement } from 'src/app/components/ErrorElement' import { useTraceSidebarDappUrl } from 'src/app/components/Trace/useTraceSidebarDappUrl' import { BaseAppContainer } from 'src/app/core/BaseAppContainer' @@ -18,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' @@ -74,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: , @@ -109,6 +122,10 @@ const router = createHashRouter([ path: SettingsRoutes.SmartWallet, element: , }, + { + path: SettingsRoutes.Storage, + element: , + }, ], }, { @@ -133,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(() => { @@ -188,8 +206,21 @@ function useDappRequestPortListener(): void { }, PORT_PING_INTERVAL) } +/** + * Creates a connection so that the background script can detect when the sidebar is closed and schedule an auto-lock alarm. + */ +function useAutoLockAlarmConnection(): void { + useEffect(() => { + const port = chrome.runtime.connect({ name: AUTO_LOCK_ALARM_NAME }) + return () => { + port.disconnect() + } + }, []) +} + function SidebarWrapper(): JSX.Element { useDappRequestPortListener() + useAutoLockAlarmConnection() useTestnetModeForLoggingAndAnalytics() const resetUnitagsQueries = useResetUnitagsQueries() diff --git a/apps/extension/src/app/core/StatsigProvider.tsx b/apps/extension/src/app/core/StatsigProvider.tsx index 63f55e72d4f..7c6aeb01b76 100644 --- a/apps/extension/src/app/core/StatsigProvider.tsx +++ b/apps/extension/src/app/core/StatsigProvider.tsx @@ -1,10 +1,9 @@ import { useQuery } from '@tanstack/react-query' import { SharedQueryClient } from '@universe/api' +import { StatsigCustomAppValue, StatsigUser } from '@universe/gating' import { useEffect, useState } from 'react' import { makeStatsigUser } from 'src/app/core/initStatSigForBrowserScripts' -import { StatsigCustomAppValue } from 'uniswap/src/features/gating/constants' import { StatsigProviderWrapper } from 'uniswap/src/features/gating/StatsigProviderWrapper' -import { StatsigUser } from 'uniswap/src/features/gating/sdk/statsig' import { initializeDatadog } from 'uniswap/src/utils/datadog' import { uniqueIdQuery } from 'utilities/src/device/uniqueIdQuery' 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/core/initStatSigForBrowserScripts.tsx b/apps/extension/src/app/core/initStatSigForBrowserScripts.tsx index 3e3ffaaa9ba..7a495477c72 100644 --- a/apps/extension/src/app/core/initStatSigForBrowserScripts.tsx +++ b/apps/extension/src/app/core/initStatSigForBrowserScripts.tsx @@ -1,6 +1,5 @@ +import { StatsigClient, StatsigCustomAppValue, StatsigUser } from '@universe/gating' import { config } from 'uniswap/src/config' -import { StatsigCustomAppValue } from 'uniswap/src/features/gating/constants' -import { StatsigClient, StatsigUser } from 'uniswap/src/features/gating/sdk/statsig' import { statsigBaseConfig } from 'uniswap/src/features/gating/statsigBaseConfig' import { getUniqueId } from 'utilities/src/device/uniqueId' import { logger } from 'utilities/src/logger/logger' diff --git a/apps/extension/src/app/features/accounts/AccountItem.tsx b/apps/extension/src/app/features/accounts/AccountItem.tsx index dc2db86aa01..9889dbdda0b 100644 --- a/apps/extension/src/app/features/accounts/AccountItem.tsx +++ b/apps/extension/src/app/features/accounts/AccountItem.tsx @@ -1,15 +1,17 @@ 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' import { removeAllDappConnectionsForAccount } from 'src/app/features/dapp/actions' -import { AppRoutes, SettingsRoutes } from 'src/app/navigation/constants' -import { useExtensionNavigation } from 'src/app/navigation/utils' +import { AppRoutes, SettingsRoutes, UnitagClaimRoutes } from 'src/app/navigation/constants' +import { focusOrCreateUnitagTab, useExtensionNavigation } from 'src/app/navigation/utils' 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,44 +90,34 @@ export function AccountItem({ address, onAccountSelect, balanceUSD }: AccountIte label: !accountHasUnitag ? t('account.wallet.menu.edit.title') : t('settings.setting.wallet.action.editProfile'), - 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() - - setShowEditLabelModal(true) + onPress: async (): Promise => { + if (accountHasUnitag) { + await focusOrCreateUnitagTab(address, UnitagClaimRoutes.EditProfile) + } else { + setShowEditLabelModal(true) + } }, Icon: Edit, }, { 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]) + }, [accountHasUnitag, onPressCopyAddress, navigateTo, t, address]) return ( <> @@ -167,16 +150,22 @@ export function AccountItem({ address, onAccountSelect, balanceUSD }: AccountIte size={iconSizes.icon40} variant="subheading2" /> - + - + {formattedBalance} @@ -88,6 +97,7 @@ export function AccountSwitcherScreen(): JSX.Element { const [showRemoveWalletModal, setShowRemoveWalletModal] = useState(false) const [showImportWalletModal, setShowImportWalletModal] = useState(false) const [showCreateWalletModal, setShowCreateWalletModal] = useState(false) + const { value: isEllipsisMenuOpen, setTrue: openEllipsisMenu, setFalse: closeEllipsisMenu } = useBooleanState(false) const [pendingWallet, setPendingWallet] = useState() @@ -154,6 +164,17 @@ export function AccountSwitcherScreen(): JSX.Element { [connectedAccounts.length, dispatch, pendingWallet], ) + const onPressWrappedCard = useCallback(() => { + try { + const url = buildWrappedUrl(UNISWAP_WEB_URL, activeAddress) + window.open(url, '_blank') + dispatch(setHasDismissedUniswapWrapped2025Banner(true)) + navigate(-1) + } catch (error) { + logger.error(error, { tags: { file: 'AccountSwitcherScreen', function: 'onPressWrappedCard' } }) + } + }, [activeAddress, dispatch]) + const addWalletMenuOptions: MenuContentItem[] = [ { label: t('account.wallet.button.create'), @@ -174,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]) @@ -252,12 +271,11 @@ export function AccountSwitcherScreen(): JSX.Element { Icon={X} rightColumn={ - + + {isWrappedBannerEnabled && ( + + + + )} + {activeAccountHasUnitag ? ( @@ -355,7 +378,7 @@ export function AccountSwitcherScreen(): JSX.Element { borderColor="$surface3" borderRadius="$rounded16" borderWidth="$spacing1" - disableRemoveScroll={false} + enableRemoveScroll={true} enterStyle={{ y: -10, opacity: 0 }} exitStyle={{ y: -10, opacity: 0 }} p="$none" diff --git a/apps/extension/src/app/features/accounts/EditLabelModal.tsx b/apps/extension/src/app/features/accounts/EditLabelModal.tsx index d5ce5ed97d2..423347d08ce 100644 --- a/apps/extension/src/app/features/accounts/EditLabelModal.tsx +++ b/apps/extension/src/app/features/accounts/EditLabelModal.tsx @@ -38,6 +38,7 @@ export function EditLabelModal({ isOpen, address, onClose }: EditLabelModalProps const { canClaimUnitag } = useCanActiveAddressClaimUnitag(address) const onConfirm = useCallback(async () => { + // 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 4541332866b..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 @@ -15,7 +15,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `

@@ -25,7 +25,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `

@@ -35,7 +35,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `

@@ -46,9 +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" >
@@ -80,43 +80,48 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` aria-expanded="false" aria-haspopup="dialog" class="_display-flex _alignItems-stretch _flexDirection-column _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0" - data-state="closed" > -
-
-
+
- - - - + + + + + +
@@ -129,105 +134,101 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _alignSelf-stretch _width-10037 _justifyContent-center" >
- - - - - - - + +
- - 0x​9EB67f...D9A2Ca -
+ + 0x​9EB67f...D9A2Ca +
- - - + + + +
@@ -244,10 +245,10 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row" >
@@ -312,6 +318,45 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` />
+ + + + + + + + + + + + + + + , "container":
@@ -334,7 +379,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `

@@ -344,7 +389,7 @@ exports[`AccountSwitcherScreen renders correctly 1`] = `

@@ -355,9 +400,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" >
@@ -389,43 +434,48 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` aria-expanded="false" aria-haspopup="dialog" class="_display-flex _alignItems-stretch _flexDirection-column _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0" - data-state="closed" > -
-
-
+
- - - - + + + + + +
@@ -438,105 +488,101 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row _alignSelf-stretch _width-10037 _justifyContent-center" >
- - - - - - - + +
- - 0x​9EB67f...D9A2Ca -
+ + 0x​9EB67f...D9A2Ca +
- - - + + + +
@@ -553,10 +599,10 @@ exports[`AccountSwitcherScreen renders correctly 1`] = ` class="_display-flex _alignItems-stretch _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-row" >
diff --git a/apps/extension/src/app/features/accounts/useSortedAccountList.ts b/apps/extension/src/app/features/accounts/useSortedAccountList.ts index 94c418963a8..db605147f6e 100644 --- a/apps/extension/src/app/features/accounts/useSortedAccountList.ts +++ b/apps/extension/src/app/features/accounts/useSortedAccountList.ts @@ -12,7 +12,7 @@ export function useSortedAccountList(addresses: Address[]): AddressWithBalance[] addresses, }) - /* + /* Why are we using previousAccountBalanceData? This is a workaround for a data fetching inefficiency. When removing an address, we send a new query diff --git a/apps/extension/src/app/features/appRating/hooks/useAppRating.ts b/apps/extension/src/app/features/appRating/hooks/useAppRating.ts deleted file mode 100644 index f9f29ceac61..00000000000 --- a/apps/extension/src/app/features/appRating/hooks/useAppRating.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { useEffect, useState } from 'react' -import { useSelector } from 'react-redux' -import { appRatingStateSelector } from 'wallet/src/features/appRating/selectors' - -export const useAppRating = (): { - appRatingModalVisible: boolean - onAppRatingModalClose: () => void -} => { - const { shouldPrompt } = useSelector(appRatingStateSelector) - const [appRatingModalVisible, setAppRatingModalVisible] = useState(false) - - useEffect(() => { - if (shouldPrompt) { - setAppRatingModalVisible(true) - } - }, [shouldPrompt]) - - const onAppRatingModalClose = (): void => { - setAppRatingModalVisible(false) - } - - return { - appRatingModalVisible, - onAppRatingModalClose, - } -} diff --git a/apps/extension/src/app/features/biometricUnlock/BiometricUnlockStorage.ts b/apps/extension/src/app/features/biometricUnlock/BiometricUnlockStorage.ts index bd06afb9236..599a36b1c07 100644 --- a/apps/extension/src/app/features/biometricUnlock/BiometricUnlockStorage.ts +++ b/apps/extension/src/app/features/biometricUnlock/BiometricUnlockStorage.ts @@ -4,6 +4,7 @@ import { PersistedStorage } from 'wallet/src/utils/persistedStorage' export type BiometricUnlockStorageData = { credentialId: string + transports: AuthenticatorTransport[] secretPayload: Omit & { ciphertext: string } } diff --git a/apps/extension/src/app/features/biometricUnlock/biometricAuthUtils.ts b/apps/extension/src/app/features/biometricUnlock/biometricAuthUtils.ts index d82aeb49f58..21aa3d19515 100644 --- a/apps/extension/src/app/features/biometricUnlock/biometricAuthUtils.ts +++ b/apps/extension/src/app/features/biometricUnlock/biometricAuthUtils.ts @@ -16,9 +16,11 @@ import { */ export async function authenticateWithBiometricCredential({ credentialId, + transports, abortSignal, }: { credentialId: string + transports: AuthenticatorTransport[] abortSignal: AbortSignal }): Promise<{ publicKeyCredential: PublicKeyCredential; encryptionKey: CryptoKey }> { // Convert stored credential ID back to binary format @@ -31,6 +33,7 @@ export async function authenticateWithBiometricCredential({ { type: 'public-key', id: credentialIdBuffer, + transports, }, ], userVerification: 'required', @@ -70,10 +73,12 @@ export async function encryptPasswordWithBiometricData({ password, encryptionKey, credentialId, + transports, }: { password: string encryptionKey: CryptoKey credentialId: string + transports: AuthenticatorTransport[] }): Promise { // Create a new secret payload for the password const secretPayload = await createEmptySecretPayload() @@ -86,7 +91,7 @@ export async function encryptPasswordWithBiometricData({ additionalData: credentialId, // Use credential ID as additional authenticated data }) - return { credentialId, secretPayload: secretPayloadWithCiphertext } + return { credentialId, transports, secretPayload: secretPayloadWithCiphertext } } /** diff --git a/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockDisableMutation.ts b/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockDisableMutation.ts index 07f13845870..76793d5b259 100644 --- a/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockDisableMutation.ts +++ b/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockDisableMutation.ts @@ -1,6 +1,6 @@ import { UseMutationResult, useMutation, useQueryClient } from '@tanstack/react-query' -import { BiometricUnlockStorage } from 'src/app/features/biometricUnlock/BiometricUnlockStorage' import { biometricUnlockCredentialQuery } from 'src/app/features/biometricUnlock/biometricUnlockCredentialQuery' +import { BiometricUnlockStorage } from 'src/app/features/biometricUnlock/BiometricUnlockStorage' import { logger } from 'utilities/src/logger/logger' export function useBiometricUnlockDisableMutation(): UseMutationResult { @@ -11,6 +11,7 @@ export function useBiometricUnlockDisableMutation(): UseMutationResult { + // oxlint-disable-next-line typescript/no-floating-promises -- biome-parity: oxlint is stricter here queryClient.invalidateQueries(biometricUnlockCredentialQuery()) }, onError: (error) => { diff --git a/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.test.ts b/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.test.ts index 1b15deca6b5..1eb0c37cb0b 100644 --- a/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.test.ts +++ b/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.test.ts @@ -42,7 +42,12 @@ const mockGetEncryptionKeyFromBuffer = jest.requireMock( // Mock PublicKeyCredential (doesn't exist in Jest environment) class MockPublicKeyCredential { - constructor(public rawId: ArrayBuffer) {} + constructor( + public rawId: ArrayBuffer, + public response = { + getTransports: () => ['internal' as AuthenticatorTransport], + }, + ) {} } Object.defineProperty(global, 'PublicKeyCredential', { writable: true, @@ -124,10 +129,12 @@ describe('useBiometricUnlockSetupMutation', () => { // Verify the stored secret payload has all required properties const storedData = mockBiometricUnlockStorage.set.mock.calls[0]![0] as { credentialId: string + transports: AuthenticatorTransport[] secretPayload: typeof mockSecretPayload } expect(storedData.credentialId).toBe(expectedCredentialId) + expect(storedData.transports).toEqual(['internal']) expect(storedData.secretPayload).toEqual( expect.objectContaining({ diff --git a/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.ts b/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.ts index 345545a74c8..b7592f2b7ee 100644 --- a/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.ts +++ b/apps/extension/src/app/features/biometricUnlock/useBiometricUnlockSetupMutation.ts @@ -1,10 +1,10 @@ import { UseMutationResult, useMutation, useQueryClient } from '@tanstack/react-query' +import { encryptPasswordWithBiometricData } from 'src/app/features/biometricUnlock/biometricAuthUtils' +import { biometricUnlockCredentialQuery } from 'src/app/features/biometricUnlock/biometricUnlockCredentialQuery' import { BiometricUnlockStorage, BiometricUnlockStorageData, } from 'src/app/features/biometricUnlock/BiometricUnlockStorage' -import { encryptPasswordWithBiometricData } from 'src/app/features/biometricUnlock/biometricAuthUtils' -import { biometricUnlockCredentialQuery } from 'src/app/features/biometricUnlock/biometricUnlockCredentialQuery' import { startNavigatorCredentialRequest } from 'src/app/features/biometricUnlock/useNavigatorCredentialAbortSignal' import { assertPublicKeyCredential } from 'src/app/features/biometricUnlock/utils/assertPublicKeyCredential' import { isUserVerifyingPlatformAuthenticatorAvailable } from 'src/app/utils/device/builtInBiometricCapabilitiesQuery' @@ -16,6 +16,11 @@ import { getEncryptionKeyFromBuffer, } from 'wallet/src/features/wallet/Keyring/crypto' +// Extend PublicKeyCredentialCreationOptions to include Chrome 128+ hints property +interface PublicKeyCredentialCreationOptionsWithHints extends PublicKeyCredentialCreationOptions { + hints?: string[] +} + export function useBiometricUnlockSetupMutation(options?: { onSuccess?: () => void onError?: (error: Error) => void @@ -35,6 +40,7 @@ export function useBiometricUnlockSetupMutation(options?: { }, retry: false, onSettled: () => { + // oxlint-disable-next-line typescript/no-floating-promises -- biome-parity: oxlint is stricter here queryClient.invalidateQueries(biometricUnlockCredentialQuery()) }, onSuccess: options?.onSuccess, @@ -69,7 +75,7 @@ async function createCredentialAndEncryptPassword({ const rawKey = await window.crypto.subtle.exportKey('raw', encryptionKey) - const { credentialId } = await createCredential({ + const { credentialId, transports } = await createCredential({ encryptionKey: rawKey, abortSignal, }) @@ -78,6 +84,7 @@ async function createCredentialAndEncryptPassword({ password, encryptionKey, credentialId, + transports, }) } @@ -116,7 +123,7 @@ async function createCredential({ }: { encryptionKey: ArrayBuffer abortSignal: AbortSignal -}): Promise<{ credentialId: string }> { +}): Promise<{ credentialId: string; transports: AuthenticatorTransport[] }> { // Create WebAuthn credential with platform authenticator (Touch ID, Windows Hello, etc.) forced const credential = await navigator.credentials.create({ publicKey: { @@ -135,12 +142,12 @@ async function createCredential({ residentKey: 'required', userVerification: 'required', }, - // @ts-expect-error - `hints` is a new property, only available in Chrome 128+. - // This forces the credential to use the built-in passkey instead of prompting the user where to save it. - hints: ['client-device'], pubKeyCredParams: CREDENTIAL_ALGORITHMS, timeout: 15 * ONE_SECOND_MS, - }, + // `hints` is a new property, only available in Chrome 128+. + // This forces the credential to use the built-in passkey instead of prompting the user where to save it. + hints: ['client-device'], + } as PublicKeyCredentialCreationOptionsWithHints, signal: abortSignal, }) @@ -149,5 +156,8 @@ async function createCredential({ // Convert raw ID to a storable string format const credentialId = btoa(String.fromCharCode(...new Uint8Array(publicKeyCredential.rawId))) - return { credentialId } + const response = publicKeyCredential.response as AuthenticatorAttestationResponse + const transports = response.getTransports() as AuthenticatorTransport[] + + return { credentialId, transports } } diff --git a/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.test.ts b/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.test.ts index fd312c48e6a..b97ee6462c5 100644 --- a/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.test.ts +++ b/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.test.ts @@ -38,7 +38,7 @@ const mockLogger = logger as jest.Mocked // Mock AuthenticatorAssertionResponse class MockAuthenticatorAssertionResponse { - // eslint-disable-next-line max-params + // oxlint-disable-next-line max-params constructor( public userHandle: ArrayBuffer | null, public authenticatorData: ArrayBuffer = new ArrayBuffer(0), @@ -108,6 +108,7 @@ describe('useChangePasswordWithBiometricMutation', () => { // Setup default mocks mockBiometricUnlockStorage.get.mockResolvedValue({ credentialId: mockCredentialId, + transports: ['internal'], secretPayload: mockOldEncryptedPayload, }) @@ -146,6 +147,7 @@ describe('useChangePasswordWithBiometricMutation', () => { { type: 'public-key', id: credentialIdBuffer, + transports: ['internal'], }, ], userVerification: 'required', @@ -160,6 +162,7 @@ describe('useChangePasswordWithBiometricMutation', () => { // 4. Should update the stored biometric data with re-encrypted password expect(mockBiometricUnlockStorage.set).toHaveBeenCalledWith({ credentialId: mockCredentialId, + transports: ['internal'], secretPayload: expect.objectContaining({ ciphertext: expect.any(String), iv: expect.any(String), @@ -189,6 +192,7 @@ describe('useChangePasswordWithBiometricMutation', () => { const newBiometricData = setCall?.[0] expect(newBiometricData?.credentialId).toBe(mockCredentialId) + expect(newBiometricData?.transports).toEqual(['internal']) expect(newBiometricData?.secretPayload).toMatchObject({ ciphertext: expect.any(String), iv: expect.any(String), diff --git a/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.ts b/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.ts index 8da478fe45c..fd0ff4fb6d4 100644 --- a/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.ts +++ b/apps/extension/src/app/features/biometricUnlock/useChangePasswordWithBiometricMutation.ts @@ -1,9 +1,9 @@ import { UseMutationResult, useMutation } from '@tanstack/react-query' -import { BiometricUnlockStorage } from 'src/app/features/biometricUnlock/BiometricUnlockStorage' import { authenticateWithBiometricCredential, encryptPasswordWithBiometricData, } from 'src/app/features/biometricUnlock/biometricAuthUtils' +import { BiometricUnlockStorage } from 'src/app/features/biometricUnlock/BiometricUnlockStorage' import { startNavigatorCredentialRequest } from 'src/app/features/biometricUnlock/useNavigatorCredentialAbortSignal' import { logger } from 'utilities/src/logger/logger' import { useEvent } from 'utilities/src/react/hooks' @@ -25,6 +25,7 @@ export function useChangePasswordWithBiometricMutation(options?: { // Authenticate with WebAuthn to get the encryption key const { encryptionKey } = await authenticateWithBiometricCredential({ credentialId: biometricUnlockCredential.credentialId, + transports: biometricUnlockCredential.transports, abortSignal, }) @@ -36,6 +37,7 @@ export function useChangePasswordWithBiometricMutation(options?: { password: newPassword, encryptionKey, credentialId: biometricUnlockCredential.credentialId, + transports: biometricUnlockCredential.transports, }) // Update the stored biometric data diff --git a/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlock.ts b/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlock.ts index 3491a94d6bb..54582813431 100644 --- a/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlock.ts +++ b/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlock.ts @@ -1,7 +1,6 @@ import { useQuery } from '@tanstack/react-query' +import { DynamicConfigs, ExtensionBiometricUnlockConfigKey, useDynamicConfigValue } from '@universe/gating' import { biometricUnlockCredentialQuery } from 'src/app/features/biometricUnlock/biometricUnlockCredentialQuery' -import { DynamicConfigs, ExtensionBiometricUnlockConfigKey } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' export function useShouldShowBiometricUnlock(): boolean { const isEnabled = useDynamicConfigValue({ diff --git a/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment.ts b/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment.ts index 7c496296c1d..f642af2d9ad 100644 --- a/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment.ts +++ b/apps/extension/src/app/features/biometricUnlock/useShouldShowBiometricUnlockEnrollment.ts @@ -1,8 +1,7 @@ import { useQuery } from '@tanstack/react-query' +import { DynamicConfigs, ExtensionBiometricUnlockConfigKey, useDynamicConfigValue } from '@universe/gating' import { useTranslation } from 'react-i18next' import { builtInBiometricCapabilitiesQuery } from 'src/app/utils/device/builtInBiometricCapabilitiesQuery' -import { DynamicConfigs, ExtensionBiometricUnlockConfigKey } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' export function useShouldShowBiometricUnlockEnrollment({ flow }: { flow: 'onboarding' | 'settings' }): boolean { const { t } = useTranslation() diff --git a/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.test.ts b/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.test.ts index 832e727c382..2f1702231fc 100644 --- a/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.test.ts +++ b/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.test.ts @@ -3,15 +3,13 @@ import { waitFor } from '@testing-library/react' import { BiometricUnlockStorage } from 'src/app/features/biometricUnlock/BiometricUnlockStorage' import { useUnlockWithBiometricCredentialMutation } from 'src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation' import { renderHookWithProviders } from 'src/test/render' -import { authActions } from 'wallet/src/features/auth/saga' -import { AuthActionType } from 'wallet/src/features/auth/types' import { encodeForStorage, encrypt, generateNew256BitRandomBuffer } from 'wallet/src/features/wallet/Keyring/crypto' jest.mock('src/app/features/biometricUnlock/BiometricUnlockStorage') -jest.mock('wallet/src/features/auth/saga', () => ({ - authActions: { - trigger: jest.fn(), - }, + +const mockUnlockWithPassword = jest.fn() +jest.mock('src/app/features/lockScreen/useUnlockWithPassword', () => ({ + useUnlockWithPassword: jest.fn(() => mockUnlockWithPassword), })) // Mock the Web Crypto API with Node.js built-in @@ -27,11 +25,10 @@ Object.defineProperty(navigator, 'credentials', { }) const mockBiometricUnlockStorage = BiometricUnlockStorage as jest.Mocked -const mockAuthActions = authActions as jest.Mocked // Mock AuthenticatorAssertionResponse class MockAuthenticatorAssertionResponse { - // eslint-disable-next-line max-params + // oxlint-disable-next-line max-params constructor( public userHandle: ArrayBuffer | null, public authenticatorData: ArrayBuffer = new ArrayBuffer(0), @@ -100,6 +97,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { // Setup default mocks mockBiometricUnlockStorage.get.mockResolvedValue({ credentialId: mockCredentialId, + transports: ['internal'], secretPayload: mockEncryptedPayload, }) @@ -107,15 +105,9 @@ describe('useUnlockWithBiometricCredentialMutation', () => { const mockPublicKeyCredential = new MockPublicKeyCredential(mockAuthResponse) mockCredentialsGet.mockResolvedValue(mockPublicKeyCredential) - mockAuthActions.trigger.mockReturnValue({ - type: 'AUTH_TRIGGER', - payload: { - type: AuthActionType.Unlock, - password: mockPassword, - }, - }) - - jest.clearAllMocks() + // Reset and configure mockUnlockWithPassword + mockUnlockWithPassword.mockReset() + mockUnlockWithPassword.mockResolvedValue(undefined) }) describe('successful unlock', () => { @@ -140,6 +132,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { { type: 'public-key', id: credentialIdBuffer, + transports: ['internal'], }, ], userVerification: 'required', @@ -148,11 +141,8 @@ describe('useUnlockWithBiometricCredentialMutation', () => { signal: expect.any(AbortSignal), }) - // 3. Should dispatch unlock action with the decrypted password - expect(mockAuthActions.trigger).toHaveBeenCalledWith({ - type: AuthActionType.Unlock, - password: mockPassword, - }) + // 3. Should call unlockWithPassword with the decrypted password + expect(mockUnlockWithPassword).toHaveBeenCalledWith({ password: mockPassword }) }) }) @@ -170,7 +160,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { expect(result.current.error?.message).toBe('No biometric unlock credential found') expect(mockCredentialsGet).not.toHaveBeenCalled() - expect(mockAuthActions.trigger).not.toHaveBeenCalled() + expect(mockUnlockWithPassword).not.toHaveBeenCalled() }) it('should throw error when biometric authentication fails', async () => { @@ -185,7 +175,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { }) expect(result.current.error?.message).toBe('Failed to create credential') - expect(mockAuthActions.trigger).not.toHaveBeenCalled() + expect(mockUnlockWithPassword).not.toHaveBeenCalled() }) it('should throw error when no user handle returned from authentication', async () => { @@ -202,7 +192,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { }) expect(result.current.error?.message).toBe('No user handle returned from biometric authentication') - expect(mockAuthActions.trigger).not.toHaveBeenCalled() + expect(mockUnlockWithPassword).not.toHaveBeenCalled() }) it('should throw error when password decryption fails', async () => { @@ -226,7 +216,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { }) expect(result.current.error?.message).toBe('Failed to decrypt password') - expect(mockAuthActions.trigger).not.toHaveBeenCalled() + expect(mockUnlockWithPassword).not.toHaveBeenCalled() }) it('should handle WebAuthn API errors', async () => { @@ -242,7 +232,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { }) expect(result.current.error).toBe(webAuthnError) - expect(mockAuthActions.trigger).not.toHaveBeenCalled() + expect(mockUnlockWithPassword).not.toHaveBeenCalled() }) it('should handle storage retrieval errors', async () => { @@ -259,7 +249,7 @@ describe('useUnlockWithBiometricCredentialMutation', () => { expect(result.current.error).toBe(storageError) expect(mockCredentialsGet).not.toHaveBeenCalled() - expect(mockAuthActions.trigger).not.toHaveBeenCalled() + expect(mockUnlockWithPassword).not.toHaveBeenCalled() }) }) diff --git a/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.ts b/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.ts index 4eac011d28f..5f472d5cf63 100644 --- a/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.ts +++ b/apps/extension/src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation.ts @@ -1,9 +1,9 @@ import { UseMutationResult, useMutation } from '@tanstack/react-query' -import { BiometricUnlockStorage } from 'src/app/features/biometricUnlock/BiometricUnlockStorage' import { authenticateWithBiometricCredential, decryptPasswordFromBiometricData, } from 'src/app/features/biometricUnlock/biometricAuthUtils' +import { BiometricUnlockStorage } from 'src/app/features/biometricUnlock/BiometricUnlockStorage' import { startNavigatorCredentialRequest } from 'src/app/features/biometricUnlock/useNavigatorCredentialAbortSignal' import { useUnlockWithPassword } from 'src/app/features/lockScreen/useUnlockWithPassword' import { logger } from 'utilities/src/logger/logger' @@ -58,10 +58,10 @@ async function getPasswordFromBiometricCredential(abortSignal: AbortSignal): Pro throw new Error('No biometric unlock credential found') } - const { credentialId } = biometricUnlockCredential + const { credentialId, transports } = biometricUnlockCredential // Authenticate with WebAuthn using the stored credential and decrypt password - const { encryptionKey } = await authenticateWithBiometricCredential({ credentialId, abortSignal }) + const { encryptionKey } = await authenticateWithBiometricCredential({ credentialId, transports, abortSignal }) const password = await decryptPasswordFromBiometricData({ encryptionKey, biometricUnlockCredential }) return password } diff --git a/apps/extension/src/app/features/dapp/DappContext.tsx b/apps/extension/src/app/features/dapp/DappContext.tsx index fd60a1566ec..af21556e257 100644 --- a/apps/extension/src/app/features/dapp/DappContext.tsx +++ b/apps/extension/src/app/features/dapp/DappContext.tsx @@ -50,7 +50,7 @@ export function DappContextProvider({ children }: { children: ReactNode }): JSX. } // need to update dapp info on mount - // eslint-disable-next-line @typescript-eslint/no-floating-promises + // oxlint-disable-next-line typescript/no-floating-promises updateDappInfo() return backgroundToSidePanelMessageChannel.addMessageListener( diff --git a/apps/extension/src/app/features/dapp/hooks.test.ts b/apps/extension/src/app/features/dapp/hooks.test.ts index 966118ec94b..579db37578d 100644 --- a/apps/extension/src/app/features/dapp/hooks.test.ts +++ b/apps/extension/src/app/features/dapp/hooks.test.ts @@ -1,4 +1,5 @@ import { + useAllDappConnectionsForAccount, useDappConnectedAccounts, useDappInfo, useDappLastChainId, @@ -8,8 +9,14 @@ import { DappState, dappStore } from 'src/app/features/dapp/store' import { act, renderHook, waitFor } from 'src/test/test-utils' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_3 } from 'uniswap/src/test/fixtures' +import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks' import { ACCOUNT, ACCOUNT2, ACCOUNT3 } from 'wallet/src/test/fixtures' +jest.mock('wallet/src/features/wallet/hooks', () => ({ + ...jest.requireActual('wallet/src/features/wallet/hooks'), + useActiveAccountAddress: jest.fn(), +})) + const SAMPLE_DAPP = 'http://example.com' const SAMPLE_DAPP_2 = 'http://uniswap.org' @@ -96,4 +103,41 @@ describe('Dapp hooks', () => { const { result } = renderHook(() => useDappConnectedAccounts(SAMPLE_DAPP)) await waitFor(() => expect(result.current).toEqual([ACCOUNT, ACCOUNT2])) }) + + describe('useAllDappConnectionsForAccount', () => { + it('should return connections for a specific address when provided', async () => { + // ACCOUNT2 (SAMPLE_SEED_ADDRESS_2) is only connected to SAMPLE_DAPP + const { result } = renderHook(() => useAllDappConnectionsForAccount(ACCOUNT2.address)) + await waitFor(() => expect(result.current).toEqual([SAMPLE_DAPP])) + }) + + it('should return connections for address connected to multiple dapps', async () => { + // ACCOUNT (SAMPLE_SEED_ADDRESS_1) is connected to both dapps + const { result } = renderHook(() => useAllDappConnectionsForAccount(ACCOUNT.address)) + await waitFor(() => expect(result.current).toEqual(expect.arrayContaining([SAMPLE_DAPP, SAMPLE_DAPP_2]))) + await waitFor(() => expect(result.current).toHaveLength(2)) + }) + + it('should return empty array when address has no connections', async () => { + const unconnectedAddress = '0x0000000000000000000000000000000000000000' + const { result } = renderHook(() => useAllDappConnectionsForAccount(unconnectedAddress)) + await waitFor(() => expect(result.current).toEqual([])) + }) + + it('should use active account when no address is provided', async () => { + // Mock useActiveAccountAddress to return ACCOUNT3's address + jest.mocked(useActiveAccountAddress).mockReturnValue(ACCOUNT3.address) + + // ACCOUNT3 (SAMPLE_SEED_ADDRESS_3) is only connected to SAMPLE_DAPP_2 + const { result } = renderHook(() => useAllDappConnectionsForAccount()) + await waitFor(() => expect(result.current).toEqual([SAMPLE_DAPP_2])) + }) + + it('should return empty array when no address provided and no active account', async () => { + jest.mocked(useActiveAccountAddress).mockReturnValue(null) + + const { result } = renderHook(() => useAllDappConnectionsForAccount()) + await waitFor(() => expect(result.current).toEqual([])) + }) + }) }) diff --git a/apps/extension/src/app/features/dapp/hooks.ts b/apps/extension/src/app/features/dapp/hooks.ts index fa65e9aee22..54a3b358dce 100644 --- a/apps/extension/src/app/features/dapp/hooks.ts +++ b/apps/extension/src/app/features/dapp/hooks.ts @@ -20,7 +20,7 @@ export function useDappStateUpdated(): boolean { export function useDappInfo(dappUrl: string | undefined): DappInfo | undefined { const [info, setInfo] = useState() const dappStateUpdated = useDappStateUpdated() - // biome-ignore lint/correctness/useExhaustiveDependencies: dappStateUpdated is used to trigger re-render when dapp store changes + // oxlint-disable-next-line react/exhaustive-deps -- dappStateUpdated is used to trigger re-render when dapp store changes useEffect(() => { setInfo(dappStore.getDappInfo(dappUrl)) }, [dappUrl, dappStateUpdated]) @@ -36,19 +36,22 @@ export function useDappConnectedAccounts(dappUrl: string | undefined): Account[] } /** - * Pairs well with `getDappInfo`, which returns the dapp info for a given dapp URL. + * Hook to retrieve all dapp connection URLs for a specific account. * - * @returns all dapp connection URLs (ie state keys) for the active account + * @param address - Optional account address. If not provided, uses the active account. + * @returns all dapp connection URLs (ie state keys) for the specified account */ -export function useAllDappConnectionsForActiveAccount(): string[] { +export function useAllDappConnectionsForAccount(address?: Address): string[] { const [dappUrls, setDappUrls] = useState([]) const dappStateUpdated = useDappStateUpdated() const activeAccount = useActiveAccountAddress() - // biome-ignore lint/correctness/useExhaustiveDependencies: dappStateUpdated is used to trigger re-render when dapp store changes + const accountAddress = address ?? activeAccount + + // oxlint-disable-next-line react/exhaustive-deps -- dappStateUpdated is used to trigger re-render when dapp store changes useEffect(() => { - setDappUrls(activeAccount ? dappStore.getConnectedDapps(activeAccount) : []) - }, [activeAccount, dappStateUpdated]) + setDappUrls(accountAddress ? dappStore.getConnectedDapps(accountAddress) : []) + }, [accountAddress, dappStateUpdated]) return dappUrls } diff --git a/apps/extension/src/app/features/dapp/store.ts b/apps/extension/src/app/features/dapp/store.ts index ed6dabeccb2..ea857febc5b 100644 --- a/apps/extension/src/app/features/dapp/store.ts +++ b/apps/extension/src/app/features/dapp/store.ts @@ -41,12 +41,12 @@ async function init(): Promise { } async function initInternal(): Promise { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition state = (await chrome.storage.local.get([STATE_STORAGE_KEY]))?.[STATE_STORAGE_KEY] || initialDappState chrome.storage.local.onChanged.addListener((changes) => { - if (changes.dappState) { - state = changes.dappState.newValue + if (changes['dappState']) { + state = changes['dappState'].newValue dappStoreEventEmitter.emit(DappStoreEvent.DappStateUpdated, state) } }) diff --git a/apps/extension/src/app/features/dappRequests/DappRequestContent.test.tsx b/apps/extension/src/app/features/dappRequests/DappRequestContent.test.tsx new file mode 100644 index 00000000000..7a805558bf7 --- /dev/null +++ b/apps/extension/src/app/features/dappRequests/DappRequestContent.test.tsx @@ -0,0 +1,225 @@ +import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' +import { REQUEST_EXPIRY_TIME_MS } from 'src/app/features/dappRequests/hooks/useIsRequestStale' +import type { DappRequestStoreItem } from 'src/app/features/dappRequests/shared' +import { DappRequestStatus } from 'src/app/features/dappRequests/shared' +import type { WithMetadata } from 'src/app/features/dappRequests/slice' +import { render, screen } from 'src/test/test-utils' +import { AccountType } from 'uniswap/src/features/accounts/types' +import { DappRequestType } from 'uniswap/src/features/dappRequests/types' + +// Mock wagmi to avoid ESM import issues +jest.mock('wagmi', () => ({ + useAccountEffect: jest.fn(), +})) + +// Mock the useIsRequestStale hook to control output +const mockUseIsRequestStale = jest.fn() +jest.mock('src/app/features/dappRequests/hooks/useIsRequestStale', () => ({ + ...jest.requireActual('src/app/features/dappRequests/hooks/useIsRequestStale'), + useIsRequestStale: (createdAt: number) => mockUseIsRequestStale(createdAt), +})) + +// Mock the context hook to return our mock value +let mockContextValue: any = null +jest.mock('src/app/features/dappRequests/DappRequestQueueContext', () => ({ + useDappRequestQueueContext: () => mockContextValue, +})) + +// Mock hooks used by DappRequestFooter +jest.mock('src/app/features/dapp/hooks', () => ({ + useDappLastChainId: jest.fn(() => 1), +})) + +jest.mock('uniswap/src/features/gas/hooks/useChainGasToken', () => ({ + useChainGasToken: jest.fn(() => ({ + gasToken: { symbol: 'ETH' }, + gasBalance: { value: '1000000000000000000', currency: { symbol: 'ETH' }, equalTo: () => false }, + isLoading: false, + })), +})) + +jest.mock('uniswap/src/features/gas/utils', () => ({ + ...jest.requireActual('uniswap/src/features/gas/utils'), + hasSufficientGasBalance: jest.fn(() => true), + hasGasEstimationFailed: jest.fn(() => false), +})) + +jest.mock('wallet/src/features/wallet/hooks', () => ({ + useActiveAccountWithThrow: jest.fn(() => ({ + address: '0x123', + type: 'readonly', + timeImportedMs: Date.now(), + pushNotificationsEnabled: false, + })), +})) + +jest.mock('uniswap/src/features/chains/hooks/useEnabledChains', () => ({ + useEnabledChains: jest.fn(() => ({ + defaultChainId: 1, + })), +})) + +jest.mock('src/app/features/dappRequests/hooks', () => ({ + useIsDappRequestConfirming: jest.fn(() => false), +})) + +// Mock the NetworkFeeFooter to avoid complex currency parsing +jest.mock('wallet/src/features/transactions/TransactionRequest/NetworkFeeFooter', () => ({ + NetworkFeeFooter: () => null, +})) + +jest.mock('wallet/src/features/transactions/TransactionRequest/AddressFooter', () => ({ + AddressFooter: () => null, +})) + +// Mock currency hooks that parse transaction data +jest.mock('uniswap/src/data/apiClients/tradingApi/useTradingApiSwapQuery', () => ({ + useTradingApiSwapQuery: jest.fn(() => ({ + data: undefined, + isLoading: false, + })), +})) + +function setupMockRequestAndContext(createdAt: number, options?: { frameUrl?: string }): void { + const request: WithMetadata = { + dappRequest: { + type: DappRequestType.SendTransaction, + requestId: 'test-request-id', + transaction: { + from: '0x123', + to: '0x456', + value: '0', + chainId: 1, + }, + }, + senderTabInfo: { + id: 1, + url: 'https://example.com', + frameUrl: options?.frameUrl, + }, + dappInfo: { + activeConnectedAddress: '0x123', + lastChainId: 1, + connectedAccounts: [ + { + address: '0x123', + type: AccountType.Readonly, + timeImportedMs: Date.now(), + pushNotificationsEnabled: false, + }, + ], + }, + createdAt, + status: DappRequestStatus.Pending, + isSidebarClosed: false, + } + + mockContextValue = { + forwards: true, + increasing: true, + request, + currentAccount: { + address: '0x123', + type: AccountType.Readonly, + timeImportedMs: Date.now(), + pushNotificationsEnabled: false, + }, + dappUrl: 'https://example.com', + frameUrl: options?.frameUrl, + dappIconUrl: '', + currentIndex: 0, + totalRequestCount: 1, + onPressNext: jest.fn(), + onPressPrevious: jest.fn(), + onConfirm: jest.fn(), + onCancel: jest.fn(), + } +} + +function renderDappRequestContent(options: { createdAt: number; isRequestStale: boolean; frameUrl?: string }) { + mockUseIsRequestStale.mockReturnValue(options.isRequestStale) + setupMockRequestAndContext(options.createdAt, { frameUrl: options.frameUrl }) + return render() +} + +describe('DappRequestContent - Stale Request Rendering', () => { + beforeEach(() => { + jest.useFakeTimers() + jest.setSystemTime(new Date('2024-01-01T12:00:00.000Z')) + mockUseIsRequestStale.mockClear() + }) + + afterEach(() => { + jest.runOnlyPendingTimers() + jest.useRealTimers() + }) + + it('should render Cancel and Confirm buttons for fresh requests', async () => { + const freshCreatedAt = Date.now() - 1000 + + renderDappRequestContent({ createdAt: freshCreatedAt, isRequestStale: false }) + + // Verify hook was called + expect(mockUseIsRequestStale).toHaveBeenCalledWith(freshCreatedAt) + + // Verify normal buttons are shown + await screen.findByText('Cancel') + await screen.findByText('Confirm') + // Verify close button is NOT shown + expect(screen.queryByText('Close')).toBeNull() + }) + + it('should render warning and Close button for stale requests', async () => { + const staleCreatedAt = Date.now() - (REQUEST_EXPIRY_TIME_MS + 60000) + + renderDappRequestContent({ createdAt: staleCreatedAt, isRequestStale: true }) + + // Verify hook was called + expect(mockUseIsRequestStale).toHaveBeenCalledWith(staleCreatedAt) + // Verify Close button is shown + await screen.findByText('Close') + // Verify Confirm button is NOT shown + expect(screen.queryByText('Confirm')).toBeNull() + }) + + it('should match snapshot for fresh request', async () => { + const freshCreatedAt = Date.now() - 1000 + + const { container } = renderDappRequestContent({ createdAt: freshCreatedAt, isRequestStale: false }) + + expect(container).toMatchSnapshot() + }) + + it('should match snapshot for stale request', async () => { + const staleCreatedAt = Date.now() - (REQUEST_EXPIRY_TIME_MS + 60000) + + const { container } = renderDappRequestContent({ createdAt: staleCreatedAt, isRequestStale: true }) + + expect(container).toMatchSnapshot() + }) + + it('should display iframe URL with "via" when frameUrl differs from url', async () => { + const freshCreatedAt = Date.now() - 1000 + + renderDappRequestContent({ + createdAt: freshCreatedAt, + isRequestStale: false, + frameUrl: 'https://app.uniswap.org', + }) + + // Should show "app.uniswap.org via example.com" in the URL label + expect(screen.queryByText(/app\.uniswap\.org via example\.com/i)).not.toBeNull() + }) + + it('should display only top-level URL when frameUrl is not present', async () => { + const freshCreatedAt = Date.now() - 1000 + + renderDappRequestContent({ createdAt: freshCreatedAt, isRequestStale: false }) + + // Should show just "example.com" (no "via") + expect(screen.queryByText(/example\.com/i)).not.toBeNull() + + // Should NOT show "via" + expect(screen.queryByText(/via/i)).toBeNull() + }) +}) diff --git a/apps/extension/src/app/features/dappRequests/DappRequestContent.tsx b/apps/extension/src/app/features/dappRequests/DappRequestContent.tsx index b150cd8ff4a..d63f78b8340 100644 --- a/apps/extension/src/app/features/dappRequests/DappRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/DappRequestContent.tsx @@ -1,52 +1,37 @@ -import { PropsWithChildren } from 'react' +import { type GasFeeResult } from '@universe/api' +import { type PropsWithChildren } from 'react' import { useTranslation } from 'react-i18next' -import { Animated } from 'react-native' +import { type Animated } from 'react-native' import { useDispatch } from 'react-redux' import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' import { handleExternallySubmittedUniswapXOrder } from 'src/app/features/dappRequests/handleUniswapX' import { useIsDappRequestConfirming } from 'src/app/features/dappRequests/hooks' -import { DappRequestStoreItem } from 'src/app/features/dappRequests/shared' -import { - DappRequest, - isBatchedSwapRequest, - isConnectionRequest, -} from 'src/app/features/dappRequests/types/DappRequestTypes' -import { - Anchor, - AnimatePresence, - Button, - Flex, - GetThemeValueForKey, - styled, - Text, - UniversalImage, - UniversalImageResizeMode, -} from 'ui/src' -import { Verified } from 'ui/src/components/icons' -import { borderRadii, iconSizes } from 'ui/src/theme' -import { DappIconPlaceholder } from 'uniswap/src/components/dapps/DappIconPlaceholder' -import { UNISWAP_WEB_HOSTNAME } from 'uniswap/src/constants/urls' +import { useIsRequestStale } from 'src/app/features/dappRequests/hooks/useIsRequestStale' +import { type DappRequestStoreItem } from 'src/app/features/dappRequests/shared' +import { type DappRequest, isBatchedSwapRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' +import { AnimatePresence, Button, Flex, type GetThemeValueForKey, styled, Text } from 'ui/src' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' -import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { type UniverseChainId } from 'uniswap/src/features/chains/types' import { DappRequestType } from 'uniswap/src/features/dappRequests/types' -import { GasFeeResult } from 'uniswap/src/features/gas/types' -import { hasSufficientFundsIncludingGas } from 'uniswap/src/features/gas/utils' -import { useOnChainNativeCurrencyBalance } from 'uniswap/src/features/portfolio/api' -import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' +import { useChainGasToken } from 'uniswap/src/features/gas/hooks/useChainGasToken' +import { hasGasEstimationFailed, hasSufficientGasBalance } from 'uniswap/src/features/gas/utils' +import { type TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl' -import { formatDappURL } from 'utilities/src/format/urls' import { logger } from 'utilities/src/logger/logger' import { useEvent } from 'utilities/src/react/hooks' import { useThrottledCallback } from 'utilities/src/react/useThrottledCallback' import { MAX_HIDDEN_CALLS_BY_DEFAULT } from 'wallet/src/components/BatchedTransactions/BatchedTransactionDetails' +import { DappRequestHeader } from 'wallet/src/components/dappRequests/DappRequestHeader' import { WarningBox } from 'wallet/src/components/WarningBox/WarningBox' +import { type DappVerificationStatus } from 'wallet/src/features/dappRequests/types' import { AddressFooter } from 'wallet/src/features/transactions/TransactionRequest/AddressFooter' import { NetworkFeeFooter } from 'wallet/src/features/transactions/TransactionRequest/NetworkFeeFooter' import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' interface DappRequestHeaderProps { title: string + verificationStatus?: DappVerificationStatus headerIcon?: JSX.Element } @@ -57,9 +42,9 @@ interface DappRequestFooterProps { maybeCloseOnConfirm?: boolean onCancel?: (requestToConfirm?: DappRequestStoreItem, transactionTypeInfo?: TransactionTypeInfo) => void onConfirm?: (requestToCancel?: DappRequestStoreItem) => void - showAllNetworks?: boolean showNetworkCost?: boolean showSmartWalletActivation?: boolean + showAddressFooter?: boolean transactionGasFeeResult?: GasFeeResult isUniswapX?: boolean disableConfirm?: boolean @@ -96,26 +81,40 @@ export const AnimatedPane = styled(Flex, { export function DappRequestContent({ chainId, title, + verificationStatus, headerIcon, confirmText, connectedAccountAddress, maybeCloseOnConfirm, onCancel, onConfirm, - showAllNetworks, showNetworkCost, showSmartWalletActivation, transactionGasFeeResult, children, isUniswapX, disableConfirm, + showAddressFooter = true, contentHorizontalPadding = '$spacing12', }: PropsWithChildren): JSX.Element { - const { forwards, currentIndex } = useDappRequestQueueContext() + const { forwards, currentIndex, dappIconUrl, dappUrl, frameUrl } = useDappRequestQueueContext() + const hostname = extractNameFromUrl(dappUrl).toUpperCase() return ( <> - + + + {children} @@ -127,9 +126,9 @@ export function DappRequestContent({ connectedAccountAddress={connectedAccountAddress} isUniswapX={isUniswapX} maybeCloseOnConfirm={maybeCloseOnConfirm} - showAllNetworks={showAllNetworks} showNetworkCost={showNetworkCost} showSmartWalletActivation={showSmartWalletActivation} + showAddressFooter={showAddressFooter} transactionGasFeeResult={transactionGasFeeResult} disableConfirm={disableConfirm} onCancel={onCancel} @@ -139,50 +138,9 @@ export function DappRequestContent({ ) } -function DappRequestHeader({ headerIcon, title }: DappRequestHeaderProps): JSX.Element { - const { dappIconUrl, dappUrl, request } = useDappRequestQueueContext() - const hostname = extractNameFromUrl(dappUrl).toUpperCase() - const fallbackIcon = - const showVerified = - request && isConnectionRequest(request.dappRequest) && formatDappURL(dappUrl) === UNISWAP_WEB_HOSTNAME - - return ( - - - - {headerIcon || ( - - )} - - - - {title} - - - - - {formatDappURL(dappUrl)} - - {showVerified && } - - - - ) -} - const WINDOW_CLOSE_DELAY = 10 +// oxlint-disable-next-line complexity -- biome-parity: oxlint is stricter here function DappRequestFooter({ chainId, connectedAccountAddress, @@ -192,6 +150,7 @@ function DappRequestFooter({ onConfirm, showNetworkCost, showSmartWalletActivation, + showAddressFooter, transactionGasFeeResult, isUniswapX, disableConfirm, @@ -220,21 +179,28 @@ function DappRequestFooter({ const sendTransactionChainId = request.dappRequest.type === DappRequestType.SendTransaction ? request.dappRequest.transaction.chainId : undefined const currentChainId = chainId || sendTransactionChainId || activeChain || defaultChainId - const { balance: nativeBalance } = useOnChainNativeCurrencyBalance(currentChainId, currentAccount.address) + const { gasBalance } = useChainGasToken({ chainId: currentChainId, accountAddress: currentAccount.address }) const isRequestConfirming = useIsDappRequestConfirming(request.dappRequest.requestId) + const isRequestStale = useIsRequestStale(request.createdAt) - const hasSufficientGas = hasSufficientFundsIncludingGas({ + const hasSufficientGas = hasSufficientGasBalance({ + chainId: currentChainId, + gasBalance, gasFee: transactionGasFeeResult?.value, - nativeCurrencyBalance: nativeBalance, }) const shouldCloseSidebar = request.isSidebarClosed && totalRequestCount <= 1 - // Disable submission if no gas fee value - const isConfirmEnabled = - request.dappRequest.type === DappRequestType.SendTransaction - ? transactionGasFeeResult?.value && hasSufficientGas - : true + // Check if this is a transaction request that needs gas estimation + const isTransactionRequest = + request.dappRequest.type === DappRequestType.SendTransaction || + request.dappRequest.type === DappRequestType.SendCalls + + // Check if gas estimation failed (has error or no value after loading) + const gasEstimationFailed = hasGasEstimationFailed(isTransactionRequest, transactionGasFeeResult) + + // Disable submission when gas estimation fails or user has insufficient funds + const isConfirmEnabled = !isTransactionRequest || (!gasEstimationFailed && hasSufficientGas) const handleOnConfirm = useEvent(async () => { if (isRequestConfirming) { @@ -275,12 +241,19 @@ function DappRequestFooter({ return ( <> - - {!hasSufficientGas && ( + + {gasEstimationFailed && ( + + + {t('dapp.request.error.gasEstimation')} + + + )} + {!hasSufficientGas && !gasEstimationFailed && ( {t('swap.warning.insufficientGas.title', { - currencySymbol: nativeBalance?.currency.symbol ?? '', + currencySymbol: gasBalance?.currency.symbol ?? '', })} @@ -295,17 +268,19 @@ function DappRequestFooter({ showSmartWalletActivation={showSmartWalletActivation} /> )} - - - + {showAddressFooter && ( + + )} + + - {confirmText && ( + {confirmText && !isRequestStale && ( + +
+

+
+ +
+`; + +exports[`DappRequestContent - Stale Request Rendering should match snapshot for stale request 1`] = ` +
+ +
+
+
+
+ + E + +
+
+ + Transaction request + +
+
+
+
+ + example.com + +
+
+
+
+
+
+
+
+
+
+ + + +
+ + This request has expired due to inactivity. Please try submitting again + +
+
+ +
+
+
+ +
+`; diff --git a/apps/extension/src/app/features/dappRequests/accounts.ts b/apps/extension/src/app/features/dappRequests/accounts.ts index 4f92b56cd29..245571460f7 100644 --- a/apps/extension/src/app/features/dappRequests/accounts.ts +++ b/apps/extension/src/app/features/dappRequests/accounts.ts @@ -1,20 +1,20 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { JsonRpcProvider } from '@ethersproject/providers' +/* oxlint-disable typescript/explicit-function-return-type */ +import { type JsonRpcProvider } from '@ethersproject/providers' import { providerErrors, serializeError } from '@metamask/rpc-errors' import { saveDappConnection } from 'src/app/features/dapp/actions' -import { DappInfo, dappStore } from 'src/app/features/dapp/store' +import { type DappInfo, dappStore } from 'src/app/features/dapp/store' import { getOrderedConnectedAddresses } from 'src/app/features/dapp/utils' import type { SenderTabInfo } from 'src/app/features/dappRequests/shared' import { - AccountResponse, - DappRequest, - ErrorResponse, - GetAccountRequest, - RequestAccountRequest, + type AccountResponse, + type DappRequest, + type ErrorResponse, + type GetAccountRequest, + type RequestAccountRequest, } from 'src/app/features/dappRequests/types/DappRequestTypes' import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels' import { call, put, select } from 'typed-redux-saga' -import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { type UniverseChainId } from 'uniswap/src/features/chains/types' import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils' import { DappResponseType } from 'uniswap/src/features/dappRequests/types' import { pushNotification } from 'uniswap/src/features/notifications/slice/slice' @@ -161,7 +161,8 @@ export function* getAccountRequest(request: RequestAccountRequest, senderTabInfo yield* call(dappResponseMessageChannel.sendMessageToTab, senderTabInfo.id, accountResponse) - sendAnalyticsEvent(ExtensionEventName.DappConnectRequest, { + // Track that a connection was established + sendAnalyticsEvent(ExtensionEventName.DappConnect, { dappUrl, chainId, activeConnectedAddress: activeAccount.address, diff --git a/apps/extension/src/app/features/dappRequests/configuredSagas.ts b/apps/extension/src/app/features/dappRequests/configuredSagas.ts index e589028b023..397a92d15e1 100644 --- a/apps/extension/src/app/features/dappRequests/configuredSagas.ts +++ b/apps/extension/src/app/features/dappRequests/configuredSagas.ts @@ -1,6 +1,6 @@ import { createPrepareAndSignDappTransactionSaga } from 'src/app/features/dappRequests/sagas/prepareAndSignDappTransactionSaga' +import { createMonitoredSaga } from 'uniswap/src/utils/saga' import { getSharedTransactionSagaDependencies } from 'wallet/src/features/transactions/configuredSagas' -import { createMonitoredSaga } from 'wallet/src/utils/saga' // Create configured saga instance using shared transaction dependencies const configuredPrepareAndSignDappTransactionSaga = createPrepareAndSignDappTransactionSaga( diff --git a/apps/extension/src/app/features/dappRequests/context/TransactionConfirmationTracker.tsx b/apps/extension/src/app/features/dappRequests/context/TransactionConfirmationTracker.tsx index 59de6da8ec3..e0df9efaf77 100644 --- a/apps/extension/src/app/features/dappRequests/context/TransactionConfirmationTracker.tsx +++ b/apps/extension/src/app/features/dappRequests/context/TransactionConfirmationTracker.tsx @@ -31,6 +31,7 @@ export function useTransactionConfirmationTracker(): TransactionConfirmationStat return context } +// oxlint-disable-next-line typescript/no-empty-interface -- biome-parity: oxlint is stricter here interface TransactionConfirmationTrackerProviderProps extends PropsWithChildren {} export function TransactionConfirmationTrackerProvider({ diff --git a/apps/extension/src/app/features/dappRequests/dappRequestApprovalWatcherSaga.ts b/apps/extension/src/app/features/dappRequests/dappRequestApprovalWatcherSaga.ts index e98260ee664..8e8332fe498 100644 --- a/apps/extension/src/app/features/dappRequests/dappRequestApprovalWatcherSaga.ts +++ b/apps/extension/src/app/features/dappRequests/dappRequestApprovalWatcherSaga.ts @@ -1,4 +1,4 @@ -/* eslint-disable complexity */ +/* oxlint-disable complexity */ import { providerErrors, serializeError } from '@metamask/rpc-errors' import { PayloadAction } from '@reduxjs/toolkit' import { getAccount, getAccountRequest } from 'src/app/features/dappRequests/accounts' diff --git a/apps/extension/src/app/features/dappRequests/getChainId.ts b/apps/extension/src/app/features/dappRequests/getChainId.ts index 6f84f7d3f7b..94678872cb9 100644 --- a/apps/extension/src/app/features/dappRequests/getChainId.ts +++ b/apps/extension/src/app/features/dappRequests/getChainId.ts @@ -7,7 +7,7 @@ import { UniverseChainId } from 'uniswap/src/features/chains/types' import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils' import { DappResponseType } from 'uniswap/src/features/dappRequests/types' -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +// oxlint-disable-next-line typescript/explicit-function-return-type export function* getChainId({ request, senderTabInfo: { id }, @@ -26,7 +26,7 @@ export function* getChainId({ yield* call(dappResponseMessageChannel.sendMessageToTab, id, response) } -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +// oxlint-disable-next-line typescript/explicit-function-return-type export function* getChainIdNoDappInfo({ request, senderTabInfo: { id }, diff --git a/apps/extension/src/app/features/dappRequests/hooks.test.tsx b/apps/extension/src/app/features/dappRequests/hooks.test.tsx index b5aaf159c6e..eb1b2ceb43d 100644 --- a/apps/extension/src/app/features/dappRequests/hooks.test.tsx +++ b/apps/extension/src/app/features/dappRequests/hooks.test.tsx @@ -10,7 +10,7 @@ describe('useIsDappRequestConfirming', () => { ['returns false when request is not confirming', MOCK_ID, DappRequestStatus.Pending, false], ['returns true when request is confirming', MOCK_ID, DappRequestStatus.Confirming, true], ['returns false when request does not exist', 'non-existent-id', DappRequestStatus.Confirming, false], - // eslint-disable-next-line max-params + // oxlint-disable-next-line max-params ])('%s', async (_, requestId, status, expected) => { const preloadedState = { dappRequests: { diff --git a/apps/extension/src/app/features/dappRequests/hooks/useConditionalPreSignDelay.ts b/apps/extension/src/app/features/dappRequests/hooks/useConditionalPreSignDelay.ts index 59883fec0cc..95a15d3d20e 100644 --- a/apps/extension/src/app/features/dappRequests/hooks/useConditionalPreSignDelay.ts +++ b/apps/extension/src/app/features/dappRequests/hooks/useConditionalPreSignDelay.ts @@ -66,6 +66,7 @@ export function useConditionalPreSignDelay(options: { // Set up execution with conditional delay timeoutRef.current = setTimeout(() => { + // oxlint-disable-next-line typescript/no-floating-promises -- biome-parity: oxlint is stricter here executeCallback() }, delayMs) diff --git a/apps/extension/src/app/features/dappRequests/hooks/useIsRequestStale.test.ts b/apps/extension/src/app/features/dappRequests/hooks/useIsRequestStale.test.ts new file mode 100644 index 00000000000..655cb70b2a1 --- /dev/null +++ b/apps/extension/src/app/features/dappRequests/hooks/useIsRequestStale.test.ts @@ -0,0 +1,109 @@ +import { act, renderHook } from '@testing-library/react' +import { + isRequestStale, + REQUEST_EXPIRY_TIME_MS, + useIsRequestStale, +} from 'src/app/features/dappRequests/hooks/useIsRequestStale' + +describe('isRequestStale', () => { + it('returns false for requests created less than 30 minutes ago', () => { + const createdAt = Date.now() - (REQUEST_EXPIRY_TIME_MS - 60000) // 1 minute before expiry + expect(isRequestStale(createdAt)).toBe(false) + }) + + it('returns true for requests created exactly 30 minutes ago', () => { + const createdAt = Date.now() - REQUEST_EXPIRY_TIME_MS // Exactly at expiry time + expect(isRequestStale(createdAt)).toBe(true) + }) + + it('returns true for requests created more than 30 minutes ago', () => { + const createdAt = Date.now() - (REQUEST_EXPIRY_TIME_MS + 60000) // 1 minute past expiry + expect(isRequestStale(createdAt)).toBe(true) + }) + + it('handles edge case where createdAt is in the future', () => { + const createdAt = Date.now() + 60 * 1000 // 1 minute in the future + expect(isRequestStale(createdAt)).toBe(false) + }) +}) + +describe('useIsRequestStale', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.runOnlyPendingTimers() + jest.useRealTimers() + }) + + it('returns false initially for fresh request', () => { + const createdAt = Date.now() - 1000 // 1 second ago + const { result } = renderHook(() => useIsRequestStale(createdAt)) + expect(result.current).toBe(false) + }) + + it('returns true initially for stale request', () => { + const createdAt = Date.now() - (REQUEST_EXPIRY_TIME_MS + 60000) // 1 minute past expiry + const { result } = renderHook(() => useIsRequestStale(createdAt)) + expect(result.current).toBe(true) + }) + + it('updates from false to true when request becomes stale', () => { + const createdAt = Date.now() - (REQUEST_EXPIRY_TIME_MS - 60000) // 1 minute before expiry + const { result } = renderHook(() => useIsRequestStale(createdAt)) + + expect(result.current).toBe(false) + + // Fast-forward past expiry time + act(() => { + jest.advanceTimersByTime(120000) // 2 minutes + }) + + expect(result.current).toBe(true) + }) + + it('recalculates when createdAt changes', () => { + const initialCreatedAt = Date.now() - 1000 // 1 second ago + const { result, rerender } = renderHook(({ timestamp }) => useIsRequestStale(timestamp), { + initialProps: { timestamp: initialCreatedAt }, + }) + + expect(result.current).toBe(false) + + // Change createdAt to a stale timestamp + const staleCreatedAt = Date.now() - (REQUEST_EXPIRY_TIME_MS + 60000) // 1 minute past expiry + act(() => { + rerender({ timestamp: staleCreatedAt }) + }) + + // Need to advance timer to trigger the interval check + act(() => { + jest.advanceTimersByTime(1000) + }) + + expect(result.current).toBe(true) + }) + + it('checks staleness every second', () => { + const createdAt = Date.now() - (REQUEST_EXPIRY_TIME_MS - 30000) // 30 seconds before expiry + const { result } = renderHook(() => useIsRequestStale(createdAt)) + + expect(result.current).toBe(false) + + // Fast-forward by 30 seconds to reach expiry time + act(() => { + jest.advanceTimersByTime(30000) + }) + + // Should now be stale + expect(result.current).toBe(true) + + // Verify it stays stale after more time passes + act(() => { + jest.advanceTimersByTime(10000) + }) + + expect(result.current).toBe(true) + }) +}) diff --git a/apps/extension/src/app/features/dappRequests/hooks/useIsRequestStale.ts b/apps/extension/src/app/features/dappRequests/hooks/useIsRequestStale.ts new file mode 100644 index 00000000000..88e6839047a --- /dev/null +++ b/apps/extension/src/app/features/dappRequests/hooks/useIsRequestStale.ts @@ -0,0 +1,33 @@ +import ms from 'ms' +import { useEffect, useState } from 'react' +import { useInterval } from 'utilities/src/time/timing' + +export const REQUEST_EXPIRY_TIME_MS = ms('30m') + +export function isRequestStale(createdAt: number): boolean { + return Date.now() - createdAt >= REQUEST_EXPIRY_TIME_MS +} + +/** + * Hook that monitors whether a request has become stale (older than REQUEST_EXPIRY_TIME_MS). + * + * @param createdAt - Timestamp when the request was created (in milliseconds) + * @returns boolean indicating whether the request is stale + */ +export function useIsRequestStale(createdAt: number): boolean { + const [isStale, setIsStale] = useState(() => isRequestStale(createdAt)) + + useEffect(() => { + setIsStale(isRequestStale(createdAt)) + }, [createdAt]) + + useInterval( + () => { + setIsStale(isRequestStale(createdAt)) + }, + 1000, + true, + ) + + return isStale +} diff --git a/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignDappTransaction.ts b/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignDappTransaction.ts index d361f37eff3..3d031de172e 100644 --- a/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignDappTransaction.ts +++ b/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignDappTransaction.ts @@ -34,7 +34,7 @@ export function usePrepareAndSignDappTransaction({ const currentPreparationRef = useRef<{ cancel: () => void } | null>(null) // Cancel ongoing preparations when dependencies change - // biome-ignore lint/correctness/useExhaustiveDependencies: chainId and request changes should reset preparation state + // oxlint-disable-next-line react/exhaustive-deps -- chainId and request changes should reset preparation state useEffect(() => { currentPreparationRef.current?.cancel() currentPreparationRef.current = null diff --git a/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignEthSendTransaction.ts b/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignEthSendTransaction.ts index 437cfb0880c..78f899f5642 100644 --- a/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignEthSendTransaction.ts +++ b/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignEthSendTransaction.ts @@ -1,9 +1,9 @@ +import { GasFeeResult } from '@universe/api' import { useMemo } from 'react' import { usePrepareAndSignDappTransaction } from 'src/app/features/dappRequests/hooks/usePrepareAndSignDappTransaction' import { useTransactionGasEstimation } from 'src/app/features/dappRequests/hooks/useTransactionGasEstimation' import { DappRequestStoreItemForEthSendTxn } from 'src/app/features/dappRequests/slice' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { GasFeeResult } from 'uniswap/src/features/gas/types' import { formatExternalTxnWithGasEstimates } from 'wallet/src/features/gas/formatExternalTxnWithGasEstimates' import { SignedTransactionRequest } from 'wallet/src/features/transactions/executeTransaction/types' import { Account } from 'wallet/src/features/wallet/accounts/types' diff --git a/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignSendCallsTransaction.ts b/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignSendCallsTransaction.ts index c7cbb49bece..3c38ed93624 100644 --- a/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignSendCallsTransaction.ts +++ b/apps/extension/src/app/features/dappRequests/hooks/usePrepareAndSignSendCallsTransaction.ts @@ -1,3 +1,4 @@ +import { GasFeeResult } from '@universe/api' import { useMemo } from 'react' import { usePrepareAndSignDappTransaction } from 'src/app/features/dappRequests/hooks/usePrepareAndSignDappTransaction' import { useTransactionGasEstimation } from 'src/app/features/dappRequests/hooks/useTransactionGasEstimation' @@ -5,7 +6,6 @@ import { DappRequestStoreItemForSendCallsTxn } from 'src/app/features/dappReques import { UNISWAP_DELEGATION_ADDRESS } from 'uniswap/src/constants/addresses' import { useWalletEncode7702Query } from 'uniswap/src/data/apiClients/tradingApi/useWalletEncode7702Query' import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { GasFeeResult } from 'uniswap/src/features/gas/types' import { EthTransaction } from 'uniswap/src/types/walletConnect' import { transformCallsToTransactionRequests } from 'wallet/src/features/batchedTransactions/utils' import { useLiveAccountDelegationDetails } from 'wallet/src/features/smartWallet/hooks/useLiveAccountDelegationDetails' diff --git a/apps/extension/src/app/features/dappRequests/hooks/useTransactionGasEstimation.test.ts b/apps/extension/src/app/features/dappRequests/hooks/useTransactionGasEstimation.test.ts index 808019e1e4c..0e687291d56 100644 --- a/apps/extension/src/app/features/dappRequests/hooks/useTransactionGasEstimation.test.ts +++ b/apps/extension/src/app/features/dappRequests/hooks/useTransactionGasEstimation.test.ts @@ -1,10 +1,10 @@ import { TransactionRequest } from '@ethersproject/providers' import { renderHook } from '@testing-library/react' +import { GasFeeResult } from '@universe/api' import { useTransactionGasEstimation } from 'src/app/features/dappRequests/hooks/useTransactionGasEstimation' import { PollingInterval } from 'uniswap/src/constants/misc' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useTransactionGasFee } from 'uniswap/src/features/gas/hooks' -import { GasFeeResult } from 'uniswap/src/features/gas/types' import { logger } from 'utilities/src/logger/logger' // Mock dependencies diff --git a/apps/extension/src/app/features/dappRequests/hooks/useTransactionGasEstimation.ts b/apps/extension/src/app/features/dappRequests/hooks/useTransactionGasEstimation.ts index 7a08299a627..7e9ddae731b 100644 --- a/apps/extension/src/app/features/dappRequests/hooks/useTransactionGasEstimation.ts +++ b/apps/extension/src/app/features/dappRequests/hooks/useTransactionGasEstimation.ts @@ -1,9 +1,9 @@ import { TransactionRequest } from '@ethersproject/providers' +import { GasFeeResult } from '@universe/api' import { useEffect, useMemo } from 'react' import { PollingInterval } from 'uniswap/src/constants/misc' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { useTransactionGasFee } from 'uniswap/src/features/gas/hooks' -import { GasFeeResult } from 'uniswap/src/features/gas/types' import { logger } from 'utilities/src/logger/logger' interface UseTransactionGasEstimationParams { diff --git a/apps/extension/src/app/features/dappRequests/permissions.ts b/apps/extension/src/app/features/dappRequests/permissions.ts index 37f5d005c23..2ce93f16d5b 100644 --- a/apps/extension/src/app/features/dappRequests/permissions.ts +++ b/apps/extension/src/app/features/dappRequests/permissions.ts @@ -1,25 +1,27 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* oxlint-disable typescript/explicit-function-return-type */ import { rpcErrors, serializeError } from '@metamask/rpc-errors' import { removeDappConnection } from 'src/app/features/dapp/actions' -import { DappInfo } from 'src/app/features/dapp/store' +import { type DappInfo } from 'src/app/features/dapp/store' import { saveAccount } from 'src/app/features/dappRequests/accounts' import type { SenderTabInfo } from 'src/app/features/dappRequests/shared' import { - ErrorResponse, - GetPermissionsRequest, - GetPermissionsResponse, - RequestPermissionsRequest, - RequestPermissionsResponse, - RevokePermissionsRequest, - RevokePermissionsResponse, + type ErrorResponse, + type GetPermissionsRequest, + type GetPermissionsResponse, + type RequestPermissionsRequest, + type RequestPermissionsResponse, + type RevokePermissionsRequest, + type RevokePermissionsResponse, } from 'src/app/features/dappRequests/types/DappRequestTypes' import { dappResponseMessageChannel } from 'src/background/messagePassing/messageChannels' -import { Permission } from 'src/contentScript/WindowEthereumRequestTypes' +import { type Permission } from 'src/contentScript/WindowEthereumRequestTypes' import { call, put } from 'typed-redux-saga' import { chainIdToHexadecimalString } from 'uniswap/src/features/chains/utils' import { DappResponseType, EthMethod } from 'uniswap/src/features/dappRequests/types' import { pushNotification } from 'uniswap/src/features/notifications/slice/slice' import { AppNotificationType } from 'uniswap/src/features/notifications/slice/types' +import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { extractBaseUrl } from 'utilities/src/format/urls' import { logger } from 'utilities/src/logger/logger' @@ -91,6 +93,14 @@ export function* handleRequestPermissions(request: RequestPermissionsRequest, se accounts, } yield* call(dappResponseMessageChannel.sendMessageToTab, senderTabInfo.id, response) + + // Track that a connection was established + sendAnalyticsEvent(ExtensionEventName.DappConnect, { + dappUrl: accountInfo?.dappUrl ?? extractBaseUrl(senderTabInfo.url), + chainId: accountInfo?.chainId, + activeConnectedAddress: accountInfo?.activeAccount.address, + connectedAddresses: accountInfo?.connectedAddresses ?? [], + }) } else { logger.info('saga.ts', 'handleRequestPermissions', 'Unknown permissions requested', requestedPermissions) yield* call(dappResponseMessageChannel.sendMessageToTab, senderTabInfo.id, { diff --git a/apps/extension/src/app/features/dappRequests/requestContent/Connection/ConnectionRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/Connection/ConnectionRequestContent.tsx index be130ca36d9..424e65d7324 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/Connection/ConnectionRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/Connection/ConnectionRequestContent.tsx @@ -1,27 +1,35 @@ import { useTranslation } from 'react-i18next' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' -import { Flex, Text } from 'ui/src' +import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' +import { AccountType } from 'uniswap/src/features/accounts/types' +import { DappConnectionContent } from 'wallet/src/components/dappRequests/DappConnectionContent' +import { useBlockaidVerification } from 'wallet/src/features/dappRequests/hooks/useBlockaidVerification' +import { useDappConnectionConfirmation } from 'wallet/src/features/dappRequests/hooks/useDappConnectionConfirmation' export function ConnectionRequestContent(): JSX.Element { const { t } = useTranslation() + const { currentAccount, dappUrl } = useDappRequestQueueContext() + const { verificationStatus } = useBlockaidVerification(dappUrl) + + const isViewOnly = currentAccount.type === AccountType.Readonly + const { confirmedWarning, setConfirmedWarning, disableConfirm } = useDappConnectionConfirmation({ + verificationStatus, + isViewOnly, + }) return ( - - - {t('dapp.request.connect.helptext')} - - + ) } diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Approve/ApproveRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Approve/ApproveRequestContent.tsx index 24c251dcb02..b4fd0dfccaf 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Approve/ApproveRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Approve/ApproveRequestContent.tsx @@ -1,4 +1,5 @@ import { BigNumber } from '@ethersproject/bignumber' +import { GasFeeResult } from '@universe/api' import { useTranslation } from 'react-i18next' import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' @@ -14,7 +15,6 @@ import { LearnMoreLink } from 'uniswap/src/components/text/LearnMoreLink' import { uniswapUrls } from 'uniswap/src/constants/urls' import { DappRequestType } from 'uniswap/src/features/dappRequests/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { GasFeeResult } from 'uniswap/src/features/gas/types' import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' import { TransactionType, TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' import { buildCurrencyId } from 'uniswap/src/utils/currencyId' diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/EthSend.tsx b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/EthSend.tsx index 7131e97a7f6..1499b776528 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/EthSend.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/EthSend.tsx @@ -1,3 +1,4 @@ +import type { GasFeeResult } from '@universe/api' import { useCallback } from 'react' import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' @@ -5,6 +6,7 @@ import { usePrepareAndSignEthSendTransaction } from 'src/app/features/dappReques import { ApproveRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/Approve/ApproveRequestContent' import { FallbackEthSendRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/FallbackEthSend/FallbackEthSend' import { LPRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/LP/LPRequestContent' +import { ParsedTransactionRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/ParsedTransaction/ParsedTransactionRequestContent' import { Permit2ApproveRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/Permit2Approve/Permit2ApproveRequestContent' import { SwapRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent' import { WrapRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/Wrap/WrapRequestContent' @@ -15,6 +17,7 @@ import { isPermit2ApproveRequest, isSwapRequest, isWrapRequest, + SendTransactionRequest, } from 'src/app/features/dappRequests/types/DappRequestTypes' import { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' import { logger } from 'utilities/src/logger/logger' @@ -54,91 +57,109 @@ export function EthSendRequestContent({ request }: EthSendRequestContentProps): await onCancel(requestWithGasValues) }, [onCancel, requestWithGasValues]) - let content + // Use Blockaid transaction scanning for ALL transaction types + // If the API fails, the ErrorBoundary will catch it and fallback to specialized UIs + return ( + + } + onError={(error) => { + if (error) { + logger.error(error, { + tags: { file: 'EthSend', function: 'ErrorBoundary-Blockaid' }, + extra: { + dappRequest, + useSimulationResultUI: true, + }, + }) + } + }} + > + + + ) +} + +/** + * Fallback component that renders the appropriate specialized UI based on transaction type + * Used when simulation result UI is disabled or fails + */ +function SpecializedTransactionFallback({ + dappRequest, + transactionGasFeeResult, + onCancel, + onConfirm, +}: { + dappRequest: SendTransactionRequest + transactionGasFeeResult: GasFeeResult + onCancel: () => Promise + onConfirm: (transactionTypeInfo?: TransactionTypeInfo) => Promise +}): JSX.Element { switch (true) { case isSwapRequest(dappRequest): - content = ( + return ( ) - break case isPermit2ApproveRequest(dappRequest): - content = ( + return ( ) - break case isWrapRequest(dappRequest): - content = ( + return ( ) - break case isLPRequest(dappRequest): - content = ( + return ( ) - break case isApproveRequest(dappRequest): - content = ( + return ( ) - break default: - content = ( + return ( ) } - - return ( - - } - onError={(error) => { - if (error) { - logger.error(error, { - tags: { file: 'EthSend', function: 'ErrorBoundary' }, - extra: { - dappRequest, - }, - }) - } - }} - > - {content} - - ) } diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/FallbackEthSend/FallbackEthSend.tsx b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/FallbackEthSend/FallbackEthSend.tsx index e959e61d694..ef55b047a5d 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/FallbackEthSend/FallbackEthSend.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/FallbackEthSend/FallbackEthSend.tsx @@ -1,3 +1,4 @@ +import { GasFeeResult } from '@universe/api' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useDappLastChainId } from 'src/app/features/dapp/hooks' @@ -8,7 +9,6 @@ import { SendTransactionRequest } from 'src/app/features/dappRequests/types/Dapp import { Anchor, Flex, Text, TouchableArea } from 'ui/src' import { AnimatedCopySheets, ExternalLink } from 'ui/src/components/icons' import { ContentRow } from 'uniswap/src/components/transactions/requests/ContentRow' -import { GasFeeResult } from 'uniswap/src/features/gas/types' import { CopyNotificationType } from 'uniswap/src/features/notifications/slice/types' import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' import { ellipseMiddle, shortenAddress } from 'utilities/src/addresses' diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/LP/LPRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/LP/LPRequestContent.tsx index 483c487f086..af545233f5c 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/LP/LPRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/LP/LPRequestContent.tsx @@ -1,8 +1,8 @@ +import { GasFeeResult } from '@universe/api' import { useTranslation } from 'react-i18next' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' import { LPSendTransactionRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' import { Flex, Text } from 'ui/src' -import { GasFeeResult } from 'uniswap/src/features/gas/types' interface LPRequestContentProps { transactionGasFeeResult: GasFeeResult diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/ParsedTransaction/ParsedTransactionRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/ParsedTransaction/ParsedTransactionRequestContent.tsx new file mode 100644 index 00000000000..ca1181604e0 --- /dev/null +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/ParsedTransaction/ParsedTransactionRequestContent.tsx @@ -0,0 +1,74 @@ +import type { GasFeeResult } from '@universe/api' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useDappLastChainId } from 'src/app/features/dapp/hooks' +import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' +import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' +import { SendTransactionRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' +import { useBooleanState } from 'utilities/src/react/useBooleanState' +import { DappTransactionScanningContent } from 'wallet/src/components/dappRequests/DappTransactionScanningContent' +import { TransactionRiskLevel } from 'wallet/src/features/dappRequests/types' +import { shouldDisableConfirm } from 'wallet/src/features/dappRequests/utils/riskUtils' + +interface ParsedTransactionRequestContentProps { + transactionGasFeeResult: GasFeeResult + dappRequest: SendTransactionRequest + onCancel: () => Promise + onConfirm: () => Promise +} + +/** + * Transaction request content with Blockaid security scanning + * Parses transaction data and displays it with asset transfers, security warnings, and detailed information + */ +export function ParsedTransactionRequestContent({ + dappRequest, + transactionGasFeeResult, + onCancel, + onConfirm, +}: ParsedTransactionRequestContentProps): JSX.Element | null { + const { t } = useTranslation() + const { dappUrl, currentAccount } = useDappRequestQueueContext() + const activeChain = useDappLastChainId(dappUrl) + const { value: confirmedRisk, setValue: setConfirmedRisk } = useBooleanState(false) + // Initialize with null to indicate scan hasn't completed yet + const [riskLevel, setRiskLevel] = useState(null) + + const { chainId: transactionChainId } = dappRequest.transaction + const chainId = transactionChainId ?? activeChain + + // If no valid chainId, throw so that we fall back to the legacy UI + if (!chainId) { + throw new Error('No valid chainId available for transaction scanning') + } + + const disableConfirm = shouldDisableConfirm({ + riskLevel, + confirmedRisk, + hasGasFee: !!transactionGasFeeResult.value, + }) + + return ( + + + + ) +} diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Permit2Approve/Permit2ApproveRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Permit2Approve/Permit2ApproveRequestContent.tsx index 8b13fcd9450..18be818039c 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Permit2Approve/Permit2ApproveRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Permit2Approve/Permit2ApproveRequestContent.tsx @@ -1,8 +1,8 @@ +import { GasFeeResult } from '@universe/api' import { useTranslation } from 'react-i18next' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' import { Permit2ApproveSendTransactionRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' import { Flex, Text } from 'ui/src' -import { GasFeeResult } from 'uniswap/src/features/gas/types' import { TransactionType, TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' interface Permit2ApproveRequestContentProps { diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/SwapDisplay.tsx b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/SwapDisplay.tsx index 18276972fbe..975e5de6abe 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/SwapDisplay.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/SwapDisplay.tsx @@ -1,3 +1,4 @@ +import { GasFeeResult } from '@universe/api' import { useTranslation } from 'react-i18next' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' import { Flex, Separator, Text } from 'ui/src' @@ -7,12 +8,12 @@ import { CurrencyLogo } from 'uniswap/src/components/CurrencyLogo/CurrencyLogo' import { SplitLogo } from 'uniswap/src/components/CurrencyLogo/SplitLogo' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { GasFeeResult } from 'uniswap/src/features/gas/types' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { getCurrencyAmount, ValueType } from 'uniswap/src/features/tokens/getCurrencyAmount' -import { useUSDCValue } from 'uniswap/src/features/transactions/hooks/useUSDCPrice' +import { useUSDCValue } from 'uniswap/src/features/transactions/hooks/useUSDCPriceWrapper' import { NumberType } from 'utilities/src/format/types' +// oxlint-disable-next-line complexity -- biome-parity: oxlint is stricter here export function SwapDisplay({ inputAmount, outputAmount, diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent.tsx index 4aec47e93aa..5758e25cb32 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent.tsx @@ -1,18 +1,18 @@ +import { GasFeeResult } from '@universe/api' import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' import { SwapDisplay } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/SwapDisplay' import { formatUnits, useSwapDetails } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/utils' -import { UniswapXSwapRequest } from 'src/app/features/dappRequests/types/Permit2Types' import { UniversalRouterCall } from 'src/app/features/dappRequests/types/UniversalRouterTypes' import { DEFAULT_NATIVE_ADDRESS, DEFAULT_NATIVE_ADDRESS_LEGACY } from 'uniswap/src/features/chains/evm/defaults' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { toSupportedChainId } from 'uniswap/src/features/chains/utils' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { GasFeeResult } from 'uniswap/src/features/gas/types' import { useCurrencyInfo, useNativeCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' import { TransactionType, TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' import { buildCurrencyId } from 'uniswap/src/utils/currencyId' import { assert } from 'utilities/src/errors' +import { UniswapXSwapRequest } from 'wallet/src/components/dappRequests/types/Permit2Types' function getTransactionTypeInfo({ inputCurrencyInfo, diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/utils.ts b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/utils.ts index 04fae5445ee..acb0fde985f 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/utils.ts +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Swap/utils.ts @@ -1,5 +1,5 @@ -/* eslint-disable max-depth */ -/* eslint-disable complexity */ +/* oxlint-disable max-depth */ +/* oxlint-disable complexity */ import { BigNumber, BigNumberish } from '@ethersproject/bignumber' import { formatUnits as formatUnitsEthers } from 'ethers/lib/utils' import { useDappLastChainId } from 'src/app/features/dapp/hooks' @@ -221,7 +221,7 @@ function getTokenDetailsFromV4SwapCommands(command: UniversalRouterCommand): { } for (const p of parsed.data.value) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition if (p.name === 'swap') { const swap = p.value @@ -246,7 +246,7 @@ function getTokenDetailsFromV4SwapCommands(command: UniversalRouterCommand): { } for (const p of parsed.data.value) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition if (p.name === 'swap') { const swap = p.value @@ -271,7 +271,7 @@ function getTokenDetailsFromV4SwapCommands(command: UniversalRouterCommand): { } for (const p of parsed.data.value) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition if (p.name === 'swap') { const swap = p.value @@ -298,7 +298,7 @@ function getTokenDetailsFromV4SwapCommands(command: UniversalRouterCommand): { } for (const p of parsed.data.value) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition if (p.name === 'swap') { const swap = p.value @@ -337,6 +337,7 @@ function getFallbackOutputValue(allCommands?: UniversalRouterCommand[]): string const sweepAmountOutParam = sweepCommand?.params.find(isAmountMinParam) const unwrapWethAmountOutParam = unwrapWethCommand?.params.find(isAmountMinParam) + // oxlint-disable-next-line typescript/no-unsafe-return -- biome-parity: oxlint is stricter here return sweepAmountOutParam?.value || unwrapWethAmountOutParam?.value || '0' } @@ -345,6 +346,7 @@ function getFallbackInputValue(command: UniversalRouterCommand): string { const potentialSettleParam = command.params.find(isSettleParam) const settleParam = potentialSettleParam && isSettleParam(potentialSettleParam) ? potentialSettleParam : undefined const settleAmountValue = settleParam?.value.find((item) => item.name === 'amount') + // oxlint-disable-next-line typescript/no-unsafe-return -- biome-parity: oxlint is stricter here return settleAmountValue?.value || '0' } diff --git a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Wrap/WrapRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Wrap/WrapRequestContent.tsx index 872825576a8..64730c7b1ab 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Wrap/WrapRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/EthSend/Wrap/WrapRequestContent.tsx @@ -1,3 +1,4 @@ +import { GasFeeResult } from '@universe/api' import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' import { SwapDisplay } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/SwapDisplay' @@ -5,7 +6,6 @@ import { formatUnits } from 'src/app/features/dappRequests/requestContent/EthSen import { WrapSendTransactionRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { CurrencyInfo } from 'uniswap/src/features/dataApi/types' -import { GasFeeResult } from 'uniswap/src/features/gas/types' import { useNativeCurrencyInfo, useWrappedNativeCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' import { TransactionType, TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' diff --git a/apps/extension/src/app/features/dappRequests/requestContent/PersonalSign/PersonalSignRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/PersonalSign/PersonalSignRequestContent.tsx index 82183fc49fe..ff09badc7b7 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/PersonalSign/PersonalSignRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/PersonalSign/PersonalSignRequestContent.tsx @@ -1,128 +1,79 @@ import { toUtf8String } from '@ethersproject/strings' import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' +import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' import { SignMessageRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' -import { Flex, IconButton, Text, Tooltip } from 'ui/src' -import { AlertTriangleFilled, Code, StickyNoteTextSquare } from 'ui/src/components/icons' +import { EthMethod } from 'uniswap/src/features/dappRequests/types' +import { logger } from 'utilities/src/logger/logger' import { containsNonPrintableChars } from 'utilities/src/primitives/string' +import { useBooleanState } from 'utilities/src/react/useBooleanState' +import { DappPersonalSignContent } from 'wallet/src/components/dappRequests/DappPersonalSignContent' +import { TransactionRiskLevel } from 'wallet/src/features/dappRequests/types' +import { shouldDisableConfirm } from 'wallet/src/features/dappRequests/utils/riskUtils' -enum ViewEncoding { - UTF8 = 0, - HEX = 1, -} interface PersonalSignRequestProps { dappRequest: SignMessageRequest } export function PersonalSignRequestContent({ dappRequest }: PersonalSignRequestProps): JSX.Element | null { const { t } = useTranslation() + const { dappUrl, currentAccount } = useDappRequestQueueContext() + const activeChain = useDappLastChainId(dappUrl) + const { value: confirmedRisk, setValue: setConfirmedRisk } = useBooleanState(false) + // Initialize with null to indicate scan hasn't completed yet + const [riskLevel, setRiskLevel] = useState(null) - const [viewEncoding, setViewEncoding] = useState(ViewEncoding.UTF8) + // Decode message to UTF-8 + const hexMessage = dappRequest.messageHex const [utf8Message, setUtf8Message] = useState() - const toggleViewEncoding = (): void => - setViewEncoding(viewEncoding === ViewEncoding.UTF8 ? ViewEncoding.HEX : ViewEncoding.UTF8) - - const hexMessage = dappRequest.messageHex - const containsUnrenderableCharacters = !utf8Message || containsNonPrintableChars(utf8Message) useEffect(() => { try { const decodedMessage = toUtf8String(hexMessage) setUtf8Message(decodedMessage) } catch { - // If the message is not valid UTF-8, we'll show the hex message instead (e.g. Polymark claim deposit message ) - setViewEncoding(ViewEncoding.HEX) + // If the message is not valid UTF-8, we'll show the hex message instead setUtf8Message(undefined) } }, [hexMessage]) - const [isScrollable, setIsScrollable] = useState(false) - const messageRef = useRef(null) - // biome-ignore lint/correctness/useExhaustiveDependencies: viewEncoding and utf8Message affect rendered content which changes scroll height - useEffect(() => { - const checkScroll = (): void => { - if (!messageRef.current) { - return - } - setIsScrollable(messageRef.current.scrollHeight > messageRef.current.clientHeight) - } + const isDecoded = Boolean(utf8Message && !containsNonPrintableChars(utf8Message)) + const message = (isDecoded ? utf8Message : hexMessage) || hexMessage + const hasLoggedError = useRef(false) - checkScroll() - window.addEventListener('resize', checkScroll) + if (!activeChain) { + if (!hasLoggedError.current) { + logger.error(new Error('No active chain found'), { + tags: { file: 'PersonalSignRequestContent', function: 'PersonalSignRequestContent' }, + }) + hasLoggedError.current = true + } + return null + } - return () => window.removeEventListener('resize', checkScroll) - }, [viewEncoding, utf8Message]) + const disableConfirm = shouldDisableConfirm({ riskLevel, confirmedRisk }) return ( - - - {viewEncoding === ViewEncoding.UTF8 ? utf8Message : hexMessage} - - - - - - : } - size="xxsmall" - variant="default" - emphasis="secondary" - onPress={toggleViewEncoding} - /> - - - - - - {viewEncoding === ViewEncoding.UTF8 - ? t('dapp.request.signature.toggleDataView.raw') - : t('dapp.request.signature.toggleDataView.readable')} - - - - {containsUnrenderableCharacters && ( - - - - {t('dapp.request.signature.containsUnrenderableCharacters')} - - - )} + ) } diff --git a/apps/extension/src/app/features/dappRequests/requestContent/SendCalls/SendCallsRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/SendCalls/SendCallsRequestContent.tsx index 3245fde4e74..6fcd5ad86f2 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/SendCalls/SendCallsRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/SendCalls/SendCallsRequestContent.tsx @@ -1,20 +1,25 @@ -import { useCallback, useMemo } from 'react' +import { type GasFeeResult } from '@universe/api' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDappLastChainId } from 'src/app/features/dapp/hooks' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' import { usePrepareAndSignSendCallsTransaction } from 'src/app/features/dappRequests/hooks/usePrepareAndSignSendCallsTransaction' import { SwapRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent' -import { DappRequestStoreItemForSendCallsTxn } from 'src/app/features/dappRequests/slice' +import { type DappRequestStoreItemForSendCallsTxn } from 'src/app/features/dappRequests/slice' import { EthSendTransactionRPCActions, isBatchedSwapRequest, - ParsedCall, - SendCallsRequest, + type ParsedCall, + type SendCallsRequest, } from 'src/app/features/dappRequests/types/DappRequestTypes' -import { GasFeeResult } from 'uniswap/src/features/gas/types' -import { TransactionType, TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' +import { type UniverseChainId } from 'uniswap/src/features/chains/types' +import { TransactionType, type TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' +import { useBooleanState } from 'utilities/src/react/useBooleanState' import { BatchedRequestDetailsContent } from 'wallet/src/components/BatchedTransactions/BatchedTransactionDetails' +import { DappSendCallsScanningContent } from 'wallet/src/components/dappRequests/DappSendCallsScanningContent' +import { type TransactionRiskLevel } from 'wallet/src/features/dappRequests/types' +import { shouldDisableConfirm } from 'wallet/src/features/dappRequests/utils/riskUtils' interface SendCallsRequestContentProps { dappRequest: SendCallsRequest @@ -24,7 +29,60 @@ interface SendCallsRequestContentProps { onCancel: () => Promise } -function SendCallsRequestContent({ +/** + * Implementation with Blockaid scanning + */ +function SendCallsRequestContentWithScanning({ + dappRequest, + chainId, + transactionGasFeeResult, + showSmartWalletActivation, + onConfirm, + onCancel, +}: SendCallsRequestContentProps & { chainId: UniverseChainId }): JSX.Element { + const { t } = useTranslation() + const { dappUrl, currentAccount } = useDappRequestQueueContext() + const { value: confirmedRisk, setValue: setConfirmedRisk } = useBooleanState(false) + // Initialize with null to indicate scan hasn't completed yet + const [riskLevel, setRiskLevel] = useState(null) + + const disableConfirm = shouldDisableConfirm({ + riskLevel, + confirmedRisk, + hasGasFee: !!transactionGasFeeResult.value, + }) + + return ( + onConfirm()} + showAddressFooter={false} + > + + + ) +} + +/** + * Fallback for when chainId is not available (required for Blockaid scanning) + */ +function SendCallsRequestContentFallback({ dappRequest, transactionGasFeeResult, showSmartWalletActivation, @@ -92,21 +150,38 @@ export function SendCallsRequestHandler({ request }: { request: DappRequestStore await onCancel(request) }, [onCancel, request]) - return parsedSwapCalldata ? ( - - ) : ( - + ) + } + + if (parsedSwapCalldata) { + return ( + + ) + } + + return ( + ) } diff --git a/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/NonStandardTypedDataRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/NonStandardTypedDataRequestContent.tsx index 36ae109cf85..37eff930616 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/NonStandardTypedDataRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/NonStandardTypedDataRequestContent.tsx @@ -2,10 +2,7 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' import { SignTypedDataRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' -import { Flex, Separator, Text } from 'ui/src' -import { Clear, Signature } from 'ui/src/components/icons' -import { InlineWarningCard } from 'uniswap/src/components/InlineWarningCard/InlineWarningCard' -import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' +import { NonStandardTypedDataContent } from 'wallet/src/components/dappRequests/SignTypedData/NonStandardTypedDataContent' interface NonStandardTypedDataRequestContentProps { dappRequest: SignTypedDataRequest @@ -17,8 +14,6 @@ export function NonStandardTypedDataRequestContent({ const { t } = useTranslation() const [checked, setChecked] = useState(false) - const hasMessageToShow = !!dappRequest.typedData - return ( - - - - - - {t('dapp.request.signature.decodeError')} - - - {hasMessageToShow && } - {hasMessageToShow && ( - - - - - {t('common.message')} - - - - {dappRequest.typedData} - - - )} - - - + ) } diff --git a/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/Permit2/Permit2RequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/Permit2/Permit2RequestContent.tsx deleted file mode 100644 index 346fb612654..00000000000 --- a/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/Permit2/Permit2RequestContent.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { useState } from 'react' -import { useTranslation } from 'react-i18next' -import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' -import { DomainContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/DomainContent' -import { MaybeExplorerLinkedAddress } from 'src/app/features/dappRequests/requestContent/SignTypeData/MaybeExplorerLinkedAddress' -import { SignTypedDataRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' -import { Flex, Text, TouchableArea } from 'ui/src' -import { RotatableChevron } from 'ui/src/components/icons' -import { toSupportedChainId } from 'uniswap/src/features/chains/utils' -import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' - -interface Permit2RequestProps { - dappRequest: SignTypedDataRequest -} - -export function Permit2RequestContent({ dappRequest }: Permit2RequestProps): JSX.Element | null { - const { t } = useTranslation() - - const parsedTypedData = JSON.parse(dappRequest.typedData) - const { name, chainId: domainChainId, verifyingContract } = parsedTypedData?.domain || {} - const chainId = toSupportedChainId(domainChainId) - - const { token: address, amount, expiration, nonce } = parsedTypedData?.message?.details || {} - const { spender, sigDeadline } = parsedTypedData?.message || {} - const [open, setOpen] = useState(false) - const toggleOpen = (): void => setOpen(!open) - - const spenderLink = chainId ? getExplorerLink({ chainId, data: spender, type: ExplorerDataType.ADDRESS }) : undefined - const tokenLink = chainId ? getExplorerLink({ chainId, data: address, type: ExplorerDataType.TOKEN }) : undefined - - return ( - - - - - {t('dapp.request.permit2.description')} - - - - - - {open && ( - <> - - - - token - - - - - - amount - - - {amount} - - - - - expiration - - - {expiration} - - - - - nonce - - - {nonce} - - - - - spender - - - - - - signature deadline - - - {sigDeadline} - - - - )} - - - ) -} diff --git a/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/SignTypedDataRequestContent.tsx b/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/SignTypedDataRequestContent.tsx index 19606509983..02a3c39f2e8 100644 --- a/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/SignTypedDataRequestContent.tsx +++ b/apps/extension/src/app/features/dappRequests/requestContent/SignTypeData/SignTypedDataRequestContent.tsx @@ -1,23 +1,25 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { DappRequestContent } from 'src/app/features/dappRequests/DappRequestContent' +import { useDappRequestQueueContext } from 'src/app/features/dappRequests/DappRequestQueueContext' import { ActionCanNotBeCompletedContent } from 'src/app/features/dappRequests/requestContent/ActionCanNotBeCompleted/ActionCanNotBeCompletedContent' import { UniswapXSwapRequestContent } from 'src/app/features/dappRequests/requestContent/EthSend/Swap/SwapRequestContent' -import { DomainContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/DomainContent' -import { MaybeExplorerLinkedAddress } from 'src/app/features/dappRequests/requestContent/SignTypeData/MaybeExplorerLinkedAddress' import { NonStandardTypedDataRequestContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/NonStandardTypedDataRequestContent' -import { Permit2RequestContent } from 'src/app/features/dappRequests/requestContent/SignTypeData/Permit2/Permit2RequestContent' import { SignTypedDataRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' -import { EIP712Message, isEIP712TypedData } from 'src/app/features/dappRequests/types/EIP712Types' -import { isPermit2, isUniswapXSwapRequest } from 'src/app/features/dappRequests/types/Permit2Types' -import { Flex, Text } from 'ui/src' +import { Flex } from 'ui/src' import { toSupportedChainId } from 'uniswap/src/features/chains/utils' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useHasAccountMismatchCallback } from 'uniswap/src/features/smartWallet/mismatch/hooks' -import { ExplorerDataType, getExplorerLink } from 'uniswap/src/utils/linking' -import { isEVMAddressWithChecksum } from 'utilities/src/addresses/evm/evm' import { logger } from 'utilities/src/logger/logger' +import { useBooleanState } from 'utilities/src/react/useBooleanState' +import { DappSignTypedDataContent } from 'wallet/src/components/dappRequests/DappSignTypedDataContent' +import { Permit2Content } from 'wallet/src/components/dappRequests/SignTypedData/Permit2Content' +import { StandardTypedDataContent } from 'wallet/src/components/dappRequests/SignTypedData/StandardTypedDataContent' +import { isEIP712TypedData } from 'wallet/src/components/dappRequests/types/EIP712Types' +import { isPermit2, isUniswapXSwapRequest } from 'wallet/src/components/dappRequests/types/Permit2Types' import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' +import { TransactionRiskLevel } from 'wallet/src/features/dappRequests/types' +import { shouldDisableConfirm } from 'wallet/src/features/dappRequests/utils/riskUtils' interface SignTypedDataRequestProps { dappRequest: SignTypedDataRequest @@ -45,6 +47,63 @@ export function SignTypedDataRequestContent({ dappRequest }: SignTypedDataReques } function SignTypedDataRequestContentInner({ dappRequest }: SignTypedDataRequestProps): JSX.Element | null { + const { t } = useTranslation() + const { dappUrl, currentAccount } = useDappRequestQueueContext() + const { value: confirmedRisk, setValue: setConfirmedRisk } = useBooleanState(false) + const enablePermitMismatchUx = useFeatureFlag(FeatureFlags.EnablePermitMismatchUX) + const getHasMismatch = useHasAccountMismatchCallback() + + // Initialize with null to indicate scan hasn't completed yet + const [riskLevel, setRiskLevel] = useState(null) + + const parsedTypedData = JSON.parse(dappRequest.typedData) + const { chainId: domainChainId } = parsedTypedData.domain || {} + const chainId = toSupportedChainId(domainChainId) + + const hasMismatch = chainId ? getHasMismatch(chainId) : false + if (enablePermitMismatchUx && hasMismatch) { + return + } + + if (!chainId) { + // chainId is required for Blockaid scanning, fall back to basic typed data UI + return + } + + // Extension SignTypedData requests default to v4 method (modern standard) + const method = 'eth_signTypedData_v4' + + // For eth_signTypedData_v4, params are [account, typedData] + const params = [currentAccount.address, dappRequest.typedData] + + const disableConfirm = shouldDisableConfirm({ riskLevel, confirmedRisk }) + + return ( + + + + ) +} + +/** + * Fallback for when chainId is not available (required for Blockaid scanning) + */ +function SignTypedDataRequestContentFallback({ dappRequest }: SignTypedDataRequestProps): JSX.Element | null { const { t } = useTranslation() const enablePermitMismatchUx = useFeatureFlag(FeatureFlags.EnablePermitMismatchUX) const getHasMismatch = useHasAccountMismatchCallback() @@ -55,7 +114,7 @@ function SignTypedDataRequestContentInner({ dappRequest }: SignTypedDataRequestP return } - const { name, version, chainId: domainChainId, verifyingContract, salt } = parsedTypedData.domain || {} + const { chainId: domainChainId } = parsedTypedData.domain || {} const chainId = toSupportedChainId(domainChainId) const hasMismatch = chainId ? getHasMismatch(chainId) : false @@ -67,59 +126,13 @@ function SignTypedDataRequestContentInner({ dappRequest }: SignTypedDataRequestP return } - if (isPermit2(parsedTypedData)) { - return - } - - // todo(EXT-883): remove this when we start rejecting unsupported chain signTypedData requests - const renderMessageContent = ( - message: EIP712Message | EIP712Message[keyof EIP712Message], - i = 1, - ): Maybe => { - if (message === null || message === undefined) { - return ( - - {String(message)} - - ) - } - if (typeof message === 'string' && isEVMAddressWithChecksum(message) && chainId) { - const href = getExplorerLink({ chainId, data: message, type: ExplorerDataType.ADDRESS }) - return - } - if (typeof message === 'string' || typeof message === 'number' || typeof message === 'boolean') { - return ( - - {message.toString()} - - ) - } else if (Array.isArray(message)) { - return ( - - {JSON.stringify(message)} - - ) - } else if (typeof message === 'object') { - return Object.entries(message).map(([key, value], index) => ( - - - {key} - - - {renderMessageContent(value, i + 1)} - - - )) - } - - return undefined - } + const isPermit2Request = isPermit2(parsedTypedData) return ( - - {renderMessageContent(parsedTypedData.message)} + {isPermit2Request ? ( + + ) : ( + + )} ) diff --git a/apps/extension/src/app/features/dappRequests/saga.ts b/apps/extension/src/app/features/dappRequests/saga.ts index 8ce3f50de43..81e9727c38b 100644 --- a/apps/extension/src/app/features/dappRequests/saga.ts +++ b/apps/extension/src/app/features/dappRequests/saga.ts @@ -1,9 +1,10 @@ -/* eslint-disable max-lines */ -import { Provider } from '@ethersproject/providers' +/* oxlint-disable max-lines */ +import { type Provider } from '@ethersproject/providers' import { providerErrors, rpcErrors, serializeError } from '@metamask/rpc-errors' +import { FeatureFlags, getFeatureFlag } from '@universe/gating' import { createSearchParams } from 'react-router' import { changeChain } from 'src/app/features/dapp/changeChain' -import { DappInfo, dappStore } from 'src/app/features/dapp/store' +import { type DappInfo, dappStore } from 'src/app/features/dapp/store' import { getActiveSignerConnectedAccount } from 'src/app/features/dapp/utils' import { addRequest, @@ -19,22 +20,22 @@ import type { } from 'src/app/features/dappRequests/shared' import { dappRequestActions, selectIsRequestConfirming } from 'src/app/features/dappRequests/slice' import { - BaseSendTransactionRequest, - ChangeChainRequest, - ErrorResponse, - GetCallsStatusRequest, - GetCallsStatusResponse, - GetCapabilitiesRequest, - ParsedCall, - SendCallsRequest, - SendCallsResponse, - SendTransactionResponse, - SignMessageRequest, - SignMessageResponse, - SignTypedDataRequest, - SignTypedDataResponse, - UniswapOpenSidebarRequest, - UniswapOpenSidebarResponse, + type BaseSendTransactionRequest, + type ChangeChainRequest, + type ErrorResponse, + type GetCallsStatusRequest, + type GetCallsStatusResponse, + type GetCapabilitiesRequest, + type ParsedCall, + type SendCallsRequest, + type SendCallsResponse, + type SendTransactionResponse, + type SignMessageRequest, + type SignMessageResponse, + type SignTypedDataRequest, + type SignTypedDataResponse, + type UniswapOpenSidebarRequest, + type UniswapOpenSidebarResponse, } from 'src/app/features/dappRequests/types/DappRequestTypes' import { HexadecimalNumberSchema } from 'src/app/features/dappRequests/types/utilityTypes' import { isWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked' @@ -45,28 +46,28 @@ import getCalldataInfoFromTransaction from 'src/background/utils/getCalldataInfo import { call, put, select, take } from 'typed-redux-saga' import { hexadecimalStringToInt, toSupportedChainId } from 'uniswap/src/features/chains/utils' import { DappRequestType, DappResponseType } from 'uniswap/src/features/dappRequests/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { getFeatureFlag } from 'uniswap/src/features/gating/hooks' import { pushNotification } from 'uniswap/src/features/notifications/slice/slice' import { AppNotificationType } from 'uniswap/src/features/notifications/slice/types' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { getEnabledChainIdsSaga } from 'uniswap/src/features/settings/saga' +import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { TransactionOriginType, TransactionType, - TransactionTypeInfo, + type TransactionTypeInfo, } from 'uniswap/src/features/transactions/types/transactionDetails' import { extractBaseUrl } from 'utilities/src/format/urls' import { logger } from 'utilities/src/logger/logger' import { getCallsStatusHelper } from 'wallet/src/features/batchedTransactions/eip5792Utils' import { addBatchedTransaction } from 'wallet/src/features/batchedTransactions/slice' -import { generateBatchId, getCapabilitiesCore } from 'wallet/src/features/batchedTransactions/utils' -import { Call } from 'wallet/src/features/dappRequests/types' +import { generateBatchId, getCapabilitiesResponse } from 'wallet/src/features/batchedTransactions/utils' +import { type Call } from 'wallet/src/features/dappRequests/types' import { - ExecuteTransactionParams, + type ExecuteTransactionParams, executeTransaction, } from 'wallet/src/features/transactions/executeTransaction/executeTransactionSaga' -import { SignedTransactionRequest } from 'wallet/src/features/transactions/executeTransaction/types' +import { type SignedTransactionRequest } from 'wallet/src/features/transactions/executeTransaction/types' import { getProvider, getSignerManager } from 'wallet/src/features/wallet/context' import { selectActiveAccount, selectHasSmartWalletConsent } from 'wallet/src/features/wallet/selectors' import { signMessage, signTypedDataMessage } from 'wallet/src/features/wallet/signing/signing' @@ -104,7 +105,7 @@ const ACCOUNT_INFO_TYPES = [DappRequestType.GetChainId, DappRequestType.GetAccou * @param requestParams DappRequest and senderTabInfo (required for sending response) * i think remove all the checks from here and push to later. */ -// eslint-disable-next-line complexity +// oxlint-disable-next-line complexity function* handleRequest(requestParams: DappRequestNoDappInfo) { if ( requestParams.dappRequest.type === DappRequestType.SendCalls || @@ -150,12 +151,13 @@ function* handleRequest(requestParams: DappRequestNoDappInfo) { const dappInfo = yield* call(dappStore.getDappInfo, dappUrl) const isConnectedToDapp = dappInfo && dappInfo.connectedAccounts.length > 0 + const isAccountRequestRequest = ACCOUNT_REQUEST_TYPES.includes(requestParams.dappRequest.type) if (!isConnectedToDapp) { if (requestParams.dappRequest.type === DappRequestType.GetChainId) { // Allows for eth_chainId for unconnected dapps to advance connection steps yield* put(confirmRequestNoDappInfo(requestParams)) - } else if (!ACCOUNT_REQUEST_TYPES.includes(requestParams.dappRequest.type)) { + } else if (!isAccountRequestRequest) { // Otherwise, only allows for accounts requests to be handled until connection is confirmed // TODO(EXT-359): show a warning when the active account is different. const response: DappRequestRejectParams = { @@ -295,16 +297,31 @@ function* handleRequest(requestParams: DappRequestNoDappInfo) { } } + // Track connection requests when they arrive, before approval + const connectRequestAnalyticsProperties = { + dappUrl, + chainId: dappInfo?.lastChainId, + activeConnectedAddress: dappInfo?.activeConnectedAddress, + connectedAddresses: dappInfo?.connectedAccounts.map((account) => account.address) ?? [], + } + if (isAccountRequestRequest) { + sendAnalyticsEvent(ExtensionEventName.DappConnectRequest, connectRequestAnalyticsProperties) + } + const shouldAutoConfirmRequest = dappInfo && isConnectedToDapp && - (ACCOUNT_REQUEST_TYPES.includes(requestParams.dappRequest.type) || + (isAccountRequestRequest || ACCOUNT_INFO_TYPES.includes(requestParams.dappRequest.type) || requestParams.dappRequest.type === DappRequestType.RevokePermissions || requestParams.dappRequest.type === DappRequestType.GetCallsStatus || requestParams.dappRequest.type === DappRequestType.GetCapabilities) if (shouldAutoConfirmRequest) { + if (isAccountRequestRequest) { + // Track that a connection was established, even if it's auto-approved + sendAnalyticsEvent(ExtensionEventName.DappConnect, connectRequestAnalyticsProperties) + } yield* call(handleConfirmRequestWithDappInfo, { ...requestParams, dappInfo }) } else { yield* put( @@ -368,7 +385,7 @@ export function* handleSendTransaction({ ) // do not block on this function call since it should happen in parallel - // eslint-disable-next-line @typescript-eslint/no-floating-promises + // oxlint-disable-next-line typescript/no-floating-promises onTransactionSentToChain(transactionHash, provider) const response: SendTransactionResponse = { @@ -531,7 +548,7 @@ export function* handleGetCapabilities(request: GetCapabilitiesRequest, senderTa const hasSmartWalletConsent = yield* select(selectHasSmartWalletConsent, request.address) const chainIds = request.chainIds?.map(hexadecimalStringToInt) ?? enabledChains.map((chain) => chain.valueOf()) - const response = yield* call(getCapabilitiesCore, { + const response = yield* call(getCapabilitiesResponse, { request, chainIds, hasSmartWalletConsent, diff --git a/apps/extension/src/app/features/dappRequests/shared.ts b/apps/extension/src/app/features/dappRequests/shared.ts index d27785901e9..c6c8249a779 100644 --- a/apps/extension/src/app/features/dappRequests/shared.ts +++ b/apps/extension/src/app/features/dappRequests/shared.ts @@ -1,13 +1,11 @@ import type { DappInfo } from 'src/app/features/dapp/store' import type { DappRequest, ErrorResponse } from 'src/app/features/dappRequests/types/DappRequestTypes' +import type { DappRequestMessageSchema } from 'src/background/messagePassing/types/requests' import type { TransactionTypeInfo } from 'uniswap/src/features/transactions/types/transactionDetails' import type { SignedTransactionRequest } from 'wallet/src/features/transactions/executeTransaction/types' +import type { z } from 'zod' -export interface SenderTabInfo { - id: number - url: string - favIconUrl?: string -} +export type SenderTabInfo = z.infer['senderTabInfo'] export enum DappRequestStatus { Pending = 'pending', diff --git a/apps/extension/src/app/features/dappRequests/slice.ts b/apps/extension/src/app/features/dappRequests/slice.ts index f60722bfc87..3b598430d27 100644 --- a/apps/extension/src/app/features/dappRequests/slice.ts +++ b/apps/extension/src/app/features/dappRequests/slice.ts @@ -1,4 +1,4 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { createSlice, type PayloadAction } from '@reduxjs/toolkit' import { confirmRequest, confirmRequestNoDappInfo } from 'src/app/features/dappRequests/actions' import type { DappRequestStoreItem } from 'src/app/features/dappRequests/shared' import { DappRequestStatus } from 'src/app/features/dappRequests/shared' @@ -11,7 +11,7 @@ import { } from 'src/app/features/dappRequests/types/DappRequestTypes' type RequestId = string -type WithMetadata = T & { +export type WithMetadata = T & { createdAt: number status: DappRequestStatus } @@ -26,22 +26,22 @@ const initialDappRequestState: DappRequestState = { } // Enforces that a request object in state is for an eth send txn request -export interface DappRequestStoreItemForEthSendTxn extends DappRequestStoreItem { - dappRequest: WithMetadata +export interface DappRequestStoreItemForEthSendTxn extends WithMetadata { + dappRequest: SendTransactionRequest } export function isDappRequestStoreItemForEthSendTxn( - request: DappRequestStoreItem, + request: WithMetadata, ): request is DappRequestStoreItemForEthSendTxn { return isSendTransactionRequest(request.dappRequest) } -export interface DappRequestStoreItemForSendCallsTxn extends DappRequestStoreItem { +export interface DappRequestStoreItemForSendCallsTxn extends WithMetadata { dappRequest: SendCallsRequest } export function isDappRequestStoreItemForSendCallsTxn( - request: DappRequestStoreItem, + request: WithMetadata, ): request is DappRequestStoreItemForSendCallsTxn { return isSendCallsRequest(request.dappRequest) } @@ -85,6 +85,7 @@ const slice = createSlice({ setMostRecent5792DappUrl: (state, action: PayloadAction) => { state.mostRecent5792DappUrl = action.payload }, + reset: () => initialDappRequestState, }, extraReducers: (builder) => { // update status of request to confirming @@ -92,7 +93,7 @@ const slice = createSlice({ builder.addMatcher( (action) => action.type === confirmRequest.type || action.type === confirmRequestNoDappInfo.type, (state, action) => { - const { dappRequest } = action.payload + const { dappRequest } = action['payload'] const request = state.requests[dappRequest.requestId] if (request) { request.status = DappRequestStatus.Confirming @@ -102,8 +103,9 @@ const slice = createSlice({ }, }) -export const selectAllDappRequests = (state: { dappRequests: DappRequestState }): DappRequestStoreItem[] => - selectDappRequestsArray(state.dappRequests) +export const selectAllDappRequests = (state: { + dappRequests: DappRequestState +}): WithMetadata[] => selectDappRequestsArray(state.dappRequests) export const selectIsRequestConfirming = (state: { dappRequests: DappRequestState }, requestId: string): boolean => state.dappRequests.requests[requestId]?.status === DappRequestStatus.Confirming diff --git a/apps/extension/src/app/features/dappRequests/types/DappRequestTypes.ts b/apps/extension/src/app/features/dappRequests/types/DappRequestTypes.ts index 57868086614..50edb9a9dce 100644 --- a/apps/extension/src/app/features/dappRequests/types/DappRequestTypes.ts +++ b/apps/extension/src/app/features/dappRequests/types/DappRequestTypes.ts @@ -1,4 +1,4 @@ -/* eslint-disable import/no-unused-modules */ +/* oxlint-disable import/no-unused-modules */ import { EthereumRpcErrorSchema } from 'src/app/features/dappRequests/types/ErrorTypes' import { EthersTransactionRequestSchema } from 'src/app/features/dappRequests/types/EthersTypes' import { NonfungiblePositionManagerCallSchema } from 'src/app/features/dappRequests/types/NonfungiblePositionManagerTypes' diff --git a/apps/extension/src/app/features/dappRequests/types/EthersTypes.ts b/apps/extension/src/app/features/dappRequests/types/EthersTypes.ts index df5e5d1d9dc..0ca097b4a35 100644 --- a/apps/extension/src/app/features/dappRequests/types/EthersTypes.ts +++ b/apps/extension/src/app/features/dappRequests/types/EthersTypes.ts @@ -6,7 +6,7 @@ import { z } from 'zod' * Ethers types copied from `ethers` package */ -// eslint-disable-next-line no-restricted-syntax +// oxlint-disable-next-line no-restricted-syntax export const BigNumberSchema = z.any() // TODO (EXT-831): Add schema once stable const AccessListEntrySchema = z.object({ @@ -32,7 +32,7 @@ const BytesLikeSchema = z.string().refine((data) => isHexString(data)) const AccessListishSchema = z.union([ AccessListSchema, z.array(z.tuple([z.string(), z.array(z.string())])), // Array of 2-element Arrays format - z.record(z.array(z.string())), // Object with addresses as keys and arrays of storage keys as values + z.record(z.string(), z.array(z.string())), // Object with addresses as keys and arrays of storage keys as values ]) export const EthersTransactionRequestSchema = z.object({ @@ -44,11 +44,11 @@ export const EthersTransactionRequestSchema = z.object({ data: BytesLikeSchema.optional(), value: BigNumberishSchema.optional(), chainId: HexadecimalNumberSchema.optional(), - type: z.number().optional(), + type: z.union([z.number(), HexadecimalNumberSchema]).optional(), accessList: AccessListishSchema.optional(), maxPriorityFeePerGas: BigNumberishSchema.optional(), maxFeePerGas: BigNumberishSchema.optional(), - // eslint-disable-next-line no-restricted-syntax - customData: z.record(z.any()).optional(), + // oxlint-disable-next-line no-restricted-syntax + customData: z.record(z.string(), z.any()).optional(), ccipReadEnabled: z.boolean().optional(), }) diff --git a/apps/extension/src/app/features/dappRequests/types/NonfungiblePositionManager.ts b/apps/extension/src/app/features/dappRequests/types/NonfungiblePositionManager.ts index 64c3deab098..59b79d1cf8c 100644 --- a/apps/extension/src/app/features/dappRequests/types/NonfungiblePositionManager.ts +++ b/apps/extension/src/app/features/dappRequests/types/NonfungiblePositionManager.ts @@ -13,7 +13,7 @@ function parseMulticallCommand(calldata: string): NFPMCommand { return NfpmCommandSchema.parse({ commandName: txDescription.name, - params: txDescription.args.params, + params: txDescription.args['params'], }) } diff --git a/apps/extension/src/app/features/dappRequests/types/UniversalRouterTypes.ts b/apps/extension/src/app/features/dappRequests/types/UniversalRouterTypes.ts index 5990962ebb1..91c12fb968e 100644 --- a/apps/extension/src/app/features/dappRequests/types/UniversalRouterTypes.ts +++ b/apps/extension/src/app/features/dappRequests/types/UniversalRouterTypes.ts @@ -11,7 +11,7 @@ const CommandNameSchema = z.enum( // TODO: remove this fallback once params are fully typed or we are able to import them from the universal router sdk const FallbackParamSchema = z.object({ name: z.string(), - // eslint-disable-next-line no-restricted-syntax + // oxlint-disable-next-line no-restricted-syntax value: z.any(), }) diff --git a/apps/extension/src/app/features/dappRequests/types/utilityTypes.tsx b/apps/extension/src/app/features/dappRequests/types/utilityTypes.tsx index 9af5f91f513..03ece495ae0 100644 --- a/apps/extension/src/app/features/dappRequests/types/utilityTypes.tsx +++ b/apps/extension/src/app/features/dappRequests/types/utilityTypes.tsx @@ -8,6 +8,6 @@ export const HexadecimalNumberSchema = z.union([z.number(), z.string()]).transfo if (!isNaN(possibleNumber)) { return possibleNumber } - ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Not a hexadecimal number' }) + ctx.addIssue({ code: 'custom', message: 'Not a hexadecimal number' }) return z.NEVER }) diff --git a/apps/extension/src/app/features/home/HomeScreen.tsx b/apps/extension/src/app/features/home/HomeScreen.tsx index f7a5e10a591..ee87afb485a 100644 --- a/apps/extension/src/app/features/home/HomeScreen.tsx +++ b/apps/extension/src/app/features/home/HomeScreen.tsx @@ -1,13 +1,13 @@ import { useApolloClient } from '@apollo/client' import { SharedEventName } from '@uniswap/analytics-events' -import { memo, useCallback, useEffect, useState } from 'react' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' +import { getIsNotificationServiceLocalOverrideEnabled } from '@universe/notifications' +import React, { memo, useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { ActivityTab } from 'src/app/components/tabs/ActivityTab' import { NftsTab } from 'src/app/components/tabs/NftsTab' import { useSmartWalletNudges } from 'src/app/context/SmartWalletNudgesContext' -import AppRatingModal from 'src/app/features/appRating/AppRatingModal' -import { useAppRating } from 'src/app/features/appRating/hooks/useAppRating' import { HomeIntroCardStack } from 'src/app/features/home/introCards/HomeIntroCardStack' import { PortfolioActionButtons } from 'src/app/features/home/PortfolioActionButtons' import { PortfolioHeader } from 'src/app/features/home/PortfolioHeader' @@ -18,11 +18,15 @@ import { PinReminder } from 'src/app/features/onboarding/PinReminder' import { useOptimizedSearchParams } from 'src/app/hooks/useOptimizedSearchParams' import { HomeQueryParams, HomeTabs } from 'src/app/navigation/constants' import { navigate } from 'src/app/navigation/state' +import { ExtensionNotificationServiceManager } from 'src/notification-service/ExtensionNotificationServiceManager' import { Flex, Loader, styled, Text, TouchableArea } from 'ui/src' import { SMART_WALLET_UPGRADE_VIDEO } from 'ui/src/assets' +import { buildWrappedUrl } from 'uniswap/src/components/banners/shared/utils' +import { UniswapWrapped2025Banner } from 'uniswap/src/components/banners/UniswapWrapped2025Banner/UniswapWrapped2025Banner' import { NFTS_TAB_DATA_DEPENDENCIES } from 'uniswap/src/components/nfts/constants' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { UNISWAP_WEB_URL } from 'uniswap/src/constants/urls' +import { selectHasDismissedUniswapWrapped2025Banner } from 'uniswap/src/features/behaviorHistory/selectors' +import { setHasDismissedUniswapWrapped2025Banner } from 'uniswap/src/features/behaviorHistory/slice' import { useSelectAddressHasNotifications } from 'uniswap/src/features/notifications/slice/hooks' import { setNotificationStatus } from 'uniswap/src/features/notifications/slice/slice' import { PortfolioBalance } from 'uniswap/src/features/portfolio/PortfolioBalance/PortfolioBalance' @@ -57,7 +61,7 @@ const MemoizedVideo = memo(() => ( MemoizedVideo.displayName = 'MemoizedVideo' -export const HomeScreen = memo(function _HomeScreen(): JSX.Element { +export const HomeScreen = memo(function HomeScreenInner(): JSX.Element { const { t } = useTranslation() const activeAccount = useActiveAccountWithThrow() const [showTabs, setShowTabs] = useState(false) @@ -73,6 +77,30 @@ export const HomeScreen = memo(function _HomeScreen(): JSX.Element { const [isSmartWalletEnabledModalOpen, setIsSmartWalletEnabledModalOpen] = useState(false) const dispatch = useDispatch() + // UniswapWrapped2025 banner state + const isWrappedBannerEnabled = useFeatureFlag(FeatureFlags.UniswapWrapped2025) + const hasDismissedWrappedBanner = useSelector(selectHasDismissedUniswapWrapped2025Banner) + const shouldShowWrappedBanner = isWrappedBannerEnabled && !hasDismissedWrappedBanner + + // Notification service feature flag + const isNotificationServiceEnabledFlag = useFeatureFlag(FeatureFlags.NotificationService) + const isNotificationServiceEnabled = + getIsNotificationServiceLocalOverrideEnabled() || isNotificationServiceEnabledFlag + + const handleDismissWrappedBanner = useCallback(() => { + dispatch(setHasDismissedUniswapWrapped2025Banner(true)) + }, [dispatch]) + + const handlePressWrappedBanner = useCallback(() => { + try { + const url = buildWrappedUrl(UNISWAP_WEB_URL, address) + window.open(url, '_blank') + dispatch(setHasDismissedUniswapWrapped2025Banner(true)) + } catch (error) { + logger.error(error, { tags: { file: 'HomeScreen', function: 'handlePressWrappedBanner' } }) + } + }, [address, dispatch]) + useEffect(() => { if (selectedTab) { sendAnalyticsEvent(SharedEventName.PAGE_VIEWED, { @@ -159,8 +187,6 @@ export const HomeScreen = memo(function _HomeScreen(): JSX.Element { } }, [apolloClient, shouldRefetchNfts]) - const { appRatingModalVisible, onAppRatingModalClose } = useAppRating() - return ( {address ? ( @@ -170,17 +196,39 @@ export const HomeScreen = memo(function _HomeScreen(): JSX.Element { )} + {shouldShowWrappedBanner && ( + + + + + )} - + - + + + {!isNotificationServiceEnabled && } @@ -243,7 +291,6 @@ export const HomeScreen = memo(function _HomeScreen(): JSX.Element { {t('home.extension.error')} )} - {appRatingModalVisible && } {isSmartWalletEnabled && !activeModal && ( void children: React.ReactNode showPendingNotificationBadge?: boolean -}): JSX.Element => { +}): React.JSX.Element => { return ( diff --git a/apps/extension/src/app/features/home/PortfolioActionButtons.tsx b/apps/extension/src/app/features/home/PortfolioActionButtons.tsx index a0ac5b56037..562209828eb 100644 --- a/apps/extension/src/app/features/home/PortfolioActionButtons.tsx +++ b/apps/extension/src/app/features/home/PortfolioActionButtons.tsx @@ -7,8 +7,6 @@ import { navigate } from 'src/app/navigation/state' import { Flex, getTokenValue, Text, TouchableArea, useMedia } from 'ui/src' import { ArrowDownCircle, Bank, CoinConvert, SendAction } from 'ui/src/components/icons' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ElementName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { TestnetModeModal } from 'uniswap/src/features/testnets/TestnetModeModal' @@ -63,11 +61,10 @@ function ActionButton({ label, Icon, onClick, url }: ActionButtonProps): JSX.Ele ) } -export const PortfolioActionButtons = memo(function _PortfolioActionButtons(): JSX.Element { +export const PortfolioActionButtons = memo(function PortfolioActionButtonsInner(): JSX.Element { const { t } = useTranslation() const media = useMedia() const { isTestnetModeEnabled } = useEnabledChains() - const isFiatOffRampEnabled = useFeatureFlag(FeatureFlags.FiatOffRamp) const onSendClick = (): void => { sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { @@ -119,11 +116,7 @@ export const PortfolioActionButtons = memo(function _PortfolioActionButtons(): J /> } label={t('home.label.swap')} onClick={onSwapClick} /> - } - label={isFiatOffRampEnabled ? t('home.label.for') : t('home.label.buy')} - onClick={onBuyClick} - /> + } label={t('home.label.for')} onClick={onBuyClick} /> } label={t('home.label.send')} onClick={onSendClick} /> diff --git a/apps/extension/src/app/features/home/PortfolioHeader.tsx b/apps/extension/src/app/features/home/PortfolioHeader.tsx index 45df41829bc..c765c8f9c24 100644 --- a/apps/extension/src/app/features/home/PortfolioHeader.tsx +++ b/apps/extension/src/app/features/home/PortfolioHeader.tsx @@ -23,8 +23,8 @@ import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ExtensionScreens } from 'uniswap/src/types/screens/extension' import { sanitizeAddressText } from 'uniswap/src/utils/addresses' -import { setClipboard } from 'uniswap/src/utils/clipboard' import { shortenAddress } from 'utilities/src/addresses' +import { setClipboard } from 'utilities/src/clipboard/clipboard' import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl' import { AnimatedUnitagDisplayName } from 'wallet/src/components/accounts/AnimatedUnitagDisplayName' import useIsFocused from 'wallet/src/features/focus/useIsFocused' @@ -55,7 +55,8 @@ const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }): }), ) } - }, [isScreenFocused, pressProgress]) + // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here + }, [isScreenFocused]) const onBegin = (): void => { pressProgress.value = withTiming(1) @@ -94,7 +95,7 @@ const RotatingSettingsIcon = ({ onPressSettings }: { onPressSettings(): void }): ) } -export const PortfolioHeader = memo(function _PortfolioHeader({ address }: PortfolioHeaderProps): JSX.Element { +export const PortfolioHeader = memo(function PortfolioHeaderInner({ address }: PortfolioHeaderProps): JSX.Element { const dispatch = useDispatch() const displayName = useDisplayName(address) @@ -152,12 +153,12 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf - + - + @@ -189,7 +190,7 @@ export const PortfolioHeader = memo(function _PortfolioHeader({ address }: Portf borderColor="$surface2" borderRadius="$rounded20" borderWidth="$spacing1" - disableRemoveScroll={false} + enableRemoveScroll={true} zIndex="$default" {...animationPresets.fadeInDownOutUp} shadowColor="$shadowColor" diff --git a/apps/extension/src/app/features/home/SwitchNetworksModal.tsx b/apps/extension/src/app/features/home/SwitchNetworksModal.tsx index 7d3702c9df7..c13d2d62b8e 100644 --- a/apps/extension/src/app/features/home/SwitchNetworksModal.tsx +++ b/apps/extension/src/app/features/home/SwitchNetworksModal.tsx @@ -53,13 +53,7 @@ export function SwitchNetworksModal({ onPress }: SwitchNetworksModalProps): JSX. - + diff --git a/apps/extension/src/app/features/home/TokenBalanceList.tsx b/apps/extension/src/app/features/home/TokenBalanceList.tsx index b29a53a4a2c..91c7473af51 100644 --- a/apps/extension/src/app/features/home/TokenBalanceList.tsx +++ b/apps/extension/src/app/features/home/TokenBalanceList.tsx @@ -1,12 +1,16 @@ -import { memo } from 'react' +import { Currency } from '@uniswap/sdk-core' +import { memo, useState } from 'react' import { useInterfaceBuyNavigator } from 'src/app/features/for/utils' import { AppRoutes } from 'src/app/navigation/constants' import { navigate } from 'src/app/navigation/state' import { TokenBalanceListWeb } from 'uniswap/src/components/portfolio/TokenBalanceListWeb' +import { ReportTokenIssueModal } from 'uniswap/src/components/reporting/ReportTokenIssueModal' import { ElementName } from 'uniswap/src/features/telemetry/constants' +import { useEvent } from 'utilities/src/react/hooks' +import { useBooleanState } from 'utilities/src/react/useBooleanState' import { usePortfolioEmptyStateBackground } from 'wallet/src/components/portfolio/empty' -export const ExtensionTokenBalanceList = memo(function _ExtensionTokenBalanceList({ +export const ExtensionTokenBalanceList = memo(function ExtensionTokenBalanceListInner({ owner, }: { owner: Address @@ -15,13 +19,35 @@ export const ExtensionTokenBalanceList = memo(function _ExtensionTokenBalanceLis navigate(`/${AppRoutes.Receive}`) } const onPressBuy = useInterfaceBuyNavigator(ElementName.EmptyStateBuy) + + const { value: isReportTokenModalOpen, setTrue: openModal, setFalse: closeReportTokenModal } = useBooleanState(false) + const [reportTokenCurrency, setReportTokenCurrency] = useState(undefined) + const [isMarkedSpam, setIsMarkedSpam] = useState>(undefined) + + const openReportTokenModal = useEvent((currency: Currency, isMarkedSpam: Maybe) => { + setReportTokenCurrency(currency) + setIsMarkedSpam(isMarkedSpam) + openModal() + }) + const backgroundImageWrapperCallback = usePortfolioEmptyStateBackground() return ( - + <> + + {reportTokenCurrency && ( + + )} + ) }) diff --git a/apps/extension/src/app/features/home/introCards/HomeIntroCardStack.tsx b/apps/extension/src/app/features/home/introCards/HomeIntroCardStack.tsx index 119a661a5d0..9d969166c42 100644 --- a/apps/extension/src/app/features/home/introCards/HomeIntroCardStack.tsx +++ b/apps/extension/src/app/features/home/introCards/HomeIntroCardStack.tsx @@ -26,8 +26,6 @@ export function HomeIntroCardStack(): JSX.Element | null { navigateToBackupFlow, }) - // Don't show cards if there are none - // or if the account is view only (not yet available on extension, adding for safety) if (!cards.length || !isSignerAccount) { return null } diff --git a/apps/extension/src/app/features/lockScreen/Locked.tsx b/apps/extension/src/app/features/lockScreen/Locked.tsx index e5d6ad735df..a493da40b24 100644 --- a/apps/extension/src/app/features/lockScreen/Locked.tsx +++ b/apps/extension/src/app/features/lockScreen/Locked.tsx @@ -7,14 +7,15 @@ import { PasswordInputWithBiometrics } from 'src/app/components/PasswordInput' import { BiometricUnlockStorage } from 'src/app/features/biometricUnlock/BiometricUnlockStorage' import { useUnlockWithBiometricCredentialMutation } from 'src/app/features/biometricUnlock/useUnlockWithBiometricCredentialMutation' import { useUnlockWithPassword } from 'src/app/features/lockScreen/useUnlockWithPassword' -import { useSagaStatus } from 'src/app/hooks/useSagaStatus' import { OnboardingRoutes, TopLevelRoutes } from 'src/app/navigation/constants' import { focusOrCreateOnboardingTab } from 'src/app/navigation/focusOrCreateOnboardingTab' +import { ExtensionState } from 'src/store/extensionReducer' import { Button, Flex, InputProps, Text } from 'ui/src' import { AlertTriangleFilled, Lock } from 'ui/src/components/icons' import { spacing, zIndexes } from 'ui/src/theme' import { uniswapUrls } from 'uniswap/src/constants/urls' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { SagaStatus, useMonitoredSagaStatus } from 'uniswap/src/utils/saga' import { useEvent } from 'utilities/src/react/hooks' import { LandingBackground } from 'wallet/src/components/landing/LandingBackground' import { authSagaName } from 'wallet/src/features/auth/saga' @@ -22,7 +23,6 @@ import { AuthSagaError } from 'wallet/src/features/auth/types' import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' -import { SagaStatus } from 'wallet/src/utils/saga' function usePasswordInput(defaultValue = ''): Pick & { value: string } { const [value, setValue] = useState(defaultValue) @@ -63,7 +63,7 @@ export function Locked(): JSX.Element { [onChangePasswordText], ) - const { status, error } = useSagaStatus({ sagaName: authSagaName, resetSagaOnSuccess: false }) + const { status, error } = useMonitoredSagaStatus(authSagaName) const unlockWithPassword = useUnlockWithPassword() const onPressUnlockWithPassword = useEvent(() => unlockWithPassword({ password: enteredPassword })) diff --git a/apps/extension/src/app/features/onboarding/Complete.tsx b/apps/extension/src/app/features/onboarding/Complete.tsx index 42517907be7..de68630fe6e 100644 --- a/apps/extension/src/app/features/onboarding/Complete.tsx +++ b/apps/extension/src/app/features/onboarding/Complete.tsx @@ -1,21 +1,16 @@ import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' +import { OpenSidebarButton } from 'src/app/components/buttons/OpenSidebarButton' +import { useFinishExtensionOnboarding } from 'src/app/features/onboarding/hooks/useFinishExtensionOnboarding' +import { useOpenSidebar } from 'src/app/features/onboarding/hooks/useOpenSidebar' import { MainContentWrapper } from 'src/app/features/onboarding/intro/MainContentWrapper' import { KeyboardKey } from 'src/app/features/onboarding/KeyboardKey' -import { useFinishExtensionOnboarding } from 'src/app/features/onboarding/useFinishExtensionOnboarding' import { useOpeningKeyboardShortCut } from 'src/app/hooks/useOpeningKeyboardShortCut' -import { getCurrentTabAndWindowId } from 'src/app/navigation/utils' -import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels' -import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages' -import { openSidePanel } from 'src/background/utils/chromeSidePanelUtils' import { terminateStoreSynchronization } from 'src/store/storeSynchronization' -import { Button, Flex, Image, Text } from 'ui/src' +import { Flex, Image, Text } from 'ui/src' import { UNISWAP_LOGO } from 'ui/src/assets' -import { RightArrow } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' -import { uniswapUrls } from 'uniswap/src/constants/urls' import { ExtensionOnboardingFlow } from 'uniswap/src/types/screens/extension' -import { logger } from 'utilities/src/logger/logger' import { useOnboardingContext } from 'wallet/src/features/onboarding/OnboardingContext' export function Complete({ @@ -30,7 +25,7 @@ export function Complete({ const address = getOnboardingAccountAddress() const existingClaim = getUnitagClaim() const [unitagClaimAttempted, setUnitagClaimAttempted] = useState(false) - const [openedSideBar, setOpenedSideBar] = useState(false) + const { openedSideBar, handleOpenSidebar, handleOpenWebApp } = useOpenSidebar() useEffect(() => { if (!tryToClaimUnitag || !address || unitagClaimAttempted) { @@ -50,33 +45,6 @@ export function Complete({ skip: tryToClaimUnitag && !unitagClaimAttempted, }) - useEffect(() => { - const onSidebarOpenedListener = onboardingMessageChannel.addMessageListener( - OnboardingMessageType.SidebarOpened, - (_message) => { - setOpenedSideBar(true) - }, - ) - return () => { - onboardingMessageChannel.removeMessageListener(OnboardingMessageType.SidebarOpened, onSidebarOpenedListener) - } - }, []) - - const handleOpenWebApp = async (): Promise => { - window.location.href = uniswapUrls.webInterfaceSwapUrl - } - - const handleOpenSidebar = async (): Promise => { - try { - const { tabId, windowId } = await getCurrentTabAndWindowId() - await openSidePanel(tabId, windowId) - } catch (error) { - logger.error(error, { - tags: { file: 'onboarding/Complete.tsx', function: 'handleOpenSidebar' }, - }) - } - } - const keys = useOpeningKeyboardShortCut(openedSideBar) return ( @@ -97,18 +65,11 @@ export function Complete({ ))} - - - + diff --git a/apps/extension/src/app/features/onboarding/OnboardingSteps.tsx b/apps/extension/src/app/features/onboarding/OnboardingSteps.tsx index 580ef0e6433..c031b329b3a 100644 --- a/apps/extension/src/app/features/onboarding/OnboardingSteps.tsx +++ b/apps/extension/src/app/features/onboarding/OnboardingSteps.tsx @@ -58,7 +58,7 @@ export function OnboardingStepsProvider({ const isOnboarded = useSelector(isOnboardedSelector) const wasAlreadyOnboardedWhenPageLoaded = useRef(isOnboarded) - // biome-ignore lint/correctness/useExhaustiveDependencies: we also want to run this effect if isOnboarded changes + // oxlint-disable-next-line react/exhaustive-deps -- we also want to run this effect if isOnboarded changes useEffect(() => { if (!isResetting && wasAlreadyOnboardedWhenPageLoaded.current && !disableRedirect) { // Redirect to the intro screen screen if user is already onboarded. @@ -116,7 +116,7 @@ export function OnboardingStepsProvider({ setState((prev) => ({ ...prev, step: nextStep })) }, []) - // biome-ignore lint/correctness/useExhaustiveDependencies: onboardingScreenKey is a helper function defined below that doesn't need to be a dependency + // oxlint-disable-next-line react/exhaustive-deps -- onboardingScreenKey is a helper function defined below that doesn't need to be a dependency const setOnboardingScreen = useCallback((next: OnboardingScreenProps) => { clearTimeout(clearScreenTimeout) setState((prev) => { @@ -135,7 +135,7 @@ export function OnboardingStepsProvider({ currentOnboardingScreen = next }, []) - // biome-ignore lint/correctness/useExhaustiveDependencies: onboardingScreenKey is a helper function defined below that doesn't need to be a dependency + // oxlint-disable-next-line react/exhaustive-deps -- onboardingScreenKey is a helper function defined below that doesn't need to be a dependency const clearOnboardingScreen = useCallback((next: OnboardingScreenProps) => { // delay clear so the next screen can beat clearing the old one to avoid flickering clearScreenTimeout = setTimeout(() => { @@ -229,7 +229,7 @@ export function OnboardingStepsProvider({ {onboardingScreen && ( <> {/* render actual screen contents "offscreen", we use context and put it on onboardingScreen */} - {/* biome-ignore lint/correctness/noRestrictedElements: probably we can replace it here */} + {/* oxlint-disable-next-line react/forbid-elements -- probably we can replace it here */}
{stepContents}
{ generateInitialAddresses().catch((error) => { logger.error(error, { tags: { file: 'PasswordImport.tsx', function: 'generateInitialAddresses' }, }) }) + // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here }, []) const onSubmit = useCallback( diff --git a/apps/extension/src/app/features/onboarding/__snapshots__/KeyboardKey.test.tsx.snap b/apps/extension/src/app/features/onboarding/__snapshots__/KeyboardKey.test.tsx.snap index f3932ce02f8..7539f9ca6bb 100644 --- a/apps/extension/src/app/features/onboarding/__snapshots__/KeyboardKey.test.tsx.snap +++ b/apps/extension/src/app/features/onboarding/__snapshots__/KeyboardKey.test.tsx.snap @@ -10,7 +10,7 @@ exports[`KeyboardKey Component renders correctly with state Highlighted 1`] = ` class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _alignItems-center _backgroundColor-surface1 _borderTopColor-accent2Hove112785 _borderRightColor-accent2Hove112785 _borderBottomColor-accent2Hove112785 _borderLeftColor-accent2Hove112785 _borderTopLeftRadius-t-radius-ro291586424 _borderTopRightRadius-t-radius-ro291586424 _borderBottomRightRadius-t-radius-ro291586424 _borderBottomLeftRadius-t-radius-ro291586424 _borderTopWidth-t-space-spa94665587 _borderRightWidth-t-space-spa94665587 _borderBottomWidth-t-space-spa94665587 _borderLeftWidth-t-space-spa94665587 _height-70px _justifyContent-center _pr-t-space-spa1360334043 _pl-t-space-spa1360334043 _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid _boxShadow-0px7px0pxva651413887" > Shift @@ -33,7 +33,7 @@ exports[`KeyboardKey Component renders correctly with state KeyDown 1`] = ` class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _alignItems-center _backgroundColor-surface1 _borderTopColor-accent2Hove112785 _borderRightColor-accent2Hove112785 _borderBottomColor-accent2Hove112785 _borderLeftColor-accent2Hove112785 _borderTopLeftRadius-t-radius-ro291586424 _borderTopRightRadius-t-radius-ro291586424 _borderBottomRightRadius-t-radius-ro291586424 _borderBottomLeftRadius-t-radius-ro291586424 _borderTopWidth-t-space-spa94665587 _borderRightWidth-t-space-spa94665587 _borderBottomWidth-t-space-spa94665587 _borderLeftWidth-t-space-spa94665587 _height-70px _justifyContent-center _pr-t-space-spa1360334043 _pl-t-space-spa1360334043 _top-7px _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid _boxShadow-0px0pxvar--100262595" > Shift @@ -56,7 +56,7 @@ exports[`KeyboardKey Component renders correctly with state KeyUp 1`] = ` class="_display-flex _flexBasis-auto _boxSizing-border-box _position-relative _minHeight-0px _minWidth-0px _flexShrink-0 _flexDirection-column _alignItems-center _backgroundColor-surface1 _borderTopColor-surface3 _borderRightColor-surface3 _borderBottomColor-surface3 _borderLeftColor-surface3 _borderTopLeftRadius-t-radius-ro291586424 _borderTopRightRadius-t-radius-ro291586424 _borderBottomRightRadius-t-radius-ro291586424 _borderBottomLeftRadius-t-radius-ro291586424 _borderTopWidth-t-space-spa94665587 _borderRightWidth-t-space-spa94665587 _borderBottomWidth-t-space-spa94665587 _borderLeftWidth-t-space-spa94665587 _height-70px _justifyContent-center _pr-t-space-spa1360334043 _pl-t-space-spa1360334043 _borderBottomStyle-solid _borderTopStyle-solid _borderLeftStyle-solid _borderRightStyle-solid _boxShadow-0px7px0pxva612953881" > Shift diff --git a/apps/extension/src/app/features/onboarding/alerts/slice.ts b/apps/extension/src/app/features/onboarding/alerts/slice.ts index 59090dc3f01..7393681232f 100644 --- a/apps/extension/src/app/features/onboarding/alerts/slice.ts +++ b/apps/extension/src/app/features/onboarding/alerts/slice.ts @@ -1,4 +1,4 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { createSlice, type PayloadAction } from '@reduxjs/toolkit' export enum AlertName { PinToToolbar = 'PinToToolbar', @@ -23,8 +23,9 @@ const slice = createSlice({ closeAlert: (state, action: PayloadAction) => { state[action.payload].isOpen = false }, + resetAlerts: () => initialState, }, }) -export const { closeAlert } = slice.actions +export const { closeAlert, resetAlerts } = slice.actions export const { reducer: alertsReducer } = slice diff --git a/apps/extension/src/app/features/onboarding/useFinishExtensionOnboarding.ts b/apps/extension/src/app/features/onboarding/hooks/useFinishExtensionOnboarding.ts similarity index 100% rename from apps/extension/src/app/features/onboarding/useFinishExtensionOnboarding.ts rename to apps/extension/src/app/features/onboarding/hooks/useFinishExtensionOnboarding.ts diff --git a/apps/extension/src/app/features/onboarding/hooks/useOpenSidebar.ts b/apps/extension/src/app/features/onboarding/hooks/useOpenSidebar.ts new file mode 100644 index 00000000000..ec389095235 --- /dev/null +++ b/apps/extension/src/app/features/onboarding/hooks/useOpenSidebar.ts @@ -0,0 +1,41 @@ +import { useEffect } from 'react' +import { getCurrentTabAndWindowId } from 'src/app/navigation/utils' +import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels' +import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages' +import { openSidePanel } from 'src/background/utils/chromeSidePanelUtils' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { logger } from 'utilities/src/logger/logger' +import { useBooleanState } from 'utilities/src/react/useBooleanState' + +export function useOpenSidebar() { + const { value: openedSideBar, setTrue: openSideBar } = useBooleanState(false) + + useEffect(() => { + const onSidebarOpenedListener = onboardingMessageChannel.addMessageListener( + OnboardingMessageType.SidebarOpened, + () => { + openSideBar() + }, + ) + return () => { + onboardingMessageChannel.removeMessageListener(OnboardingMessageType.SidebarOpened, onSidebarOpenedListener) + } + }, [openSideBar]) + + const handleOpenSidebar = async (): Promise => { + try { + const { tabId, windowId } = await getCurrentTabAndWindowId() + await openSidePanel(tabId, windowId) + } catch (error) { + logger.error(error, { + tags: { file: 'useOpenSidebar.ts', function: 'handleOpenSidebar' }, + }) + } + } + + const handleOpenWebApp = async (): Promise => { + window.location.href = uniswapUrls.webInterfaceSwapUrl + } + + return { openedSideBar, handleOpenSidebar, handleOpenWebApp } +} diff --git a/apps/extension/src/app/features/onboarding/import/ImportMnemonic.tsx b/apps/extension/src/app/features/onboarding/import/ImportMnemonic.tsx index 0378892eb3a..3a3a34572d2 100644 --- a/apps/extension/src/app/features/onboarding/import/ImportMnemonic.tsx +++ b/apps/extension/src/app/features/onboarding/import/ImportMnemonic.tsx @@ -2,10 +2,10 @@ import { wordlists } from '@ethersproject/wordlists' import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { - NativeSyntheticEvent, - TextInputChangeEventData, - TextInputFocusEventData, - TextInputKeyPressEventData, + type NativeSyntheticEvent, + type TextInputChangeEventData, + type TextInputFocusEventData, + type TextInputKeyPressEventData, } from 'react-native' import { useDispatch } from 'react-redux' import { OnboardingScreen } from 'src/app/features/onboarding/OnboardingScreen' @@ -117,7 +117,7 @@ export function ImportMnemonic(): JSX.Element { if (!word) { return } - const wordInList = wordlists.en?.getWordIndex(word) !== -1 + const wordInList = wordlists['en']?.getWordIndex(word) !== -1 setErrors({ ...errors, [index]: !wordInList }) }, [errors], @@ -160,6 +160,7 @@ export function ImportMnemonic(): JSX.Element { if (isResetting) { // Remove all accounts before importing mnemonic. + // oxlint-disable-next-line typescript/await-thenable -- biome-parity: oxlint is stricter here await dispatch( editAccountActions.trigger({ type: EditAccountAction.Remove, @@ -234,7 +235,9 @@ export function ImportMnemonic(): JSX.Element { (inputRefs[index] = ref)} + ref={(ref) => { + inputRefs[index] = ref + }} handleBlur={handleBlur} handleChange={handleChange} handleKeyPress={handleKeyPress} @@ -250,9 +253,7 @@ export function ImportMnemonic(): JSX.Element { + {isRunning && ( + + )} + + +
+ + {/* Current Progress */} + + + {/* Results */} + {Object.entries(groupedResults).map(([difficulty, impls]) => ( + + ))} + + {/* Empty State */} + {results.length === 0 && !isRunning && ( + + + Select difficulty and implementation, then run a benchmark. + + + )} + + {/* Operation Log */} + +
+ + ) +} diff --git a/apps/extension/src/app/features/settings/SessionsDebugScreen.tsx b/apps/extension/src/app/features/settings/SessionsDebugScreen.tsx new file mode 100644 index 00000000000..dcd6a035d2e --- /dev/null +++ b/apps/extension/src/app/features/settings/SessionsDebugScreen.tsx @@ -0,0 +1,553 @@ +/* oxlint-disable max-lines */ +import { getEntryGatewayUrl, provideSessionService } from '@universe/api' +import { + ChallengeType, + createHashcashSolver, + createHashcashWorkerChannel, + type SessionService, +} from '@universe/sessions' +import { memo, useCallback, useEffect, useRef } from 'react' +import { ScreenHeader } from 'src/app/components/layout/ScreenHeader' +import { type LogEntry, useSessionsDebugStore } from 'src/app/features/settings/stores/sessionsDebugStore' +import { Button, Flex, ScrollView, Text, TouchableArea } from 'ui/src' +import { CopyAlt } from 'ui/src/components/icons' +import { setClipboard } from 'utilities/src/clipboard/clipboard' +import { logger } from 'utilities/src/logger/logger' +import { useShallow } from 'zustand/shallow' + +// Storage keys (must match session storage) +const SESSION_ID_KEY = 'UNISWAP_SESSION_ID' +const DEVICE_ID_KEY = 'UNISWAP_DEVICE_ID' +const UNISWAP_IDENTIFIER_KEY = 'UNISWAP_IDENTIFIER' + +function formatTime(date: Date): string { + return date.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) +} + +function truncateId(id: string | null, length = 16): string { + if (!id) { + return 'None' + } + if (id.length <= length) { + return id + } + return `${id.slice(0, length)}...` +} + +// Memoized log entry component +const LogEntryRow = memo(function LogEntryRow({ log, index }: { log: LogEntry; index: number }): JSX.Element { + return ( + + {formatTime(log.timestamp)} - {log.message} + + ) +}) + +// Operation log section +const LogSection = memo(function LogSection(): JSX.Element | null { + const logs = useSessionsDebugStore((state) => state.logs) + const clearLogs = useSessionsDebugStore((state) => state.clearLogs) + + if (logs.length === 0) { + return null + } + + return ( + + + Operation Log + + + Clear + + + + + {logs.map((log, index) => ( + + ))} + + ) +}) + +// Hashcash progress section +const HashcashProgressSection = memo(function HashcashProgressSection(): JSX.Element | null { + const progress = useSessionsDebugStore( + useShallow((state) => ({ + isRunning: state.hashcashProgress.isRunning, + difficulty: state.hashcashProgress.difficulty, + estimatedAttempts: state.hashcashProgress.estimatedAttempts, + elapsedMs: state.hashcashProgress.elapsedMs, + actualResult: state.hashcashProgress.actualResult, + })), + ) + + if (!progress.isRunning && !progress.actualResult) { + return null + } + + return ( + + Hashcash Progress + + + + Status: + + + {progress.isRunning ? 'Solving...' : 'Complete'} + + + + + + Difficulty: + + + {progress.difficulty} + + + + + + Attempts: + + + {progress.actualResult + ? (progress.actualResult.iterationCount ?? 0).toLocaleString() + : `~${progress.estimatedAttempts.toLocaleString()}`} + + + + + + Time: + + + {progress.actualResult + ? `${(progress.actualResult.durationMs / 1000).toFixed(2)}s` + : `${(progress.elapsedMs / 1000).toFixed(2)}s`} + + + + {progress.actualResult && ( + + + Hash Rate: + + + {((progress.actualResult.iterationCount ?? 0) / (progress.actualResult.durationMs / 1000)).toLocaleString( + undefined, + { maximumFractionDigits: 0 }, + )}{' '} + h/s + + + )} + + ) +}) + +// Current operation display +const CurrentOperationSection = memo(function CurrentOperationSection(): JSX.Element | null { + const currentOperation = useSessionsDebugStore((state) => state.currentOperation) + + if (!currentOperation) { + return null + } + + return ( + + + {currentOperation} + + + ) +}) + +/** + * Sessions Debug Screen for testing session initialization flow. + * Access via Dev menu in development builds. + */ +export function SessionsDebugScreen(): JSX.Element { + // Individual selectors for minimal re-renders + const session = useSessionsDebugStore( + useShallow((state) => ({ + sessionId: state.session.sessionId, + deviceId: state.session.deviceId, + uniswapIdentifier: state.session.uniswapIdentifier, + })), + ) + const challenge = useSessionsDebugStore((state) => state.challenge) + const isLoading = useSessionsDebugStore((state) => state.isLoading) + const hashcashIsRunning = useSessionsDebugStore((state) => state.hashcashProgress.isRunning) + const hashcashStartTime = useSessionsDebugStore((state) => state.hashcashProgress.startTime) + + // Actions (stable references) + const setSession = useSessionsDebugStore((state) => state.setSession) + const setChallenge = useSessionsDebugStore((state) => state.setChallenge) + const startOperation = useSessionsDebugStore((state) => state.startOperation) + const endOperation = useSessionsDebugStore((state) => state.endOperation) + const addLog = useSessionsDebugStore((state) => state.addLog) + const startHashcash = useSessionsDebugStore((state) => state.startHashcash) + const updateHashcashProgress = useSessionsDebugStore((state) => state.updateHashcashProgress) + const completeHashcash = useSessionsDebugStore((state) => state.completeHashcash) + const stopHashcash = useSessionsDebugStore((state) => state.stopHashcash) + const reset = useSessionsDebugStore((state) => state.reset) + + const sessionServiceRef = useRef(null) + + const getSessionService = useCallback((): SessionService => { + if (!sessionServiceRef.current) { + sessionServiceRef.current = provideSessionService({ + getBaseUrl: getEntryGatewayUrl, + getIsSessionServiceEnabled: () => true, // Always enabled for debug + getLogger: () => logger, + }) + } + return sessionServiceRef.current + }, []) + + const refreshSessionState = useCallback(async (): Promise => { + const [sessionId, deviceId, uniswapIdentifier] = await Promise.all([ + localStorage.getItem(SESSION_ID_KEY), + localStorage.getItem(DEVICE_ID_KEY), + localStorage.getItem(UNISWAP_IDENTIFIER_KEY), + ]) + setSession({ + sessionId: sessionId || null, + deviceId: deviceId || null, + uniswapIdentifier: uniswapIdentifier || null, + }) + }, [setSession]) + + // Initial load + useEffect(() => { + const loadInitialState = async (): Promise => { + const [sessionId, deviceId, uniswapIdentifier] = await Promise.all([ + localStorage.getItem(SESSION_ID_KEY), + localStorage.getItem(DEVICE_ID_KEY), + localStorage.getItem(UNISWAP_IDENTIFIER_KEY), + ]) + setSession({ + sessionId: sessionId || null, + deviceId: deviceId || null, + uniswapIdentifier: uniswapIdentifier || null, + }) + } + // oxlint-disable-next-line typescript/no-floating-promises -- biome-parity: oxlint is stricter here + loadInitialState() + }, [setSession]) + + // Progress timer for hashcash + useEffect(() => { + if (hashcashIsRunning && hashcashStartTime !== null) { + const interval = setInterval(() => { + const elapsed = performance.now() - hashcashStartTime + // Estimate ~500k hashes/sec on web worker + const estimatedAttempts = Math.floor((elapsed / 1000) * 500000) + updateHashcashProgress(elapsed, estimatedAttempts) + }, 100) + + return (): void => { + clearInterval(interval) + } + } + return undefined + }, [hashcashIsRunning, hashcashStartTime, updateHashcashProgress]) + + const clearAllState = useCallback(async (): Promise => { + startOperation('Clearing all state...') + try { + localStorage.removeItem(SESSION_ID_KEY) + localStorage.removeItem(DEVICE_ID_KEY) + localStorage.removeItem(UNISWAP_IDENTIFIER_KEY) + sessionServiceRef.current = null + setChallenge(null) + reset() + addLog('Cleared all session state', 'success') + await refreshSessionState() + } catch (error) { + addLog(`Failed to clear state: ${error}`, 'error') + logger.error(error, { tags: { file: 'SessionsDebugScreen', function: 'clearAllState' } }) + } finally { + endOperation() + } + }, [startOperation, setChallenge, reset, addLog, refreshSessionState, endOperation]) + + const handleInitSession = useCallback(async (): Promise => { + startOperation('Initializing session...') + addLog('Init session started') + try { + const service = getSessionService() + const result = await service.initSession() + addLog(`Session initialized. needChallenge: ${result.needChallenge}`, 'success') + if (result.sessionId) { + addLog(`Session ID: ${truncateId(result.sessionId)}`) + } + await refreshSessionState() + } catch (error) { + addLog(`Init session failed: ${error}`, 'error') + logger.error(error, { tags: { file: 'SessionsDebugScreen', function: 'handleInitSession' } }) + } finally { + endOperation() + } + }, [startOperation, addLog, getSessionService, refreshSessionState, endOperation]) + + const handleRequestChallenge = useCallback(async (): Promise => { + startOperation('Requesting challenge...') + addLog('Request challenge started') + try { + const service = getSessionService() + const challengeResult = await service.requestChallenge() + setChallenge(challengeResult) + const challengeTypeName = ChallengeType[challengeResult.challengeType] || 'Unknown' + addLog(`Challenge received: ${challengeTypeName}`, 'success') + addLog(`Challenge ID: ${truncateId(challengeResult.challengeId)}`) + + if (challengeResult.challengeType === ChallengeType.HASHCASH && challengeResult.extra['challengeData']) { + try { + const challengeData = JSON.parse(challengeResult.extra['challengeData']) + addLog(`Difficulty: ${challengeData.difficulty}`) + } catch { + // Ignore parse errors + } + } + } catch (error) { + addLog(`Request challenge failed: ${error}`, 'error') + logger.error(error, { tags: { file: 'SessionsDebugScreen', function: 'handleRequestChallenge' } }) + } finally { + endOperation() + } + }, [startOperation, addLog, getSessionService, setChallenge, endOperation]) + + const handleSolveChallenge = useCallback(async (): Promise => { + const currentChallenge = useSessionsDebugStore.getState().challenge + if (!currentChallenge) { + addLog('No challenge to solve. Request a challenge first.', 'error') + return + } + + if (currentChallenge.challengeType !== ChallengeType.HASHCASH) { + addLog('Only Hashcash challenges are supported', 'error') + return + } + + startOperation('Solving hashcash challenge...') + addLog('Hashcash solve started') + + // Parse difficulty for progress display + let difficulty = 0 + if (currentChallenge.extra['challengeData']) { + try { + const challengeData = JSON.parse(currentChallenge.extra['challengeData']) + difficulty = challengeData.difficulty || 0 + } catch { + // Use default + } + } + + startHashcash(difficulty) + + try { + const solver = createHashcashSolver({ + performanceTracker: { + now: () => performance.now(), + }, + getWorkerChannel: () => + createHashcashWorkerChannel({ + getWorker: () => + new Worker( + new URL('@universe/sessions/src/challenge-solvers/hashcash/worker/hashcash.worker.ts', import.meta.url), + { type: 'module' }, + ), + }), + onSolveCompleted: (data) => { + completeHashcash(data) + }, + }) + + const solution = await solver.solve({ + challengeId: currentChallenge.challengeId, + challengeType: currentChallenge.challengeType, + extra: currentChallenge.extra, + }) + + addLog(`Challenge solved!`, 'success') + addLog(`Solution: ${truncateId(solution, 32)}`) + + // Verify with backend + startOperation('Verifying session...') + addLog('Verifying session with backend...') + const service = getSessionService() + const verifyResult = await service.verifySession({ + solution, + challengeId: currentChallenge.challengeId, + challengeType: currentChallenge.challengeType, + }) + + if (verifyResult.retry) { + addLog('Verification returned retry=true. May need another challenge.', 'info') + } else { + addLog('Session verified successfully!', 'success') + } + + setChallenge(null) + await refreshSessionState() + } catch (error) { + stopHashcash() + addLog(`Solve challenge failed: ${error}`, 'error') + logger.error(error, { tags: { file: 'SessionsDebugScreen', function: 'handleSolveChallenge' } }) + } finally { + endOperation() + } + }, [ + addLog, + startOperation, + startHashcash, + completeHashcash, + getSessionService, + setChallenge, + refreshSessionState, + stopHashcash, + endOperation, + ]) + + const copyToClipboard = useCallback( + async (value: string | null, label: string): Promise => { + if (!value) { + return + } + await setClipboard(value) + addLog(`Copied ${label} to clipboard`, 'info') + }, + [addLog], + ) + + const hasChallenge = challenge !== null + + return ( + + + + + + Test session initialization flow step by step. + + + {/* Session Status Section */} + + Session Status + + + + + Session ID: + + + + {truncateId(session.sessionId)} + + {session.sessionId && ( + copyToClipboard(session.sessionId, 'Session ID')}> + + + )} + + + + + + Device ID: + + + + {truncateId(session.deviceId)} + + {session.deviceId && ( + copyToClipboard(session.deviceId, 'Device ID')}> + + + )} + + + + + + Uniswap ID: + + + + {truncateId(session.uniswapIdentifier)} + + {session.uniswapIdentifier && ( + copyToClipboard(session.uniswapIdentifier, 'Uniswap ID')}> + + + )} + + + + + + Challenge Pending: + + + {hasChallenge ? 'Yes' : 'No'} + + + + + + {/* Action Buttons */} + + + + + + {/* Step-by-Step Testing */} + + Step-by-Step Testing + + + + + + + + {/* Current Operation */} + + + {/* Hashcash Progress */} + + + {/* Operation Log */} + + + + ) +} diff --git a/apps/extension/src/app/features/settings/SettingsDropdown.tsx b/apps/extension/src/app/features/settings/SettingsDropdown.tsx index 8df26d5fe71..9ac5caa304b 100644 --- a/apps/extension/src/app/features/settings/SettingsDropdown.tsx +++ b/apps/extension/src/app/features/settings/SettingsDropdown.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { Flex, Popover, ScrollView, Text, TouchableArea } from 'ui/src' +import { animationPresets, Flex, Popover, Text, TouchableArea, useScrollbarStyles } from 'ui/src' import { Check, RotatableChevron } from 'ui/src/components/icons' import { iconSizes, zIndexes } from 'ui/src/theme' @@ -16,10 +16,11 @@ export type SettingsDropdownProps = { } const MAX_DROPDOWN_HEIGHT = 220 -const MAX_DROPDOWN_WIDTH = 200 +const MAX_DROPDOWN_WIDTH = 250 export function SettingsDropdown({ selected, items, disableDropdown, onSelect }: SettingsDropdownProps): JSX.Element { const [isOpen, setIsOpen] = useState(false) + const scrollbarStyles = useScrollbarStyles() return ( @@ -38,22 +39,25 @@ export function SettingsDropdown({ selected, items, disableDropdown, onSelect }: {selected} - + - + - - + + {items.map((item, index) => ( { onSelect(item.value) setIsOpen(false) @@ -90,7 +103,7 @@ export function SettingsDropdown({ selected, items, disableDropdown, onSelect }: ))} - + diff --git a/apps/extension/src/app/features/settings/SettingsManageConnectionsScreen/SettingsManageConnectionsScreen.tsx b/apps/extension/src/app/features/settings/SettingsManageConnectionsScreen/SettingsManageConnectionsScreen.tsx index f3a2503780f..74980be6025 100644 --- a/apps/extension/src/app/features/settings/SettingsManageConnectionsScreen/SettingsManageConnectionsScreen.tsx +++ b/apps/extension/src/app/features/settings/SettingsManageConnectionsScreen/SettingsManageConnectionsScreen.tsx @@ -1,12 +1,13 @@ -import { useCallback, useMemo } from 'react' +import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' +import { useSearchParams } from 'react-router' import { ScreenHeader } from 'src/app/components/layout/ScreenHeader' import { removeAllDappConnectionsForAccount, removeDappConnection } from 'src/app/features/dapp/actions' -import { useAllDappConnectionsForActiveAccount } from 'src/app/features/dapp/hooks' +import { useAllDappConnectionsForAccount } from 'src/app/features/dapp/hooks' import { dappStore } from 'src/app/features/dapp/store' import { NoDappConnections } from 'src/app/features/settings/SettingsManageConnectionsScreen/internal/NoDappConnections' -import { Flex, Text, TouchableArea, UniversalImage, useSporeColors } from 'ui/src' +import { Flex, Text, TouchableArea, UniversalImage } from 'ui/src' import { MinusCircle } from 'ui/src/components/icons' import { borderRadii, breakpoints, fonts, gap, iconSizes } from 'ui/src/theme' import { DappIconPlaceholder } from 'uniswap/src/components/dapps/DappIconPlaceholder' @@ -16,6 +17,7 @@ import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import Trace from 'uniswap/src/features/telemetry/Trace' import { ExtensionScreens } from 'uniswap/src/types/screens/extension' +import { isEVMAddress } from 'utilities/src/addresses/evm/evm' import { extractNameFromUrl } from 'utilities/src/format/extractNameFromUrl' import { extractUrlHost } from 'utilities/src/format/urls' import { DappEllipsisDropdown } from 'wallet/src/components/settings/DappEllipsisDropdown/DappEllipsisDropdown' @@ -32,12 +34,21 @@ const textGap: number = gap.gap4 const textAreaHeight = fonts[titleVariant].lineHeight + fonts[subtitleVariant].lineHeight + textGap export function SettingsManageConnectionsScreen(): JSX.Element { - const colors = useSporeColors() const dispatch = useDispatch() const { t } = useTranslation() + const [searchParams] = useSearchParams() const activeAccount = useActiveAccountWithThrow() - const dappUrls = useAllDappConnectionsForActiveAccount() + // Capture address on mount to prevent screen flash when URL clears during navigation exit + const [initialAddress] = useState(() => { + const param = searchParams.get('address') + return param && isEVMAddress(param) ? param : null + }) + + const targetAddress = initialAddress ?? activeAccount.address + const targetAccount = initialAddress ? { ...activeAccount, address: initialAddress } : activeAccount + + const dappUrls = useAllDappConnectionsForAccount(targetAddress) const getHandleRemoveConnection = useCallback( (dappUrl: string) => async () => { @@ -55,9 +66,10 @@ export function SettingsManageConnectionsScreen(): JSX.Element { activeConnectedAddress: dappInfo?.activeConnectedAddress, connectedAddresses: dappInfo?.connectedAccounts.map((account) => account.address) ?? [], }) - await removeDappConnection(dappUrl, activeAccount) + await removeDappConnection(dappUrl, targetAccount) }, - [dispatch, activeAccount], + // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here + [dispatch, targetAccount], ) const DappTiles = useMemo( @@ -71,6 +83,7 @@ export function SettingsManageConnectionsScreen(): JSX.Element { const DeleteDappButton = ( - + ) @@ -141,7 +154,7 @@ export function SettingsManageConnectionsScreen(): JSX.Element {
) }), - [dappUrls, getHandleRemoveConnection, colors.neutral3], + [dappUrls, getHandleRemoveConnection], ) const hasConnections = Boolean(DappTiles.length) diff --git a/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/RemoveRecoveryPhraseVerify.tsx b/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/RemoveRecoveryPhraseVerify.tsx index f60a84d67a5..b348e91ae42 100644 --- a/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/RemoveRecoveryPhraseVerify.tsx +++ b/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/RemoveRecoveryPhraseVerify.tsx @@ -59,10 +59,12 @@ export function RemoveRecoveryPhraseVerify(): JSX.Element { await Keyring.removePassword() await removeAllDappConnectionsFromExtension() + /* oxlint-disable typescript/await-thenable -- biome-parity: oxlint is stricter here */ await dispatch(setIsTestnetModeEnabled(false)) await dispatch( editAccountActions.trigger({ + /* oxlint-enable typescript/await-thenable -- biome-parity: oxlint is stricter here */ type: EditAccountAction.Remove, accounts: accountsToRemove, }), diff --git a/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/RemoveRecoveryPhraseWallets.tsx b/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/RemoveRecoveryPhraseWallets.tsx index b452724e723..76bddd0d140 100644 --- a/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/RemoveRecoveryPhraseWallets.tsx +++ b/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/RemoveRecoveryPhraseWallets.tsx @@ -102,13 +102,7 @@ function AssociatedAccountRow({ py="$spacing12" > - + diff --git a/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/SeedPhraseDisplay.tsx b/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/SeedPhraseDisplay.tsx index d92beeb1737..2488bea6c65 100644 --- a/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/SeedPhraseDisplay.tsx +++ b/apps/extension/src/app/features/settings/SettingsRecoveryPhraseScreen/SeedPhraseDisplay.tsx @@ -6,7 +6,7 @@ import { Flex, Separator, Text } from 'ui/src' import { spacing } from 'ui/src/theme' import { WalletEventName } 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 { logger } from 'utilities/src/logger/logger' import { mnemonicUnlockedQuery } from 'wallet/src/features/wallet/Keyring/queries' @@ -112,15 +112,6 @@ export function SeedPhraseDisplay({ mnemonicId }: { mnemonicId: string }): JSX.E useEffect(() => { sendAnalyticsEvent(WalletEventName.ViewRecoveryPhrase) - - // Clear clipboard when the component unmounts - return () => { - navigator.clipboard.writeText('').catch((error) => { - logger.error(error, { - tags: { file: 'SeedPhraseDisplay.tsx', function: 'navigator.clipboard.writeText' }, - }) - }) - } }, []) return ( diff --git a/apps/extension/src/app/features/settings/SettingsScreen.tsx b/apps/extension/src/app/features/settings/SettingsScreen.tsx index 4dd4947e716..0e97a2afc5a 100644 --- a/apps/extension/src/app/features/settings/SettingsScreen.tsx +++ b/apps/extension/src/app/features/settings/SettingsScreen.tsx @@ -1,12 +1,14 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useDispatch, useSelector } from 'react-redux' +import { useDispatch } from 'react-redux' +import { useLocation } from 'react-router' import { ScreenHeader } from 'src/app/components/layout/ScreenHeader' import { SettingsItem } from 'src/app/features/settings/components/SettingsItem' import { SettingsSection } from 'src/app/features/settings/components/SettingsSection' import { SettingsToggleRow } from 'src/app/features/settings/components/SettingsToggleRow' import { SettingsItemWithDropdown } from 'src/app/features/settings/SettingsItemWithDropdown' -import ThemeToggle from 'src/app/features/settings/ThemeToggle' +import { ThemeToggleWithLabel } from 'src/app/features/settings/ThemeToggle' import { AppRoutes, SettingsRoutes } from 'src/app/navigation/constants' import { useExtensionNavigation } from 'src/app/navigation/utils' import { getIsDefaultProviderFromStorage, setIsDefaultProviderToStorage } from 'src/app/utils/provider' @@ -16,9 +18,8 @@ import { Chart, Coins, FileListLock, - Global, HelpCenter, - Language, + Language as LanguageIcon, LineChartDots, Lock, Passkey, @@ -31,29 +32,29 @@ import { resetUniswapBehaviorHistory } from 'uniswap/src/features/behaviorHistor import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { FiatCurrency, ORDERED_CURRENCIES } from 'uniswap/src/features/fiatCurrency/constants' import { getFiatCurrencyName, useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks' +import { Language, WALLET_SUPPORTED_LANGUAGES } from 'uniswap/src/features/language/constants' +import { getLanguageInfo, useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks' import { PasskeyManagementModal } from 'uniswap/src/features/passkey/PasskeyManagementModal' -import { setCurrentFiatCurrency, setIsTestnetModeEnabled } from 'uniswap/src/features/settings/slice' +import { + setCurrentFiatCurrency, + setCurrentLanguage, + setIsTestnetModeEnabled, +} from 'uniswap/src/features/settings/slice' import { WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import Trace from 'uniswap/src/features/telemetry/Trace' -import { ConnectionCardLoggingName } from 'uniswap/src/features/telemetry/types' import { TestnetModeModal } from 'uniswap/src/features/testnets/TestnetModeModal' +import { changeLanguage } from 'uniswap/src/i18n' +import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ExtensionScreens } from 'uniswap/src/types/screens/extension' import { isDevEnv } from 'utilities/src/environment/env' import { logger } from 'utilities/src/logger/logger' -import { noop } from 'utilities/src/react/noop' -import { CardType, IntroCard, IntroCardGraphicType } from 'wallet/src/components/introCards/IntroCard' -import { SettingsLanguageModal } from 'wallet/src/components/settings/language/SettingsLanguageModal' import { PermissionsModal } from 'wallet/src/components/settings/permissions/PermissionsModal' import { PortfolioBalanceModal } from 'wallet/src/components/settings/portfolioBalance/PortfolioBalanceModal' import { SmartWalletAdvancedSettingsModal } from 'wallet/src/components/smartWallet/modals/SmartWalletAdvancedSettingsModal' import { authActions } from 'wallet/src/features/auth/saga' import { AuthActionType } from 'wallet/src/features/auth/types' -import { selectHasViewedConnectionMigration } from 'wallet/src/features/behaviorHistory/selectors' -import { resetWalletBehaviorHistory, setHasViewedConnectionMigration } from 'wallet/src/features/behaviorHistory/slice' +import { resetWalletBehaviorHistory } from 'wallet/src/features/behaviorHistory/slice' import { BackupType } from 'wallet/src/features/wallet/accounts/types' import { hasBackup } from 'wallet/src/features/wallet/accounts/utils' import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' @@ -63,17 +64,16 @@ const manifestVersion = chrome.runtime.getManifest().version export function SettingsScreen(): JSX.Element { const { t } = useTranslation() const dispatch = useDispatch() + const location = useLocation() const { navigateTo, navigateBack } = useExtensionNavigation() const currentLanguageInfo = useCurrentLanguageInfo() const appFiatCurrencyInfo = useAppFiatCurrencyInfo() - const hasViewedConnectionMigration = useSelector(selectHasViewedConnectionMigration) const isSmartWalletEnabled = useFeatureFlag(FeatureFlags.SmartWalletSettings) const signerAccount = useSignerAccounts()[0] const hasPasskeyBackup = hasBackup(BackupType.Passkey, signerAccount) - const [isLanguageModalOpen, setIsLanguageModalOpen] = useState(false) const [isPortfolioBalanceModalOpen, setIsPortfolioBalanceModalOpen] = useState(false) const [isTestnetModalOpen, setIsTestnetModalOpen] = useState(false) const [isAdvancedModalOpen, setIsAdvancedModalOpen] = useState(false) @@ -81,8 +81,17 @@ export function SettingsScreen(): JSX.Element { const [isPasskeyModalOpen, setIsPasskeyModalOpen] = useState(false) const [isDefaultProvider, setIsDefaultProvider] = useState(true) + // Auto-open advanced settings modal if navigating with openAdvancedSettings state + useEffect(() => { + const state = location.state as { openAdvancedSettings?: boolean } | undefined + if (state?.openAdvancedSettings) { + setIsAdvancedModalOpen(true) + } + }, [location.state]) + const onPressLockWallet = async (): Promise => { navigateBack() + // oxlint-disable-next-line typescript/await-thenable -- biome-parity: oxlint is stricter here await dispatch(authActions.trigger({ type: AuthActionType.Lock })) } @@ -99,6 +108,7 @@ export function SettingsScreen(): JSX.Element { // trigger before toggling on (ie disabling analytics) if (isChecked) { // doesn't fire on time without await and i have no idea why + // oxlint-disable-next-line typescript/await-thenable -- biome-parity: oxlint is stricter here await fireAnalytic() } @@ -120,6 +130,11 @@ export function SettingsScreen(): JSX.Element { setIsAdvancedModalOpen(false) }, [navigateTo]) + const handleStoragePress = useCallback(() => { + navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.Storage}`) + setIsAdvancedModalOpen(false) + }, [navigateTo]) + useEffect(() => { getIsDefaultProviderFromStorage() .then((newIsDefaultProvider) => setIsDefaultProvider(newIsDefaultProvider)) @@ -135,15 +150,8 @@ export function SettingsScreen(): JSX.Element { await setIsDefaultProviderToStorage(!!isChecked) } - const setConnectionMigrationAsViewed = (): void => { - dispatch(setHasViewedConnectionMigration(true)) - } - return ( - {isLanguageModalOpen ? ( - setIsLanguageModalOpen(false)} /> - ) : undefined} {isPortfolioBalanceModalOpen ? ( {hasPasskeyBackup && ( )} - + { @@ -216,24 +225,27 @@ export function SettingsScreen(): JSX.Element { }} /> { + return { value: language, label: getLanguageInfo(t, language).displayName } + })} selected={currentLanguageInfo.displayName} title={t('settings.setting.language.title')} - onDisabledDropdownPress={() => { - setIsLanguageModalOpen(true) + onSelect={async (value) => { + const language = value as Language + await changeLanguage(getLanguageInfo(t, language).locale) + dispatch(setCurrentLanguage(language)) }} - onSelect={noop} /> setIsPortfolioBalanceModalOpen(true)} /> {isSmartWalletEnabled ? ( setIsAdvancedModalOpen(true)} /> @@ -246,29 +258,6 @@ export function SettingsScreen(): JSX.Element { /> )} - {!hasViewedConnectionMigration && ( - - { - setConnectionMigrationAsViewed() - }} - onClose={(): void => { - setConnectionMigrationAsViewed() - }} - /> - - )} appStateResetter.resetAccountHistory()) + const onPressClearUserSettings = useEvent(() => appStateResetter.resetUserSettings()) + const onPressClearCachedData = useEvent(() => appStateResetter.resetQueryCaches()) + const onPressClearAllData = useEvent(() => appStateResetter.resetAll()) + + return ( + + } /> + + + ) +} diff --git a/apps/extension/src/app/features/settings/ThemeToggle.tsx b/apps/extension/src/app/features/settings/ThemeToggle.tsx index bc9e8086f84..705ac710720 100644 --- a/apps/extension/src/app/features/settings/ThemeToggle.tsx +++ b/apps/extension/src/app/features/settings/ThemeToggle.tsx @@ -1,39 +1,11 @@ -import { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { useDispatch } from 'react-redux' import { SCREEN_ITEM_HORIZONTAL_PAD } from 'src/app/constants' -import { Flex, SegmentedControl, Text } from 'ui/src' -import { Contrast, Moon, Sun } from 'ui/src/components/icons' -import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks' -import { AppearanceSettingType, setSelectedAppearanceSettings } from 'wallet/src/features/appearance/slice' +import { Flex, Text } from 'ui/src' +import { Contrast } from 'ui/src/components/icons' +import { ThemeToggle } from 'uniswap/src/components/appearance/ThemeToggle' -export default function ThemeToggle(): JSX.Element { - const dispatch = useDispatch() +export function ThemeToggleWithLabel(): JSX.Element { const { t } = useTranslation() - const currentAppearanceSetting = useCurrentAppearanceSetting() - - const defaultOptions = [ - { - value: AppearanceSettingType.System, - display: ( - - {t('settings.setting.appearance.option.auto')} - - ), - }, - { - value: AppearanceSettingType.Light, - display: , - }, - { - value: AppearanceSettingType.Dark, - display: , - }, - ] - const switchMode = useCallback( - (mode: AppearanceSettingType) => dispatch(setSelectedAppearanceSettings(mode)), - [dispatch], - ) return ( {t('settings.setting.appearance.title')} - +
) diff --git a/apps/extension/src/app/features/settings/components/SettingsItem.tsx b/apps/extension/src/app/features/settings/components/SettingsItem.tsx index be41154c66d..65e850ee36c 100644 --- a/apps/extension/src/app/features/settings/components/SettingsItem.tsx +++ b/apps/extension/src/app/features/settings/components/SettingsItem.tsx @@ -1,7 +1,6 @@ import { Link } from 'react-router' import { ColorTokens, Flex, GeneratedIcon, Text, TouchableArea, useSporeColors } from 'ui/src' import { RotatableChevron } from 'ui/src/components/icons' -import { iconSizes } from 'ui/src/theme' export function SettingsItem({ Icon, @@ -13,6 +12,7 @@ export function SettingsItem({ count, hideChevron = false, RightIcon, + testID, }: { Icon: GeneratedIcon title: string @@ -24,6 +24,7 @@ export function SettingsItem({ themeProps?: { color?: string; hoverColor?: string } url?: string count?: number + testID?: string }): JSX.Element { const colors = useSporeColors() const hoverColor = themeProps?.hoverColor ?? colors.surface2.val @@ -41,6 +42,7 @@ export function SettingsItem({ justifyContent="space-between" px="$spacing12" py="$spacing8" + testID={testID} onPress={onPress} > @@ -64,9 +66,7 @@ export function SettingsItem({ {RightIcon ? ( ) : ( - !hideChevron && ( - - ) + !hideChevron && )} ) diff --git a/apps/extension/src/app/features/settings/password/ChangePasswordForm.test.tsx b/apps/extension/src/app/features/settings/password/ChangePasswordForm.test.tsx new file mode 100644 index 00000000000..f4721fb4899 --- /dev/null +++ b/apps/extension/src/app/features/settings/password/ChangePasswordForm.test.tsx @@ -0,0 +1,166 @@ +import { act, fireEvent, waitFor } from '@testing-library/react' +import { ChangePasswordForm } from 'src/app/features/settings/password/ChangePasswordForm' +import { cleanup, render, screen } from 'src/test/test-utils' +import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' + +// Mock the Keyring +jest.mock('wallet/src/features/wallet/Keyring/Keyring', () => ({ + Keyring: { + changePassword: jest.fn().mockResolvedValue(undefined), + }, +})) + +// Mock analytics +jest.mock('uniswap/src/features/telemetry/send', () => ({ + sendAnalyticsEvent: jest.fn(), +})) + +const mockChangePassword = Keyring.changePassword as jest.MockedFunction + +describe('ChangePasswordForm', () => { + const mockOnNext = jest.fn() + const oldPassword = 'MyOldPassword123!' + + beforeEach(() => { + jest.clearAllMocks() + mockChangePassword.mockClear() + }) + + afterEach(() => { + cleanup() + }) + + it('renders without error', () => { + const tree = render() + expect(tree).toMatchSnapshot() + }) + + it('renders password input fields', () => { + render() + + // Check for translated placeholders + expect(screen.getByPlaceholderText('New password')).toBeDefined() + expect(screen.getByPlaceholderText('Confirm password')).toBeDefined() + }) + + it('renders continue button', () => { + render() + + expect(screen.getByText('Continue')).toBeDefined() + }) + + it('shows error when new password matches old password', async () => { + render() + + const newPasswordInput = screen.getByPlaceholderText('New password') + const confirmPasswordInput = screen.getByPlaceholderText('Confirm password') + + // Type the same password as the old password + act(() => { + fireEvent.change(newPasswordInput, { target: { value: oldPassword } }) + fireEvent.change(confirmPasswordInput, { target: { value: oldPassword } }) + }) + + // Wait for error to appear + await waitFor(() => { + const errorText = screen.getByText('New password must be different from current password') + expect(errorText).toBeDefined() + }) + }) + + it('clears error when password changes to be different', async () => { + render() + + const newPasswordInput = screen.getByPlaceholderText('New password') + const confirmPasswordInput = screen.getByPlaceholderText('Confirm password') + + // First, type the same password as old password + act(() => { + fireEvent.change(newPasswordInput, { target: { value: oldPassword } }) + fireEvent.change(confirmPasswordInput, { target: { value: oldPassword } }) + }) + + // Wait for error to appear + await waitFor(() => { + expect(screen.getByText('New password must be different from current password')).toBeDefined() + }) + + // Clear and type a different password + act(() => { + fireEvent.change(newPasswordInput, { target: { value: 'DifferentPassword789!' } }) + fireEvent.change(confirmPasswordInput, { target: { value: 'DifferentPassword789!' } }) + }) + + // Error should be cleared + await waitFor(() => { + expect(screen.queryByText('New password must be different from current password')).toBeNull() + }) + }) + + it('does not call onNext when passwords match old password', async () => { + render() + + const newPasswordInput = screen.getByPlaceholderText('New password') + const confirmPasswordInput = screen.getByPlaceholderText('Confirm password') + const submitButton = screen.getByText('Continue') + + // Type the same password as the old password + act(() => { + fireEvent.change(newPasswordInput, { target: { value: oldPassword } }) + fireEvent.change(confirmPasswordInput, { target: { value: oldPassword } }) + }) + + // Try to submit + await act(async () => { + fireEvent.click(submitButton) + }) + + expect(mockOnNext).not.toHaveBeenCalled() + expect(mockChangePassword).not.toHaveBeenCalled() + }) + + it('calls onNext and changePassword when passwords are different and valid', async () => { + render() + + const newPasswordInput = screen.getByPlaceholderText('New password') + const confirmPasswordInput = screen.getByPlaceholderText('Confirm password') + const submitButton = screen.getByText('Continue') + + const newPassword = 'MyNewStrongPassword456!' + + // Type a different password + act(() => { + fireEvent.change(newPasswordInput, { target: { value: newPassword } }) + fireEvent.change(confirmPasswordInput, { target: { value: newPassword } }) + }) + + // Submit the form + await act(async () => { + fireEvent.click(submitButton) + }) + + await waitFor(() => { + expect(mockChangePassword).toHaveBeenCalledWith(newPassword) + expect(mockOnNext).toHaveBeenCalledWith(newPassword) + }) + }) + + it('handles undefined oldPassword gracefully', async () => { + render() + + const newPasswordInput = screen.getByPlaceholderText('New password') + const confirmPasswordInput = screen.getByPlaceholderText('Confirm password') + + const newPassword = 'AnyPassword123!' + + // Type any password - should not show "same password" error since oldPassword is undefined + act(() => { + fireEvent.change(newPasswordInput, { target: { value: newPassword } }) + fireEvent.change(confirmPasswordInput, { target: { value: newPassword } }) + }) + + await waitFor(() => { + expect(screen.queryByText('New password must be different from current password')).toBeNull() + }) + }) +}) diff --git a/apps/extension/src/app/features/settings/password/ChangePasswordForm.tsx b/apps/extension/src/app/features/settings/password/ChangePasswordForm.tsx index c03b41a1cac..d82b56ba129 100644 --- a/apps/extension/src/app/features/settings/password/ChangePasswordForm.tsx +++ b/apps/extension/src/app/features/settings/password/ChangePasswordForm.tsx @@ -1,13 +1,19 @@ -import { useCallback } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { PADDING_STRENGTH_INDICATOR, PasswordInput } from 'src/app/components/PasswordInput' import { Button, Flex, Text } from 'ui/src' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' -import { usePasswordForm } from 'wallet/src/utils/password' +import { PasswordErrors, usePasswordForm } from 'wallet/src/utils/password' -export function ChangePasswordForm({ onNext }: { onNext: (password: string) => void }): JSX.Element { +export function ChangePasswordForm({ + oldPassword, + onNext, +}: { + oldPassword: string | undefined + onNext: (password: string) => void +}): JSX.Element { const { t } = useTranslation() const { @@ -20,18 +26,45 @@ export function ChangePasswordForm({ onNext }: { onNext: (password: string) => v confirmPassword, onChangeConfirmPassword, setHideInput, - errorText, + errorText: baseErrorText, checkSubmit, } = usePasswordForm() + const [customError, setCustomError] = useState(undefined) + + // Check if new password is same as old password + const isSamePassword = useMemo( + () => Boolean(password && oldPassword && password === oldPassword), + [password, oldPassword], + ) + + // Update custom error when password matches old password + useEffect(() => { + setCustomError(isSamePassword ? PasswordErrors.SamePassword : undefined) + }, [isSamePassword]) + + // Override error text if custom error exists + const errorText = useMemo(() => { + if (customError === PasswordErrors.SamePassword) { + return t('common.input.password.error.same') + } + return baseErrorText + }, [customError, baseErrorText, t]) + const onSubmit = useCallback(async () => { + // Check for same password error + if (isSamePassword) { + setCustomError(PasswordErrors.SamePassword) + return + } + if (checkSubmit()) { // Just change the password and pass it to the parent await Keyring.changePassword(password) sendAnalyticsEvent(ExtensionEventName.PasswordChanged) onNext(password) } - }, [checkSubmit, password, onNext]) + }, [checkSubmit, password, onNext, isSamePassword]) return ( @@ -70,7 +103,7 @@ export function ChangePasswordForm({ onNext }: { onNext: (password: string) => v - diff --git a/apps/extension/src/app/features/settings/password/CreateNewPasswordModal.tsx b/apps/extension/src/app/features/settings/password/CreateNewPasswordModal.tsx index 65860afaa14..9411c1fa006 100644 --- a/apps/extension/src/app/features/settings/password/CreateNewPasswordModal.tsx +++ b/apps/extension/src/app/features/settings/password/CreateNewPasswordModal.tsx @@ -7,10 +7,12 @@ import { ModalName } from 'uniswap/src/features/telemetry/constants' export function CreateNewPasswordModal({ isOpen, + oldPassword, onNext, onClose, }: { isOpen: boolean + oldPassword: string | undefined onNext: (password: string) => void onClose: () => void }): JSX.Element { @@ -36,7 +38,7 @@ export function CreateNewPasswordModal({ {t('settings.setting.password.change.title')} - + ) diff --git a/apps/extension/src/app/features/settings/password/__snapshots__/ChangePasswordForm.test.tsx.snap b/apps/extension/src/app/features/settings/password/__snapshots__/ChangePasswordForm.test.tsx.snap new file mode 100644 index 00000000000..2c5a5aea32f --- /dev/null +++ b/apps/extension/src/app/features/settings/password/__snapshots__/ChangePasswordForm.test.tsx.snap @@ -0,0 +1,281 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChangePasswordForm renders without error 1`] = ` +{ + "asFragment": [Function], + "baseElement": +
+ +
+
+
+ + + +
+
+ + + +
+ + + +
+
+ +
+
+ +
+
+
+ +
+ , + "container":
+ +
+
+
+ + + +
+
+ + + +
+ + + +
+
+ +
+
+ +
+
+
+ +
, + "debug": [Function], + "findAllByAltText": [Function], + "findAllByDisplayValue": [Function], + "findAllByLabelText": [Function], + "findAllByPlaceholderText": [Function], + "findAllByRole": [Function], + "findAllByTestId": [Function], + "findAllByText": [Function], + "findAllByTitle": [Function], + "findByAltText": [Function], + "findByDisplayValue": [Function], + "findByLabelText": [Function], + "findByPlaceholderText": [Function], + "findByRole": [Function], + "findByTestId": [Function], + "findByText": [Function], + "findByTitle": [Function], + "getAllByAltText": [Function], + "getAllByDisplayValue": [Function], + "getAllByLabelText": [Function], + "getAllByPlaceholderText": [Function], + "getAllByRole": [Function], + "getAllByTestId": [Function], + "getAllByText": [Function], + "getAllByTitle": [Function], + "getByAltText": [Function], + "getByDisplayValue": [Function], + "getByLabelText": [Function], + "getByPlaceholderText": [Function], + "getByRole": [Function], + "getByTestId": [Function], + "getByText": [Function], + "getByTitle": [Function], + "queryAllByAltText": [Function], + "queryAllByDisplayValue": [Function], + "queryAllByLabelText": [Function], + "queryAllByPlaceholderText": [Function], + "queryAllByRole": [Function], + "queryAllByTestId": [Function], + "queryAllByText": [Function], + "queryAllByTitle": [Function], + "queryByAltText": [Function], + "queryByDisplayValue": [Function], + "queryByLabelText": [Function], + "queryByPlaceholderText": [Function], + "queryByRole": [Function], + "queryByTestId": [Function], + "queryByText": [Function], + "queryByTitle": [Function], + "rerender": [Function], + "store": { + "dispatch": [Function], + "getState": [Function], + "replaceReducer": [Function], + "subscribe": [Function], + Symbol(observable): [Function], + }, + "unmount": [Function], +} +`; diff --git a/apps/extension/src/app/features/settings/password/usePasswordResetFlow.test.ts b/apps/extension/src/app/features/settings/password/usePasswordResetFlow.test.ts index 9ccb4c71a73..3685be9bde1 100644 --- a/apps/extension/src/app/features/settings/password/usePasswordResetFlow.test.ts +++ b/apps/extension/src/app/features/settings/password/usePasswordResetFlow.test.ts @@ -67,6 +67,12 @@ describe('usePasswordResetFlow', () => { expect(result.current.flowState).toBe(PasswordResetFlowState.None) }) + it('should initialize with undefined oldPassword', () => { + const { result } = renderHook(() => usePasswordResetFlow()) + + expect(result.current.oldPassword).toBeUndefined() + }) + it('should transition to EnterCurrentPassword when starting password reset', () => { const { result } = renderHook(() => usePasswordResetFlow()) @@ -91,6 +97,22 @@ describe('usePasswordResetFlow', () => { expect(result.current.flowState).toBe(PasswordResetFlowState.EnterNewPassword) }) + it('should store oldPassword when valid password is provided', () => { + const { result } = renderHook(() => usePasswordResetFlow()) + const testPassword = 'myOldPassword123!' + + act(() => { + result.current.startPasswordReset() + }) + + act(() => { + result.current.onPasswordModalNext(testPassword) + }) + + expect(result.current.oldPassword).toBe(testPassword) + expect(result.current.flowState).toBe(PasswordResetFlowState.EnterNewPassword) + }) + it('should return to None state when no password is provided', () => { const { result } = renderHook(() => usePasswordResetFlow()) @@ -105,6 +127,32 @@ describe('usePasswordResetFlow', () => { expect(result.current.flowState).toBe(PasswordResetFlowState.None) }) + it('should clear oldPassword when no password is provided', () => { + const { result } = renderHook(() => usePasswordResetFlow()) + + act(() => { + result.current.startPasswordReset() + }) + + act(() => { + result.current.onPasswordModalNext('validPassword') + }) + + expect(result.current.oldPassword).toBe('validPassword') + + // Go back and provide no password + act(() => { + result.current.startPasswordReset() + }) + + act(() => { + result.current.onPasswordModalNext() + }) + + expect(result.current.oldPassword).toBeUndefined() + expect(result.current.flowState).toBe(PasswordResetFlowState.None) + }) + it('should transition to BiometricAuth when biometric is enabled', () => { mockUseHasBiometricUnlockCredential.mockReturnValue(true) const { result } = renderHook(() => usePasswordResetFlow()) @@ -160,6 +208,28 @@ describe('usePasswordResetFlow', () => { expect(result.current.flowState).toBe(PasswordResetFlowState.None) }) + it('should clear oldPassword when closeModal is called with matching state', () => { + const { result } = renderHook(() => usePasswordResetFlow()) + + act(() => { + result.current.startPasswordReset() + }) + + act(() => { + result.current.onPasswordModalNext('testPassword123') + }) + + expect(result.current.oldPassword).toBe('testPassword123') + expect(result.current.flowState).toBe(PasswordResetFlowState.EnterNewPassword) + + act(() => { + result.current.closeModal(PasswordResetFlowState.EnterNewPassword) + }) + + expect(result.current.flowState).toBe(PasswordResetFlowState.None) + expect(result.current.oldPassword).toBeUndefined() + }) + it('should not close modal when closeModal is called with non-matching state', () => { const { result } = renderHook(() => usePasswordResetFlow()) @@ -176,6 +246,29 @@ describe('usePasswordResetFlow', () => { expect(result.current.flowState).toBe(PasswordResetFlowState.EnterCurrentPassword) }) + it('should not clear oldPassword when closeModal is called with non-matching state', () => { + const { result } = renderHook(() => usePasswordResetFlow()) + + act(() => { + result.current.startPasswordReset() + }) + + act(() => { + result.current.onPasswordModalNext('testPassword456') + }) + + expect(result.current.oldPassword).toBe('testPassword456') + expect(result.current.flowState).toBe(PasswordResetFlowState.EnterNewPassword) + + act(() => { + result.current.closeModal(PasswordResetFlowState.BiometricAuth) + }) + + // Should not clear oldPassword or change state + expect(result.current.flowState).toBe(PasswordResetFlowState.EnterNewPassword) + expect(result.current.oldPassword).toBe('testPassword456') + }) + it('should transition to BiometricAuth state when biometric is enabled and trigger internal mutation', () => { mockUseHasBiometricUnlockCredential.mockReturnValue(true) const { result } = renderHook(() => usePasswordResetFlow()) diff --git a/apps/extension/src/app/features/settings/password/usePasswordResetFlow.ts b/apps/extension/src/app/features/settings/password/usePasswordResetFlow.ts index 48e02692ef5..9c2b61b4eda 100644 --- a/apps/extension/src/app/features/settings/password/usePasswordResetFlow.ts +++ b/apps/extension/src/app/features/settings/password/usePasswordResetFlow.ts @@ -54,6 +54,7 @@ export enum PasswordResetFlowState { interface PasswordResetFlowResult { // State flowState: PasswordResetFlowState + oldPassword: string | undefined // Actions startPasswordReset: () => void @@ -68,6 +69,7 @@ interface PasswordResetFlowResult { export function usePasswordResetFlow(): PasswordResetFlowResult { const dispatch = useDispatch() const [flowState, setFlowState] = useState(PasswordResetFlowState.None) + const [oldPassword, setOldPassword] = useState(undefined) const hasBiometricUnlockCredential = useHasBiometricUnlockCredential() @@ -95,15 +97,18 @@ export function usePasswordResetFlow(): PasswordResetFlowResult { // This check ensures the close action is from user interaction, not from modal state changes. if (flowState === expectedState) { setFlowState(PasswordResetFlowState.None) + setOldPassword(undefined) } }) const onPasswordModalNext = useEvent((password?: string): void => { if (!password) { setFlowState(PasswordResetFlowState.None) + setOldPassword(undefined) return } + setOldPassword(password) setFlowState(PasswordResetFlowState.EnterNewPassword) }) @@ -127,6 +132,7 @@ export function usePasswordResetFlow(): PasswordResetFlowResult { return { // State flowState, + oldPassword, // Actions startPasswordReset, diff --git a/apps/extension/src/app/features/settings/stores/hashcashBenchmarkStore.ts b/apps/extension/src/app/features/settings/stores/hashcashBenchmarkStore.ts new file mode 100644 index 00000000000..5c905ac15e7 --- /dev/null +++ b/apps/extension/src/app/features/settings/stores/hashcashBenchmarkStore.ts @@ -0,0 +1,118 @@ +import { create } from 'zustand' + +export type Implementation = 'worker' | 'multi-worker' | 'js' | 'all' + +export interface BenchmarkResult { + implementation: 'worker' | 'multi-worker' | 'js' + difficulty: number + counter: string | null + attempts: number + timeMs: number + hashRate: number +} + +interface BenchmarkProgress { + isRunning: boolean + currentImpl: 'worker' | 'multi-worker' | 'js' | null + difficulty: number + startTime: number | null + elapsedMs: number + estimatedAttempts: number +} + +export interface LogEntry { + timestamp: Date + message: string + type: 'info' | 'success' | 'error' +} + +interface HashcashBenchmarkState { + // State + results: BenchmarkResult[] + selectedDifficulty: number + selectedImpl: Implementation + logs: LogEntry[] + progress: BenchmarkProgress + measuredHashRate: number | null + isCancelled: boolean + + // Actions + setDifficulty: (difficulty: number) => void + setImpl: (impl: Implementation) => void + addResult: (result: BenchmarkResult) => void + clearResults: () => void + addLog: (message: string, type?: 'info' | 'success' | 'error') => void + clearLogs: () => void + startBenchmark: (impl: 'worker' | 'multi-worker' | 'js', difficulty: number) => void + updateProgress: (elapsedMs: number, estimatedAttempts: number) => void + endBenchmark: () => void + cancel: () => void + resetCancel: () => void +} + +const initialProgress: BenchmarkProgress = { + isRunning: false, + currentImpl: null, + difficulty: 0, + startTime: null, + elapsedMs: 0, + estimatedAttempts: 0, +} + +export const useHashcashBenchmarkStore = create((set) => ({ + results: [], + selectedDifficulty: 2, + selectedImpl: 'all', + logs: [], + progress: initialProgress, + measuredHashRate: null, + isCancelled: false, + + setDifficulty: (difficulty) => set({ selectedDifficulty: difficulty }), + + setImpl: (impl) => set({ selectedImpl: impl }), + + addResult: (result) => + set((state) => ({ + results: [...state.results, result], + // Auto-update measured hash rate from worker/multi-worker results + measuredHashRate: + (result.implementation === 'worker' || result.implementation === 'multi-worker') && result.hashRate > 0 + ? result.hashRate + : state.measuredHashRate, + })), + + clearResults: () => set({ results: [] }), + + addLog: (message, type = 'info') => + set((state) => ({ + logs: [...state.logs.slice(-19), { timestamp: new Date(), message, type }], + })), + + clearLogs: () => set({ logs: [] }), + + startBenchmark: (impl, difficulty) => + set({ + isCancelled: false, + progress: { + isRunning: true, + currentImpl: impl, + difficulty, + // Worker/multi-worker runs async so we can track progress, JS blocks the thread + startTime: impl === 'worker' || impl === 'multi-worker' ? performance.now() : null, + elapsedMs: 0, + estimatedAttempts: 0, + }, + }), + + updateProgress: (elapsedMs, estimatedAttempts) => + set((state) => ({ + progress: { ...state.progress, elapsedMs, estimatedAttempts }, + })), + + endBenchmark: () => set({ progress: initialProgress }), + + cancel: () => set({ isCancelled: true, progress: initialProgress }), + + resetCancel: () => set({ isCancelled: false }), +})) diff --git a/apps/extension/src/app/features/settings/stores/sessionsDebugStore.ts b/apps/extension/src/app/features/settings/stores/sessionsDebugStore.ts new file mode 100644 index 00000000000..d34ef2b3deb --- /dev/null +++ b/apps/extension/src/app/features/settings/stores/sessionsDebugStore.ts @@ -0,0 +1,112 @@ +import type { ChallengeResponse, HashcashSolveAnalytics } from '@universe/sessions' +import { create } from 'zustand' + +interface SessionState { + sessionId: string | null + deviceId: string | null + uniswapIdentifier: string | null +} + +export interface LogEntry { + timestamp: Date + message: string + type: 'info' | 'success' | 'error' +} + +interface HashcashProgress { + isRunning: boolean + difficulty: number + estimatedAttempts: number + elapsedMs: number + startTime: number | null + actualResult: HashcashSolveAnalytics | null +} + +interface SessionsDebugState { + // State + session: SessionState + challenge: ChallengeResponse | null + isLoading: boolean + currentOperation: string | null + logs: LogEntry[] + hashcashProgress: HashcashProgress + + // Actions + setSession: (session: SessionState) => void + setChallenge: (challenge: ChallengeResponse | null) => void + startOperation: (operation: string) => void + endOperation: () => void + addLog: (message: string, type?: 'info' | 'success' | 'error') => void + clearLogs: () => void + startHashcash: (difficulty: number) => void + updateHashcashProgress: (elapsedMs: number, estimatedAttempts: number) => void + completeHashcash: (result: HashcashSolveAnalytics) => void + stopHashcash: () => void + reset: () => void +} + +const initialHashcashProgress: HashcashProgress = { + isRunning: false, + difficulty: 0, + estimatedAttempts: 0, + elapsedMs: 0, + startTime: null, + actualResult: null, +} + +const initialState = { + session: { sessionId: null, deviceId: null, uniswapIdentifier: null }, + challenge: null, + isLoading: false, + currentOperation: null, + logs: [] as LogEntry[], + hashcashProgress: initialHashcashProgress, +} + +export const useSessionsDebugStore = create((set) => ({ + ...initialState, + + setSession: (session) => set({ session }), + + setChallenge: (challenge) => set({ challenge }), + + startOperation: (operation) => set({ isLoading: true, currentOperation: operation }), + + endOperation: () => set({ isLoading: false, currentOperation: null }), + + addLog: (message, type = 'info') => + set((state) => ({ + logs: [...state.logs.slice(-19), { timestamp: new Date(), message, type }], + })), + + clearLogs: () => set({ logs: [] }), + + startHashcash: (difficulty) => + set({ + hashcashProgress: { + isRunning: true, + difficulty, + estimatedAttempts: 0, + elapsedMs: 0, + startTime: performance.now(), + actualResult: null, + }, + }), + + updateHashcashProgress: (elapsedMs, estimatedAttempts) => + set((state) => ({ + hashcashProgress: { ...state.hashcashProgress, elapsedMs, estimatedAttempts }, + })), + + completeHashcash: (result) => + set((state) => ({ + hashcashProgress: { ...state.hashcashProgress, isRunning: false, actualResult: result }, + })), + + stopHashcash: () => + set((state) => ({ + hashcashProgress: { ...state.hashcashProgress, isRunning: false }, + })), + + reset: () => set(initialState), +})) diff --git a/apps/extension/src/app/features/swap/SwapFlowScreen.tsx b/apps/extension/src/app/features/swap/SwapFlowScreen.tsx index 0521dd17a17..83ab433e9b0 100644 --- a/apps/extension/src/app/features/swap/SwapFlowScreen.tsx +++ b/apps/extension/src/app/features/swap/SwapFlowScreen.tsx @@ -4,7 +4,8 @@ import { useExtensionNavigation } from 'src/app/navigation/utils' import { Flex } from 'ui/src' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useHighestBalanceNativeCurrencyId } from 'uniswap/src/features/dataApi/balances/balances' -import { clearNotificationQueue } from 'uniswap/src/features/notifications/slice/slice' +import { clearNotificationsByType } from 'uniswap/src/features/notifications/slice/slice' +import { AppNotificationType } from 'uniswap/src/features/notifications/slice/types' import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/form/hooks/useSwapPrefilledState' import { selectFilteredChainIds } from 'uniswap/src/features/transactions/swap/state/selectors' import { prepareSwapFormState, TransactionState } from 'uniswap/src/features/transactions/types/transactionState' @@ -12,7 +13,7 @@ import { CurrencyField } from 'uniswap/src/types/currency' import { logger } from 'utilities/src/logger/logger' import { WalletSwapFlow } from 'wallet/src/features/transactions/swap/WalletSwapFlow' import { invalidateAndRefetchWalletDelegationQueries } from 'wallet/src/features/transactions/watcher/transactionFinalizationSaga' -import { useActiveAccountWithThrow, useSignerAccounts } from 'wallet/src/features/wallet/hooks' +import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' export function SwapFlowScreen(): JSX.Element { const dispatch = useDispatch() @@ -31,16 +32,12 @@ export function SwapFlowScreen(): JSX.Element { filteredChainIdsOverride: ignorePersistedFilteredChainIds ? undefined : persistedFilteredChainIds, }) - const signerMnemonicAccounts = useSignerAccounts() - const chains = useEnabledChains() - const accountAddresses = signerMnemonicAccounts.map((account) => account.address) - // Update flow start timestamp every time modal is opened for logging useEffect(() => { - invalidateAndRefetchWalletDelegationQueries({ accountAddresses, chainIds: chains.chains }).catch((error) => + invalidateAndRefetchWalletDelegationQueries().catch((error) => logger.debug('SwapFlowScreen', 'useEffect', 'Failed to invalidate and refetch wallet delegation queries', error), ) - }, [accountAddresses, chains.chains]) + }, []) const preservedTransactionStateRef = useRef(null) const initialTransactionState = useMemo(() => { @@ -61,9 +58,13 @@ export function SwapFlowScreen(): JSX.Element { const swapPrefilledState = useSwapPrefilledState(initialTransactionState) - // Clear all notification toasts when the swap flow closes + // Clear network change notification toasts when the swap flow closes const onClose = useCallback(() => { - dispatch(clearNotificationQueue()) + dispatch( + clearNotificationsByType({ + types: [AppNotificationType.NetworkChanged, AppNotificationType.NetworkChangedBridge], + }), + ) navigateBack() }, [dispatch, navigateBack]) diff --git a/apps/extension/src/app/features/unitags/EditUnitagProfileScreen.tsx b/apps/extension/src/app/features/unitags/EditUnitagProfileScreen.tsx index 40513a6d0bf..e01f9789c9e 100644 --- a/apps/extension/src/app/features/unitags/EditUnitagProfileScreen.tsx +++ b/apps/extension/src/app/features/unitags/EditUnitagProfileScreen.tsx @@ -8,11 +8,12 @@ import { backgroundToSidePanelMessageChannel } from 'src/background/messagePassi import { BackgroundToSidePanelRequestType } from 'src/background/messagePassing/types/requests' import { AnimatePresence, Flex } from 'ui/src' import { Edit, Ellipsis, Trash } from 'ui/src/components/icons' +import { ContextMenu, MenuOptionItem } from 'uniswap/src/components/menus/ContextMenu' +import { ContextMenuTriggerMode } from 'uniswap/src/components/menus/types' import { useUnitagsAddressQuery } from 'uniswap/src/data/apiClients/unitagsApi/useUnitagsAddressQuery' import Trace from 'uniswap/src/features/telemetry/Trace' import { UnitagScreens } from 'uniswap/src/types/screens/mobile' -import { ContextMenu } from 'wallet/src/components/menu/ContextMenu' -import { MenuContentItem } from 'wallet/src/components/menu/types' +import { useBooleanState } from 'utilities/src/react/useBooleanState' import { ChangeUnitagModal } from 'wallet/src/features/unitags/ChangeUnitagModal' import { DeleteUnitagModal } from 'wallet/src/features/unitags/DeleteUnitagModal' import { EditUnitagProfileContent } from 'wallet/src/features/unitags/EditUnitagProfileContent' @@ -40,8 +41,9 @@ export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: b const [showDeleteUnitagModal, setShowDeleteUnitagModal] = useState(false) const [showChangeUnitagModal, setShowChangeUnitagModal] = useState(false) + const { value: isMenuOpen, setTrue: openMenu, setFalse: closeMenu } = useBooleanState(false) - const menuOptions = useMemo((): MenuContentItem[] => { + const menuOptions = useMemo((): MenuOptionItem[] => { return [ { label: t('unitags.profile.action.edit'), @@ -69,7 +71,13 @@ export function EditUnitagProfileScreen({ enableBack = false }: { enableBack?: b noTopPadding title={t('settings.setting.wallet.action.editProfile')} endAdornment={ - + diff --git a/apps/extension/src/app/hooks/useIsExtensionPasskeyImportEnabled.ts b/apps/extension/src/app/hooks/useIsExtensionPasskeyImportEnabled.ts index 8abf941051b..823ac6ba868 100644 --- a/apps/extension/src/app/hooks/useIsExtensionPasskeyImportEnabled.ts +++ b/apps/extension/src/app/hooks/useIsExtensionPasskeyImportEnabled.ts @@ -1,5 +1,4 @@ -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' export function useIsExtensionPasskeyImportEnabled(): boolean { return useFeatureFlag(FeatureFlags.EmbeddedWallet) diff --git a/apps/extension/src/app/hooks/useIsWalletUnlocked.ts b/apps/extension/src/app/hooks/useIsWalletUnlocked.ts index de24439d68b..4f6b2efe3a9 100644 --- a/apps/extension/src/app/hooks/useIsWalletUnlocked.ts +++ b/apps/extension/src/app/hooks/useIsWalletUnlocked.ts @@ -53,6 +53,7 @@ export function useIsWalletUnlocked(): boolean | null { }, [checkWalletStatus]) useEffect(() => { + // oxlint-disable-next-line typescript/no-floating-promises -- biome-parity: oxlint is stricter here checkWalletStatus() }, [checkWalletStatus]) diff --git a/apps/extension/src/app/hooks/useOpeningKeyboardShortCut.ts b/apps/extension/src/app/hooks/useOpeningKeyboardShortCut.ts index f6852ad245c..839910b6d30 100644 --- a/apps/extension/src/app/hooks/useOpeningKeyboardShortCut.ts +++ b/apps/extension/src/app/hooks/useOpeningKeyboardShortCut.ts @@ -15,7 +15,7 @@ export enum State { type ReducerAction = { type: 'keyUp' | 'keyDown' | 'highlight'; key: string } | { type: 'highlight' } export const useOpeningKeyboardShortCut = (shortCutPressed: boolean): KeyboardKeyProps[] => { - // eslint-disable-next-line consistent-return + // oxlint-disable-next-line consistent-return const reducer = (state: KeyboardKeyProps[], action: ReducerAction): KeyboardKeyProps[] => { switch (action.type) { case 'keyDown': diff --git a/apps/extension/src/app/hooks/useSagaStatus.ts b/apps/extension/src/app/hooks/useSagaStatus.ts deleted file mode 100644 index efbe9d67b7d..00000000000 --- a/apps/extension/src/app/hooks/useSagaStatus.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { useEffect } from 'react' -import { useDispatch, useSelector } from 'react-redux' -import { monitoredSagas } from 'src/app/saga' -import { ExtensionState } from 'src/store/extensionReducer' -import { SagaState, SagaStatus } from 'wallet/src/utils/saga' - -// Convenience hook to get the status + error of an active saga -export function useSagaStatus({ - sagaName, - onSuccess, - resetSagaOnSuccess = true, -}: { - sagaName: string - onSuccess?: () => void - resetSagaOnSuccess?: boolean -}): SagaState { - const dispatch = useDispatch() - const sagaState = useSelector((s: ExtensionState): SagaState | undefined => s.saga[sagaName]) - if (!sagaState) { - throw new Error(`No saga state found, is sagaName valid? Name: ${sagaName}`) - } - - const saga = monitoredSagas[sagaName] - if (!saga) { - throw new Error(`No saga found, is sagaName valid? Name: ${sagaName}`) - } - - const { status, error } = sagaState - - // biome-ignore lint/correctness/useExhaustiveDependencies: error is tracked for saga state changes even if not directly used in effect body - useEffect(() => { - if (status === SagaStatus.Success) { - if (resetSagaOnSuccess) { - dispatch(saga.actions.reset()).catch(() => undefined) - } - onSuccess?.() - } - }, [saga, status, error, onSuccess, resetSagaOnSuccess, dispatch]) - - useEffect(() => { - return () => { - if (resetSagaOnSuccess) { - dispatch(saga.actions.reset()).catch(() => undefined) - } - } - }, [saga, resetSagaOnSuccess, dispatch]) - - return sagaState -} diff --git a/apps/extension/src/app/navigation/constants.ts b/apps/extension/src/app/navigation/constants.ts index f086442eae5..5b50dec4e55 100644 --- a/apps/extension/src/app/navigation/constants.ts +++ b/apps/extension/src/app/navigation/constants.ts @@ -40,9 +40,12 @@ export enum SettingsRoutes { BiometricUnlockSetUp = 'biometric-unlock-set-up', DevMenu = 'dev-menu', DeviceAccess = 'device-access', + HashcashBenchmark = 'hashcash-benchmark', ManageConnections = 'manage-connections', RemoveRecoveryPhrase = 'remove-recovery-phrase', + SessionsDebug = 'sessions-debug', SmartWallet = 'smart-wallet', + Storage = 'storage', ViewRecoveryPhrase = 'view-recovery-phrase', } diff --git a/apps/extension/src/app/navigation/navigation.tsx b/apps/extension/src/app/navigation/navigation.tsx index 7c0848212a5..3575212028c 100644 --- a/apps/extension/src/app/navigation/navigation.tsx +++ b/apps/extension/src/app/navigation/navigation.tsx @@ -4,12 +4,12 @@ import { useSelector } from 'react-redux' import { NavigationType, Outlet, ScrollRestoration, useLocation } from 'react-router' import { AutoLockProvider } from 'src/app/components/AutoLockProvider' import { SmartWalletNudgeModals } from 'src/app/components/modals/SmartWalletNudgeModals' +import { SmartWalletNudgesProvider } from 'src/app/context/SmartWalletNudgesContext' import { DappRequestQueue } from 'src/app/features/dappRequests/DappRequestQueue' import { ForceUpgradeModal } from 'src/app/features/forceUpgrade/ForceUpgradeModal' import { HomeScreen } from 'src/app/features/home/HomeScreen' import { Locked } from 'src/app/features/lockScreen/Locked' import { NotificationToastWrapper } from 'src/app/features/notifications/NotificationToastWrapper' -import { StorageWarningModal } from 'src/app/features/warnings/StorageWarningModal' import { useIsWalletUnlocked } from 'src/app/hooks/useIsWalletUnlocked' import { AppRoutes } from 'src/app/navigation/constants' import { focusOrCreateOnboardingTab } from 'src/app/navigation/focusOrCreateOnboardingTab' @@ -20,8 +20,10 @@ import { isOnboardedSelector } from 'src/app/utils/isOnboardedSelector' import { AnimatePresence, Flex, SpinningLoader, styled } from 'ui/src' import { TestnetModeBanner } from 'uniswap/src/components/banners/TestnetModeBanner' import { useIsChromeWindowFocusedWithTimeout } from 'uniswap/src/extension/useIsChromeWindowFocused' +import { TokenPriceProvider } from 'uniswap/src/features/prices/TokenPriceContext' import { useEvent, usePrevious } from 'utilities/src/react/hooks' import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { AccountsStoreContextProvider } from 'wallet/src/features/accounts/store/provider' import { useHeartbeatReporter } from 'wallet/src/features/telemetry/hooks/useHeartbeatReporter' import { useLastBalancesReporter } from 'wallet/src/features/telemetry/hooks/useLastBalancesReporter' import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts/WalletUniswapContext' @@ -40,7 +42,6 @@ export function MainContent(): JSX.Element { return ( <> - ) @@ -151,12 +152,16 @@ export function WebNavigation(): JSX.Element { return ( - - - {shouldRestoreScroll && } - {childrenMemo} - {isLoggedIn && } - + + + + + {shouldRestoreScroll && } + {childrenMemo} + {isLoggedIn && } + + + ) @@ -202,7 +207,7 @@ const AnimatedPane = styled(Flex, { const isVertical = (dir: Direction): boolean => dir === 'up' || dir === 'down' function useConstant(c: A): A { - const out = useRef() + const out = useRef(undefined) if (!out.current) { out.current = c } @@ -235,7 +240,7 @@ function LoggedIn(): JSX.Element { const isChromeWindowFocused = useIsChromeWindowFocusedWithTimeout(30 * ONE_SECOND_MS) return ( - <> + {contents} @@ -245,7 +250,7 @@ function LoggedIn(): JSX.Element { - + ) } diff --git a/apps/extension/src/app/navigation/providers.tsx b/apps/extension/src/app/navigation/providers.tsx index 602a33f8952..709d7e8e80e 100644 --- a/apps/extension/src/app/navigation/providers.tsx +++ b/apps/extension/src/app/navigation/providers.tsx @@ -16,7 +16,7 @@ import { CopyNotificationType } from 'uniswap/src/features/notifications/slice/t import { WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { ShareableEntity } from 'uniswap/src/types/sharing' -import { getPoolDetailsURL, getTokenUrl } from 'uniswap/src/utils/linking' +import { getPoolDetailsURL, getPortfolioUrl, getTokenUrl } from 'uniswap/src/utils/linking' import { logger } from 'utilities/src/logger/logger' import { escapeRegExp } from 'utilities/src/primitives/string' import { noop } from 'utilities/src/react/noop' @@ -24,6 +24,7 @@ import { useCopyToClipboard } from 'wallet/src/components/copy/useCopyToClipboar import { getNavigateToSendFlowArgsInitialState, getNavigateToSwapFlowArgsInitialState, + NavigateToExternalProfileArgs, NavigateToFiatOnRampArgs, NavigateToSendFlowArgs, NavigateToSwapFlowArgs, @@ -77,14 +78,13 @@ function SharedExtensionNavigationProvider({ const navigateToReceive = useNavigateToReceive() const navigateToSend = useNavigateToSend() const navigateToTokenDetails = useNavigateToTokenDetails() - const navigateToNftCollection = useCallback(() => { - // no-op until we have proper NFT collection - }, []) const navigateToFiatOnRamp = useNavigateToFiatOnRamp() - const navigateToExternalProfile = useCallback(() => { - // no-op until we have an external profile screen on extension + const navigateToExternalProfile = useCallback(({ address }: NavigateToExternalProfileArgs) => { + // oxlint-disable-next-line typescript/no-floating-promises -- biome-parity: oxlint is stricter here + focusOrCreateUniswapInterfaceTab({ url: getPortfolioUrl(address) }) }, []) const navigateToPoolDetails = useNavigateToPoolDetails() + const navigateToAdvancedSettings = useNavigateToAdvancedSettings() return ( {children} @@ -206,7 +206,7 @@ function useNavigateToPoolDetails(): (args: { poolId: Address; chainId: Universe await focusOrCreateUniswapInterfaceTab({ url: getPoolDetailsURL(poolId, chainId), // We want to reuse the active tab only if it's already in any other PDP. - // eslint-disable-next-line security/detect-non-literal-regexp + // oxlint-disable-next-line security/detect-non-literal-regexp reuseActiveTabIfItMatches: new RegExp(`^${escapeRegExp(uniswapUrls.webInterfacePoolsUrl)}`), }) }, []) @@ -223,3 +223,9 @@ function useNavigateToFiatOnRamp(): (args: NavigateToFiatOnRampArgs) => void { navigateToInterfaceFiatOnRamp(prefilledCurrency?.currencyInfo?.currency.chainId) }, []) } + +function useNavigateToAdvancedSettings(): () => void { + return useCallback((): void => { + navigate(`/${AppRoutes.Settings}`, { state: { openAdvancedSettings: true } }) + }, []) +} diff --git a/apps/extension/src/app/navigation/state.ts b/apps/extension/src/app/navigation/state.ts index 7aa2e832c93..3dae446ce08 100644 --- a/apps/extension/src/app/navigation/state.ts +++ b/apps/extension/src/app/navigation/state.ts @@ -76,10 +76,10 @@ type RouterNavigateArgs = Parameters // note: useNavigation().navigate() returns void, so making this match that function for easier swapping out export const navigate = (to: RouterNavigateArgs[0] | number, opts?: RouterNavigateArgs[1]): void => { if (typeof to === 'number') { - // biome-ignore lint/complexity/noVoid: Router navigation returns Promise requiring explicit void handling + // oxlint-disable-next-line no-void -- Router navigation returns Promise requiring explicit void handling void getRouter().navigate(to) return } - // biome-ignore lint/complexity/noVoid: Router navigation returns Promise requiring explicit void handling + // oxlint-disable-next-line no-void -- Router navigation returns Promise requiring explicit void handling void getRouter().navigate(to, opts) } diff --git a/apps/extension/src/app/navigation/utils.ts b/apps/extension/src/app/navigation/utils.ts index 98f9fc818b5..4e8825392b3 100644 --- a/apps/extension/src/app/navigation/utils.ts +++ b/apps/extension/src/app/navigation/utils.ts @@ -136,7 +136,7 @@ export async function focusOrCreateTokensExploreTab({ currencyId }: { currencyId return focusOrCreateUniswapInterfaceTab({ url, // We want to reuse the active tab only if it's already in any other TDP. - // eslint-disable-next-line security/detect-non-literal-regexp + // oxlint-disable-next-line security/detect-non-literal-regexp reuseActiveTabIfItMatches: new RegExp(`^${escapeRegExp(uniswapUrls.webInterfaceTokensUrl)}`), }) } diff --git a/apps/extension/src/app/saga.ts b/apps/extension/src/app/saga.ts index a5e6ddbf951..acc25708388 100644 --- a/apps/extension/src/app/saga.ts +++ b/apps/extension/src/app/saga.ts @@ -8,9 +8,9 @@ import { import { dappRequestApprovalWatcher } from 'src/app/features/dappRequests/dappRequestApprovalWatcherSaga' import { dappRequestWatcher } from 'src/app/features/dappRequests/saga' import { call, spawn } from 'typed-redux-saga' +import { getMonitoredSagaReducers, type MonitoredSaga } from 'uniswap/src/utils/saga' import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient' import { authActions, authReducer, authSaga, authSagaName } from 'wallet/src/features/auth/saga' -import { deviceLocaleWatcher } from 'wallet/src/features/i18n/deviceLocaleWatcherSaga' import { initProviders } from 'wallet/src/features/providers/saga' import { removeDelegationActions, @@ -19,6 +19,10 @@ import { removeDelegationSagaName, } from 'wallet/src/features/smartWallet/sagas/removeDelegationSaga' import { + executePlanActions, + executePlanReducer, + executePlanSaga, + executePlanSagaName, executeSwapActions, executeSwapReducer, executeSwapSaga, @@ -28,13 +32,6 @@ import { prepareAndSignSwapSaga, prepareAndSignSwapSagaName, } from 'wallet/src/features/transactions/swap/configuredSagas' -import { swapActions, swapReducer, swapSaga, swapSagaName } from 'wallet/src/features/transactions/swap/swapSaga' -import { - tokenWrapActions, - tokenWrapReducer, - tokenWrapSaga, - tokenWrapSagaName, -} from 'wallet/src/features/transactions/swap/wrapSaga' import { watchTransactionEvents } from 'wallet/src/features/transactions/watcher/transactionFinalizationSaga' import { transactionWatcher } from 'wallet/src/features/transactions/watcher/transactionWatcherSaga' import { @@ -49,10 +46,9 @@ import { createAccountsSaga, createAccountsSagaName, } from 'wallet/src/features/wallet/create/createAccountsSaga' -import { getMonitoredSagaReducers, MonitoredSaga } from 'wallet/src/state/saga' // Stateful sagas that are registered with the store on startup -export const monitoredSagas: Record = { +const monitoredSagas: Record = { [authSagaName]: { name: authSagaName, wrappedSaga: authSaga, @@ -83,17 +79,11 @@ export const monitoredSagas: Record = { reducer: executeSwapReducer, actions: executeSwapActions, }, - [swapSagaName]: { - name: swapSagaName, - wrappedSaga: swapSaga, - reducer: swapReducer, - actions: swapActions, - }, - [tokenWrapSagaName]: { - name: tokenWrapSagaName, - wrappedSaga: tokenWrapSaga, - reducer: tokenWrapReducer, - actions: tokenWrapActions, + [executePlanSagaName]: { + name: executePlanSagaName, + wrappedSaga: executePlanSaga, + reducer: executePlanReducer, + actions: executePlanActions, }, [removeDelegationSagaName]: { name: removeDelegationSagaName, @@ -115,7 +105,6 @@ const sagasInitializedOnStartup = [ dappRequestWatcher, initProviders, watchTransactionEvents, - deviceLocaleWatcher, ] as const export const monitoredSagaReducers = getMonitoredSagaReducers(monitoredSagas) @@ -129,6 +118,6 @@ export function* rootExtensionSaga() { yield* spawn(transactionWatcher, { apolloClient }) for (const m of Object.values(monitoredSagas)) { - yield* spawn(m.wrappedSaga) + yield* spawn(m['wrappedSaga']) } } diff --git a/apps/extension/src/app/utils/analytics.ts b/apps/extension/src/app/utils/analytics.ts index b24911e58b1..7bbf9145c61 100644 --- a/apps/extension/src/app/utils/analytics.ts +++ b/apps/extension/src/app/utils/analytics.ts @@ -1,15 +1,21 @@ import '@tamagui/core/reset.css' import 'src/app/Global.css' import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, order matters - import { EXTENSION_ORIGIN_APPLICATION } from 'src/app/version' import { uniswapUrls } from 'uniswap/src/constants/urls' import { getUniqueId } from 'utilities/src/device/uniqueId' -import { ApplicationTransport } from 'utilities/src/telemetry/analytics/ApplicationTransport' -// biome-ignore lint/style/noRestrictedImports: Direct utilities import required for analytics initialization +import { isTestEnv } from 'utilities/src/environment/env' +import { logger } from 'utilities/src/logger/logger' +// oxlint-disable-next-line no-restricted-imports -- Direct utilities import required for analytics initialization import { analytics, getAnalyticsAtomDirect } from 'utilities/src/telemetry/analytics/analytics' +import { ApplicationTransport } from 'utilities/src/telemetry/analytics/ApplicationTransport' export async function initExtensionAnalytics(): Promise { + if (isTestEnv()) { + logger.debug('analytics.ts', 'initExtensionAnalytics', 'Skipping Amplitude initialization in test environment') + return + } + const analyticsAllowed = await getAnalyticsAtomDirect(true) await analytics.init({ transportProvider: new ApplicationTransport({ diff --git a/apps/extension/src/app/utils/chrome.ts b/apps/extension/src/app/utils/chrome.ts index f495cdca06e..42d1a6e4d38 100644 --- a/apps/extension/src/app/utils/chrome.ts +++ b/apps/extension/src/app/utils/chrome.ts @@ -21,6 +21,6 @@ export function isAndroid(): boolean { * @returns true if chrome environment supports side panel */ export function checksIfSupportsSidePanel(): boolean { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition return !!chrome.sidePanel && !isArcBrowser() && !isAndroid() } diff --git a/apps/extension/src/app/utils/device/builtInBiometricCapabilitiesQuery.ts b/apps/extension/src/app/utils/device/builtInBiometricCapabilitiesQuery.ts index d7fe7545705..7659baf20eb 100644 --- a/apps/extension/src/app/utils/device/builtInBiometricCapabilitiesQuery.ts +++ b/apps/extension/src/app/utils/device/builtInBiometricCapabilitiesQuery.ts @@ -61,7 +61,7 @@ async function getBuiltInBiometricCapabilities({ t }: { t: TFunction }): Promise export async function isUserVerifyingPlatformAuthenticatorAvailable(): Promise { // Check if WebAuthn is supported in this browser. - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition if (!navigator.credentials || !navigator.credentials.create || !window.PublicKeyCredential) { return false } diff --git a/apps/extension/src/app/utils/provider.ts b/apps/extension/src/app/utils/provider.ts index 39a6839707d..1f5f0d79ba1 100644 --- a/apps/extension/src/app/utils/provider.ts +++ b/apps/extension/src/app/utils/provider.ts @@ -39,7 +39,7 @@ export async function setIsDefaultProviderToStorage(isDefault: boolean): Promise } export async function getIsDefaultProviderFromStorage(): Promise { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition const isDefaultProvider = (await chrome.storage.local.get(IS_DEFAULT_PROVIDER_KEY))?.[IS_DEFAULT_PROVIDER_KEY] if (isDefaultProvider !== undefined) { diff --git a/apps/extension/src/background/backgroundDappRequests.ts b/apps/extension/src/background/backgroundDappRequests.ts index 968024943b1..cbd318ee388 100644 --- a/apps/extension/src/background/backgroundDappRequests.ts +++ b/apps/extension/src/background/backgroundDappRequests.ts @@ -1,13 +1,14 @@ +/* oxlint-disable max-lines */ import { rpcErrors, serializeError } from '@metamask/rpc-errors' import { removeDappConnection } from 'src/app/features/dapp/actions' import { changeChain } from 'src/app/features/dapp/changeChain' import { dappStore } from 'src/app/features/dapp/store' import type { SenderTabInfo } from 'src/app/features/dappRequests/shared' import { - ChangeChainRequest, - DappRequest, - GetCapabilitiesRequest, - RevokePermissionsRequest, + type ChangeChainRequest, + type DappRequest, + type GetCapabilitiesRequest, + type RevokePermissionsRequest, } from 'src/app/features/dappRequests/types/DappRequestTypes' import { focusOrCreateOnboardingTab } from 'src/app/navigation/focusOrCreateOnboardingTab' import { focusOrCreateDappRequestWindow } from 'src/app/navigation/utils' @@ -15,13 +16,13 @@ import { contentScriptToBackgroundMessageChannel, contentScriptUtilityMessageChannel, createBackgroundToSidePanelMessagePort, - DappBackgroundPortChannel, + type DappBackgroundPortChannel, dappResponseMessageChannel, } from 'src/background/messagePassing/messageChannels' import { BackgroundToSidePanelRequestType, ContentScriptUtilityMessageType, - DappRequestMessage, + type DappRequestMessage, } from 'src/background/messagePassing/types/requests' import { checkAreMigrationsPending, readReduxStateFromStorage } from 'src/background/utils/persistedStateUtils' import { getFeatureFlaggedChainIds } from 'uniswap/src/features/chains/hooks/useFeatureFlaggedChainIds' @@ -29,10 +30,10 @@ import { getEnabledChains, hexadecimalStringToInt, toSupportedChainId } from 'un import { DappRequestType, DappResponseType, EthMethod } from 'uniswap/src/features/dappRequests/types' import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { WindowEthereumRequestProperties } from 'uniswap/src/features/telemetry/types' +import { type WindowEthereumRequestProperties } from 'uniswap/src/features/telemetry/types' import { extractBaseUrl } from 'utilities/src/format/urls' import { logger } from 'utilities/src/logger/logger' -import { getCapabilitiesCore } from 'wallet/src/features/batchedTransactions/utils' +import { getCapabilitiesResponse } from 'wallet/src/features/batchedTransactions/utils' import { walletContextValue } from 'wallet/src/features/wallet/context' import { selectHasSmartWalletConsent } from 'wallet/src/features/wallet/selectors' @@ -110,6 +111,7 @@ export function initMessageBridge(): void { windowId, onSuccess: () => { // Process request after panel opens (async operations safe here) + // oxlint-disable-next-line typescript/no-floating-promises -- biome-parity: oxlint is stricter here handleRequestAsync({ message, sender }) }, onError: (error, fallbackOpened) => { @@ -144,6 +146,7 @@ export function initMessageBridge(): void { senderTabInfo: { id: sender.tab.id, url: sender.tab.url, + frameUrl: getFrameUrl(sender), favIconUrl: sender.tab.favIconUrl, }, }) @@ -151,6 +154,7 @@ export function initMessageBridge(): void { }) } else { // Non-interactive request or panel already open - async handling is safe + // oxlint-disable-next-line typescript/no-floating-promises -- biome-parity: oxlint is stricter here handleRequestAsync({ message, sender }) } }) @@ -171,8 +175,8 @@ export function initMessageBridge(): void { ContentScriptUtilityMessageType.AnalyticsLog, async (message) => { const properties: WindowEthereumRequestProperties = { - method: message.tags.method ?? '', - dappUrl: message.tags.dappUrl ?? '', + method: message.tags['method'] ?? '', + dappUrl: message.tags['dappUrl'] ?? '', } const eventName = message.message switch (eventName) { @@ -320,7 +324,7 @@ async function handleGetCapabilities({ }) const chainIds = request.chainIds?.map(hexadecimalStringToInt) ?? enabledChains.map((chain) => chain.valueOf()) - const response = await getCapabilitiesCore({ + const response = await getCapabilitiesResponse({ request, chainIds, hasSmartWalletConsent, @@ -373,6 +377,7 @@ async function handleRequestAsync({ const senderTabInfo: SenderTabInfo = { id: sender.tab.id, url: sender.tab.url, + frameUrl: getFrameUrl(sender), favIconUrl: sender.tab.favIconUrl, } @@ -522,3 +527,29 @@ function queueMessageForPanel({ windowIdToPendingRequestsMap.get(windowIdString)?.push(queuedMessage) } + +/** + * Gets the frame URL from the message sender if the request is from an iframe with a different origin than the top-level page + * @param sender - The message sender + * @returns The frame URL if applicable, undefined otherwise + */ +function getFrameUrl(sender: chrome.runtime.MessageSender): string | undefined { + if (!sender.tab?.url || !sender.url) { + return undefined + } + + try { + const tabOrigin = new URL(sender.tab.url).origin + const senderOrigin = new URL(sender.url).origin + const isFrame = tabOrigin !== senderOrigin + return isFrame ? sender.url : undefined + } catch (error) { + logger.error(error, { + tags: { + file: 'backgroundDappRequests.ts', + function: 'getFrameUrl', + }, + }) + return undefined + } +} diff --git a/apps/extension/src/background/messagePassing/messageUtils.test.ts b/apps/extension/src/background/messagePassing/messageUtils.test.ts new file mode 100644 index 00000000000..c2132e223ef --- /dev/null +++ b/apps/extension/src/background/messagePassing/messageUtils.test.ts @@ -0,0 +1,108 @@ +import { addWindowMessageListener } from 'src/background/messagePassing/messageUtils' + +jest.mock('src/contentScript/isSandboxedFrame', () => ({ + isSandboxedFrame: jest.fn(() => false), +})) + +const { isSandboxedFrame } = require('src/contentScript/isSandboxedFrame') as { + isSandboxedFrame: jest.Mock +} + +interface TestMessage { + type: 'TEST' + payload: string +} + +function isTestMessage(message: unknown): message is TestMessage { + // oxlint-disable-next-line typescript/no-unnecessary-condition -- biome-parity: oxlint is stricter here + return typeof message === 'object' && message !== null && (message as TestMessage).type === 'TEST' +} + +function dispatchWindowMessage(data: unknown, source: MessageEventSource | null = window): void { + const event = new MessageEvent('message', { data, source }) + window.dispatchEvent(event) +} + +describe('addWindowMessageListener', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + describe('normal frame (isSandboxedFrame returns false)', () => { + beforeEach(() => { + isSandboxedFrame.mockReturnValue(false) + }) + + it('calls handler when validator passes and source is window', () => { + const handler = jest.fn() + addWindowMessageListener({ validator: isTestMessage, handler }) + + dispatchWindowMessage({ type: 'TEST', payload: 'hello' }) + + expect(handler).toHaveBeenCalledWith({ type: 'TEST', payload: 'hello' }, window) + }) + + it('calls invalidMessageHandler when validator fails', () => { + const handler = jest.fn() + const invalidMessageHandler = jest.fn() + addWindowMessageListener({ validator: isTestMessage, handler, invalidMessageHandler }) + + dispatchWindowMessage({ type: 'INVALID' }) + + expect(handler).not.toHaveBeenCalled() + expect(invalidMessageHandler).toHaveBeenCalledWith({ type: 'INVALID' }, window) + }) + + it('rejects when event.source is not window', () => { + const handler = jest.fn() + const invalidMessageHandler = jest.fn() + addWindowMessageListener({ validator: isTestMessage, handler, invalidMessageHandler }) + + dispatchWindowMessage({ type: 'TEST', payload: 'hello' }, null) + + expect(handler).not.toHaveBeenCalled() + expect(invalidMessageHandler).toHaveBeenCalled() + }) + + it('removes listener when removeAfterHandled is true', () => { + const handler = jest.fn() + addWindowMessageListener({ + validator: isTestMessage, + handler, + options: { removeAfterHandled: true }, + }) + + dispatchWindowMessage({ type: 'TEST', payload: 'first' }) + dispatchWindowMessage({ type: 'TEST', payload: 'second' }) + + expect(handler).toHaveBeenCalledTimes(1) + expect(handler).toHaveBeenCalledWith({ type: 'TEST', payload: 'first' }, window) + }) + }) + + describe('sandboxed frame (isSandboxedFrame returns true)', () => { + beforeEach(() => { + isSandboxedFrame.mockReturnValue(true) + }) + + it('does NOT call handler even when validator passes and source is window', () => { + const handler = jest.fn() + addWindowMessageListener({ validator: isTestMessage, handler }) + + dispatchWindowMessage({ type: 'TEST', payload: 'hello' }) + + expect(handler).not.toHaveBeenCalled() + }) + + it('calls invalidMessageHandler when rejected due to sandbox', () => { + const handler = jest.fn() + const invalidMessageHandler = jest.fn() + addWindowMessageListener({ validator: isTestMessage, handler, invalidMessageHandler }) + + dispatchWindowMessage({ type: 'TEST', payload: 'hello' }) + + expect(handler).not.toHaveBeenCalled() + expect(invalidMessageHandler).toHaveBeenCalledWith({ type: 'TEST', payload: 'hello' }, window) + }) + }) +}) diff --git a/apps/extension/src/background/messagePassing/messageUtils.ts b/apps/extension/src/background/messagePassing/messageUtils.ts index 980ffb06fd1..8dd601cb5df 100644 --- a/apps/extension/src/background/messagePassing/messageUtils.ts +++ b/apps/extension/src/background/messagePassing/messageUtils.ts @@ -1,3 +1,4 @@ +import { isSandboxedFrame } from 'src/contentScript/isSandboxedFrame' import { Message } from 'uniswap/src/extension/messagePassing/messageTypes' type MessageValidator = (message: unknown) => message is T @@ -19,7 +20,7 @@ export function addWindowMessageListener({ options?: { removeAfterHandled?: boolean } }): (event: MessageEvent) => void { const listener = (event: MessageEvent): void => { - if (event.source !== window || !validator(event.data)) { + if (event.source !== window || isSandboxedFrame() || !validator(event.data)) { invalidMessageHandler?.(event.data, event.source) return } diff --git a/apps/extension/src/background/messagePassing/platform.ts b/apps/extension/src/background/messagePassing/platform.ts index 1147a6c258a..39609e8257f 100644 --- a/apps/extension/src/background/messagePassing/platform.ts +++ b/apps/extension/src/background/messagePassing/platform.ts @@ -1,4 +1,4 @@ -/* biome-ignore-all lint/suspicious/noExplicitAny: Chrome extension message passing requires flexible typing for arbitrary message payloads */ +/* oxlint-disable typescript/no-explicit-any -- Chrome extension message passing requires flexible typing for arbitrary message payloads */ import { MessageParsers } from 'uniswap/src/extension/messagePassing/platform' import { logger } from 'utilities/src/logger/logger' @@ -9,6 +9,7 @@ class ChromeMessageChannel { protected readonly channelName: string readonly port?: chrome.runtime.Port + // oxlint-disable-next-line typescript/no-explicit-any -- biome-parity: oxlint is stricter here protected listeners: MessageListener[] = [] constructor({ @@ -23,6 +24,7 @@ class ChromeMessageChannel { this.channelName = channelName this.port = port + // oxlint-disable-next-line typescript/no-explicit-any -- biome-parity: oxlint is stricter here const mainListener: MessageListener = (message, sender) => { const targetMessage = message[this.channelName] @@ -44,7 +46,7 @@ class ChromeMessageChannel { if (this.port) { this.port.onMessage.addListener((message, senderPort) => mainListener(message, senderPort.sender)) } else { - // eslint-disable-next-line no-restricted-syntax + // oxlint-disable-next-line no-restricted-syntax chrome.runtime.onMessage.addListener(mainListener) } @@ -55,20 +57,23 @@ class ChromeMessageChannel { this.removeMessageListener = this.removeMessageListener.bind(this) } + // oxlint-disable-next-line typescript/no-explicit-any -- biome-parity: oxlint is stricter here async sendMessage(message: any): Promise { if (this.port) { this.port.postMessage({ [this.channelName]: message }) } else { - // eslint-disable-next-line no-restricted-syntax + // oxlint-disable-next-line no-restricted-syntax chrome.runtime.sendMessage({ [this.channelName]: message }).catch(() => {}) } } + // oxlint-disable-next-line typescript/no-explicit-any -- biome-parity: oxlint is stricter here async sendMessageToTab(tabId: number, message: any): Promise { - // eslint-disable-next-line no-restricted-syntax + // oxlint-disable-next-line no-restricted-syntax await chrome.tabs.sendMessage(tabId, { [this.channelName]: message }) } + // oxlint-disable-next-line typescript/no-explicit-any -- biome-parity: oxlint is stricter here async sendMessageToTabUrl(tabUrl: string, message: any): Promise { const urlMatcher = `${tabUrl}/*` const promises: Promise[] = [] @@ -76,12 +81,10 @@ class ChromeMessageChannel { tabs.forEach((tab) => { if (tab.id) { promises.push( - // eslint-disable-next-line no-restricted-syntax - chrome.tabs - .sendMessage(tab.id, { [this.channelName]: message }) - .catch(() => { - // Not logging error here because it is expected that inactive tabs will not be able to receive the message - }), + // oxlint-disable-next-line no-restricted-syntax + chrome.tabs.sendMessage(tab.id, { [this.channelName]: message }).catch(() => { + // Not logging error here because it is expected that inactive tabs will not be able to receive the message + }), ) } }) @@ -89,12 +92,14 @@ class ChromeMessageChannel { return Promise.all(promises) } + // oxlint-disable-next-line typescript/no-explicit-any -- biome-parity: oxlint is stricter here addMessageListener(listener: MessageListener): () => void { this.listeners.push(listener) return () => this.removeMessageListener(listener) } + // oxlint-disable-next-line typescript/no-explicit-any -- biome-parity: oxlint is stricter here removeMessageListener(listener: MessageListener): void { this.listeners = this.listeners.filter((l) => l !== listener) } @@ -160,6 +165,7 @@ abstract class TypedMessageChannel< this.removeMessageListener = this.removeMessageListener.bind(this) } + // oxlint-disable-next-line typescript/no-explicit-any -- biome-parity: oxlint is stricter here private processMessage(message: any): { type: T; messageParser: (message: unknown) => R[T] } { const type = message.type as Maybe if (!type) { @@ -167,7 +173,7 @@ abstract class TypedMessageChannel< } const messageParser = this.messageParsers[type] - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition if (!messageParser) { throw new Error(`No message parser found for type ${type}`) } diff --git a/apps/extension/src/background/messagePassing/types/requests.ts b/apps/extension/src/background/messagePassing/types/requests.ts index eb2cbc705c2..8825cfb946a 100644 --- a/apps/extension/src/background/messagePassing/types/requests.ts +++ b/apps/extension/src/background/messagePassing/types/requests.ts @@ -17,8 +17,8 @@ export const ErrorLogSchema = MessageSchema.extend({ message: z.string(), fileName: z.string(), functionName: z.string(), - tags: z.record(z.string()).optional(), - extra: z.record(z.unknown()).optional(), + tags: z.record(z.string(), z.string()).optional(), + extra: z.record(z.string(), z.unknown()).optional(), }) export type ErrorLog = z.infer @@ -32,7 +32,7 @@ export type ArcBrowserCheckMessage = z.infer @@ -54,6 +54,7 @@ export const DappRequestMessageSchema = z.object({ senderTabInfo: z.object({ id: z.number(), url: z.string(), + frameUrl: z.string().optional(), favIconUrl: z.string().optional(), }), isSidebarClosed: z.optional(z.boolean()), diff --git a/apps/extension/src/background/utils/getCalldataInfoFromTransaction.ts b/apps/extension/src/background/utils/getCalldataInfoFromTransaction.ts index adff8212d30..171a82321d8 100644 --- a/apps/extension/src/background/utils/getCalldataInfoFromTransaction.ts +++ b/apps/extension/src/background/utils/getCalldataInfoFromTransaction.ts @@ -1,9 +1,9 @@ -import { CommandParser, UniversalRouterCall } from '@uniswap/universal-router-sdk' -import { V4BaseActionsParser, V4RouterCall } from '@uniswap/v4-sdk' +import { CommandParser, CommandType, type UniversalRouterCall } from '@uniswap/universal-router-sdk' +import { Actions, V4BaseActionsParser, type V4RouterCall } from '@uniswap/v4-sdk' import { EthSendTransactionRPCActions } from 'src/app/features/dappRequests/types/DappRequestTypes' import { parseCalldata as parseNfPMCalldata } from 'src/app/features/dappRequests/types/NonfungiblePositionManager' -import { NonfungiblePositionManagerCall } from 'src/app/features/dappRequests/types/NonfungiblePositionManagerTypes' -import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { type NonfungiblePositionManagerCall } from 'src/app/features/dappRequests/types/NonfungiblePositionManagerTypes' +import { type UniverseChainId } from 'uniswap/src/features/chains/types' import { wrappedNativeCurrency } from 'uniswap/src/utils/currency' import methodHashToFunctionSignature from 'utilities/src/calldata/methodHashToFunctionSignature' import { noop } from 'utilities/src/react/noop' @@ -45,18 +45,43 @@ export default function getCalldataInfoFromTransaction({ try { const v4Calldata = V4BaseActionsParser.parseCalldata(data) - result.contractInteractions = EthSendTransactionRPCActions.Swap - result.parsedCalldata = v4Calldata - return result + + // Validate that the V4 call actually contains swap actions + const hasSwapAction = v4Calldata.actions.some( + (action) => + action.actionType === Actions.SWAP_EXACT_IN || + action.actionType === Actions.SWAP_EXACT_OUT || + action.actionType === Actions.SWAP_EXACT_IN_SINGLE || + action.actionType === Actions.SWAP_EXACT_OUT_SINGLE, + ) + + if (hasSwapAction) { + result.contractInteractions = EthSendTransactionRPCActions.Swap + result.parsedCalldata = v4Calldata + return result + } } catch { noop() } try { const URCalldata = CommandParser.parseCalldata(data) - result.contractInteractions = EthSendTransactionRPCActions.Swap - result.parsedCalldata = URCalldata - return result + + // Validate that the UR call actually contains swap commands + const hasSwapCommand = URCalldata.commands.some( + (command) => + command.commandType === CommandType.V2_SWAP_EXACT_IN || + command.commandType === CommandType.V2_SWAP_EXACT_OUT || + command.commandType === CommandType.V3_SWAP_EXACT_IN || + command.commandType === CommandType.V3_SWAP_EXACT_OUT || + command.commandType === CommandType.V4_SWAP, + ) + + if (hasSwapCommand) { + result.contractInteractions = EthSendTransactionRPCActions.Swap + result.parsedCalldata = URCalldata + return result + } } catch { noop() } @@ -71,8 +96,8 @@ export default function getCalldataInfoFromTransaction({ } const isWrapUnwrapSignature = functionSignature === 'deposit()' || functionSignature === 'withdraw(uint256)' - const isNativeWrappedCurrencyTo = - chainId && to?.toLowerCase() === wrappedNativeCurrency(chainId).address.toLowerCase() + const wrappedNative = chainId ? wrappedNativeCurrency(chainId) : undefined + const isNativeWrappedCurrencyTo = wrappedNative && to?.toLowerCase() === wrappedNative.address.toLowerCase() if (functionSignature.includes('wrap') || (isWrapUnwrapSignature && isNativeWrappedCurrencyTo)) { result.contractInteractions = EthSendTransactionRPCActions.Wrap return result diff --git a/apps/extension/src/background/utils/persistedStateUtils.ts b/apps/extension/src/background/utils/persistedStateUtils.ts index 50f2b49cdc3..2b646c64565 100644 --- a/apps/extension/src/background/utils/persistedStateUtils.ts +++ b/apps/extension/src/background/utils/persistedStateUtils.ts @@ -2,6 +2,7 @@ import { isOnboardedSelector } from 'src/app/utils/isOnboardedSelector' import { STATE_STORAGE_KEY } from 'src/store/constants' import { ExtensionState } from 'src/store/extensionReducer' import { EXTENSION_STATE_VERSION } from 'src/store/migrations' +import { deviceAccessTimeoutToMinutes } from 'uniswap/src/features/settings/constants' import { logger } from 'utilities/src/logger/logger' export async function readReduxStateFromStorage(storageChanges?: { @@ -30,6 +31,11 @@ export async function readIsOnboardedFromStorage(): Promise { return state ? isOnboardedSelector(state) : false } +export async function readDeviceAccessTimeoutMinutesFromStorage(): Promise { + const state = await readReduxStateFromStorage() + return state ? deviceAccessTimeoutToMinutes(state.userSettings.deviceAccessTimeout) : undefined +} + /** * Checks if Redux migrations are pending by comparing persisted version with current version * @returns true if migrations are pending and sidebar should handle the request diff --git a/apps/extension/src/contentScript/WindowEthereumRequestTypes.ts b/apps/extension/src/contentScript/WindowEthereumRequestTypes.ts index 883123e8b61..b5f12c187e4 100644 --- a/apps/extension/src/contentScript/WindowEthereumRequestTypes.ts +++ b/apps/extension/src/contentScript/WindowEthereumRequestTypes.ts @@ -2,7 +2,7 @@ import { EthersTransactionRequestSchema } from 'src/app/features/dappRequests/ty import { HexadecimalNumberSchema } from 'src/app/features/dappRequests/types/utilityTypes' import { HomeTabs } from 'src/app/navigation/constants' import { GetCallsStatusParamsSchema, SendCallsParamsSchema } from 'wallet/src/features/dappRequests/types' -import { ZodIssueCode, z } from 'zod' +import { z } from 'zod' /** * Schemas + types for requests that come via `window.ethereum.request` @@ -77,7 +77,8 @@ export const PersonalSignRequestSchema = EthereumRequestWithIdSchema.extend({ { message: 'Params array must contain at least two elements', path: ['params'], - code: ZodIssueCode.custom, + code: 'custom', + input: undefined, }, ]) } @@ -107,7 +108,8 @@ export const EthSignTypedDataV4RequestSchema = EthereumRequestWithIdSchema.exten { message: 'Params array must contain at least two elements', path: ['params'], - code: ZodIssueCode.custom, + code: 'custom', + input: undefined, }, ]) } @@ -122,7 +124,8 @@ export const EthSignTypedDataV4RequestSchema = EthereumRequestWithIdSchema.exten { message: 'Typed data must contain a chainId', path: ['params', '1'], - code: ZodIssueCode.custom, + code: 'custom', + input: undefined, }, ]) } @@ -150,7 +153,8 @@ export const WalletSwitchEthereumChainRequestSchema = EthereumRequestWithIdSchem { message: 'Params array must contain at least one element', path: ['params'], - code: ZodIssueCode.custom, + code: 'custom', + input: undefined, }, ]) } @@ -161,7 +165,8 @@ export const WalletSwitchEthereumChainRequestSchema = EthereumRequestWithIdSchem { message: 'Chain id should be specified as a hexadecimal string within object', path: ['params', '0'], - code: ZodIssueCode.custom, + code: 'custom', + input: undefined, }, ]) } @@ -177,12 +182,12 @@ export const WalletSwitchEthereumChainRequestSchema = EthereumRequestWithIdSchem }) export type WalletSwitchEthereumChainRequest = z.infer -// eslint-disable-next-line no-restricted-syntax -export const PermissionRequestSchema = z.record(z.record(z.any())) +// oxlint-disable-next-line no-restricted-syntax +export const PermissionRequestSchema = z.record(z.string(), z.record(z.string(), z.any())) const CaveatSchema = z.object({ type: z.string(), - // eslint-disable-next-line no-restricted-syntax + // oxlint-disable-next-line no-restricted-syntax value: z.any(), }) @@ -203,7 +208,8 @@ export const WalletRequestPermissionsRequestSchema = EthereumRequestWithIdSchema { message: 'Params array must contain at least one element', path: ['params'], - code: ZodIssueCode.custom, + code: 'custom', + input: undefined, }, ]) } @@ -230,7 +236,8 @@ export const WalletRevokePermissionsRequestSchema = EthereumRequestWithIdSchema. { message: 'Params array must contain at least one element', path: ['params'], - code: ZodIssueCode.custom, + code: 'custom', + input: undefined, }, ]) } @@ -263,7 +270,8 @@ export const WalletGetCapabilitiesRequestSchema = EthereumRequestWithIdSchema.ex { message: 'Params array must contain at least one element (address)', path: ['params'], - code: ZodIssueCode.custom, + code: 'custom', + input: undefined, }, ]) } @@ -305,7 +313,8 @@ export const WalletSendCallsRequestSchema = EthereumRequestWithIdSchema.extend({ { message: 'Params array must contain at least one element', path: ['params'], - code: ZodIssueCode.custom, + code: 'custom', + input: undefined, }, ]) } @@ -348,7 +357,8 @@ export const WalletGetCallsStatusRequestSchema = EthereumRequestWithIdSchema.ext { message: 'Params array must contain at least one element', path: ['params'], - code: ZodIssueCode.custom, + code: 'custom', + input: undefined, }, ]) } diff --git a/apps/extension/src/contentScript/ethereum.test.ts b/apps/extension/src/contentScript/ethereum.test.ts new file mode 100644 index 00000000000..e1cd0911c63 --- /dev/null +++ b/apps/extension/src/contentScript/ethereum.test.ts @@ -0,0 +1,113 @@ +let mockIsSandboxed = false + +jest.mock('src/contentScript/isSandboxedFrame', () => ({ + isSandboxedFrame: jest.fn(() => mockIsSandboxed), +})) + +jest.mock('wxt/utils/define-content-script', () => ({ + defineContentScript: jest.fn((definition) => definition), +})) + +jest.mock('uuid', () => ({ + v4: jest.fn(() => 'mock-uuid'), +})) + +jest.mock('src/background/messagePassing/messageUtils', () => ({ + addWindowMessageListener: jest.fn(), + removeWindowMessageListener: jest.fn(), +})) + +jest.mock('utilities/src/logger/logger', () => ({ + logger: { + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + }, +})) + +jest.mock('src/contentScript/WindowEthereumProxy', () => ({ + WindowEthereumProxy: jest.fn().mockImplementation(() => ({ + emit: jest.fn(), + isMetaMask: false, + })), +})) + +describe('ethereum.content', () => { + let definition: { main: () => void } + let postMessageSpy: jest.SpyInstance + + beforeEach(() => { + jest.resetModules() + // Reset the mock flag before re-requiring + mockIsSandboxed = false + // Clear window.ethereum + Object.defineProperty(window, 'ethereum', { value: undefined, writable: true, configurable: true }) + // jsdom's postMessage requires a targetOrigin; spy to prevent the call from throwing + postMessageSpy = jest.spyOn(window, 'postMessage').mockImplementation(jest.fn()) + }) + + afterEach(() => { + postMessageSpy.mockRestore() + }) + + describe('normal frame', () => { + beforeEach(() => { + mockIsSandboxed = false + // eslint-disable-next-line @typescript-eslint/no-var-requires + definition = require('../entrypoints/ethereum.content').default + }) + + it('assigns window.ethereum after main()', () => { + const eip6963Listener = jest.fn() + window.addEventListener('eip6963:announceProvider', eip6963Listener) + + definition.main() + + expect(window.ethereum).toBeDefined() + + window.removeEventListener('eip6963:announceProvider', eip6963Listener) + }) + + it('fires EIP-6963 announceProvider event', () => { + const eip6963Listener = jest.fn() + window.addEventListener('eip6963:announceProvider', eip6963Listener) + + definition.main() + + expect(eip6963Listener).toHaveBeenCalled() + + window.removeEventListener('eip6963:announceProvider', eip6963Listener) + }) + }) + + describe('sandboxed frame', () => { + beforeEach(() => { + mockIsSandboxed = true + // eslint-disable-next-line @typescript-eslint/no-var-requires + definition = require('../entrypoints/ethereum.content').default + }) + + it('does NOT assign window.ethereum', () => { + const eip6963Listener = jest.fn() + window.addEventListener('eip6963:announceProvider', eip6963Listener) + + definition.main() + + expect(window.ethereum).toBeUndefined() + + window.removeEventListener('eip6963:announceProvider', eip6963Listener) + }) + + it('does NOT fire EIP-6963 announceProvider event', () => { + const eip6963Listener = jest.fn() + window.addEventListener('eip6963:announceProvider', eip6963Listener) + + definition.main() + + expect(eip6963Listener).not.toHaveBeenCalled() + + window.removeEventListener('eip6963:announceProvider', eip6963Listener) + }) + }) +}) diff --git a/apps/extension/src/contentScript/injected.test.ts b/apps/extension/src/contentScript/injected.test.ts index 36fc38160cf..0425eabce35 100644 --- a/apps/extension/src/contentScript/injected.test.ts +++ b/apps/extension/src/contentScript/injected.test.ts @@ -2,14 +2,49 @@ jest.mock('src/background/messagePassing/messageChannels') jest.mock('wxt/utils/define-content-script', () => ({ defineContentScript: jest.fn((definition) => definition), })) +jest.mock('src/contentScript/isSandboxedFrame', () => ({ + isSandboxedFrame: jest.fn(() => false), +})) + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { isSandboxedFrame } = require('src/contentScript/isSandboxedFrame') as { + isSandboxedFrame: jest.Mock +} describe('injected', () => { it('should run without throwing an error', () => { // This does not exist in the extension execution environment for content scripts Object.defineProperty(document, 'head', { value: undefined, writable: true }) + // oxlint-disable-next-line typescript/no-var-requires + const injected = require('../entrypoints/injected.content') + expect(injected).toBeTruthy() + }) +}) + +describe('injected - sandboxed frame', () => { + beforeEach(() => { + isSandboxedFrame.mockReturnValue(true) + }) + + afterEach(() => { + isSandboxedFrame.mockReturnValue(false) + }) + + it('should load without error in sandbox mode', () => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { addWindowMessageListener } = require('src/background/messagePassing/messageUtils') as { + addWindowMessageListener: jest.Mock + } + // eslint-disable-next-line @typescript-eslint/no-var-requires const injected = require('../entrypoints/injected.content') expect(injected).toBeTruthy() + + // In sandbox mode, isSandboxedFrame() returns true and makeInjected() bails out early, + // so addWindowMessageListener should NOT have been called for request handling. + // Note: since the module was already required above, this test verifies the module + // loads without error when the sandbox check is active. + expect(addWindowMessageListener).toBeDefined() }) }) diff --git a/apps/extension/src/contentScript/isSandboxedFrame.test.ts b/apps/extension/src/contentScript/isSandboxedFrame.test.ts new file mode 100644 index 00000000000..ee9e0b827ea --- /dev/null +++ b/apps/extension/src/contentScript/isSandboxedFrame.test.ts @@ -0,0 +1,33 @@ +import { isSandboxedFrame } from 'src/contentScript/isSandboxedFrame' + +describe('isSandboxedFrame', () => { + const originalOrigin = window.origin + + afterEach(() => { + Object.defineProperty(window, 'origin', { + value: originalOrigin, + writable: true, + configurable: true, + }) + }) + + it('returns true when window.origin is "null" (sandboxed without allow-same-origin)', () => { + Object.defineProperty(window, 'origin', { value: 'null', writable: true, configurable: true }) + expect(isSandboxedFrame()).toBe(true) + }) + + it('returns false when window.origin is a normal https URL', () => { + Object.defineProperty(window, 'origin', { value: 'https://example.com', writable: true, configurable: true }) + expect(isSandboxedFrame()).toBe(false) + }) + + it('returns false when window.origin is localhost', () => { + Object.defineProperty(window, 'origin', { value: 'http://localhost', writable: true, configurable: true }) + expect(isSandboxedFrame()).toBe(false) + }) + + it('returns false when window.origin is empty string', () => { + Object.defineProperty(window, 'origin', { value: '', writable: true, configurable: true }) + expect(isSandboxedFrame()).toBe(false) + }) +}) diff --git a/apps/extension/src/contentScript/isSandboxedFrame.ts b/apps/extension/src/contentScript/isSandboxedFrame.ts new file mode 100644 index 00000000000..24653affc48 --- /dev/null +++ b/apps/extension/src/contentScript/isSandboxedFrame.ts @@ -0,0 +1,13 @@ +/** + * Detects whether the current frame is sandboxed without the `allow-same-origin` flag. + * + * Browsers set `window.origin` to the string `"null"` for frames with a `sandbox` + * attribute that does not include `allow-same-origin`. These frames cannot be trusted + * because an attacker can embed malicious scripts inside them on trusted domains + * (e.g., OpenSea NFT embeds) and trigger wallet prompts attributed to the parent domain. + * + * See bug bounty finding #621. + */ +export function isSandboxedFrame(): boolean { + return typeof window !== 'undefined' && window.origin === 'null' +} diff --git a/apps/extension/src/contentScript/methodHandlers/BaseMethodHandler.ts b/apps/extension/src/contentScript/methodHandlers/BaseMethodHandler.ts index 5afd7e628cc..bee2872dda6 100644 --- a/apps/extension/src/contentScript/methodHandlers/BaseMethodHandler.ts +++ b/apps/extension/src/contentScript/methodHandlers/BaseMethodHandler.ts @@ -2,7 +2,7 @@ import { JsonRpcProvider } from '@ethersproject/providers' import { WindowEthereumRequest } from 'src/contentScript/types' export abstract class BaseMethodHandler { - // eslint-disable-next-line max-params + // oxlint-disable-next-line max-params constructor( protected readonly getChainId: () => string | undefined, protected readonly getProvider: () => JsonRpcProvider | undefined, diff --git a/apps/extension/src/contentScript/methodHandlers/ExtensionEthMethodHandler.ts b/apps/extension/src/contentScript/methodHandlers/ExtensionEthMethodHandler.ts index 044d6f96a83..59fefc1fb63 100644 --- a/apps/extension/src/contentScript/methodHandlers/ExtensionEthMethodHandler.ts +++ b/apps/extension/src/contentScript/methodHandlers/ExtensionEthMethodHandler.ts @@ -1,4 +1,4 @@ -/* eslint-disable max-lines */ +/* oxlint-disable max-lines */ import { JsonRpcProvider } from '@ethersproject/providers' import { getPermissions } from 'src/app/features/dappRequests/permissions' import { SendTransactionRequest } from 'src/app/features/dappRequests/types/DappRequestTypes' @@ -48,6 +48,8 @@ import { chainIdToHexadecimalString, toSupportedChainId } from 'uniswap/src/feat import { DappRequestType, DappResponseType, EthMethod } from 'uniswap/src/features/dappRequests/types' import { isSelfCallWithData } from 'uniswap/src/features/dappRequests/utils' import { Platform } from 'uniswap/src/features/platforms/types/Platform' +import { InstrumentedJsonRpcProvider } from 'uniswap/src/features/providers/observability/InstrumentedJsonRpcProvider' +import { getRpcObserver } from 'uniswap/src/features/providers/observability/rpcObserver' import { areAddressesEqual } from 'uniswap/src/utils/addresses' import { extractBaseUrl } from 'utilities/src/format/urls' @@ -129,7 +131,13 @@ export class ExtensionEthMethodHandler extends BaseMethodHandler { switch (request.method) { case EthMethod.EthChainId: { @@ -667,7 +681,7 @@ export class ExtensionEthMethodHandler extends BaseMethodHandler { private methodHandlers: { - // biome-ignore lint/suspicious/noExplicitAny: Provider method handlers accept varied parameter types from JSON-RPC calls + // oxlint-disable-next-line typescript/no-explicit-any -- Provider method handlers accept varied parameter types from JSON-RPC calls [key: string]: (provider: JsonRpcProvider, params: any[]) => Promise } @@ -41,7 +41,7 @@ export class ProviderDirectMethodHandler extends BaseMethodHandler provider.getBalance(params[0]), [ProviderDirectMethods.eth_getCode]: (provider, params) => provider.getCode(params[0]), [ProviderDirectMethods.eth_getStorageAt]: (provider, params) => provider.getStorageAt(params[0], params[1]), @@ -54,8 +54,11 @@ export class ProviderDirectMethodHandler extends BaseMethodHandler provider.getTransaction(params[0]), [ProviderDirectMethods.eth_getTransactionReceipt]: (provider, params) => provider.getTransactionReceipt(params[0]), + // oxlint-disable-next-line typescript/no-unsafe-return -- biome-parity: oxlint is stricter here [ProviderDirectMethods.net_version]: async (provider, params) => provider.send('net_version', params), + // oxlint-disable-next-line typescript/no-unsafe-return -- biome-parity: oxlint is stricter here [ProviderDirectMethods.web3_clientVersion]: async (provider, params) => + // oxlint-disable-next-line typescript/no-unsafe-return -- biome-parity: oxlint is stricter here provider.send('web3_clientVersion', params), } } @@ -72,6 +75,7 @@ export class ProviderDirectMethodHandler extends BaseMethodHandler source: MessageEventSource | null requestId: string @@ -101,17 +105,17 @@ export class ProviderDirectMethodHandler extends BaseMethodHandler { if (!value) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + // oxlint-disable-next-line typescript/no-unsafe-return return value } else if (BigNumber.isBigNumber(value)) { return value.toHexString() } else if (value.type === 'BigNumber' && value.hex) { // Unsure of why but sometimes the provider has converted the BigNumber with BigNumber.toJSON() e.g. eth_getBlockByNumber // which is a format not currently accepted by some dapps e.g. Morpho - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + // oxlint-disable-next-line typescript/no-unsafe-return return value.hex } - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + // oxlint-disable-next-line typescript/no-unsafe-return return value }), ), diff --git a/apps/extension/src/contentScript/methodHandlers/UniswapMethodHandler.ts b/apps/extension/src/contentScript/methodHandlers/UniswapMethodHandler.ts index 4e459e095ab..d2d48b0d792 100644 --- a/apps/extension/src/contentScript/methodHandlers/UniswapMethodHandler.ts +++ b/apps/extension/src/contentScript/methodHandlers/UniswapMethodHandler.ts @@ -22,7 +22,7 @@ import { logger } from 'utilities/src/logger/logger' export class UniswapMethodHandler extends BaseMethodHandler { private readonly requestIdToSourceMap: Map = new Map() - // eslint-disable-next-line max-params + // oxlint-disable-next-line max-params constructor({ getChainId, getProvider, diff --git a/apps/extension/src/contentScript/methodHandlers/emitUtils.ts b/apps/extension/src/contentScript/methodHandlers/emitUtils.ts index 64530fd49b1..b830f3c3989 100644 --- a/apps/extension/src/contentScript/methodHandlers/emitUtils.ts +++ b/apps/extension/src/contentScript/methodHandlers/emitUtils.ts @@ -1,12 +1,12 @@ export function emitChainChanged(newChainId: string): void { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition window?.postMessage({ emitKey: 'chainChanged', emitValue: newChainId, }) } export function emitAccountsChanged(newConnectedAddresses: Address[]): void { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition window?.postMessage({ emitKey: 'accountsChanged', emitValue: newConnectedAddresses, diff --git a/apps/extension/src/contentScript/methodHandlers/requestMethods.ts b/apps/extension/src/contentScript/methodHandlers/requestMethods.ts index 8edeb23b564..1a377392584 100644 --- a/apps/extension/src/contentScript/methodHandlers/requestMethods.ts +++ b/apps/extension/src/contentScript/methodHandlers/requestMethods.ts @@ -1,11 +1,11 @@ // Custom Uniswap methods that the extension will handle -/* eslint-disable @typescript-eslint/naming-convention */ +/* oxlint-disable typescript/naming-convention */ export enum UniswapMethods { uniswap_openSidebar = 'uniswap_openSidebar', } // Methods that are not supported by the extension because they are deprecated -/* eslint-disable @typescript-eslint/naming-convention */ +/* oxlint-disable typescript/naming-convention */ export enum DeprecatedEthMethods { eth_sign = 'eth_sign', // Security risk eth_signTypedData_v3 = 'eth_signTypedData_v3', @@ -19,7 +19,7 @@ export enum DeprecatedEthMethods { // Depending on the frequency with which we see these methods we could show an error // in the sidebar for users. // The methods come from: https://docs.metamask.io/wallet/reference/json-rpc-api/ -/* eslint-disable @typescript-eslint/naming-convention */ +/* oxlint-disable typescript/naming-convention */ export enum UnsupportedEthMethods { wallet_addEthereumChain = 'wallet_addEthereumChain', wallet_registerOnboarding = 'wallet_registerOnboarding', diff --git a/apps/extension/src/contentScript/methodHandlers/utils.ts b/apps/extension/src/contentScript/methodHandlers/utils.ts index a9c59443473..ee737460464 100644 --- a/apps/extension/src/contentScript/methodHandlers/utils.ts +++ b/apps/extension/src/contentScript/methodHandlers/utils.ts @@ -127,6 +127,7 @@ export function getPendingResponseInfo({ requestIdToSourceMap.delete(requestId) if (type !== DappResponseType.ErrorResponse && type !== pendingResponseInfo.type) { + // oxlint-disable-next-line typescript/no-floating-promises -- biome-parity: oxlint is stricter here logContentScriptError({ errorMessage: `Response type doesn't match expected type, expected: ${pendingResponseInfo.type}, actual: ${type}`, fileName: 'methodHandlers/utils.ts', diff --git a/apps/extension/src/contentScript/types.ts b/apps/extension/src/contentScript/types.ts index f43e278922d..e02253e9346 100644 --- a/apps/extension/src/contentScript/types.ts +++ b/apps/extension/src/contentScript/types.ts @@ -5,7 +5,7 @@ export enum ETH_PROVIDER_CONFIG { RESPONSE = 'ETHEREUM_PROVIDER_SCHEMA_RESPONSE', } -/* eslint-disable no-restricted-syntax */ +/* oxlint-disable no-restricted-syntax */ const ExtensionResponseSchema = z .object({ requestId: z.string(), diff --git a/apps/extension/src/contentScript/utils.ts b/apps/extension/src/contentScript/utils.ts index be694ce40ee..6225b19a0a3 100644 --- a/apps/extension/src/contentScript/utils.ts +++ b/apps/extension/src/contentScript/utils.ts @@ -25,7 +25,7 @@ export async function logContentScriptError({ } if (__DEV__) { - // eslint-disable-next-line no-restricted-syntax + // oxlint-disable-next-line no-restricted-syntax logger.error(new Error(errorMessage), { tags: { file: fileName, diff --git a/apps/extension/src/declarations.d.ts b/apps/extension/src/declarations.d.ts index e21c834e516..d08db6d0f4b 100644 --- a/apps/extension/src/declarations.d.ts +++ b/apps/extension/src/declarations.d.ts @@ -2,6 +2,6 @@ declare module '*.svg' { import React from 'react' import { SvgProps } from 'react-native-svg' const content: React.FC - // eslint-disable-next-line import/no-unused-modules + // oxlint-disable-next-line import/no-unused-modules export default content } diff --git a/apps/extension/src/entrypoints/background.ts b/apps/extension/src/entrypoints/background.ts index f75bfd172b9..24331608c9c 100644 --- a/apps/extension/src/entrypoints/background.ts +++ b/apps/extension/src/entrypoints/background.ts @@ -1,5 +1,5 @@ import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, order matters - +import { AUTO_LOCK_ALARM_NAME } from 'src/app/components/AutoLockProvider' import { initStatSigForBrowserScripts } from 'src/app/core/initStatSigForBrowserScripts' import { focusOrCreateOnboardingTab } from 'src/app/navigation/focusOrCreateOnboardingTab' import { initExtensionAnalytics } from 'src/app/utils/analytics' @@ -14,9 +14,15 @@ import { ContentScriptUtilityMessageType, } from 'src/background/messagePassing/types/requests' import { setSidePanelBehavior, setSidePanelOptions } from 'src/background/utils/chromeSidePanelUtils' -import { readIsOnboardedFromStorage } from 'src/background/utils/persistedStateUtils' +import { + readDeviceAccessTimeoutMinutesFromStorage, + readIsOnboardedFromStorage, +} from 'src/background/utils/persistedStateUtils' import { uniswapUrls } from 'uniswap/src/constants/urls' +import { ExtensionEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { logger } from 'utilities/src/logger/logger' +import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' import { defineBackground } from 'wxt/utils/define-background' async function enableSidebar(): Promise { @@ -69,6 +75,54 @@ function makeBackground(): void { await checkAndHandleOnboarding() }) + // Auto-lock alarm listener + chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === AUTO_LOCK_ALARM_NAME) { + Keyring.lock() + .then(() => { + sendAnalyticsEvent(ExtensionEventName.ChangeLockedState, { + locked: true, + location: 'background', + }) + }) + .catch((error) => { + logger.error(error, { + tags: { + file: 'background.ts', + function: 'alarms.onAlarm', + }, + }) + }) + } + }) + + // Listen for sidebar port disconnects to schedule auto-lock alarm + chrome.runtime.onConnect.addListener((port) => { + if (port.name === AUTO_LOCK_ALARM_NAME) { + port.onDisconnect.addListener(async () => { + try { + // Get timeout setting from Redux state + const delayInMinutes = await readDeviceAccessTimeoutMinutesFromStorage() + if (delayInMinutes === undefined) { + return + } + + // Schedule alarm + // oxlint-disable-next-line typescript/no-floating-promises -- biome-parity: oxlint is stricter here + chrome.alarms.create(AUTO_LOCK_ALARM_NAME, { delayInMinutes }) + logger.debug('background', 'port.onDisconnect', `Scheduled auto-lock alarm for ${delayInMinutes} minutes`) + } catch (error) { + logger.error(error, { + tags: { + file: 'background.ts', + function: 'port.onDisconnect', + }, + }) + } + }) + } + }) + // on arc browser, show unsupported browser page (lives on onboarding flow) // this is because arc doesn't support the sidebar contentScriptUtilityMessageChannel.addMessageListener( @@ -130,7 +184,7 @@ function makeBackground(): void { }) } -// eslint-disable-next-line import/no-unused-modules +// oxlint-disable-next-line import/no-unused-modules export default defineBackground({ type: 'module', main() { diff --git a/apps/extension/src/entrypoints/ethereum.content.ts b/apps/extension/src/entrypoints/ethereum.content.ts index 3cfdf612e45..5d981bfbbc5 100644 --- a/apps/extension/src/entrypoints/ethereum.content.ts +++ b/apps/extension/src/entrypoints/ethereum.content.ts @@ -1,4 +1,5 @@ import { addWindowMessageListener } from 'src/background/messagePassing/messageUtils' +import { isSandboxedFrame } from 'src/contentScript/isSandboxedFrame' import { ETH_PROVIDER_CONFIG, isValidContentScriptToProxyEmission, @@ -23,6 +24,11 @@ function makeEthereum(): void { if (typeof window === 'undefined') { return } + + // Do not inject the provider into sandboxed frames without allow-same-origin. + if (isSandboxedFrame()) { + return + } // TODO(xtine): Get this working by importing the svg file directly. The svg text comes from packages/ui/src/assets/icons/uniswap-logo.svg const UNISWAP_LOGO = `data:image/svg+xml,${encodeURIComponent(` @@ -63,7 +69,7 @@ function makeEthereum(): void { } catch (error) { if (__DEV__) { // Only log in dev env for debugging purposes to avoid spamming DD with these errors. - // eslint-disable-next-line no-restricted-syntax + // oxlint-disable-next-line no-restricted-syntax logger.error(error, { tags: { file: 'ethereum.ts', function: 'assignWindowEthereum' } }) } } @@ -150,7 +156,7 @@ function makeEthereum(): void { } } -// eslint-disable-next-line import/no-unused-modules +// oxlint-disable-next-line import/no-unused-modules export default defineContentScript({ matches: __DEV__ || process.env.BUILD_ENV === 'dev' @@ -159,6 +165,7 @@ export default defineContentScript({ runAt: 'document_start', // TODO(INFRA-1010): not supported by firefox world: 'MAIN', + allFrames: true, main() { makeEthereum() }, diff --git a/apps/extension/src/entrypoints/fallback-popup/main.tsx b/apps/extension/src/entrypoints/fallback-popup/main.tsx index 676edbe15ab..581a852749d 100644 --- a/apps/extension/src/entrypoints/fallback-popup/main.tsx +++ b/apps/extension/src/entrypoints/fallback-popup/main.tsx @@ -1,18 +1,18 @@ -// eslint-disable-next-line @typescript-eslint/triple-slash-reference +// oxlint-disable-next-line typescript/triple-slash-reference /// -// eslint-disable-next-line @typescript-eslint/triple-slash-reference +// oxlint-disable-next-line typescript/triple-slash-reference /// import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import PopupApp from 'src/app/core/PopupApp' import { initializeReduxStore } from 'src/store/store' -// biome-ignore lint/suspicious/noExplicitAny: Global polyfill cleanup requires any type for runtime modification +// oxlint-disable-next-line typescript/no-explicit-any -- Global polyfill cleanup requires any type for runtime modification ;(globalThis as any).regeneratorRuntime = undefined function makeFallbackPopup(): void { function initFallbackPopup() { - // biome-ignore lint/style/noNonNullAssertion: popup root element guaranteed to exist in extension + // oxlint-disable-next-line typescript/no-non-null-assertion -- popup root element guaranteed to exist in extension const container = document.getElementById('fallback-popup-root')! const root = createRoot(container) diff --git a/apps/extension/src/entrypoints/injected.content.ts b/apps/extension/src/entrypoints/injected.content.ts index b252771cfa5..77e9d9d8e7e 100644 --- a/apps/extension/src/entrypoints/injected.content.ts +++ b/apps/extension/src/entrypoints/injected.content.ts @@ -15,8 +15,9 @@ import { ContentScriptUtilityMessageType, ExtensionToDappRequestType, } from 'src/background/messagePassing/types/requests' -import { ExtensionEthMethodHandler } from 'src/contentScript/methodHandlers/ExtensionEthMethodHandler' +import { isSandboxedFrame } from 'src/contentScript/isSandboxedFrame' import { emitAccountsChanged, emitChainChanged } from 'src/contentScript/methodHandlers/emitUtils' +import { ExtensionEthMethodHandler } from 'src/contentScript/methodHandlers/ExtensionEthMethodHandler' import { ProviderDirectMethodHandler } from 'src/contentScript/methodHandlers/ProviderDirectMethodHandler' import { UniswapMethodHandler } from 'src/contentScript/methodHandlers/UniswapMethodHandler' import { @@ -51,6 +52,11 @@ import { defineContentScript } from 'wxt/utils/define-content-script' import { ZodError } from 'zod' function makeInjected(): void { + // Do not inject into sandboxed frames without allow-same-origin. + if (isSandboxedFrame()) { + return + } + // arc styles aren't available on load const ARC_STYLE_INJECTION_DELAY = ONE_SECOND_MS @@ -285,7 +291,7 @@ function makeInjected(): void { // notify background script if arc browser detected so we can disable the extension window.addEventListener('load', () => { // if styles aren't available at all, then we cannot check for the arc styles - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition const isStylesAvailable = document.documentElement && !!getComputedStyle(document.documentElement).length if (!isStylesAvailable) { return @@ -300,13 +306,14 @@ function makeInjected(): void { }) } -// eslint-disable-next-line import/no-unused-modules +// oxlint-disable-next-line import/no-unused-modules export default defineContentScript({ matches: __DEV__ || process.env.BUILD_ENV === 'dev' ? ['http://127.0.0.1/*', 'http://localhost/*', 'https://*/*'] : ['https://*/*'], runAt: 'document_start', + allFrames: true, main() { makeInjected() }, diff --git a/apps/extension/src/entrypoints/onboarding/main.tsx b/apps/extension/src/entrypoints/onboarding/main.tsx index bdff83544bd..9f56158169d 100644 --- a/apps/extension/src/entrypoints/onboarding/main.tsx +++ b/apps/extension/src/entrypoints/onboarding/main.tsx @@ -1,18 +1,18 @@ -// eslint-disable-next-line @typescript-eslint/triple-slash-reference +// oxlint-disable-next-line typescript/triple-slash-reference /// -// eslint-disable-next-line @typescript-eslint/triple-slash-reference +// oxlint-disable-next-line typescript/triple-slash-reference /// import React from 'react' import { createRoot } from 'react-dom/client' import OnboardingApp from 'src/app/core/OnboardingApp' import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization' -// biome-ignore lint/suspicious/noExplicitAny: Global polyfill cleanup requires any type for runtime modification +// oxlint-disable-next-line typescript/no-explicit-any -- Global polyfill cleanup requires any type for runtime modification ;(globalThis as any).regeneratorRuntime = undefined function makeOnboarding(): void { function initOnboarding() { - // biome-ignore lint/style/noNonNullAssertion: DOM onboarding root element guaranteed to exist in extension + // oxlint-disable-next-line typescript/no-non-null-assertion -- DOM onboarding root element guaranteed to exist in extension const container = document.getElementById('onboarding-root')! const root = createRoot(container) diff --git a/apps/extension/src/entrypoints/sidepanel/index.html b/apps/extension/src/entrypoints/sidepanel/index.html index 4b7a356fcf7..be59579ab46 100644 --- a/apps/extension/src/entrypoints/sidepanel/index.html +++ b/apps/extension/src/entrypoints/sidepanel/index.html @@ -41,12 +41,6 @@ #root { min-height: 100%; } - @media (prefers-color-scheme: dark) { - body { - /* Avoid flash of white background when opening the extension. */ - background-color: #131313; - } - } * { font-family: 'Basel', sans-serif; diff --git a/apps/extension/src/entrypoints/sidepanel/loadSidebar.ts b/apps/extension/src/entrypoints/sidepanel/loadSidebar.ts index 6ebd69821cc..5b92352218c 100644 --- a/apps/extension/src/entrypoints/sidepanel/loadSidebar.ts +++ b/apps/extension/src/entrypoints/sidepanel/loadSidebar.ts @@ -11,6 +11,7 @@ function makeLoadSidebar(): void { setTimeout(() => { + // oxlint-disable-next-line typescript/no-floating-promises -- biome-parity: oxlint is stricter here import('./main') }, 10) } diff --git a/apps/extension/src/entrypoints/sidepanel/main.tsx b/apps/extension/src/entrypoints/sidepanel/main.tsx index 4cbeaaa35d0..1a830835b82 100644 --- a/apps/extension/src/entrypoints/sidepanel/main.tsx +++ b/apps/extension/src/entrypoints/sidepanel/main.tsx @@ -1,20 +1,21 @@ -// eslint-disable-next-line @typescript-eslint/triple-slash-reference +// oxlint-disable-next-line typescript/triple-slash-reference /// -// eslint-disable-next-line @typescript-eslint/triple-slash-reference +// oxlint-disable-next-line typescript/triple-slash-reference /// import 'src/app/utils/devtools' import 'symbol-observable' // Needed by `reduxed-chrome-storage` as polyfill, order matters - import React from 'react' import { createRoot } from 'react-dom/client' import SidebarApp from 'src/app/core/SidebarApp' import { onboardingMessageChannel } from 'src/background/messagePassing/messageChannels' import { OnboardingMessageType } from 'src/background/messagePassing/types/ExtensionMessages' +import { getReduxStore } from 'src/store/store' import { ExtensionAppLocation, StoreSynchronization } from 'src/store/storeSynchronization' import { initializeScrollWatcher } from 'uniswap/src/components/modals/ScrollLock' +import { initializePortfolioQueryOverrides } from 'uniswap/src/data/rest/portfolioBalanceOverrides' import { logger } from 'utilities/src/logger/logger' -// biome-ignore lint/suspicious/noExplicitAny: Global polyfill cleanup requires any type for runtime modification +// oxlint-disable-next-line typescript/no-explicit-any -- Global polyfill cleanup requires any type for runtime modification ;(globalThis as any).regeneratorRuntime = undefined export function makeSidebar(): void { @@ -32,7 +33,7 @@ export function makeSidebar(): void { }) }) - // biome-ignore lint/style/noNonNullAssertion: DOM root element guaranteed to exist in extension context + // oxlint-disable-next-line typescript/no-non-null-assertion -- DOM root element guaranteed to exist in extension context const container = window.document.querySelector('#root')! const root = createRoot(container) @@ -44,6 +45,7 @@ export function makeSidebar(): void { } StoreSynchronization.init(ExtensionAppLocation.SidePanel) + initializePortfolioQueryOverrides({ store: getReduxStore() }) initSidebar() initializeScrollWatcher() } diff --git a/apps/extension/src/entrypoints/unitagClaim/main.tsx b/apps/extension/src/entrypoints/unitagClaim/main.tsx index 09cf888d5b3..cbc5887b42b 100644 --- a/apps/extension/src/entrypoints/unitagClaim/main.tsx +++ b/apps/extension/src/entrypoints/unitagClaim/main.tsx @@ -1,18 +1,18 @@ -// eslint-disable-next-line @typescript-eslint/triple-slash-reference +// oxlint-disable-next-line typescript/triple-slash-reference /// -// eslint-disable-next-line @typescript-eslint/triple-slash-reference +// oxlint-disable-next-line typescript/triple-slash-reference /// import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import UnitagClaimApp from 'src/app/core/UnitagClaimApp' import { initializeReduxStore } from 'src/store/store' -// biome-ignore lint/suspicious/noExplicitAny: Global polyfill cleanup requires any type for runtime modification +// oxlint-disable-next-line typescript/no-explicit-any -- Global polyfill cleanup requires any type for runtime modification ;(globalThis as any).regeneratorRuntime = undefined function makeUnitagClaim(): void { function initUnitagClaim(): void { - // biome-ignore lint/style/noNonNullAssertion: DOM unitag claim root element guaranteed to exist in extension + // oxlint-disable-next-line typescript/no-non-null-assertion -- DOM unitag claim root element guaranteed to exist in extension const container = document.getElementById('unitag-claim-root')! const root = createRoot(container) diff --git a/apps/extension/src/env.d.ts b/apps/extension/src/env.d.ts index e2d3c2d91ed..a7c89fb9f40 100644 --- a/apps/extension/src/env.d.ts +++ b/apps/extension/src/env.d.ts @@ -3,7 +3,7 @@ import { config, TamaguiGroupNames } from 'ui/src/tamagui.config' type Conf = typeof config declare module 'tamagui' { - // eslint-disable-next-line @typescript-eslint/no-empty-interface + // oxlint-disable-next-line typescript/no-empty-interface interface TamaguiCustomConfig extends Conf {} interface TypeOverride { diff --git a/apps/extension/src/manifest.json b/apps/extension/src/manifest.json index 4b599a3f4bd..00df9742863 100644 --- a/apps/extension/src/manifest.json +++ b/apps/extension/src/manifest.json @@ -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.61.0", + "version": "1.71.0", "minimum_chrome_version": "116", "icons": { "16": "assets/icon16.png", diff --git a/apps/extension/src/notification-service/ExtensionNotificationService.tsx b/apps/extension/src/notification-service/ExtensionNotificationService.tsx new file mode 100644 index 00000000000..ffbfb4b3596 --- /dev/null +++ b/apps/extension/src/notification-service/ExtensionNotificationService.tsx @@ -0,0 +1,226 @@ +import { queryOptions } from '@tanstack/react-query' +import { PlatformType } from '@uniswap/client-notification-service/dist/uniswap/notificationservice/v1/api_pb' +import { + createFetchClient, + createNotificationsApiClient, + getEntryGatewayUrl, + provideSessionService, + SharedQueryClient, +} from '@universe/api' +import { SESSION_INIT_QUERY_KEY } from '@universe/api/src/components/ApiInit' +import { getIsSessionServiceEnabled } from '@universe/gating' +import { + createApiNotificationTracker, + createBaseNotificationProcessor, + createNotificationService, + createPollingNotificationDataSource, + createReactiveDataSource, + getNotificationQueryOptions, + type NotificationService, +} from '@universe/notifications' +import ms from 'ms' +import { UnitagClaimRoutes } from 'src/app/navigation/constants' +import { focusOrCreateUniswapInterfaceTab, focusOrCreateUnitagTab } from 'src/app/navigation/utils' +import { createChromeStorageAdapter } from 'src/notification-service/createChromeStorageAdapter' +import { createExtensionLegacyBannersNotificationDataSource } from 'src/notification-service/data-sources/createExtensionLegacyBannersNotificationDataSource' +import { createStorageWarningCondition } from 'src/notification-service/data-sources/reactive/storageWarningCondition' +import { createExtensionNotificationRenderer } from 'src/notification-service/notification-renderer/createExtensionNotificationRenderer' +import { extensionNotificationStore } from 'src/notification-service/notification-renderer/notificationStore' +import { getNotificationTelemetry } from 'src/notification-service/notification-telemetry/getNotificationTelemetry' +import { createExtensionLocalTriggerDataSource } from 'src/notification-service/triggers/createExtensionLocalTriggerDataSource' +import { getReduxStore } from 'src/store/store' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { mapLocaleToBackendLocale } from 'uniswap/src/features/language/constants' +import { getLocale } from 'uniswap/src/features/language/navigatorLocale' +import { selectCurrentLanguage } from 'uniswap/src/features/settings/selectors' +import { getLogger } from 'utilities/src/logger/logger' +import { REQUEST_SOURCE } from 'utilities/src/platform/requestSource' +import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache' +import { type QueryOptionsResult } from 'utilities/src/reactQuery/queryOptions' + +/** + * Checks if the session has been initialized by looking at the React Query cache. + * Returns true if the session initialization query has completed successfully. + */ +function getIsSessionInitialized(): boolean { + const sessionState = SharedQueryClient.getQueryState(SESSION_INIT_QUERY_KEY) + return sessionState?.status === 'success' +} + +/** + * Creates the notification service with all necessary dependencies + */ +function provideExtensionNotificationService(ctx: { + navigate: (path: string) => void + getIsApiDataSourceEnabled: () => boolean + getReduxStore: () => ReturnType +}): NotificationService { + const isApiDataSourceEnabled = ctx.getIsApiDataSourceEnabled() + const notifApiBaseUrl = getEntryGatewayUrl() + + const fetchClient = createFetchClient({ + baseUrl: notifApiBaseUrl, + getHeaders: () => { + const currentLanguage = selectCurrentLanguage(getReduxStore().getState()) + const locale = getLocale(currentLanguage) + const backendLocale = mapLocaleToBackendLocale(locale) + + return { + 'Content-Type': 'application/json', + 'x-request-source': REQUEST_SOURCE, + 'x-uniswap-locale': backendLocale, + 'x-app-version': (process.env.VERSION ?? '').split('.').slice(0, 3).join('.'), + } + }, + getSessionService: () => + provideSessionService({ + getBaseUrl: () => getEntryGatewayUrl(), + getIsSessionServiceEnabled, + }), + }) + + const apiClient = createNotificationsApiClient({ + fetchClient, + getApiPathPrefix: () => '', // Empty prefix if the full path is in the base URL + }) + + const notifQueryOptions = getNotificationQueryOptions({ + apiClient, + getPlatformType: () => PlatformType.EXTENSION, + pollIntervalMs: 120000, // Poll every 2 minutes + getIsSessionInitialized, // Check session state before making API calls + }) + + const backendDataSource = createPollingNotificationDataSource({ + queryClient: SharedQueryClient, + queryOptions: notifQueryOptions, + }) + + const tracker = createApiNotificationTracker({ + notificationsApiClient: apiClient, + queryClient: SharedQueryClient, + storage: createChromeStorageAdapter(), + }) + + const bannersDataSource = createExtensionLegacyBannersNotificationDataSource({ + tracker, + pollIntervalMs: ms('10s'), + }) + + const localTriggersDataSource = createExtensionLocalTriggerDataSource({ + // oxlint-disable-next-line typescript/no-unsafe-return -- biome-parity: oxlint is stricter here + getState: () => ctx.getReduxStore().getState(), + dispatch: ctx.getReduxStore().dispatch, + tracker, + pollIntervalMs: ms('5s'), + }) + + // Reactive data source for storage warning - shows when storage is low + // Note: isOnboarding=false for the main app (onboarding has its own context) + const storageWarningDataSource = createReactiveDataSource({ + condition: createStorageWarningCondition({ isOnboarding: false }), + tracker, + source: 'system_alerts', + logFileTag: 'storageWarningCondition', + }) + + const processor = createBaseNotificationProcessor(tracker) + + const renderer = createExtensionNotificationRenderer({ + store: extensionNotificationStore, + }) + + const telemetry = getNotificationTelemetry() + + const dataSources = isApiDataSourceEnabled + ? [backendDataSource, bannersDataSource, localTriggersDataSource, storageWarningDataSource] + : [bannersDataSource, localTriggersDataSource, storageWarningDataSource] + + const onNavigate = (url: string) => { + // Handle explore paths by opening in web interface + if (url.startsWith('/explore/')) { + focusOrCreateUniswapInterfaceTab({ + url: `${uniswapUrls.requestOriginUrl}${url}`, + }).catch((error) => { + getLogger().error(error, { + tags: { + file: 'ExtensionNotificationService', + function: 'onNavigate', + }, + extra: { url }, + }) + }) + return + } + + // Handle internal navigation (paths starting with /) + if (url.startsWith('/')) { + ctx.navigate(url) + return + } + + // Handle special unitag:// protocol for opening unitag claim tabs + if (url.startsWith('unitag://claim/')) { + const route = url.replace('unitag://claim/', '') + const state = ctx.getReduxStore().getState() + const activeAddress = state.wallet.activeAccountAddress + if (activeAddress) { + focusOrCreateUnitagTab(activeAddress, route as UnitagClaimRoutes).catch((error) => { + getLogger().error(error, { + tags: { + file: 'ExtensionNotificationService', + function: 'onNavigate', + }, + extra: { url }, + }) + }) + } + return + } + + // All other URLs are external - open in new tab + window.open(url, '_blank') + } + + const notificationService = createNotificationService({ + dataSources, + tracker, + processor, + renderer, + telemetry, + onNavigate, + }) + + return notificationService +} + +/** + * Query options factory for the notification service. + * + * NOTE: The query key includes isApiDataSourceEnabled to ensure a new service + * is created when the feature flag changes. Without this, the cached service + * would continue using stale data sources. + */ +export function getNotificationServiceQueryOptions(ctx: { + navigate: (path: string) => void + getIsEnabled: () => boolean + getIsApiDataSourceEnabled: () => boolean + getReduxStore: () => ReturnType +}): QueryOptionsResult< + NotificationService, + Error, + NotificationService, + [ReactQueryCacheKey.NotificationService, { isApiDataSourceEnabled: boolean }] +> { + const isApiDataSourceEnabled = ctx.getIsApiDataSourceEnabled() + const isEnabled = ctx.getIsEnabled() + + return queryOptions({ + // Include feature flag in query key so service is recreated when flag changes + queryKey: [ReactQueryCacheKey.NotificationService, { isApiDataSourceEnabled }], + queryFn: () => provideExtensionNotificationService(ctx), + enabled: isEnabled, + staleTime: Infinity, // Never refetch while mounted + gcTime: 0, // Don't persist in cache - NotificationService has methods that can't be serialized + }) +} diff --git a/apps/extension/src/notification-service/ExtensionNotificationServiceManager.tsx b/apps/extension/src/notification-service/ExtensionNotificationServiceManager.tsx new file mode 100644 index 00000000000..eca325e59e0 --- /dev/null +++ b/apps/extension/src/notification-service/ExtensionNotificationServiceManager.tsx @@ -0,0 +1,57 @@ +import { useQuery } from '@tanstack/react-query' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' +import { getIsNotificationServiceLocalOverrideEnabled } from '@universe/notifications' +import React, { useEffect } from 'react' +import { navigate } from 'src/app/navigation/state' +import { getNotificationServiceQueryOptions } from 'src/notification-service/ExtensionNotificationService' +import { NotificationContainer } from 'src/notification-service/notification-renderer/NotificationContainer' +import { getReduxStore } from 'src/store/store' +import { getLogger } from 'utilities/src/logger/logger' + +/** + * Manages the lifecycle of the notification service in the extension. + */ +export function ExtensionNotificationServiceManager(): React.JSX.Element | null { + const isNotificationServiceEnabledFlag = useFeatureFlag(FeatureFlags.NotificationService) + const isNotificationServiceEnabled = + getIsNotificationServiceLocalOverrideEnabled() || isNotificationServiceEnabledFlag + const isApiDataSourceEnabledFlag = useFeatureFlag(FeatureFlags.NotificationApiDataSource) + + const { data: notificationService } = useQuery( + getNotificationServiceQueryOptions({ + navigate: (path: string) => navigate({ pathname: path }), + getIsEnabled: () => isNotificationServiceEnabled, + getIsApiDataSourceEnabled: () => isApiDataSourceEnabledFlag, + getReduxStore: () => getReduxStore(), + }), + ) + + useEffect(() => { + if (!notificationService) { + return undefined + } + + notificationService.initialize().catch((error) => { + getLogger().error(error, { + tags: { file: 'ExtensionNotificationServiceManager', function: 'initialize' }, + extra: { message: 'Failed to initialize notification service' }, + }) + }) + + return () => { + notificationService.destroy() + } + }, [notificationService]) + + if (!isNotificationServiceEnabled || !notificationService) { + return null + } + + return ( + + ) +} diff --git a/apps/extension/src/notification-service/createChromeStorageAdapter.ts b/apps/extension/src/notification-service/createChromeStorageAdapter.ts new file mode 100644 index 00000000000..5f94c4b7645 --- /dev/null +++ b/apps/extension/src/notification-service/createChromeStorageAdapter.ts @@ -0,0 +1,87 @@ +import type { ApiNotificationTrackerContext } from '@universe/notifications' +import { getLogger } from 'utilities/src/logger/logger' +import { z } from 'zod' + +const NOTIFICATION_STORAGE_KEY = 'uniswap_notifications_processed' + +const NotificationStorageSchema = z.record( + z.string(), + z.object({ + timestamp: z.number(), + }), +) + +type NotificationStorage = z.infer + +/** + * Parses and validates notification storage data from chrome.storage.local + * @param functionName - Name of the calling function for error logging + * @returns Validated storage data or empty object if parsing fails + */ +async function parseNotificationStorage(functionName: string): Promise { + try { + const result = await chrome.storage.local.get(NOTIFICATION_STORAGE_KEY) + const stored = result[NOTIFICATION_STORAGE_KEY] + + if (!stored) { + return {} + } + + const parsed = NotificationStorageSchema.safeParse(stored) + if (!parsed.success) { + getLogger().error(parsed.error, { + tags: { file: 'createChromeStorageAdapter', function: functionName }, + }) + return {} + } + return parsed.data + } catch (error) { + getLogger().error(error, { + tags: { file: 'createChromeStorageAdapter', function: functionName }, + }) + return {} + } +} + +/** + * Creates a chrome.storage.local adapter that implements the storage interface + * required by the API notification tracker. + * + * This adapter stores notification IDs with timestamps to enable: + * - Offline tracking (when API calls fail) + * - Deduplication (avoid sending duplicate ACKs) + * - Cleanup of old entries + */ +export function createChromeStorageAdapter(): NonNullable { + return { + has: async (notificationId: string): Promise => { + const processedIds = await parseNotificationStorage('has') + return notificationId in processedIds + }, + + add: async (notificationId: string, metadata?: { timestamp: number }): Promise => { + const processedIds = await parseNotificationStorage('add') + processedIds[notificationId] = { timestamp: metadata?.timestamp ?? Date.now() } + await chrome.storage.local.set({ [NOTIFICATION_STORAGE_KEY]: processedIds }) + }, + + getAll: async (): Promise> => { + const processedIds = await parseNotificationStorage('getAll') + return new Set(Object.keys(processedIds)) + }, + + deleteOlderThan: async (timestamp: number): Promise => { + try { + const processedIds = await parseNotificationStorage('deleteOlderThan') + const filtered = Object.fromEntries( + Object.entries(processedIds).filter(([, value]) => value.timestamp > timestamp), + ) + await chrome.storage.local.set({ [NOTIFICATION_STORAGE_KEY]: filtered }) + } catch (error) { + getLogger().error(error, { + tags: { file: 'createChromeStorageAdapter', function: 'deleteOlderThan' }, + }) + } + }, + } +} diff --git a/apps/extension/src/notification-service/data-sources/createExtensionLegacyBannersNotificationDataSource.ts b/apps/extension/src/notification-service/data-sources/createExtensionLegacyBannersNotificationDataSource.ts new file mode 100644 index 00000000000..360ed8f1050 --- /dev/null +++ b/apps/extension/src/notification-service/data-sources/createExtensionLegacyBannersNotificationDataSource.ts @@ -0,0 +1,290 @@ +import { + Background, + Content, + Notification, + NotificationVersion, + OnClick, +} from '@uniswap/client-notification-service/dist/uniswap/notificationservice/v1/api_pb' +import { BackgroundType, ContentStyle, type InAppNotification, OnClickAction, SharedQueryClient } from '@universe/api' +import { + createNotificationDataSource, + type NotificationDataSource, + type NotificationTracker, +} from '@universe/notifications' +import { AppRoutes, SettingsRoutes, UnitagClaimRoutes } from 'src/app/navigation/constants' +import { getReduxStore } from 'src/store/store' +import { AccountType } from 'uniswap/src/features/accounts/types' +import { Platform } from 'uniswap/src/features/platforms/types/Platform' +import { UNITAG_SUFFIX_NO_LEADING_DOT } from 'uniswap/src/features/unitags/constants' +import i18n from 'uniswap/src/i18n' +import { getValidAddress } from 'uniswap/src/utils/addresses' +import { logger } from 'utilities/src/logger/logger' +import { ReactQueryCacheKey } from 'utilities/src/reactQuery/cache' +import { selectHasSkippedUnitagPrompt } from 'wallet/src/features/behaviorHistory/selectors' +import { hasExternalBackup } from 'wallet/src/features/wallet/accounts/utils' + +// Using 'local:' prefix to indicate these are client-only notifications +// This prevents the API tracker from sending AckNotification calls to the backend +const RECOVERY_BACKUP_BANNER_ID = 'local:recovery_backup_banner' +const UNITAG_CLAIM_BANNER_ID = 'local:unitag_claim_banner' + +interface CreateExtensionLegacyBannersNotificationDataSourceContext { + tracker: NotificationTracker + pollIntervalMs?: number +} + +/** + * Creates a notification data source that converts HomeIntroCardStack cards + * into InAppNotifications compatible with the notification system. + * + * This replaces the legacy HomeIntroCardStack with notification system equivalents: + * - Recovery backup reminder + * - Unitag claim prompt + * + * **Migration Logic:** + * This data source checks for legacy dismissal state in Redux and automatically + * migrates it to the notification system on first run. + */ +export function createExtensionLegacyBannersNotificationDataSource( + ctx: CreateExtensionLegacyBannersNotificationDataSourceContext, +): NotificationDataSource { + const { tracker, pollIntervalMs = 5000 } = ctx + + let intervalId: NodeJS.Timeout | null = null + let currentCallback: ((notifications: InAppNotification[], source: string) => void) | null = null + let hasMigratedLegacyState = false + + /** + * Migrates legacy dismissal state from Redux to the notification system. + * This runs once on the first poll to ensure users who dismissed cards in the old system + * don't see them again. + */ + const migrateLegacyDismissalState = async (): Promise => { + if (hasMigratedLegacyState) { + return + } + + try { + const state = getReduxStore().getState() + + // Migrate Unitag skip state + const hasSkippedUnitag = selectHasSkippedUnitagPrompt(state) + if (hasSkippedUnitag) { + logger.info( + 'createExtensionLegacyBannersNotificationDataSource', + 'migrateLegacyDismissalState', + 'Migrating Unitag skip from legacy Redux state', + ) + await tracker.track(UNITAG_CLAIM_BANNER_ID, { timestamp: Date.now() }) + } + + hasMigratedLegacyState = true + } catch (error) { + logger.error(error, { + tags: { + file: 'createExtensionLegacyBannersNotificationDataSource', + function: 'migrateLegacyDismissalState', + }, + }) + } + } + + const pollForNotifications = async (): Promise => { + if (!currentCallback) { + return + } + + try { + // Run migration on first poll + await migrateLegacyDismissalState() + + const notifications = await fetchNotifications() + currentCallback(notifications, 'legacy_intro_cards') + } catch (error) { + logger.error(error, { + tags: { file: 'createExtensionLegacyBannersNotificationDataSource', function: 'pollForNotifications' }, + }) + } + } + + const start = (onNotifications: (notifications: InAppNotification[], source: string) => void): void => { + if (intervalId) { + return + } + + currentCallback = onNotifications + + pollForNotifications().catch((error) => { + logger.error(error, { + tags: { file: 'createExtensionLegacyBannersNotificationDataSource', function: 'start' }, + }) + }) + + intervalId = setInterval(() => { + pollForNotifications().catch((error) => { + logger.error(error, { + tags: { file: 'createExtensionLegacyBannersNotificationDataSource', function: 'setInterval' }, + }) + }) + }, pollIntervalMs) + } + + const stop = async (): Promise => { + if (intervalId) { + clearInterval(intervalId) + intervalId = null + } + currentCallback = null + } + + return createNotificationDataSource({ start, stop }) +} + +/** + * Fetches all notifications based on current conditions. + * The processor will handle filtering based on tracked/processed state. + * + * Priority order (matches useSharedIntroCards): + * 1. Recovery backup (if no external backup) + * 2. Unitag claim (if eligible) + */ +async function fetchNotifications(): Promise { + const notifications: InAppNotification[] = [] + + // Priority 1: Recovery backup reminder + const backupNotification = await checkRecoveryBackup() + if (backupNotification) { + notifications.push(backupNotification) + } + + // Priority 2: Unitag claim + const unitagNotification = await checkUnitagClaim() + if (unitagNotification) { + notifications.push(unitagNotification) + } + + return notifications +} + +/** + * Check if recovery backup reminder should be shown. + */ +async function checkRecoveryBackup(): Promise { + const state = getReduxStore().getState() + const activeAccount = state.wallet.accounts[state.wallet.activeAccountAddress ?? ''] + + if (!activeAccount || activeAccount.type !== AccountType.SignerMnemonic) { + return null + } + + const hasBackup = hasExternalBackup(activeAccount) + if (hasBackup) { + return null + } + + return createRecoveryBackupBanner() +} + +/** + * Check if Unitag claim prompt should be shown. + */ +async function checkUnitagClaim(): Promise { + const state = getReduxStore().getState() + const activeAccount = state.wallet.accounts[state.wallet.activeAccountAddress ?? ''] + + if (!activeAccount || activeAccount.type !== AccountType.SignerMnemonic) { + return null + } + + // Check if any account has a unitag by reading from React Query cache + // Unitags are fetched via API and cached, not stored in Redux + const accounts = Object.values(state.wallet.accounts) as Array<{ address?: string }> + const hasAnyUnitag = accounts.some((account) => { + if (!account.address) { + return false + } + // Normalize the address the same way useUnitagsAddressQuery does + // This ensures we match the exact query key format used when caching + const validatedAddress = getValidAddress({ address: account.address, platform: Platform.EVM }) + if (!validatedAddress) { + return false + } + + // Read from React Query cache using the same query key as useUnitagsAddressQuery + const queryKey = [ReactQueryCacheKey.UnitagsApi, 'address', { address: validatedAddress }] + const cachedUnitag = SharedQueryClient.getQueryData<{ username?: string }>(queryKey) + + return !!cachedUnitag?.username + }) + + if (hasAnyUnitag) { + return null + } + + // Note: We can't check canClaimUnitag here since it requires an async query + // The notification will be shown and processor will handle dismissal if already claimed + return createUnitagClaimBanner() +} + +/** + * Create recovery backup banner notification + */ +function createRecoveryBackupBanner(): InAppNotification { + return new Notification({ + id: RECOVERY_BACKUP_BANNER_ID, + content: new Content({ + version: NotificationVersion.V0, + style: ContentStyle.LOWER_LEFT_BANNER, + title: i18n.t('onboarding.home.intro.backup.title'), + subtitle: i18n.t('onboarding.home.intro.backup.description.extension'), + background: new Background({ + backgroundType: BackgroundType.UNSPECIFIED, + backgroundOnClick: new OnClick({ + // No ACK here - required notifications should reappear until the user completes the backup + // The notification will stop showing once hasExternalBackup() returns true + onClick: [OnClickAction.EXTERNAL_LINK, OnClickAction.DISMISS], + onClickLink: `/${AppRoutes.Settings}/${SettingsRoutes.BackupRecoveryPhrase}`, + }), + }), + // No onDismissClick - required cards cannot be dismissed + buttons: [], + iconLink: 'custom:shield-check-$accent1', + // Encode cardType in extra field for IntroCard rendering + extra: JSON.stringify({ cardType: 'required', graphicType: 'icon' }), + }), + }) +} + +/** + * Create Unitag claim banner notification + */ +function createUnitagClaimBanner(): InAppNotification { + // We need to construct a special action that will call focusOrCreateUnitagTab + // Since we can't directly call functions from notification actions, we'll use a special internal link + // that the navigation handler will recognize and handle specially + const unitagClaimLink = `unitag://claim/${UnitagClaimRoutes.ClaimIntro}` + + return new Notification({ + id: UNITAG_CLAIM_BANNER_ID, + content: new Content({ + version: NotificationVersion.V0, + style: ContentStyle.LOWER_LEFT_BANNER, + title: i18n.t('onboarding.home.intro.unitag.title', { unitagDomain: UNITAG_SUFFIX_NO_LEADING_DOT }), + subtitle: i18n.t('onboarding.home.intro.unitag.description'), + background: new Background({ + backgroundType: BackgroundType.UNSPECIFIED, + backgroundOnClick: new OnClick({ + onClick: [OnClickAction.EXTERNAL_LINK, OnClickAction.DISMISS, OnClickAction.ACK], + onClickLink: unitagClaimLink, + }), + }), + onDismissClick: new OnClick({ + onClick: [OnClickAction.DISMISS, OnClickAction.ACK], + }), + buttons: [], + iconLink: 'custom:person-$accent1', + // Encode cardType in extra field for IntroCard rendering + extra: JSON.stringify({ cardType: 'dismissible', graphicType: 'icon' }), + }), + }) +} diff --git a/apps/extension/src/notification-service/data-sources/reactive/storageWarningCondition.ts b/apps/extension/src/notification-service/data-sources/reactive/storageWarningCondition.ts new file mode 100644 index 00000000000..aa92fe14721 --- /dev/null +++ b/apps/extension/src/notification-service/data-sources/reactive/storageWarningCondition.ts @@ -0,0 +1,140 @@ +import { + Content, + Metadata, + Notification, + OnClick, +} from '@uniswap/client-notification-service/dist/uniswap/notificationservice/v1/api_pb' +import { ContentStyle, type InAppNotification, OnClickAction } from '@universe/api' +import { type ReactiveCondition } from '@universe/notifications' +import { GlobalErrorEvent } from 'src/app/events/constants' +import { globalEventEmitter } from 'src/app/events/global' +import { logger } from 'utilities/src/logger/logger' + +/** + * Storage threshold in bytes (500KB). + * When remaining storage quota falls below this, show warning. + */ +const REMAINING_STORAGE_THRESHOLD_BYTES = 500_000 + +/** + * Unique ID for the storage warning notification. + * Uses 'local:' prefix to distinguish from backend-generated notifications. + */ +const STORAGE_WARNING_NOTIFICATION_ID = 'local:session:storage_warning' + +/** + * State tracked by the storage warning condition. + */ +interface StorageWarningConditionState { + isLowStorage: boolean + /** Whether the user is in onboarding flow (affects modal UI) */ + isOnboarding: boolean +} + +/** + * Context required to create the storage warning condition. + */ +interface CreateStorageWarningConditionContext { + /** Whether this is being used in the onboarding context */ + isOnboarding: boolean +} + +/** + * Creates a reactive condition for the storage warning modal. + * + * The warning will show when: + * - Remaining storage (quota - usage) is below 500KB (checked on mount) + * - OR a ReduxStorageExceeded event is emitted + * + * The warning only shows once per session (like the original implementation). + * + * @see useCheckLowStorage for the original implementation + */ +export function createStorageWarningCondition( + ctx: CreateStorageWarningConditionContext, +): ReactiveCondition { + const { isOnboarding } = ctx + + // Track if we've already shown the warning this session + let hasShownWarning = false + + return { + notificationId: STORAGE_WARNING_NOTIFICATION_ID, + + subscribe: (onStateChange) => { + // Check storage on initial subscription (if not onboarding) + if (!isOnboarding) { + navigator.storage + .estimate() + .then(({ quota, usage }) => { + const remaining = (quota ?? 0) - (usage ?? 0) + if (remaining < REMAINING_STORAGE_THRESHOLD_BYTES && !hasShownWarning) { + hasShownWarning = true + logger.info('storageWarningCondition', 'subscribe', 'Low storage warning triggered by quota check') + onStateChange({ isLowStorage: true, isOnboarding }) + } + }) + .catch(() => { + // Silently ignore storage estimation errors + }) + } + + // Listen for Redux storage exceeded events + const listener = (): void => { + if (!hasShownWarning) { + hasShownWarning = true + logger.info('storageWarningCondition', 'subscribe', 'Low storage warning triggered by ReduxStorageExceeded') + onStateChange({ isLowStorage: true, isOnboarding }) + } + } + + globalEventEmitter.addListener(GlobalErrorEvent.ReduxStorageExceeded, listener) + + // Return unsubscribe function + return () => { + globalEventEmitter.removeListener(GlobalErrorEvent.ReduxStorageExceeded, listener) + } + }, + + shouldShow: (state) => { + return state.isLowStorage + }, + + createNotification: (state): InAppNotification => { + return new Notification({ + id: STORAGE_WARNING_NOTIFICATION_ID, + content: new Content({ + // Use SYSTEM_BANNER style for system alerts + style: ContentStyle.SYSTEM_BANNER, + title: '', // Title is rendered by the custom renderer using i18n + version: 0, + buttons: [], + onDismissClick: new OnClick({ + onClick: [OnClickAction.DISMISS, OnClickAction.ACK], + }), + }), + // Store isOnboarding in metadata business field for the renderer + metadata: new Metadata({ + owner: 'local', + business: state.isOnboarding ? 'storage_warning_onboarding' : 'storage_warning', + }), + }) + }, + } +} + +/** + * Type guard to check if a notification is the storage warning notification. + * Used by NotificationContainer to route to the correct renderer. + */ +export function isStorageWarningNotification(notification: InAppNotification): boolean { + return notification.id === STORAGE_WARNING_NOTIFICATION_ID +} + +/** + * Extract isOnboarding flag from notification metadata. + * Uses the business field to determine if this is an onboarding notification. + */ +export function getIsOnboardingFromNotification(notification: InAppNotification): boolean { + return notification.metadata?.business === 'storage_warning_onboarding' +} diff --git a/apps/extension/src/notification-service/notification-renderer/NotificationContainer.tsx b/apps/extension/src/notification-service/notification-renderer/NotificationContainer.tsx new file mode 100644 index 00000000000..a2ba8cabac7 --- /dev/null +++ b/apps/extension/src/notification-service/notification-renderer/NotificationContainer.tsx @@ -0,0 +1,229 @@ +import { ContentStyle, type InAppNotification } from '@universe/api' +import { type NotificationClickTarget } from '@universe/notifications' +import { InlineBannerNotification } from '@universe/notifications/src/notification-renderer/components/InlineBannerNotification' +import { memo, useEffect, useMemo } from 'react' +import { isStorageWarningNotification } from 'src/notification-service/data-sources/reactive/storageWarningCondition' +import { + extensionNotificationStore, + type NotificationState, +} from 'src/notification-service/notification-renderer/notificationStore' +import { AppRatingModalRenderer } from 'src/notification-service/renderers/AppRatingModalRenderer' +import { StorageWarningModalRenderer } from 'src/notification-service/renderers/StorageWarningModalRenderer' +import { isAppRatingNotification } from 'src/notification-service/triggers/appRatingTrigger' +import { isLocalTriggerNotification } from 'src/notification-service/triggers/createExtensionLocalTriggerDataSource' +import { ModalNotification } from 'uniswap/src/components/notifications/ModalNotification' +import { getLogger } from 'utilities/src/logger/logger' +import { useEvent } from 'utilities/src/react/hooks' +import { type IntroCardProps } from 'wallet/src/components/introCards/IntroCard' +import { IntroCardStack } from 'wallet/src/components/introCards/IntroCardStack' +import { + convertNotificationToIntroCard, + shouldRenderAsIntroCard, +} from 'wallet/src/features/notifications/convertNotificationToIntroCard' +import { type StoreApi, type UseBoundStore } from 'zustand' + +/** + * Routes a notification to the appropriate renderer based on its style + */ +function Notification({ + notification, + onRenderFailed, + onNotificationClick, + onNotificationShown, +}: { + notification: InAppNotification + onRenderFailed?: (id: string) => void + onNotificationClick?: (notificationId: string, target: NotificationClickTarget) => void + onNotificationShown?: (notificationId: string) => void +}) { + const style = notification.content?.style + const isUnknownStyle = style !== ContentStyle.MODAL && style !== ContentStyle.LOWER_LEFT_BANNER + + // Handle unknown/invalid notification styles as a side effect + // This handles cases where the server sends string enums on first request and numeric + // enums on subsequent requests. Clean up without marking as processed to allow retry. + useEffect(() => { + if (!isUnknownStyle) { + return () => null + } + + getLogger().warn( + 'NotificationRenderer', + 'renderNotification', + `Unknown notification style: ${style}, cleaning up failed render to allow retry with correct data`, + { + notification, + }, + ) + + const timeoutId = setTimeout(() => { + onRenderFailed?.(notification.id) + }, 1) + + return () => clearTimeout(timeoutId) + }, [isUnknownStyle, style, notification, onRenderFailed]) + + if (isUnknownStyle) { + return null + } + + if (style === ContentStyle.MODAL) { + return ( + + ) + } + + // ContentStyle.LOWER_LEFT_BANNER + // Intro cards are handled by IntroCardStack in NotificationContainer + if (shouldRenderAsIntroCard(notification)) { + // Intro cards shouldn't reach here since they're filtered in NotificationContainer + getLogger().warn( + 'NotificationRenderer', + 'renderNotification', + 'IntroCard notification reached NotificationRenderer - should be handled by IntroCardStack', + { notification }, + ) + return null + } + + // Standard banner notification + return +} + +/** + * Subscribes to the notification store and renders active notifications depending on their style. + */ +export const NotificationContainer = memo(function NotificationContainer({ + onRenderFailed, + onNotificationClick, + onNotificationShown, + store = extensionNotificationStore, +}: { + onRenderFailed?: (notificationId: string) => void + onNotificationClick?: (notificationId: string, target: NotificationClickTarget) => void + onNotificationShown?: (notificationId: string) => void + store?: UseBoundStore> +}) { + const activeNotifications = store((state) => state.activeNotifications) + const removeNotification = store((state) => state.removeNotification) + + const handleRenderFailed = useEvent((notificationId: string) => { + removeNotification(notificationId) + onRenderFailed?.(notificationId) + }) + + const handleIntroCardPress = useEvent((notificationId: string) => { + onNotificationClick?.(notificationId, { type: 'background' }) + }) + + const handleIntroCardClose = useEvent((notificationId: string) => { + onNotificationClick?.(notificationId, { type: 'dismiss' }) + }) + + // Separate notifications by type: intro cards, local triggers, system banners, and standard notifications + const { introCardNotifications, localTriggerNotifications, systemBannerNotifications, standardNotifications } = + useMemo(() => { + const introCards: InAppNotification[] = [] + const localTriggers: InAppNotification[] = [] + const systemBanners: InAppNotification[] = [] + const standard: InAppNotification[] = [] + + activeNotifications.forEach((notification) => { + if (notification.content?.style === ContentStyle.SYSTEM_BANNER) { + // System banners (storage warning, etc.) use SYSTEM_BANNER style + systemBanners.push(notification) + } else if (shouldRenderAsIntroCard(notification)) { + introCards.push(notification) + } else if (isLocalTriggerNotification(notification.id)) { + localTriggers.push(notification) + } else { + standard.push(notification) + } + }) + + return { + introCardNotifications: introCards, + localTriggerNotifications: localTriggers, + systemBannerNotifications: systemBanners, + standardNotifications: standard, + } + }, [activeNotifications]) + + // Convert intro card notifications to IntroCardProps + const introCards: IntroCardProps[] = useMemo(() => { + return introCardNotifications + .map((notification) => { + return convertNotificationToIntroCard(notification, { + onPress: () => handleIntroCardPress(notification.id), + onClose: () => handleIntroCardClose(notification.id), + }) + }) + .filter((card): card is IntroCardProps => card !== null) + }, [introCardNotifications, handleIntroCardPress, handleIntroCardClose]) + + return ( + <> + {/* Render intro cards in a stack */} + {introCards.length > 0 && } + + {/* Render local trigger notifications with custom renderers */} + {localTriggerNotifications.map((notification) => { + if (isAppRatingNotification(notification)) { + return ( + + ) + } + // Add more local trigger renderers here as they are migrated + // e.g., if (isSmartWalletCreatedNotification(notification)) { ... } + getLogger().warn( + 'NotificationContainer', + 'localTriggerNotifications', + `Unknown local trigger notification: ${notification.id}`, + { notification }, + ) + return null + })} + + {/* Render system banner notifications (storage warning, etc.) */} + {systemBannerNotifications.map((notification) => { + if (isStorageWarningNotification(notification)) { + return ( + + ) + } + getLogger().warn( + 'NotificationContainer', + 'systemBannerNotifications', + `Unknown system banner notification: ${notification.id}`, + { notification }, + ) + return null + })} + + {/* Render standard notification types (modals, banners, etc) */} + {standardNotifications.map((notification) => ( + + ))} + + ) +}) diff --git a/apps/extension/src/notification-service/notification-renderer/createExtensionNotificationRenderer.ts b/apps/extension/src/notification-service/notification-renderer/createExtensionNotificationRenderer.ts new file mode 100644 index 00000000000..89977ffdea5 --- /dev/null +++ b/apps/extension/src/notification-service/notification-renderer/createExtensionNotificationRenderer.ts @@ -0,0 +1,44 @@ +import { ContentStyle, type InAppNotification } from '@universe/api' +import { createNotificationRenderer, type NotificationRenderer } from '@universe/notifications' +import { type NotificationState } from 'src/notification-service/notification-renderer/notificationStore' +import { type StoreApi, type UseBoundStore } from 'zustand' + +interface CreateExtensionNotificationRendererContext { + store: UseBoundStore> +} + +/** + * Creates an extension-specific NotificationRenderer that uses Zustand store. + * This renderer coordinates rendering for all notification types in the extension. + */ +export function createExtensionNotificationRenderer( + ctx: CreateExtensionNotificationRendererContext, +): NotificationRenderer { + const store = ctx.store + + return createNotificationRenderer({ + render: (notification: InAppNotification): (() => void) => { + // Add notification to the store, which will trigger React to render it + store.getState().addNotification(notification) + + // Return cleanup function that removes the notification from the store + return (): void => { + store.getState().removeNotification(notification.id) + } + }, + + canRender: (notification: InAppNotification): boolean => { + const { activeNotifications } = store.getState() + const style = notification.content?.style + + // Only one modal at a time + if (style === ContentStyle.MODAL) { + const hasActiveModal = activeNotifications.some((n) => n.content?.style === ContentStyle.MODAL) + return !hasActiveModal + } + + // Other notification types can be rendered concurrently (up to limits defined in the processor) + return true + }, + }) +} diff --git a/apps/extension/src/notification-service/notification-renderer/notificationStore.ts b/apps/extension/src/notification-service/notification-renderer/notificationStore.ts new file mode 100644 index 00000000000..b2f9c53e067 --- /dev/null +++ b/apps/extension/src/notification-service/notification-renderer/notificationStore.ts @@ -0,0 +1,37 @@ +import { type InAppNotification } from '@universe/api' +import { create, type StoreApi, type UseBoundStore } from 'zustand' + +export interface NotificationState { + // Currently active notifications + activeNotifications: InAppNotification[] + // Add a notification to be rendered + addNotification: (notification: InAppNotification) => void + // Remove a notification from the active list + removeNotification: (notificationId: string) => void +} + +export const extensionNotificationStore: UseBoundStore> = create( + (set) => ({ + activeNotifications: [], + + addNotification: (notification: InAppNotification): void => { + set((state) => { + // The NotificationService should prevent duplicates, but we check here defensively just in case. + const exists = state.activeNotifications.some((n) => n.id === notification.id) + if (exists) { + return state + } + + return { + activeNotifications: [...state.activeNotifications, notification], + } + }) + }, + + removeNotification: (notificationId: string): void => { + set((state) => ({ + activeNotifications: state.activeNotifications.filter((n) => n.id !== notificationId), + })) + }, + }), +) diff --git a/apps/extension/src/notification-service/notification-telemetry/getNotificationTelemetry.ts b/apps/extension/src/notification-service/notification-telemetry/getNotificationTelemetry.ts new file mode 100644 index 00000000000..ed34b17751a --- /dev/null +++ b/apps/extension/src/notification-service/notification-telemetry/getNotificationTelemetry.ts @@ -0,0 +1,36 @@ +import { createNotificationTelemetry, type NotificationTelemetry } from '@universe/notifications' +import { InterfaceEventName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' + +/** + * Creates a NotificationTelemetry implementation that sends events to Amplitude + * via the existing extension analytics infrastructure. + */ +export function getNotificationTelemetry(): NotificationTelemetry { + return createNotificationTelemetry({ + onNotificationReceived(params) { + sendAnalyticsEvent(InterfaceEventName.NotificationReceived, { + notification_id: params.notificationId, + notification_type: params.type, + source: params.source, + timestamp: params.timestamp, + }) + }, + + onNotificationShown(params) { + sendAnalyticsEvent(InterfaceEventName.NotificationShown, { + notification_id: params.notificationId, + notification_type: params.type, + timestamp: params.timestamp, + }) + }, + + onNotificationInteracted(params) { + sendAnalyticsEvent(InterfaceEventName.NotificationInteracted, { + notification_id: params.notificationId, + notification_type: params.type, + action: params.action, + }) + }, + }) +} diff --git a/apps/extension/src/notification-service/renderers/AppRatingModalRenderer.tsx b/apps/extension/src/notification-service/renderers/AppRatingModalRenderer.tsx new file mode 100644 index 00000000000..89fcc6765d6 --- /dev/null +++ b/apps/extension/src/notification-service/renderers/AppRatingModalRenderer.tsx @@ -0,0 +1,40 @@ +import { type InAppNotification } from '@universe/api' +import { type NotificationClickTarget } from '@universe/notifications' +import { useEffect } from 'react' +import AppRatingModal from 'src/app/features/appRating/AppRatingModal' + +interface AppRatingModalRendererProps { + notification: InAppNotification + onNotificationClick?: (notificationId: string, target: NotificationClickTarget) => void + onNotificationShown?: (notificationId: string) => void +} + +/** + * Wrapper component that renders the AppRatingModal within the notification service. + * + * This component: + * 1. Reports when the modal is shown (for telemetry) + * 2. Reports when the modal is dismissed (for tracking) + * 3. Preserves the existing modal UI exactly (no visual changes) + * + * The AppRatingModal handles its own internal state (Initial, NotReally, Yes states) + * and Redux updates. This wrapper just handles the notification service integration. + */ +export function AppRatingModalRenderer({ + notification, + onNotificationClick, + onNotificationShown, +}: AppRatingModalRendererProps): JSX.Element { + // Report when the modal is shown + useEffect(() => { + onNotificationShown?.(notification.id) + }, [notification.id, onNotificationShown]) + + const handleClose = (): void => { + // Report to notification service that user dismissed the modal + // The AppRatingModal internally handles analytics and Redux updates + onNotificationClick?.(notification.id, { type: 'dismiss' }) + } + + return +} diff --git a/apps/extension/src/notification-service/renderers/StorageWarningModalRenderer.tsx b/apps/extension/src/notification-service/renderers/StorageWarningModalRenderer.tsx new file mode 100644 index 00000000000..6ea9244bc15 --- /dev/null +++ b/apps/extension/src/notification-service/renderers/StorageWarningModalRenderer.tsx @@ -0,0 +1,70 @@ +import { type InAppNotification } from '@universe/api' +import { type NotificationClickTarget } from '@universe/notifications' +import { useCallback, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { ONBOARDING_CONTENT_WIDTH } from 'src/app/features/onboarding/utils' +import { AppRoutes, SettingsRoutes } from 'src/app/navigation/constants' +import { useExtensionNavigation } from 'src/app/navigation/utils' +import { getIsOnboardingFromNotification } from 'src/notification-service/data-sources/reactive/storageWarningCondition' +import { spacing } from 'ui/src/theme' +import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' +import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' +import { ModalName } from 'uniswap/src/features/telemetry/constants' + +interface StorageWarningModalRendererProps { + notification: InAppNotification + onNotificationClick?: (notificationId: string, target: NotificationClickTarget) => void + onNotificationShown?: (notificationId: string) => void +} + +/** + * Renderer for the storage warning modal notification. + * + * This component preserves the exact UI of the original StorageWarningModal: + * - High severity warning modal + * - "Close" button always shown + * - "View Recovery Phrase" button shown only when NOT in onboarding + * + * @see StorageWarningModal for the original implementation + */ +export function StorageWarningModalRenderer({ + notification, + onNotificationClick, + onNotificationShown, +}: StorageWarningModalRendererProps): JSX.Element { + const { t } = useTranslation() + const { navigateTo } = useExtensionNavigation() + const isOnboarding = getIsOnboardingFromNotification(notification) + + // Report when the modal is shown + useEffect(() => { + onNotificationShown?.(notification.id) + }, [notification.id, onNotificationShown]) + + const handleClose = useCallback((): void => { + // Report to notification service that user dismissed the modal + onNotificationClick?.(notification.id, { type: 'dismiss' }) + }, [notification.id, onNotificationClick]) + + const handleAcknowledge = useCallback((): void => { + // Close the modal first + onNotificationClick?.(notification.id, { type: 'dismiss' }) + // Navigate to recovery phrase settings + navigateTo(`/${AppRoutes.Settings}/${SettingsRoutes.ViewRecoveryPhrase}`) + }, [notification.id, onNotificationClick, navigateTo]) + + return ( + + ) +} diff --git a/apps/extension/src/notification-service/triggers/appRatingTrigger.test.ts b/apps/extension/src/notification-service/triggers/appRatingTrigger.test.ts new file mode 100644 index 00000000000..8ca88155910 --- /dev/null +++ b/apps/extension/src/notification-service/triggers/appRatingTrigger.test.ts @@ -0,0 +1,144 @@ +import { + Content, + Metadata, + Notification, +} from '@uniswap/client-notification-service/dist/uniswap/notificationservice/v1/api_pb' +import { ContentStyle } from '@universe/api' +import { + APP_RATING_NOTIFICATION_ID, + createAppRatingTrigger, + isAppRatingNotification, +} from 'src/notification-service/triggers/appRatingTrigger' +import { type ExtensionState } from 'src/store/extensionReducer' +import { appRatingStateSelector } from 'wallet/src/features/appRating/selectors' +import { setAppRating } from 'wallet/src/features/wallet/slice' + +jest.mock('wallet/src/features/appRating/selectors') +const mockAppRatingStateSelector = appRatingStateSelector as jest.MockedFunction + +describe('appRatingTrigger', () => { + const mockDispatch = jest.fn() + const mockGetState = jest.fn() + + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('createAppRatingTrigger', () => { + it('returns a trigger with the correct ID', () => { + const trigger = createAppRatingTrigger({ + getState: mockGetState, + dispatch: mockDispatch, + }) + + expect(trigger.id).toBe(APP_RATING_NOTIFICATION_ID) + expect(trigger.id.startsWith('local:')).toBe(true) + }) + + describe('shouldShow', () => { + it('returns true when appRatingStateSelector.shouldPrompt is true', () => { + mockAppRatingStateSelector.mockReturnValue({ + shouldPrompt: true, + consecutiveSwapsCondition: true, + appRatingPromptedMs: undefined, + appRatingProvidedMs: undefined, + }) + + const trigger = createAppRatingTrigger({ + getState: mockGetState, + dispatch: mockDispatch, + }) + + expect(trigger.shouldShow()).toBe(true) + expect(mockAppRatingStateSelector).toHaveBeenCalledWith(mockGetState()) + }) + + it('returns false when appRatingStateSelector.shouldPrompt is false', () => { + mockAppRatingStateSelector.mockReturnValue({ + shouldPrompt: false, + consecutiveSwapsCondition: false, + appRatingPromptedMs: Date.now(), + appRatingProvidedMs: undefined, + }) + + const trigger = createAppRatingTrigger({ + getState: mockGetState, + dispatch: mockDispatch, + }) + + expect(trigger.shouldShow()).toBe(false) + }) + }) + + describe('createNotification', () => { + it('returns a notification with the correct ID and style', () => { + const trigger = createAppRatingTrigger({ + getState: mockGetState, + dispatch: mockDispatch, + }) + + const notification = trigger.createNotification() + + expect(notification.id).toBe(APP_RATING_NOTIFICATION_ID) + expect(notification.content?.style).toBe(ContentStyle.MODAL) + }) + + it('returns a notification with local metadata', () => { + const trigger = createAppRatingTrigger({ + getState: mockGetState, + dispatch: mockDispatch, + }) + + const notification = trigger.createNotification() + + expect(notification.metadata?.owner).toBe('local') + expect(notification.metadata?.business).toBe('app_rating') + }) + }) + + describe('onAcknowledge', () => { + it('dispatches setAppRating when called', () => { + const trigger = createAppRatingTrigger({ + getState: mockGetState, + dispatch: mockDispatch, + }) + + trigger.onAcknowledge?.() + + expect(mockDispatch).toHaveBeenCalledWith(setAppRating({})) + }) + }) + }) + + describe('isAppRatingNotification', () => { + it('returns true for app rating notification', () => { + const notification = new Notification({ + id: APP_RATING_NOTIFICATION_ID, + content: new Content({ style: ContentStyle.MODAL, title: '' }), + metadata: new Metadata({ owner: 'local', business: 'app_rating' }), + }) + + expect(isAppRatingNotification(notification)).toBe(true) + }) + + it('returns false for other notifications', () => { + const notification = new Notification({ + id: 'some-other-notification', + content: new Content({ style: ContentStyle.MODAL, title: '' }), + metadata: new Metadata({ owner: 'test', business: 'test' }), + }) + + expect(isAppRatingNotification(notification)).toBe(false) + }) + + it('returns false for other local notifications', () => { + const notification = new Notification({ + id: 'local:other_trigger', + content: new Content({ style: ContentStyle.MODAL, title: '' }), + metadata: new Metadata({ owner: 'local', business: 'other' }), + }) + + expect(isAppRatingNotification(notification)).toBe(false) + }) + }) +}) diff --git a/apps/extension/src/notification-service/triggers/appRatingTrigger.ts b/apps/extension/src/notification-service/triggers/appRatingTrigger.ts new file mode 100644 index 00000000000..5a361b177ae --- /dev/null +++ b/apps/extension/src/notification-service/triggers/appRatingTrigger.ts @@ -0,0 +1,87 @@ +import { + Content, + Metadata, + Notification, + OnClick, +} from '@uniswap/client-notification-service/dist/uniswap/notificationservice/v1/api_pb' +import { ContentStyle, type InAppNotification, OnClickAction } from '@universe/api' +import { type TriggerCondition } from '@universe/notifications/src/notification-data-source/implementations/createLocalTriggerDataSource' +import { type ExtensionState } from 'src/store/extensionReducer' +import { appRatingStateSelector } from 'wallet/src/features/appRating/selectors' +import { setAppRating } from 'wallet/src/features/wallet/slice' + +/** + * Unique ID for the app rating notification. + * Uses 'local:' prefix to distinguish from backend-generated notifications. + */ +export const APP_RATING_NOTIFICATION_ID = 'local:app_rating_modal' + +/** + * Context required to create the app rating trigger. + */ +interface CreateAppRatingTriggerContext { + /** Function to get the current Redux state */ + getState: () => ExtensionState + /** Redux dispatch function */ + dispatch: (action: ReturnType) => void +} + +/** + * Creates a trigger condition for the app rating modal. + * + * The trigger will show the modal when: + * - User has completed 2+ consecutive successful swaps within 1 minute + * - Either user has never been prompted, or enough time has passed since last prompt + * (120 days without feedback, 180 days with feedback) + * + * @see appRatingStateSelector for the full logic + */ +export function createAppRatingTrigger(ctx: CreateAppRatingTriggerContext): TriggerCondition { + const { getState, dispatch } = ctx + + return { + id: APP_RATING_NOTIFICATION_ID, + + shouldShow: () => { + const state = getState() + const { shouldPrompt } = appRatingStateSelector(state) + return shouldPrompt + }, + + createNotification: (): InAppNotification => { + // Create a minimal notification - the actual UI is rendered by AppRatingModalRenderer + return new Notification({ + id: APP_RATING_NOTIFICATION_ID, + metadata: new Metadata({ + owner: 'local', + business: 'app_rating', + }), + content: new Content({ + style: ContentStyle.MODAL, + title: '', // Title handled by AppRatingModal component + version: 0, + buttons: [], // Buttons handled by AppRatingModal component + // Required: notifications must have a DISMISS action to be valid + onDismissClick: new OnClick({ + onClick: [OnClickAction.DISMISS], + }), + }), + }) + }, + + onAcknowledge: () => { + // Update Redux to mark that the user has been prompted + // This is also done in AppRatingModal's useEffect, but we include it here + // for consistency with the trigger pattern + dispatch(setAppRating({})) + }, + } +} + +/** + * Type guard to check if a notification is the app rating notification. + * Used by NotificationContainer to route to the correct renderer. + */ +export function isAppRatingNotification(notification: InAppNotification): boolean { + return notification.id === APP_RATING_NOTIFICATION_ID +} diff --git a/apps/extension/src/notification-service/triggers/createExtensionLocalTriggerDataSource.ts b/apps/extension/src/notification-service/triggers/createExtensionLocalTriggerDataSource.ts new file mode 100644 index 00000000000..3c566692893 --- /dev/null +++ b/apps/extension/src/notification-service/triggers/createExtensionLocalTriggerDataSource.ts @@ -0,0 +1,70 @@ +import { + createLocalTriggerDataSource, + type TriggerCondition, +} from '@universe/notifications/src/notification-data-source/implementations/createLocalTriggerDataSource' +import { type NotificationDataSource } from '@universe/notifications/src/notification-data-source/NotificationDataSource' +import { type NotificationTracker } from '@universe/notifications/src/notification-tracker/NotificationTracker' +import { createAppRatingTrigger } from 'src/notification-service/triggers/appRatingTrigger' +import { type ExtensionState } from 'src/store/extensionReducer' +import { setAppRating } from 'wallet/src/features/wallet/slice' + +/** + * Context required to create the extension local trigger data source. + */ +interface CreateExtensionLocalTriggerDataSourceContext { + /** Function to get the current Redux state */ + getState: () => ExtensionState + /** Redux dispatch function */ + dispatch: (action: ReturnType) => void + /** Notification tracker for checking processed state */ + tracker: NotificationTracker + /** How often to check triggers in milliseconds (default: 5000ms) */ + pollIntervalMs?: number +} + +/** + * All trigger conditions for the extension. + * Add new triggers here as they are migrated. + */ +function getExtensionTriggers(ctx: { + getState: () => ExtensionState + dispatch: (action: ReturnType) => void +}): TriggerCondition[] { + return [ + createAppRatingTrigger(ctx), + // Future triggers can be added here: + // createSmartWalletCreatedTrigger(ctx), + // createSmartWalletNudgeTrigger(ctx), + // createSmartWalletEnabledTrigger(ctx), + ] +} + +/** + * Creates a data source for all extension local trigger notifications. + * + * This combines all extension-specific triggers (app rating, smart wallet nudges, etc.) + * into a single data source that can be added to the notification service. + */ +export function createExtensionLocalTriggerDataSource( + ctx: CreateExtensionLocalTriggerDataSourceContext, +): NotificationDataSource { + const { getState, dispatch, tracker, pollIntervalMs = 5000 } = ctx + + const triggers = getExtensionTriggers({ getState, dispatch }) + + return createLocalTriggerDataSource({ + triggers, + tracker, + pollIntervalMs, + source: 'extension_local_triggers', + logFileTag: 'createExtensionLocalTriggerDataSource', + }) +} + +/** + * Check if a notification ID is a local trigger notification. + * Local trigger notifications use the 'local:' prefix. + */ +export function isLocalTriggerNotification(notificationId: string): boolean { + return notificationId.startsWith('local:') +} diff --git a/apps/extension/src/public/assets/index.ts b/apps/extension/src/public/assets/index.ts index bd7b5badfa5..3f74421e252 100644 --- a/apps/extension/src/public/assets/index.ts +++ b/apps/extension/src/public/assets/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable check-file/no-index */ +/* oxlint-disable typescript/no-var-requires */ export const ONBOARDING_BACKGROUND_LIGHT = require('./onboarding-background-light.png') export const ONBOARDING_BACKGROUND_DARK = require('./onboarding-background-dark.png') diff --git a/apps/extension/src/public/logo.svg b/apps/extension/src/public/logo.svg deleted file mode 100644 index 6b60c1042f5..00000000000 --- a/apps/extension/src/public/logo.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/apps/extension/src/publicAssetsByEnv/beta/icon128.png b/apps/extension/src/publicAssetsByEnv/beta/icon128.png new file mode 100644 index 00000000000..3730b112b76 Binary files /dev/null and b/apps/extension/src/publicAssetsByEnv/beta/icon128.png differ diff --git a/apps/extension/src/publicAssetsByEnv/beta/icon16.png b/apps/extension/src/publicAssetsByEnv/beta/icon16.png new file mode 100644 index 00000000000..acbe9bc4233 Binary files /dev/null and b/apps/extension/src/publicAssetsByEnv/beta/icon16.png differ diff --git a/apps/extension/src/publicAssetsByEnv/beta/icon32.png b/apps/extension/src/publicAssetsByEnv/beta/icon32.png new file mode 100644 index 00000000000..c585645008b Binary files /dev/null and b/apps/extension/src/publicAssetsByEnv/beta/icon32.png differ diff --git a/apps/extension/src/publicAssetsByEnv/beta/icon48.png b/apps/extension/src/publicAssetsByEnv/beta/icon48.png new file mode 100644 index 00000000000..ef5c09ad57d Binary files /dev/null and b/apps/extension/src/publicAssetsByEnv/beta/icon48.png differ diff --git a/apps/extension/src/publicAssetsByEnv/beta/icon64.png b/apps/extension/src/publicAssetsByEnv/beta/icon64.png new file mode 100644 index 00000000000..9e48b83a955 Binary files /dev/null and b/apps/extension/src/publicAssetsByEnv/beta/icon64.png differ diff --git a/apps/extension/src/publicAssetsByEnv/local/icon128.png b/apps/extension/src/publicAssetsByEnv/local/icon128.png new file mode 100644 index 00000000000..5c0321dc1cc Binary files /dev/null and b/apps/extension/src/publicAssetsByEnv/local/icon128.png differ diff --git a/apps/extension/src/publicAssetsByEnv/local/icon16.png b/apps/extension/src/publicAssetsByEnv/local/icon16.png new file mode 100644 index 00000000000..97a3eaf2469 Binary files /dev/null and b/apps/extension/src/publicAssetsByEnv/local/icon16.png differ diff --git a/apps/extension/src/publicAssetsByEnv/local/icon32.png b/apps/extension/src/publicAssetsByEnv/local/icon32.png new file mode 100644 index 00000000000..b318ae9d015 Binary files /dev/null and b/apps/extension/src/publicAssetsByEnv/local/icon32.png differ diff --git a/apps/extension/src/publicAssetsByEnv/local/icon48.png b/apps/extension/src/publicAssetsByEnv/local/icon48.png new file mode 100644 index 00000000000..d42343a377b Binary files /dev/null and b/apps/extension/src/publicAssetsByEnv/local/icon48.png differ diff --git a/apps/extension/src/public/assets/icon128.png b/apps/extension/src/publicAssetsByEnv/prod/icon128.png similarity index 100% rename from apps/extension/src/public/assets/icon128.png rename to apps/extension/src/publicAssetsByEnv/prod/icon128.png diff --git a/apps/extension/src/public/assets/icon16.png b/apps/extension/src/publicAssetsByEnv/prod/icon16.png similarity index 100% rename from apps/extension/src/public/assets/icon16.png rename to apps/extension/src/publicAssetsByEnv/prod/icon16.png diff --git a/apps/extension/src/public/assets/icon32.png b/apps/extension/src/publicAssetsByEnv/prod/icon32.png similarity index 100% rename from apps/extension/src/public/assets/icon32.png rename to apps/extension/src/publicAssetsByEnv/prod/icon32.png diff --git a/apps/extension/src/public/assets/icon48.png b/apps/extension/src/publicAssetsByEnv/prod/icon48.png similarity index 100% rename from apps/extension/src/public/assets/icon48.png rename to apps/extension/src/publicAssetsByEnv/prod/icon48.png diff --git a/apps/extension/src/public/assets/icon64.png b/apps/extension/src/publicAssetsByEnv/prod/icon64.png similarity index 100% rename from apps/extension/src/public/assets/icon64.png rename to apps/extension/src/publicAssetsByEnv/prod/icon64.png diff --git a/apps/extension/src/store/PrimaryAppInstanceDebugger.tsx b/apps/extension/src/store/PrimaryAppInstanceDebugger.tsx index 55be74ceb99..f1984abc9a0 100644 --- a/apps/extension/src/store/PrimaryAppInstanceDebugger.tsx +++ b/apps/extension/src/store/PrimaryAppInstanceDebugger.tsx @@ -1,4 +1,4 @@ -/* eslint-disable react/forbid-elements */ +/* oxlint-disable react/forbid-elements */ import { useIsPrimaryAppInstance } from 'src/store/storeSynchronization' // This is a dev-only component that renders a small green/red dot in the bottom right corner of the screen @@ -7,7 +7,7 @@ export default function PrimaryAppInstanceDebugger(): JSX.Element | null { const isPrimaryAppInstance = useIsPrimaryAppInstance() return ( - // biome-ignore lint/correctness/noRestrictedElements: needed here + // oxlint-disable-next-line react/forbid-elements -- needed here
=> { + const client = new ApolloClient({ + cache: new InMemoryCache(), + }) + jest.spyOn(client, 'resetStore').mockResolvedValue([]) + return client +} + +const createMockQueryClient = (): QueryClient => { + const client = new QueryClient() + jest.spyOn(client, 'resetQueries').mockResolvedValue() + return client +} + +describe('createExtensionAppStateResetter', () => { + let store: ReturnType> + let apolloClient: ApolloClient + let queryClient: QueryClient + let resetter: ReturnType + + beforeEach(() => { + store = configureStore({ + reducer: extensionReducer, + }) + apolloClient = createMockApolloClient() + queryClient = createMockQueryClient() + resetter = createExtensionAppStateResetter({ + dispatch: store.dispatch, + apolloClient, + queryClient, + }) + jest.clearAllMocks() + }) + + describe('resetAccountHistory', () => { + it('dispatches extension-specific account history reset actions', async () => { + // Modify extension-specific state - add a dapp request + store.dispatch( + dappRequestActions.add({ + dappRequest: { + type: DappRequestType.SignMessage, + requestId: 'test-request-1', + messageHex: '0x123', + address: '0x123', + }, + senderTabInfo: { id: 1, url: 'https://test.com' }, + isSidebarClosed: false, + }), + ) + + // Verify state was modified + expect(Object.keys(store.getState().dappRequests.requests).length).toBe(1) + + await resetter.resetAccountHistory() + + // Verify extension-specific state was reset + const state = store.getState() + expect(Object.keys(state.dappRequests.requests).length).toBe(0) + }) + }) + + describe('resetUserSettings', () => { + it('completes without errors', async () => { + // Extension has no additional user settings to reset beyond base + await expect(resetter.resetUserSettings()).resolves.not.toThrow() + }) + }) + + describe('resetQueryCaches', () => { + it('clears Apollo and React Query caches', async () => { + await resetter.resetQueryCaches() + + // Verify cache clearing methods were called + expect(apolloClient.resetStore).toHaveBeenCalledTimes(1) + expect(queryClient.resetQueries).toHaveBeenCalledTimes(1) + }) + }) + + describe('resetAll', () => { + it('resets all state and clears all caches', async () => { + // Modify state first + store.dispatch( + pushNotification({ + type: AppNotificationType.Success, + title: 'Test notification', + }), + ) + + // Verify state was modified + expect(store.getState().notifications.notificationQueue.length).toBe(1) + + await resetter.resetAll() + + // Verify all resets worked + const state = store.getState() + expect(state.notifications.notificationQueue).toEqual([]) + expect(apolloClient.resetStore).toHaveBeenCalledTimes(1) + expect(queryClient.resetQueries).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/apps/extension/src/store/appStateResetter.tsx b/apps/extension/src/store/appStateResetter.tsx new file mode 100644 index 00000000000..f23f017a616 --- /dev/null +++ b/apps/extension/src/store/appStateResetter.tsx @@ -0,0 +1,78 @@ +import { type ApolloClient, useApolloClient } from '@apollo/client' +import { type Dispatch } from '@reduxjs/toolkit' +import { type QueryClient, useQueryClient } from '@tanstack/react-query' +import { useMemo } from 'react' +import { useDispatch } from 'react-redux' +import { dappRequestActions } from 'src/app/features/dappRequests/slice' +import { resetAlerts } from 'src/app/features/onboarding/alerts/slice' +import { resetPopups } from 'src/app/features/popups/slice' +import { type AppStateResetter } from 'uniswap/src/state/createAppStateResetter' +import { createLogger } from 'utilities/src/logger/logger' +import { createWalletStateResetter } from 'wallet/src/state/createWalletStateResetter' + +/** + * Creates the extension app's state resetter instance. + * This wraps the base createAppStateResetter and adds extension-specific reset actions. + * + * @param apolloClient - Optional Apollo client for cache clearing. If not provided, cache clearing is skipped. + * @param queryClient - Optional React Query client for cache clearing. If not provided, cache clearing is skipped. + */ +export function createExtensionAppStateResetter({ + dispatch, + apolloClient, + queryClient, +}: { + dispatch: Dispatch + apolloClient?: ApolloClient + queryClient?: QueryClient +}): AppStateResetter { + const logger = createLogger('appResetter.tsx', 'createExtensionAppStateResetter') + + return createWalletStateResetter({ + dispatch, + + onResetAccountHistory: () => { + dispatch(dappRequestActions.reset()) + dispatch(resetPopups()) + dispatch(resetAlerts()) + }, + + onResetUserSettings: () => { + // No extension-specific settings resets are currently required + }, + + onResetQueryCaches: async () => { + const cachePromises: Promise[] = [] + if (apolloClient) { + cachePromises.push(apolloClient.resetStore().then(() => logger.info('Apollo cache cleared successfully'))) + } + if (queryClient) { + cachePromises.push(queryClient.resetQueries().then(() => logger.info('React Query cache cleared successfully'))) + } + if (cachePromises.length > 0) { + await Promise.all(cachePromises) + } + }, + }) +} + +export function useAppStateResetter(): AppStateResetter { + const dispatch = useDispatch() + const apolloClient = useApolloClient() + const queryClient = useQueryClient() + return useMemo( + () => createExtensionAppStateResetter({ dispatch, apolloClient, queryClient }), + [dispatch, apolloClient, queryClient], + ) +} + +/** + * Creates a resetter for use in crash recovery scenarios (e.g., error boundaries). + * This version does not require the Apollo context, which is not + * available when rendering the error fallback UI. + */ +export function useOnCrashAppStateResetter(): AppStateResetter { + const dispatch = useDispatch() + const queryClient = useQueryClient() + return useMemo(() => createExtensionAppStateResetter({ dispatch, queryClient }), [dispatch, queryClient]) +} diff --git a/apps/extension/src/store/enhancePersistReducer.ts b/apps/extension/src/store/enhancePersistReducer.ts index 028bbccddfa..d745cb403ed 100644 --- a/apps/extension/src/store/enhancePersistReducer.ts +++ b/apps/extension/src/store/enhancePersistReducer.ts @@ -3,7 +3,7 @@ import { logger } from 'utilities/src/logger/logger' // We use `any` in a few places in this file because those values truly can be anything, so that's the proper type. -// biome-ignore lint/suspicious/noExplicitAny: PersistPartial type allows any shape for redux-persist compatibility +// oxlint-disable-next-line typescript/no-explicit-any -- PersistPartial type allows any shape for redux-persist compatibility type PersistPartial = { _persist: undefined } | any export function enhancePersistReducer( @@ -29,7 +29,7 @@ function forceRehydrationFromDiskWhenResumingPersistence { return (state, action) => { if (action.type !== 'persist/PERSIST') { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return + // oxlint-disable-next-line typescript/no-unsafe-return return reducer(state, action) } @@ -40,7 +40,7 @@ function forceRehydrationFromDiskWhenResumingPersistence { + it('removes dapp from state', () => { + const state = { dapp: { someData: true }, otherData: 'preserved' } + const result = removeDappInfoToChromeLocalStorage(state) + expect(result.dapp).toBeUndefined() + expect(result.otherData).toBe('preserved') + }) + + it('handles state without dapp', () => { + const state = { otherData: 'preserved' } + const result = removeDappInfoToChromeLocalStorage(state) + expect(result.otherData).toBe('preserved') + }) +}) + +describe('migratePendingDappRequestsToRecord', () => { + it('migrates pending array to requests record', () => { + const state = { + dappRequests: { + pending: [ + { dappRequest: { requestId: 'req1', data: 'test1' } }, + { dappRequest: { requestId: 'req2', data: 'test2' } }, + ], + }, + } + const result = migratePendingDappRequestsToRecord(state) + expect(result.dappRequests.requests.req1).toBeDefined() + expect(result.dappRequests.requests.req1.status).toBe(DappRequestStatus.Pending) + expect(result.dappRequests.requests.req1.createdAt).toBeDefined() + expect(result.dappRequests.requests.req2).toBeDefined() + expect(result.dappRequests.pending).toBeUndefined() + }) + + it('returns state unchanged if no dappRequests', () => { + const state = { otherData: 'preserved' } + const result = migratePendingDappRequestsToRecord(state) + expect(result).toEqual(state) + }) + + it('returns state unchanged if already migrated (has requests)', () => { + const state = { + dappRequests: { + requests: { req1: { status: DappRequestStatus.Pending } }, + }, + } + const result = migratePendingDappRequestsToRecord(state) + expect(result).toEqual(state) + }) + + it('returns state unchanged if no pending array', () => { + const state = { dappRequests: {} } + const result = migratePendingDappRequestsToRecord(state) + expect(result).toEqual(state) + }) + + it('clears dappRequests if pending is not an array', () => { + const state = { dappRequests: { pending: 'invalid' } } + const result = migratePendingDappRequestsToRecord(state) + expect(result.dappRequests).toEqual({ requests: {} }) + }) + + it('skips invalid items in pending array', () => { + const state = { + dappRequests: { + pending: [ + null, + 'invalid', + { notDappRequest: true }, + { dappRequest: null }, + { dappRequest: { noRequestId: true } }, + { dappRequest: { requestId: 'validReq', data: 'test' } }, + ], + }, + } + const result = migratePendingDappRequestsToRecord(state) + expect(Object.keys(result.dappRequests.requests)).toEqual(['validReq']) + }) + + it('falls back to empty requests on error', () => { + const state = { + dappRequests: { + pending: createThrowingProxy([], { throwingMethods: ['forEach'] }), + }, + } + const result = migratePendingDappRequestsToRecord(state) + expect(result.dappRequests).toEqual({ requests: {} }) + }) +}) + +describe('migrateUnknownBackupAccountsToMaybeManualBackup', () => { + it('adds maybe-manual backup to accounts without backups', () => { + const state = { + wallet: { + accounts: { + '0x123': { address: '0x123' }, + '0x456': { address: '0x456', backups: [] }, + }, + }, + } + const result = migrateUnknownBackupAccountsToMaybeManualBackup(state) + expect(result.wallet.accounts['0x123'].backups).toEqual(['maybe-manual']) + expect(result.wallet.accounts['0x456'].backups).toEqual(['maybe-manual']) + }) + + it('preserves existing backups', () => { + const state = { + wallet: { + accounts: { + '0x123': { address: '0x123', backups: ['cloud'] }, + }, + }, + } + const result = migrateUnknownBackupAccountsToMaybeManualBackup(state) + expect(result.wallet.accounts['0x123'].backups).toEqual(['cloud']) + }) + + it('returns state unchanged if no wallet', () => { + const state = { otherData: 'preserved' } + const result = migrateUnknownBackupAccountsToMaybeManualBackup(state) + expect(result).toEqual(state) + }) + + it('returns state unchanged if no accounts', () => { + const state = { wallet: { otherData: 'preserved' } } + const result = migrateUnknownBackupAccountsToMaybeManualBackup(state) + expect(result).toEqual(state) + }) + + it('returns state unchanged if accounts is not an object', () => { + const state = { wallet: { accounts: 'invalid' } } + const result = migrateUnknownBackupAccountsToMaybeManualBackup(state) + expect(result).toEqual(state) + }) + + it('skips non-object accounts', () => { + const state = { + wallet: { + accounts: { + '0x123': 'invalid', + '0x456': { address: '0x456' }, + }, + }, + } + const result = migrateUnknownBackupAccountsToMaybeManualBackup(state) + expect(result.wallet.accounts['0x123']).toBeUndefined() + expect(result.wallet.accounts['0x456'].backups).toEqual(['maybe-manual']) + }) +}) + +describe('setLanguageToNavigatorLanguage', () => { + it('sets language from navigator', () => { + const state = { userSettings: { otherSetting: true } } + const result = setLanguageToNavigatorLanguage(state) + expect(result.userSettings.currentLanguage).toBeDefined() + expect(result.userSettings.otherSetting).toBe(true) + }) + + it('returns state unchanged if no userSettings', () => { + const state = { otherData: 'preserved' } + const result = setLanguageToNavigatorLanguage(state) + expect(result).toEqual(state) + }) + + it('falls back to English on error', () => { + const state = { + userSettings: createThrowingProxy({}, { throwingMethods: ['*'] }), + } + const result = setLanguageToNavigatorLanguage(state) + expect(result.userSettings.currentLanguage).toBe(Language.English) + }) +}) diff --git a/apps/extension/src/store/extensionMigrations.ts b/apps/extension/src/store/extensionMigrations.ts index 104cee3030d..f10d4604662 100644 --- a/apps/extension/src/store/extensionMigrations.ts +++ b/apps/extension/src/store/extensionMigrations.ts @@ -1,57 +1,78 @@ import { DappRequestStatus } from 'src/app/features/dappRequests/shared' import type { DappRequestState } from 'src/app/features/dappRequests/slice' -import { BackupType } from 'wallet/src/features/wallet/accounts/types' +import { Language } from 'uniswap/src/features/language/constants' +import { getCurrentLanguageFromNavigator } from 'uniswap/src/features/language/utils' +import { createSafeMigrationFactory } from 'uniswap/src/state/createSafeMigration' +import { type BackupType } from 'wallet/src/features/wallet/accounts/types' + +const createSafeMigration = createSafeMigrationFactory('extensionMigrations') export function removeDappInfoToChromeLocalStorage({ dapp: _dapp, ...state }: any): any { return state } // migrates pending dapp requests array without status or timestamp to a record with status (pending|confirming) and timestamp -export function migratePendingDappRequestsToRecord(state: any): any { - // If there's no dappRequests state or it's already in the new format, return unchanged - if (!state.dappRequests || !state.dappRequests.pending || state.dappRequests.requests) { - return state - } - - // Create new record object to hold requests - const requests: DappRequestState['requests'] = {} +export const migratePendingDappRequestsToRecord = createSafeMigration({ + name: 'migratePendingDappRequestsToRecord', + migrate: (state: any) => { + // If there's no dappRequests state or it's already in the new format, return unchanged + if (!state?.dappRequests || !state.dappRequests.pending || state.dappRequests.requests) { + return state + } - // Convert each pending request to the record format with status - state.dappRequests.pending.forEach((item: unknown, index: number) => { - if ( - item !== null && - typeof item === 'object' && - 'dappRequest' in item && - typeof item.dappRequest === 'object' && - item.dappRequest !== null && - 'requestId' in item.dappRequest && - typeof item.dappRequest.requestId === 'string' - ) { - const updatedRequest = { - ...item, - // Map to new structure with status and timestamp - status: DappRequestStatus.Pending, - createdAt: Date.now() + index * 1000, // Add timestamp for sorting - } as DappRequestState['requests'][string] + // Create new record object to hold requests + const requests: DappRequestState['requests'] = {} - requests[item.dappRequest.requestId] = updatedRequest + const pending = state.dappRequests.pending + if (!Array.isArray(pending)) { + // If pending is not an array, just clear the dapp requests + return { + ...state, + dappRequests: { requests: {} }, + } } - }) - // Return state with updated dappRequests slice - return { + // Convert each pending request to the record format with status + pending.forEach((item: unknown, index: number) => { + if ( + item !== null && + typeof item === 'object' && + 'dappRequest' in item && + typeof item.dappRequest === 'object' && + item.dappRequest !== null && + 'requestId' in item.dappRequest && + typeof item.dappRequest.requestId === 'string' + ) { + const updatedRequest = { + ...item, + // Map to new structure with status and timestamp + status: DappRequestStatus.Pending, + createdAt: Date.now() + index * 1000, // Add timestamp for sorting + } as DappRequestState['requests'][string] + + requests[item.dappRequest.requestId] = updatedRequest + } + }) + + // Return state with updated dappRequests slice + return { + ...state, + dappRequests: { + requests, + }, + } + }, + onError: (state: any) => ({ ...state, - dappRequests: { - requests, - }, - } -} + dappRequests: { requests: {} }, + }), +}) // Migrates accounts with no backup method to have `maybe-manual` backup method. // Before this migration, we were not setting the backup method on accounts created during Extension onboarding, // so we're unsure if the user completed the backup flow during onboarding or if they hit "Skip". export function migrateUnknownBackupAccountsToMaybeManualBackup(state: any): any { - if (!state.wallet?.accounts) { + if (!state?.wallet?.accounts || typeof state.wallet.accounts !== 'object') { return state } @@ -81,3 +102,27 @@ export function migrateUnknownBackupAccountsToMaybeManualBackup(state: any): any }, } } + +export const setLanguageToNavigatorLanguage = createSafeMigration({ + name: 'setLanguageToNavigatorLanguage', + migrate: (state: any) => { + if (!state?.userSettings) { + return state + } + + return { + ...state, + userSettings: { + ...state.userSettings, + currentLanguage: getCurrentLanguageFromNavigator(), + }, + } + }, + onError: (state: any) => ({ + ...state, + userSettings: { + ...state?.userSettings, + currentLanguage: Language.English, + }, + }), +}) diff --git a/apps/extension/src/store/extensionMigrationsTests.ts b/apps/extension/src/store/extensionMigrationsTests.ts index 38cc388465d..e51dcb09bf7 100644 --- a/apps/extension/src/store/extensionMigrationsTests.ts +++ b/apps/extension/src/store/extensionMigrationsTests.ts @@ -1,3 +1,32 @@ +/** + * Test helpers for testing migrations run in sequence. + * + * Called by migrations.test.ts to verify migrations work correctly with realistic + * data that has passed through all prior migrations in the chain. + * + * For unit tests of individual migrations, see extensionMigrations.test.ts. + */ +import { createThrowingProxy } from 'utilities/src/test/utils' + +export function testRemoveDappInfoToChromeLocalStorage(migration: (state: any) => any, _prevSchema: any): void { + // Test: removes dapp property from state + const result = migration({ + dapp: { someData: 'value' }, + otherData: 'preserved', + }) + + expect(result.dapp).toBeUndefined() + expect(result.otherData).toBe('preserved') + + // Test: handles state without dapp property + const resultWithoutDapp = migration({ + otherData: 'preserved', + }) + + expect(resultWithoutDapp.dapp).toBeUndefined() + expect(resultWithoutDapp.otherData).toBe('preserved') +} + export function testMigratePendingDappRequestsToRecord(migration: (state: any) => any, _prevSchema: any): void { // Test: empty pending → empty requests expect( @@ -45,6 +74,14 @@ export function testMigratePendingDappRequestsToRecord(migration: (state: any) = meta: 'kept', createdAt: expect.any(Number), }) + + // Test: fallback on error - uses a throwing proxy to trigger catch block + const errorResult = migration({ + dappRequests: { pending: createThrowingProxy([], { throwingMethods: ['forEach'] }) }, + otherData: 'preserved', + }) + expect(errorResult.dappRequests).toEqual({ requests: {} }) + expect(errorResult.otherData).toBe('preserved') } export function testMigrateUnknownBackupAccountsToMaybeManualBackup( @@ -108,3 +145,24 @@ export function testMigrateUnknownBackupAccountsToMaybeManualBackup( expect(migration3.wallet.accounts['0x1'].backups).toEqual(['cloud']) expect(migration3.wallet.accounts['0x2'].backups).toEqual(['cloud']) } + +export function testSetLanguageToNavigatorLanguage(migration: (state: any) => any, _prevSchema: any): void { + // Test: sets language when userSettings exists + const result = migration({ + userSettings: { + currentLanguage: 'es', + otherSetting: 'preserved', + }, + }) + + expect(result.userSettings.currentLanguage).toBeDefined() + expect(result.userSettings.otherSetting).toBe('preserved') + + // Test: returns state unchanged when userSettings doesn't exist + const resultWithoutUserSettings = migration({ + otherData: 'preserved', + }) + + expect(resultWithoutUserSettings.userSettings).toBeUndefined() + expect(resultWithoutUserSettings.otherData).toBe('preserved') +} diff --git a/apps/extension/src/store/migrations.test.ts b/apps/extension/src/store/migrations.test.ts index f78dd2d2f16..d90570a9385 100644 --- a/apps/extension/src/store/migrations.test.ts +++ b/apps/extension/src/store/migrations.test.ts @@ -1,9 +1,11 @@ -/* eslint-disable jest/expect-expect */ +/* oxlint-disable jest/expect-expect */ import { BigNumber } from '@ethersproject/bignumber' import { toIncludeSameMembers } from 'jest-extended' import { testMigratePendingDappRequestsToRecord, testMigrateUnknownBackupAccountsToMaybeManualBackup, + testRemoveDappInfoToChromeLocalStorage, + testSetLanguageToNavigatorLanguage, } from 'src/store/extensionMigrationsTests' import { EXTENSION_STATE_VERSION, migrations } from 'src/store/migrations' import { @@ -36,7 +38,12 @@ import { v24Schema, v25Schema, v26Schema, + v27Schema, + v29Schema, + v30Schema, } from 'src/store/schema' +import { USDC } from 'uniswap/src/constants/tokens' +import { initialAppearanceSettingsState } from 'uniswap/src/features/appearance/slice' import { initialUniswapBehaviorHistoryState } from 'uniswap/src/features/behaviorHistory/slice' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { initialFavoritesState } from 'uniswap/src/features/favorites/slice' @@ -44,13 +51,17 @@ import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' import { initialNotificationsState } from 'uniswap/src/features/notifications/slice/slice' import { initialSearchHistoryState } from 'uniswap/src/features/search/searchHistorySlice' import { initialUserSettingsState } from 'uniswap/src/features/settings/slice' -import { initialTokensState } from 'uniswap/src/features/tokens/slice/slice' +import { initialTokensState } from 'uniswap/src/features/tokens/warnings/slice/slice' import { initialTransactionsState } from 'uniswap/src/features/transactions/slice' import { TransactionStatus, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' import { initialVisibilityState } from 'uniswap/src/features/visibility/slice' -import { testMigrateSearchHistory, testRemoveTHBFromCurrency } from 'uniswap/src/state/uniswapMigrationTests' +import { + testAddActivityVisibility, + testMigrateDismissedTokenWarnings, + testMigrateSearchHistory, + testRemoveTHBFromCurrency, +} from 'uniswap/src/state/uniswapMigrationTests' import { getAllKeysOfNestedObject } from 'utilities/src/primitives/objects' -import { initialAppearanceSettingsState } from 'wallet/src/features/appearance/slice' import { initialBatchedTransactionsState } from 'wallet/src/features/batchedTransactions/slice' import { initialBehaviorHistoryState } from 'wallet/src/features/behaviorHistory/slice' import { initialWalletState } from 'wallet/src/features/wallet/slice' @@ -236,9 +247,7 @@ describe('Redux state migrations', () => { }) it('migrates from v3 to v4', async () => { - const v3Stub = { ...v3Schema } - const v4 = await migrations[4](v3Stub) - expect(v4.dapp).toBe(undefined) + testRemoveDappInfoToChromeLocalStorage(migrations[4], v3Schema) }) it('migrates from v4 to v5', async () => { @@ -347,4 +356,28 @@ describe('Redux state migrations', () => { it('migrates from v26 to v27', () => { testMigrateSearchHistory(migrations[27], v26Schema) }) + + it('migrates from v27 to v29', () => { + testAddActivityVisibility(migrations[29], v27Schema) + }) + + it('migrates from v29 to v30', () => { + testMigrateDismissedTokenWarnings(migrations[30], { + ...v29Schema, + tokens: { + dismissedTokenWarnings: { + [UniverseChainId.Mainnet]: { + [USDC.address]: { + chainId: UniverseChainId.Mainnet, + address: USDC.address, + }, + }, + }, + }, + }) + }) + + it('migrates from v30 to v31', () => { + testSetLanguageToNavigatorLanguage(migrations[31], v30Schema) + }) }) diff --git a/apps/extension/src/store/migrations.ts b/apps/extension/src/store/migrations.ts index e9534188698..1eb98c369bb 100644 --- a/apps/extension/src/store/migrations.ts +++ b/apps/extension/src/store/migrations.ts @@ -1,13 +1,16 @@ -/* biome-ignore-all lint/suspicious/noExplicitAny: Migration functions handle arbitrary state shapes from different versions */ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* oxlint-disable typescript/no-explicit-any -- Migration functions handle arbitrary state shapes from different versions */ +/* oxlint-disable typescript/explicit-function-return-type */ import { migratePendingDappRequestsToRecord, migrateUnknownBackupAccountsToMaybeManualBackup, removeDappInfoToChromeLocalStorage, + setLanguageToNavigatorLanguage, } from 'src/store/extensionMigrations' import { + addActivityVisibility, addDismissedBridgedAndCompatibleWarnings, + migrateDismissedTokenWarnings, migrateSearchHistory, removeThaiBahtFromFiatCurrency, unchecksumDismissedTokenWarningKeys, @@ -67,6 +70,9 @@ export const migrations = { 26: migrateLiquidityTransactionInfo, 27: migrateSearchHistory, 28: addDismissedBridgedAndCompatibleWarnings, + 29: addActivityVisibility, + 30: migrateDismissedTokenWarnings, + 31: setLanguageToNavigatorLanguage, } -export const EXTENSION_STATE_VERSION = 28 +export const EXTENSION_STATE_VERSION = 31 diff --git a/apps/extension/src/store/schema.ts b/apps/extension/src/store/schema.ts index 0fd7dc87697..0e14d98e668 100644 --- a/apps/extension/src/store/schema.ts +++ b/apps/extension/src/store/schema.ts @@ -264,7 +264,7 @@ const v24SchemaIntermediate = { }, userSettings: { ...v23Schema.userSettings, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition hapticsEnabled: v23Schema.appearanceSettings.hapticsEnabled ?? false, }, } @@ -276,6 +276,12 @@ export const v25Schema = { ...v24Schema } export const v26Schema = { ...v25Schema } -const v27Schema = { ...v26Schema } +export const v27Schema = { ...v26Schema } -export const getSchema = (): typeof v27Schema => v27Schema +export const v29Schema = { ...v27Schema, visibility: { ...v27Schema.visibility, activity: {} } } + +export const v30Schema = { ...v29Schema } + +const v31Schema = { ...v30Schema } + +export const getSchema = (): typeof v31Schema => v31Schema diff --git a/apps/extension/src/store/store.ts b/apps/extension/src/store/store.ts index e3588bf1ffd..4745745b432 100644 --- a/apps/extension/src/store/store.ts +++ b/apps/extension/src/store/store.ts @@ -6,7 +6,6 @@ import { PERSIST_KEY } from 'src/store/constants' import { enhancePersistReducer } from 'src/store/enhancePersistReducer' import { ExtensionState, extensionPersistedStateList, extensionReducer } from 'src/store/extensionReducer' import { EXTENSION_STATE_VERSION, migrations } from 'src/store/migrations' -import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' import { delegationListenerMiddleware } from 'uniswap/src/features/smartWallet/delegation/slice' import { createDatadogReduxEnhancer } from 'utilities/src/logger/datadog/Datadog' import { logger } from 'utilities/src/logger/logger' @@ -36,7 +35,7 @@ const setupStore = (): ReturnType => { reducer: persistedReducer, additionalSagas: [rootExtensionSaga], middlewareBefore: __DEV__ ? [loggerMiddleware] : [], - middlewareAfter: [fiatOnRampAggregatorApi.middleware, delegationListenerMiddleware.middleware], + middlewareAfter: [delegationListenerMiddleware.middleware], enhancers: [dataDogReduxEnhancer], }) } diff --git a/apps/extension/src/test/babel.config.js b/apps/extension/src/test/babel.config.js index 9118275b087..c607512286d 100644 --- a/apps/extension/src/test/babel.config.js +++ b/apps/extension/src/test/babel.config.js @@ -1,8 +1,21 @@ // This file is used only by jest in the test environment. To check the extension // build set up, see the webpack.config.js file. +// Inline Babel plugin to transform import.meta.url for Jest compatibility. +// Jest runs in CommonJS mode where import.meta is not available. +function importMetaTransformPlugin() { + return { + visitor: { + MetaProperty(path) { + path.replaceWithSourceString('({url: "file:///test.js"})') + }, + }, + } +} + module.exports = function (api) { api.cache.using(() => process.env.NODE_ENV) + // oxlint-disable-next-line no-var -- biome-parity: oxlint is stricter here var plugins = [ 'react-native-web', [ @@ -16,10 +29,13 @@ module.exports = function (api) { ], // https://github.com/software-mansion/react-native-reanimated/issues/3364#issuecomment-1268591867 '@babel/plugin-proposal-export-namespace-from', + '@babel/plugin-transform-new-target', + importMetaTransformPlugin, + 'react-native-reanimated/plugin', ].filter(Boolean) return { - presets: ['module:@react-native/babel-preset'], + presets: ['babel-preset-expo'], plugins, } } diff --git a/apps/extension/src/test/fixtures/redux.ts b/apps/extension/src/test/fixtures/redux.ts index fbdbe85ec9b..773a07165bf 100644 --- a/apps/extension/src/test/fixtures/redux.ts +++ b/apps/extension/src/test/fixtures/redux.ts @@ -5,8 +5,13 @@ import { preloadedWalletPackageState } from 'wallet/src/test/fixtures' type PreloadedExtensionStateOptions = Record -export const preloadedExtensionState = createFixture, PreloadedExtensionStateOptions>( - {}, -)(() => ({ +type PreloadedExtensionStateFactory = ( + overrides?: Partial & PreloadedExtensionStateOptions>, +) => PreloadedState + +export const preloadedExtensionState: PreloadedExtensionStateFactory = createFixture< + PreloadedState, + PreloadedExtensionStateOptions +>({})(() => ({ ...preloadedWalletPackageState(), })) diff --git a/apps/extension/tsconfig.json b/apps/extension/tsconfig.json index 007f0494490..0ed8bf07749 100644 --- a/apps/extension/tsconfig.json +++ b/apps/extension/tsconfig.json @@ -17,16 +17,28 @@ { "path": "../../packages/ui" }, + { + "path": "../../packages/sessions" + }, + { + "path": "../../packages/notifications" + }, + { + "path": "../../packages/gating" + }, { "path": "../../packages/api" } ], "compilerOptions": { - "baseUrl": "./", "moduleResolution": "Bundler", "lib": ["dom", "dom.iterable", "esnext"], "jsx": "preserve", "resolveJsonModule": true, - "types": ["chrome", "jest", "node"] + "types": ["chrome", "jest", "node"], + "paths": { + "src/*": ["./src/*"], + "e2e/*": ["./e2e/*"] + } } } diff --git a/apps/extension/tsconfig.eslint.json b/apps/extension/tsconfig.lint.json similarity index 100% rename from apps/extension/tsconfig.eslint.json rename to apps/extension/tsconfig.lint.json diff --git a/apps/extension/webpack.config.js b/apps/extension/webpack.config.js index 2893c62943f..fdc015988c2 100644 --- a/apps/extension/webpack.config.js +++ b/apps/extension/webpack.config.js @@ -29,6 +29,7 @@ const compileNodeModules = [ 'expo-linear-gradient', 'react-native-image-picker', 'expo-modules-core', + 'react-native-reanimated', ] // This is needed for webpack to compile JavaScript. @@ -48,8 +49,8 @@ const babelLoaderConfiguration = { loader: 'babel-loader', options: { cacheDirectory: true, - // The 'metro-react-native-babel-preset' preset is recommended to match React Native's packager - presets: ['module:@react-native/babel-preset'], + // The 'babel-preset-expo' preset is recommended to match React Native's packager + presets: ['babel-preset-expo'], // Re-write paths to import only the modules needed by the app plugins: ['react-native-web'], }, @@ -155,8 +156,22 @@ module.exports = (env) => { const BUILD_ENV = env.BUILD_ENV const BUILD_NUM = env.BUILD_NUM || 0 + const publicAssetsVariant = isDevelopment + ? 'local' + : BUILD_ENV === 'dev' + ? 'dev' + : BUILD_ENV === 'beta' + ? 'beta' + : 'prod' + // Title Postfix - const EXTENSION_NAME_POSTFIX = BUILD_ENV === 'dev' ? 'DEV' : BUILD_ENV === 'beta' ? 'BETA' : '' + const EXTENSION_NAME_POSTFIX = isDevelopment + ? 'LOCAL' + : BUILD_ENV === 'dev' + ? 'DEV' + : BUILD_ENV === 'beta' + ? 'BETA' + : '' // Description let EXTENSION_DESCRIPTION = manifest.description @@ -263,7 +278,8 @@ module.exports = (env) => { // for example if you have constants.ts then constants.js goes here and it will eval them // at build time and if it can flatten views even if they use imports from that file importsWhitelist: ['constants.js'], - disableExtraction: process.env.NODE_ENV === 'development', + // TODO: test re-enabling extraction in production when #27138 merges + disableExtraction: true, }, }, @@ -342,6 +358,7 @@ module.exports = (env) => { { from: 'src/manifest.json', force: true, + // oxlint-disable-next-line no-unused-vars -- biome-parity: oxlint is stricter here transform(content) { const transformedManifest = { ...manifest, @@ -363,6 +380,7 @@ module.exports = (env) => { matches: ['http://127.0.0.1/*', 'http://localhost/*', 'https://*/*'], js: ['injected.js'], run_at: 'document_start', + all_frames: true, }, { id: 'ethereum', @@ -371,6 +389,7 @@ module.exports = (env) => { run_at: 'document_start', // Ethereum provider must run in the MAIN world to attach to window.ethereum world: 'MAIN', + all_frames: true, }, ], } @@ -389,8 +408,8 @@ module.exports = (env) => { force: true, }, { - from: 'src/public/logo.svg', - to: 'logo.svg', + from: `src/publicAssetsByEnv/${publicAssetsVariant}/*.{png,svg}`, + to: 'assets/[name][ext]', force: true, }, { diff --git a/apps/extension/wxt.config.ts b/apps/extension/wxt.config.ts index 37e4fc458e7..091d1dafb14 100644 --- a/apps/extension/wxt.config.ts +++ b/apps/extension/wxt.config.ts @@ -1,3 +1,5 @@ +import fs from 'fs' +import { createHash } from 'node:crypto' import path from 'path' import { loadEnv, transformWithEsbuild } from 'vite' import commonjs from 'vite-plugin-commonjs' @@ -5,6 +7,7 @@ import { nodePolyfills } from 'vite-plugin-node-polyfills' import svgr from 'vite-plugin-svgr' import tsconfigPaths from 'vite-tsconfig-paths' import { defineConfig } from 'wxt' +// oxlint-disable-next-line universe-custom/no-relative-import-paths -- biome-parity: oxlint is stricter here import { getTsconfigAliases } from './config/getTsconfigAliases' const icons = { @@ -14,14 +17,68 @@ const icons = { 128: 'assets/icon128.png', } +function getPublicAssetsVariant(): 'prod' | 'beta' | 'dev' | 'local' { + if (process.env.NODE_ENV === 'development') { + return 'local' + } + if (process.env.BUILD_ENV === 'dev') { + return 'dev' + } + if (process.env.BUILD_ENV === 'beta') { + return 'beta' + } + return 'prod' +} + +const publicAssetsVariant = getPublicAssetsVariant() + const BASE_NAME = 'Uniswap Extension' const BASE_DESCRIPTION = "The Uniswap Extension is a self-custody crypto wallet that's built for swapping." -const BASE_VERSION = '1.61.0' +const BASE_VERSION = '1.71.0' const BUILD_NUM = parseInt(process.env.BUILD_NUM || '0') const EXTENSION_VERSION = `${BASE_VERSION}.${BUILD_NUM}` -// eslint-disable-next-line import/no-unused-modules +/** + * Vite's optimizeDeps cache hash doesn't include `define` values, so changing env vars + * (which are injected via `define` as `process.env.X` replacements) won't invalidate the + * pre-bundled deps cache. This compares a hash of the resolved env defines against a stored + * hash and forces a re-bundle only when env values actually changed. + */ +function shouldInvalidateOptimizeDepsForEnv({ + defines, + cacheDir, +}: { + defines: Record + cacheDir: string +}): boolean { + const hash = createHash('md5').update(JSON.stringify(defines)).digest('hex').slice(0, 16) + const hashFile = path.join(cacheDir, '.env-defines-hash') + + try { + if (fs.existsSync(hashFile)) { + const stored = fs.readFileSync(hashFile, 'utf-8').trim() + if (stored === hash) { + return false + } + } + } catch { + return true + } + + try { + if (!fs.existsSync(cacheDir)) { + fs.mkdirSync(cacheDir, { recursive: true }) + } + fs.writeFileSync(hashFile, hash) + } catch { + return true + } + + return true +} + +// oxlint-disable-next-line import/no-unused-modules export default defineConfig({ // WXT Configuration srcDir: 'src', @@ -38,14 +95,58 @@ export default defineConfig({ // Enable React support modules: ['@wxt-dev/module-react'], + hooks: { + // Hook for dynamic asset copying based on build variant. + // All assets in `src/publicAssetsByEnv/` will be copied to `assets/` at build time. + 'build:publicAssets': (_wxt, files) => { + const envDir = path.resolve(import.meta.dirname, 'src/publicAssetsByEnv', publicAssetsVariant) + const entries = fs.readdirSync(envDir) + for (const entry of entries) { + const absoluteSrc = path.resolve(envDir, entry) + if (fs.statSync(absoluteSrc).isFile()) { + files.push({ + relativeDest: `assets/${entry}`, + absoluteSrc, + }) + } + } + }, + // Validate build output after dev builds complete + 'build:done': async (wxt) => { + // Only validate in development mode (dev server) + if (wxt.config.mode !== 'development') { + return + } + const { execSync } = await import('node:child_process') + try { + // Run script directly to avoid Nx dependsOn chain that would trigger a full rebuild + execSync('bunx tsx scripts/validateBuildOutput.ts --dev', { + cwd: wxt.config.root, + stdio: 'inherit', + }) + } catch { + // oxlint-disable-next-line no-console -- CLI output for build validation + console.error('Build validation failed!') + process.exit(1) + } + }, + }, + // Dynamic manifest generation + // oxlint-disable-next-line no-unused-vars -- biome-parity: oxlint is stricter here manifest: (env) => { // BUILD_ENV logic: no build_env for dev command, otherwise use vite build mode const isDevelopment = process.env.NODE_ENV === 'development' const BUILD_ENV = isDevelopment ? undefined : process.env.BUILD_ENV // Extension name postfix - const EXTENSION_NAME_POSTFIX = BUILD_ENV === 'dev' ? 'DEV' : BUILD_ENV === 'beta' ? 'BETA' : '' + const EXTENSION_NAME_POSTFIX = isDevelopment + ? 'LOCAL' + : BUILD_ENV === 'dev' + ? 'DEV' + : BUILD_ENV === 'beta' + ? 'BETA' + : '' // Name logic: some builds don't have names (when postfix is empty) const name = EXTENSION_NAME_POSTFIX ? `${BASE_NAME} ${EXTENSION_NAME_POSTFIX}` : BASE_NAME @@ -133,6 +234,22 @@ export default defineConfig({ Object.entries(envVars).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]), ) + const defines = { + __DEV__: !isProduction, + global: 'globalThis', + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), + 'process.env.DEBUG': JSON.stringify(process.env.DEBUG || '0'), + 'process.env.VERSION': JSON.stringify(EXTENSION_VERSION), + 'process.env.IS_STATIC': '""', + 'process.env.EXPO_OS': '"web"', + ...envDefines, + 'process.env.REACT_APP_IS_UNISWAP_INTERFACE': '"false"', + 'process.env.IS_UNISWAP_EXTENSION': '"true"', + } + + const cacheDir = path.resolve(__dirname, 'node_modules/.vite') + const forceOptimize = shouldInvalidateOptimizeDepsForEnv({ defines, cacheDir }) + // External package aliases from web config const overrides = { buffer: 'buffer', @@ -147,18 +264,7 @@ export default defineConfig({ } return { - define: { - __DEV__: !isProduction, - global: 'globalThis', - 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), - 'process.env.DEBUG': JSON.stringify(process.env.DEBUG || '0'), - 'process.env.VERSION': JSON.stringify(EXTENSION_VERSION), - 'process.env.IS_STATIC': '""', - 'process.env.EXPO_OS': '"web"', - ...envDefines, - 'process.env.REACT_APP_IS_UNISWAP_INTERFACE': '"false"', - 'process.env.IS_UNISWAP_EXTENSION': '"true"', - }, + define: defines, resolve: { extensions: ['.web.tsx', '.web.ts', '.web.js', '.tsx', '.ts', '.js'], @@ -187,10 +293,14 @@ export default defineConfig({ plugins: [ { - name: 'transform-expo-blur-jsx', + name: 'transform-react-native-jsx', async transform(code, id) { - // Only transform expo-blur .js files - if (!id.includes('node_modules/expo-blur') || !id.endsWith('.js')) { + // Transform JSX in react-native libraries that ship JSX in .js files + const needsJsxTransform = ['node_modules/expo-blur', 'node_modules/react-native-reanimated'].some((path) => + id.includes(path), + ) + + if (!needsJsxTransform || !id.endsWith('.js')) { return null } @@ -240,6 +350,7 @@ export default defineConfig({ name: 'svg-import-fix', transform(code: string) { const regex = /import\s+([a-zA-Z0-9_$]+)\s+from\s+['"]([^'"]+\.svg)['"]/g + // oxlint-disable-next-line max-params -- biome-parity: oxlint is stricter here const transformed = code.replace(regex, (match, varName, path) => { if (match.includes('{')) { return match @@ -265,6 +376,7 @@ export default defineConfig({ ].filter(Boolean), optimizeDeps: { + force: forceOptimize, entries: [], // noDiscovery: true, include: [ @@ -300,13 +412,12 @@ export default defineConfig({ 'elliptic', 'bn.js', ], - exclude: ['expo-clipboard'], - rollupOptions: { - resolve: { - extensions: ['.web.js', '.web.ts', '.web.tsx', '.js', '.ts', '.tsx'], - }, - }, + exclude: ['expo-clipboard', 'vite-plugin-node-polyfills'], esbuildOptions: { + // Prefer .web.* extensions so react-native packages resolve to their web variants + // (e.g. react-native-svg/ReactNativeSVG.web.js instead of ReactNativeSVG.js which + // imports Fabric/codegen internals that don't exist on web). + resolveExtensions: ['.web.tsx', '.web.ts', '.web.js', '.tsx', '.ts', '.js'], loader: { '.js': 'jsx', '.ts': 'ts', diff --git a/apps/mobile/.depcheckrc b/apps/mobile/.depcheckrc index 6190990de06..a0bc1c9da0c 100644 --- a/apps/mobile/.depcheckrc +++ b/apps/mobile/.depcheckrc @@ -12,7 +12,8 @@ ignores: [ "babel-plugin-transform-remove-console", "cross-fetch", "@datadog/datadog-ci", - "@rnef/cli", + "dotenv", + "expo-image", "expo-localization", "expo-linking", "expo-modules-core", @@ -29,6 +30,8 @@ ignores: [ "react-native-passkey", "react-native-restart", "sp-react-native-in-app-updates", + "typescript", + "@typescript/native-preview", # Dependencies that depcheck thinks are missing but are actually present or never used ## Internal packages / workspaces "e2e", @@ -43,4 +46,7 @@ ignores: [ "metro-config", ## used in sessions/api packages "expo-secure-store", + ## used for expo remote build caching + "eas-build-cache-provider", + "expo-dev-client", ] diff --git a/apps/mobile/.eslintignore b/apps/mobile/.eslintignore deleted file mode 100644 index f065a060ffc..00000000000 --- a/apps/mobile/.eslintignore +++ /dev/null @@ -1,19 +0,0 @@ -.eslintrc.js -babel.config.js -jest.config.js -metro.config.js -node_modules - -storybook-static - -coverage - -# Ignore compiled and generated Maestro JavaScript files -.maestro/scripts/dist/ -.maestro/scripts/performance/dist/ - -# Ignore Maestro JavaScript files (these run in Maestro's GraalJS environment, not Node.js) -.maestro/scripts/performance/**/*.js - -# Don't ignore Maestro TypeScript source files -!.maestro/scripts/**/*.ts diff --git a/apps/mobile/.eslintrc.js b/apps/mobile/.eslintrc.js deleted file mode 100644 index 8a2ee3a2cee..00000000000 --- a/apps/mobile/.eslintrc.js +++ /dev/null @@ -1,73 +0,0 @@ -const rulesDirPlugin = require('eslint-plugin-rulesdir') -rulesDirPlugin.RULES_DIR = '../../packages/uniswap/eslint_rules' - -module.exports = { - root: true, - extends: ['@uniswap/eslint-config/mobile'], - plugins: ['rulesdir'], - ignorePatterns: [ - '.storybook/storybook.requires.ts', - '!.maestro', // Don't ignore .maestro directory - '!.maestro/**', // Don't ignore files in .maestro - ], - parserOptions: { - project: 'tsconfig.eslint.json', - tsconfigRootDir: __dirname, - ecmaFeatures: { - jsx: true, - }, - ecmaVersion: 2018, - sourceType: 'module', - }, - overrides: [ - { - files: ['index.js', 'src/index.ts', 'src/polyfills/index.ts', 'src/test/fixtures/*'], - rules: { - 'check-file/no-index': 'off', - }, - }, - { - files: ['*.ts', '*.tsx'], - rules: { - 'no-relative-import-paths/no-relative-import-paths': [ - 'error', - { - allowSameFolder: false, - prefix: 'src', - }, - ], - }, - }, - { - files: ['.maestro/scripts/**/*.ts'], - rules: { - // Maestro scripts have different import requirements - 'no-relative-import-paths/no-relative-import-paths': 'off', - // Allow console.log for Maestro scripts (needed for metrics output) - 'no-console': 'off', - // These scripts run in GraalJS environment, not React Native - 'react-native/no-unused-styles': 'off', - 'react-native/no-color-literals': 'off', - // Triple-slash references are needed for globals in Maestro environment - '@typescript-eslint/triple-slash-reference': 'off', - // Don't require React in scope for these non-React files - 'react/react-in-jsx-scope': 'off', - // Allow any for error handling in compile script - '@typescript-eslint/no-explicit-any': 'warn', - // These are utility modules that may not all be used immediately - 'import/no-unused-modules': 'off', - }, - }, - { - // Allow test files to exceed max-lines limit - files: ['**/*.test.ts', '**/*.test.tsx', '**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'], - rules: { - 'max-lines': 'off', - }, - }, - ], - rules: { - 'rulesdir/i18n': 'error', - 'rulesdir/no-redux-modals': 'error', - }, -} diff --git a/apps/mobile/.fingerprintignore b/apps/mobile/.fingerprintignore new file mode 100644 index 00000000000..2bcbe3b6c40 --- /dev/null +++ b/apps/mobile/.fingerprintignore @@ -0,0 +1,49 @@ +# Generated files that shouldn't trigger rebuilds +ios/WidgetsCore/MobileSchema/**/*.swift +ios/WidgetsCore/Env.swift +ios/OneSignalNotificationServiceExtension/Env.swift +.maestro/scripts/dist/**/* +.maestro/scripts/performance/dist/**/* + +# Cache/temporary files +.expo/**/* +coverage/**/* +.tamagui/**/* +storybook-static/**/* +dist/**/* +build/**/* + +# Environment files +.env* +!.env.example + +# All node_modules - native deps are tracked via lock files +node_modules/**/* +../../node_modules/**/* + +# Build configuration that doesn't affect native +.gitignore + +# Autolinking outputs (redundant with lock files) +# These are derived from node_modules and package.json +expoAutolinkingConfig:android +expoAutolinkingConfig:ios +rncoreAutolinkingConfig:android +rncoreAutolinkingConfig:ios + +# Android build artifacts (if not already in native .gitignore) +android/app/build/**/* +android/.gradle/**/* +android/build/**/* +android/.cxx/**/* + +# iOS build artifacts (if not already in native .gitignore) +ios/build/**/* +ios/Pods/**/* +!ios/Podfile +!ios/Podfile.lock +ios/*.xcworkspace/**/* +!ios/*.xcworkspace/contents.xcworkspacedata + +# ensure patches are tracked +!../../patches/**/* diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore index f6487b4f10a..6d1ce5cd0e8 100644 --- a/apps/mobile/.gitignore +++ b/apps/mobile/.gitignore @@ -36,6 +36,7 @@ local.properties *.jks keystore.properties *.aab +.kotlin/ # node.js # @@ -120,8 +121,16 @@ ios/WidgetsCore/Env.swift ios/OneSignalNotificationServiceExtension/Env.swift # Expo -.expo/ +.expo +dist/ +web-build/ # Maestro E2E Scripts (compiled/generated) .maestro/scripts/dist/ .maestro/scripts/performance/dist/ + +# rnef (deprecated) +.rnef/ + +coverage/ + diff --git a/apps/mobile/.maestro/flows/deeplinks/deeplink-comprehensive.yaml b/apps/mobile/.maestro/flows/deeplinks/deeplink-comprehensive.yaml index 7986a843f72..b67fed648ac 100644 --- a/apps/mobile/.maestro/flows/deeplinks/deeplink-comprehensive.yaml +++ b/apps/mobile/.maestro/flows/deeplinks/deeplink-comprehensive.yaml @@ -1,5 +1,7 @@ appId: com.uniswap.mobile.dev jsEngine: graaljs +tags: + - ios-only env: E2E_RECOVERY_PHRASE: ${E2E_RECOVERY_PHRASE} DATADOG_API_KEY: ${DATADOG_API_KEY} @@ -12,7 +14,7 @@ env: - runScript: file: ../../scripts/performance/dist/actions/start-flow.js env: - FLOW_NAME: 'deeplink-comprehensive' + FLOW_NAME: "deeplink-comprehensive" # Run prerequisite flows (tracked as sub-flows) - runFlow: ../../shared-flows/start.yaml @@ -22,15 +24,15 @@ env: - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'onramp-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "onramp-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://app/fiatonramp?userAddress=0xEEf806b3Cae8fcecAe1793EE1e0B2c738F61C6bB&source=push' + link: "uniswap://app/fiatonramp?userAddress=0xEEf806b3Cae8fcecAe1793EE1e0B2c738F61C6bB&source=push" autoVerify: true # Handle iOS deeplink permission dialog (optional - only appears on first run) - tapOn: - text: 'Open' + text: "Open" optional: true - waitForAnimationToEnd: timeout: 5000 @@ -41,43 +43,43 @@ env: - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'onramp-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "onramp-deeplink" + PHASE: "end" - killApp # Open widget deeplink - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'widget-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "widget-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://widget/#/tokens/ethereum/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984' + link: "uniswap://widget/#/tokens/ethereum/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984" autoVerify: true - waitForAnimationToEnd: timeout: 3000 - assertVisible: id: ${output.testIds.TokenDetailsHeaderText} - text: 'Uniswap' + text: "Uniswap" - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'widget-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "widget-deeplink" + PHASE: "end" - killApp # Open swap deeplink - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'swap-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "swap-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://redirect?screen=swap&userAddress=0xEEf806b3Cae8fcecAe1793EE1e0B2c738F61C6bB&inputCurrencyId=1-0x6B175474E89094C44Da98b954EedeAC495271d0F&outputCurrencyId=1-0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984¤cyField=input&amount=100' + link: "uniswap://redirect?screen=swap&userAddress=0xEEf806b3Cae8fcecAe1793EE1e0B2c738F61C6bB&inputCurrencyId=1-0x6B175474E89094C44Da98b954EedeAC495271d0F&outputCurrencyId=1-0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984¤cyField=input&amount=100" autoVerify: true - waitForAnimationToEnd: timeout: 3000 @@ -92,20 +94,20 @@ env: - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'swap-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "swap-deeplink" + PHASE: "end" - killApp # Open token details deeplink - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'token-details-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "token-details-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://app/tokendetails?currencyId=10-0x6fd9d7ad17242c41f7131d257212c54a0e816691&source=push' + link: "uniswap://app/tokendetails?currencyId=10-0x6fd9d7ad17242c41f7131d257212c54a0e816691&source=push" autoVerify: true - waitForAnimationToEnd: timeout: 3000 @@ -116,20 +118,20 @@ env: - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'token-details-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "token-details-deeplink" + PHASE: "end" - killApp # Open transaction history deeplink - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'transaction-history-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "transaction-history-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://redirect?screen=transaction&fiatOnRamp=true&userAddress=0xEEf806b3Cae8fcecAe1793EE1e0B2c738F61C6bB' + link: "uniswap://redirect?screen=transaction&fiatOnRamp=true&userAddress=0xEEf806b3Cae8fcecAe1793EE1e0B2c738F61C6bB" autoVerify: true - waitForAnimationToEnd: timeout: 3000 @@ -138,20 +140,20 @@ env: - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'transaction-history-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "transaction-history-deeplink" + PHASE: "end" - killApp # Invalid deeplink (should fail gracefully and remain functional) - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'invalid-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "invalid-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://invalid-path' + link: "uniswap://invalid-path" autoVerify: true - waitForAnimationToEnd: timeout: 3000 @@ -160,46 +162,46 @@ env: - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'invalid-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "invalid-deeplink" + PHASE: "end" - killApp # Open moonpayOnly onramp deeplink - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'moonpay-onramp-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "moonpay-onramp-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://app/fiatonramp?source=push&moonpayOnly=true&moonpayCurrencyCode=usdc&amount=200' + link: "uniswap://app/fiatonramp?source=push&moonpayOnly=true&moonpayCurrencyCode=usdc&amount=200" autoVerify: true - waitForAnimationToEnd: timeout: 5000 - assertVisible: id: ${output.testIds.ForFormTokenSelected} - text: 'USD Coin' + text: "USD Coin" - assertVisible: id: ${output.testIds.BuyFormAmountInput} - text: '200' + text: "200" - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'moonpay-onramp-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "moonpay-onramp-deeplink" + PHASE: "end" - killApp # Open scantastic deeplink (when user scans QR code on the extension) - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'scantastic-deeplink' - PHASE: 'start' + ACTION: "openLink" + TARGET: "scantastic-deeplink" + PHASE: "start" - openLink: - link: 'uniswap://scantastic?pubKey=%7B%22alg%22%3A%22RSA-OAEP-256%22%2C%22kty%22%3A%22RSA%22%2C%22n%22%3A%224X4nRAEZ8FWoVmoQ5KrxcssIR7XpdcVo_y7yD1SgmYuXekvHMIYuLxxkxVTjsyxj2s9jctIHOhZ-g96w4oM8-HXjCJG_v55w6FZyDskllcmaGeUlZFwWkiqZ-PKkHCWxCe_dZGvL33sazS_L8P3eAxXEPEJMG9p9lxsIlPp7ki0GSyVjq4rrHgW0lIz6qy6WqHbnyJWQAMSPnZTGM697ZCdkW_GTD3MyqitBwK5xNQN8Pxgbu6S7xbQglanYNBbeMYpJ3X1PDl37sp16YwPm6ryGaX1ESDPHa3M7-_we_yQEUQvtU5t2dd8chISJX8L1D7s8iNxM1LxG_nZTwKnccRPtrzKj-osBMbfCoU4fiNS2LC7q6zsyHxgDpeFlrV--iboQ9TsaQ7RGaFOSKs0l74_dt8GvX2JtNJ0ah8K__eNg9q0xBD8DTdeY2duMTEKJZIKgEyX0KUiRpsbsNmm_76iqhhZyYvcb6mwvNnVcXPg_TabX7lQEEippd7JTWVnF2LKzldlUonchQSsbLEUlN_ALa0Nuq6GG1MVJ0JjSsNMcpin6rH9fPzmDKkqzM2qvhdyuV66vkS82Wj9tQpqXL_jkRk7bQsDlB-HiVbzM2oNPk6or5u6p5tJni0th6BZm4z-sYgmMj3D5xHeusyap-8dmS9J4mXDxGLL_NloaHY8%22%2C%22e%22%3A%22AQAB%22%7D&uuid=28c01911-8e69-46e9-b2f0-f5e719bb714b&vendor=Apple&model=Macintosh&browser=Chrome' + link: "uniswap://scantastic?pubKey=%7B%22alg%22%3A%22RSA-OAEP-256%22%2C%22kty%22%3A%22RSA%22%2C%22n%22%3A%224X4nRAEZ8FWoVmoQ5KrxcssIR7XpdcVo_y7yD1SgmYuXekvHMIYuLxxkxVTjsyxj2s9jctIHOhZ-g96w4oM8-HXjCJG_v55w6FZyDskllcmaGeUlZFwWkiqZ-PKkHCWxCe_dZGvL33sazS_L8P3eAxXEPEJMG9p9lxsIlPp7ki0GSyVjq4rrHgW0lIz6qy6WqHbnyJWQAMSPnZTGM697ZCdkW_GTD3MyqitBwK5xNQN8Pxgbu6S7xbQglanYNBbeMYpJ3X1PDl37sp16YwPm6ryGaX1ESDPHa3M7-_we_yQEUQvtU5t2dd8chISJX8L1D7s8iNxM1LxG_nZTwKnccRPtrzKj-osBMbfCoU4fiNS2LC7q6zsyHxgDpeFlrV--iboQ9TsaQ7RGaFOSKs0l74_dt8GvX2JtNJ0ah8K__eNg9q0xBD8DTdeY2duMTEKJZIKgEyX0KUiRpsbsNmm_76iqhhZyYvcb6mwvNnVcXPg_TabX7lQEEippd7JTWVnF2LKzldlUonchQSsbLEUlN_ALa0Nuq6GG1MVJ0JjSsNMcpin6rH9fPzmDKkqzM2qvhdyuV66vkS82Wj9tQpqXL_jkRk7bQsDlB-HiVbzM2oNPk6or5u6p5tJni0th6BZm4z-sYgmMj3D5xHeusyap-8dmS9J4mXDxGLL_NloaHY8%22%2C%22e%22%3A%22AQAB%22%7D&uuid=28c01911-8e69-46e9-b2f0-f5e719bb714b&vendor=Apple&model=Macintosh&browser=Chrome" autoVerify: true - waitForAnimationToEnd: timeout: 3000 @@ -207,16 +209,16 @@ env: id: ${output.testIds.ScantasticConfirmationTitle} - assertVisible: id: ${output.testIds.ScantasticDevice} - text: 'Apple Macintosh' + text: "Apple Macintosh" - assertVisible: id: ${output.testIds.ScantasticBrowser} - text: 'Chrome' + text: "Chrome" - runScript: file: ../../scripts/performance/dist/actions/track-action.js env: - ACTION: 'openLink' - TARGET: 'scantastic-deeplink' - PHASE: 'end' + ACTION: "openLink" + TARGET: "scantastic-deeplink" + PHASE: "end" - killApp # End flow tracking @@ -228,4 +230,4 @@ env: file: ../../scripts/performance/upload-metrics.js env: DATADOG_API_KEY: ${DATADOG_API_KEY} - ENVIRONMENT: 'maestro_cloud' + ENVIRONMENT: "maestro_cloud" diff --git a/apps/mobile/.maestro/flows/explore/filters-and-sorts.yaml b/apps/mobile/.maestro/flows/explore/filters-and-sorts.yaml index 89740a599f7..76c7fa635fe 100644 --- a/apps/mobile/.maestro/flows/explore/filters-and-sorts.yaml +++ b/apps/mobile/.maestro/flows/explore/filters-and-sorts.yaml @@ -1,7 +1,5 @@ appId: com.uniswap.mobile.dev jsEngine: graaljs -tags: - - language-agnostic env: E2E_RECOVERY_PHRASE: ${E2E_RECOVERY_PHRASE} DATADOG_API_KEY: ${DATADOG_API_KEY} @@ -72,11 +70,11 @@ env: # Wait for sort options to appear - extendedWaitUntil: visible: - id: "MARKET_CAP" + text: ".*Market.*" timeout: 3000 - tapOn: - id: "MARKET_CAP" + text: ".*Market.*" - runScript: file: ../../scripts/performance/dist/actions/track-action.js @@ -106,11 +104,11 @@ env: - waitForAnimationToEnd - extendedWaitUntil: visible: - id: "TOTAL_VALUE_LOCKED" + text: ".*TVL.*" timeout: 3000 - tapOn: - id: "TOTAL_VALUE_LOCKED" + text: ".*TVL.*" - runScript: file: ../../scripts/performance/dist/actions/track-action.js @@ -142,11 +140,11 @@ env: # Wait for sort options to appear - extendedWaitUntil: visible: - id: "PRICE_PERCENT_CHANGE_1_DAY_DESC" + text: ".*Price.*increase.*" timeout: 3000 - tapOn: - id: "PRICE_PERCENT_CHANGE_1_DAY_DESC" + text: ".*Price.*increase.*" - runScript: file: ../../scripts/performance/dist/actions/track-action.js @@ -178,11 +176,11 @@ env: # Wait for sort options to appear - extendedWaitUntil: visible: - id: "PRICE_PERCENT_CHANGE_1_DAY_ASC" + text: ".*Price.*decrease.*" timeout: 3000 - tapOn: - id: "PRICE_PERCENT_CHANGE_1_DAY_ASC" + text: ".*Price.*decrease.*" - runScript: file: ../../scripts/performance/dist/actions/track-action.js @@ -214,11 +212,11 @@ env: # Wait for sort options to appear - extendedWaitUntil: visible: - id: "VOLUME" + text: ".*Volume.*" timeout: 3000 - tapOn: - id: "VOLUME" + text: ".*Volume.*" - runScript: file: ../../scripts/performance/dist/actions/track-action.js diff --git a/apps/mobile/.maestro/flows/portfolio/portfolio-chart-happy-path.yaml b/apps/mobile/.maestro/flows/portfolio/portfolio-chart-happy-path.yaml new file mode 100644 index 00000000000..cf24e993c5f --- /dev/null +++ b/apps/mobile/.maestro/flows/portfolio/portfolio-chart-happy-path.yaml @@ -0,0 +1,151 @@ +appId: com.uniswap.mobile.dev +jsEngine: graaljs +env: + E2E_RECOVERY_PHRASE: ${E2E_RECOVERY_PHRASE} + DATADOG_API_KEY: ${DATADOG_API_KEY} +--- +- runScript: + file: ../../scripts/performance/dist/actions/init-tracking.js + +- runScript: + file: ../../scripts/performance/dist/actions/start-flow.js + env: + FLOW_NAME: 'portfolio-chart-happy-path' + +- runFlow: ../../shared-flows/start.yaml +- runFlow: ../../shared-flows/recover-fast.yaml +- openLink: uniswap://e2e/override-gates?gates=profit_loss +- tapOn: + text: "Open" + optional: true +- waitForAnimationToEnd + +- extendedWaitUntil: + visible: + id: ${output.testIds.PortfolioChartCollapsed} + timeout: 15000 + +- assertVisible: + id: ${output.testIds.PortfolioChartCollapsed} + +- assertNotVisible: + id: ${output.testIds.PortfolioChartExpanded} + +- assertNotVisible: + id: ${output.testIds.PortfolioChartPeriodPrefix}1w + +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: 'tapOn' + TARGET: 'PortfolioChartToggleExpand' + PHASE: 'start' +- tapOn: + id: ${output.testIds.PortfolioChartToggle} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: 'tapOn' + TARGET: 'PortfolioChartToggleExpand' + PHASE: 'end' +- waitForAnimationToEnd + +- assertVisible: + id: ${output.testIds.PortfolioChartExpanded} + +- assertNotVisible: + id: ${output.testIds.PortfolioChartCollapsed} + +- assertVisible: + id: ${output.testIds.PortfolioPerformance} + +- assertVisible: + id: ${output.testIds.PortfolioChartPeriodPrefix}1h + +- assertVisible: + id: ${output.testIds.PortfolioChartPeriodPrefix}1d + +- assertVisible: + id: ${output.testIds.PortfolioChartPeriodPrefix}1w + +- assertVisible: + id: ${output.testIds.PortfolioChartPeriodPrefix}1m + +- assertVisible: + id: ${output.testIds.PortfolioChartPeriodPrefix}1y + +- assertVisible: + id: ${output.testIds.PortfolioChartPeriodPrefix}all + +- assertVisible: + id: ${output.testIds.PortfolioChartSelectedPeriodPrefix}1d + +- tapOn: + id: ${output.testIds.PortfolioChartPeriodPrefix}1h +- waitForAnimationToEnd +- assertVisible: + id: ${output.testIds.PortfolioChartSelectedPeriodPrefix}1h + +- tapOn: + id: ${output.testIds.PortfolioChartPeriodPrefix}1w +- waitForAnimationToEnd +- assertVisible: + id: ${output.testIds.PortfolioChartSelectedPeriodPrefix}1w + +- tapOn: + id: ${output.testIds.PortfolioChartPeriodPrefix}1m +- waitForAnimationToEnd +- assertVisible: + id: ${output.testIds.PortfolioChartSelectedPeriodPrefix}1m + +- tapOn: + id: ${output.testIds.PortfolioChartPeriodPrefix}1y +- waitForAnimationToEnd +- assertVisible: + id: ${output.testIds.PortfolioChartSelectedPeriodPrefix}1y + +- tapOn: + id: ${output.testIds.PortfolioChartPeriodPrefix}all +- waitForAnimationToEnd +- assertVisible: + id: ${output.testIds.PortfolioChartSelectedPeriodPrefix}all + +- tapOn: + id: ${output.testIds.PortfolioChartPeriodPrefix}1d +- waitForAnimationToEnd +- assertVisible: + id: ${output.testIds.PortfolioChartSelectedPeriodPrefix}1d + +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: 'tapOn' + TARGET: 'PortfolioChartToggleCollapse' + PHASE: 'start' +- tapOn: + id: ${output.testIds.PortfolioChartToggle} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: 'tapOn' + TARGET: 'PortfolioChartToggleCollapse' + PHASE: 'end' +- waitForAnimationToEnd + +- assertVisible: + id: ${output.testIds.PortfolioChartCollapsed} + +- assertNotVisible: + id: ${output.testIds.PortfolioChartExpanded} + +- assertNotVisible: + id: ${output.testIds.PortfolioChartPeriodPrefix}1w + +- runScript: + file: ../../scripts/performance/dist/actions/end-flow.js + +- runScript: + file: ../../scripts/performance/upload-metrics.js + env: + DATADOG_API_KEY: ${DATADOG_API_KEY} + ENVIRONMENT: 'maestro_cloud' diff --git a/apps/mobile/.maestro/flows/restore/restore-new-device.yaml b/apps/mobile/.maestro/flows/restore/restore-new-device.yaml index 0f73d3201f3..08ab78e29fa 100644 --- a/apps/mobile/.maestro/flows/restore/restore-new-device.yaml +++ b/apps/mobile/.maestro/flows/restore/restore-new-device.yaml @@ -184,10 +184,29 @@ env: PHASE: 'end' - waitForAnimationToEnd +# Wait for cloud backup to fail - handle both possible error states +# First try waiting for "No backups found" - extendedWaitUntil: visible: text: 'No backups found' - timeout: 5000 # wait for cloud backup to fail + timeout: 5000 + optional: true + +# If that didn't appear, wait for "Error while importing backups" +- extendedWaitUntil: + visible: + text: 'Error while importing backups' + timeout: 5000 + optional: true + +# If error while importing backups appeared, tap to enter recovery phrase manually +- runFlow: + when: + visible: + text: 'Enter recovery phrase' + commands: + - tapOn: + text: 'Enter recovery phrase' # Track seed phrase input - runScript: diff --git a/apps/mobile/.maestro/flows/send/send-eth-to-self.yaml b/apps/mobile/.maestro/flows/send/send-eth-to-self.yaml new file mode 100644 index 00000000000..2091b274c39 --- /dev/null +++ b/apps/mobile/.maestro/flows/send/send-eth-to-self.yaml @@ -0,0 +1,248 @@ +appId: com.uniswap.mobile.dev +jsEngine: graaljs +tags: + - send + - language-agnostic +env: + E2E_RECOVERY_PHRASE: ${E2E_RECOVERY_PHRASE} +--- +# Initialize tracking at the very beginning +- runScript: + file: ../../scripts/performance/dist/actions/init-tracking.js + +# Start flow tracking (includes setup time) +- runScript: + file: ../../scripts/performance/dist/actions/start-flow.js + env: + FLOW_NAME: "send-to-self" + +# Step 1: Initialize app and recover wallet +- runFlow: ../../shared-flows/start.yaml +- runFlow: ../../shared-flows/recover-fast.yaml + +# Step 2: Copy our wallet address from the Receive QR modal +- runFlow: ../../shared-flows/copy-wallet-address.yaml + +# Step 3: Tap on Send button to open the send flow +- assertVisible: + id: ${output.testIds.Send} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "Send" + PHASE: "start" +- tapOn: + id: ${output.testIds.Send} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "Send" + PHASE: "end" +- waitForAnimationToEnd + +# Step 4: Tap on the search input and paste the wallet address +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "ExploreSearchInput" + PHASE: "start" +- tapOn: + id: ${output.testIds.ExploreSearchInput} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "ExploreSearchInput" + PHASE: "end" +- pasteText +- waitForAnimationToEnd + +# Hide keyboard to see search results +- hideKeyboard +- waitForAnimationToEnd + +# Step 5: Tap on the wallet search result to select it as recipient +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "SelectRecipientRow" + PHASE: "start" +- tapOn: + id: ${output.testIds.SelectRecipientRow} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "SelectRecipientRow" + PHASE: "end" +- waitForAnimationToEnd + +# Step 6: Acknowledge the "This is your current wallet" warning +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "Confirm" + PHASE: "start" +- tapOn: + id: ${output.testIds.Confirm} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "Confirm" + PHASE: "end" +- waitForAnimationToEnd + +# Step 7: Open token selector and select Base ETH +# Tap on the token selector button to open the token selector modal +- assertVisible: + id: ${output.testIds.ChooseInputToken} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "ChooseInputToken" + PHASE: "start" +- tapOn: + id: ${output.testIds.ChooseInputToken} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "ChooseInputToken" + PHASE: "end" +- waitForAnimationToEnd + +# Step 8: Search for Base ETH in token selector +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "ExploreSearchInput" + PHASE: "start" +- tapOn: + id: ${output.testIds.ExploreSearchInput} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "ExploreSearchInput" + PHASE: "end" + +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "inputText" + TARGET: "Base ETH" + PHASE: "start" +- inputText: "Base ETH" +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "inputText" + TARGET: "Base ETH" + PHASE: "end" +- waitForAnimationToEnd + +# Step 9: Select Base ETH from search results +- assertVisible: + id: "token-option-8453-ETH" +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "token-option-8453-ETH" + PHASE: "start" +- tapOn: + id: "token-option-8453-ETH" +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "token-option-8453-ETH" + PHASE: "end" +- waitForAnimationToEnd + +# Step 10: Enter amount (0.00001 ETH) using decimal pad +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "inputAmount" + TARGET: "0.00001" + PHASE: "start" +- tapOn: + id: ${output.testIds.DecimalPadNumber0} +- tapOn: + id: ${output.testIds.DecimalPadDecimal} +- tapOn: + id: ${output.testIds.DecimalPadNumber0} +- tapOn: + id: ${output.testIds.DecimalPadNumber0} +- tapOn: + id: ${output.testIds.DecimalPadNumber0} +- tapOn: + id: ${output.testIds.DecimalPadNumber0} +- tapOn: + id: ${output.testIds.DecimalPadNumber1} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "inputAmount" + TARGET: "0.00001" + PHASE: "end" +- waitForAnimationToEnd + +# Step 11: Tap Review Transfer button to proceed +- assertVisible: + id: ${output.testIds.ReviewTransfer} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "ReviewTransfer" + PHASE: "start" +- tapOn: + id: ${output.testIds.ReviewTransfer} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "ReviewTransfer" + PHASE: "end" +- waitForAnimationToEnd + +# Step 12: Wait for review modal and confirm send (testID is 'send' from ElementName.Send) +- assertVisible: + id: ${output.testIds.Send} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "ConfirmSend" + PHASE: "start" +- tapOn: + id: ${output.testIds.Send} +- runScript: + file: ../../scripts/performance/dist/actions/track-action.js + env: + ACTION: "tapOn" + TARGET: "ConfirmSend" + PHASE: "end" +- waitForAnimationToEnd + +# Step 13: Wait for transaction to be submitted and return to activity screen +# After submitting, the app navigates to the Activity tab showing the pending transaction +- extendedWaitUntil: + visible: + id: ${output.testIds.HomeTab} + timeout: 30000 +- waitForAnimationToEnd + +# End flow tracking +- runScript: + file: ../../scripts/performance/dist/actions/end-flow.js diff --git a/apps/mobile/.maestro/scripts/yarn/e2e-interactive.ts b/apps/mobile/.maestro/scripts/e2e-interactive.ts similarity index 98% rename from apps/mobile/.maestro/scripts/yarn/e2e-interactive.ts rename to apps/mobile/.maestro/scripts/e2e-interactive.ts index dca82e196ed..ed19e15f3c5 100644 --- a/apps/mobile/.maestro/scripts/yarn/e2e-interactive.ts +++ b/apps/mobile/.maestro/scripts/e2e-interactive.ts @@ -2,7 +2,7 @@ /** * Interactive script to run E2E tests with flow selection and metro bundler option - * This script is called by the yarn e2e:interactive command + * This script is called by the bun e2e:interactive command */ import type { ChildProcess } from 'child_process' @@ -157,7 +157,7 @@ async function startMetro(): Promise { console.log(`${colors.dim}Metro logs will appear below. The E2E test will start in 8 seconds...${colors.reset}\n`) // Start Metro in a child process but keep it attached to show logs - const metroProcess = spawn('yarn', ['start:e2e'], { + const metroProcess = spawn('bun', ['start:e2e'], { stdio: ['inherit', 'inherit', 'inherit'], shell: true, detached: true, @@ -272,7 +272,7 @@ async function main(): Promise { const { E2E_RECOVERY_PHRASE, DATADOG_API_KEY } = validateEnvironment() // Change to apps/mobile directory - const mobileDir = path.resolve(__dirname, '../../../') + const mobileDir = path.resolve(__dirname, '../../') process.chdir(mobileDir) // Get test files diff --git a/apps/mobile/.maestro/scripts/performance/BUILD.md b/apps/mobile/.maestro/scripts/performance/BUILD.md index 4abb6b8cdeb..047f306257d 100644 --- a/apps/mobile/.maestro/scripts/performance/BUILD.md +++ b/apps/mobile/.maestro/scripts/performance/BUILD.md @@ -18,7 +18,7 @@ The build process uses **esbuild** to: Run the build command: ```bash -yarn e2e:build-js +bun e2e:build-js ``` This executes `.maestro/scripts/tooling/buildPerformanceScripts.ts` which: @@ -63,7 +63,7 @@ dist/ To add a new script: 1. Create the TypeScript file in `src/actions/` or `src/utils/` -2. Run `yarn e2e:build-js` to build +2. Run `bun e2e:build-js` to build The build script automatically discovers and builds: diff --git a/apps/mobile/.maestro/scripts/performance/LOCAL_INSTRUMENTATION_GUIDE.md b/apps/mobile/.maestro/scripts/performance/LOCAL_INSTRUMENTATION_GUIDE.md index a131e060c2f..9e417f27e0a 100644 --- a/apps/mobile/.maestro/scripts/performance/LOCAL_INSTRUMENTATION_GUIDE.md +++ b/apps/mobile/.maestro/scripts/performance/LOCAL_INSTRUMENTATION_GUIDE.md @@ -81,11 +81,11 @@ We've instrumented our Maestro E2E tests to collect performance metrics across t ```bash # Run all e2e tests or a specific test -yarn e2e:interactive +bun e2e:interactive # Process and submit metrics export DATADOG_API_KEY=your-key-here -yarn e2e:local:process-metrics +bun e2e:local:process-metrics ``` #### Maestro Cloud @@ -187,7 +187,7 @@ appId: com.uniswap.mobile.dev - Track meaningful user actions (taps, inputs, swipes) - Let shared flows track themselves internally - Use descriptive action targets -- Clear logs regularly with `yarn e2e:clear-logs` +- Clear logs regularly with `bun e2e:clear-logs` ### DON'T ❌ diff --git a/apps/mobile/.maestro/scripts/performance/submit-local.sh b/apps/mobile/.maestro/scripts/performance/submit-local.sh index 738334f2b51..5621921f941 100755 --- a/apps/mobile/.maestro/scripts/performance/submit-local.sh +++ b/apps/mobile/.maestro/scripts/performance/submit-local.sh @@ -24,14 +24,12 @@ else DRY_RUN="" fi -# Extract metrics from latest test logs -echo "Extracting metrics from Maestro logs..." -"$SCRIPT_DIR/extract-metrics.sh" "$MAESTRO_LOGS_DIR" "$METRICS_FILE" - -# Check if metrics were extracted +# Check if metrics file already exists (extracted by e2e:local:extract-metrics) if [ ! -s "$METRICS_FILE" ]; then echo "❌ No metrics found in logs" exit 1 +else + echo "✅ Using existing metrics file: $METRICS_FILE" fi # Add local development tags diff --git a/apps/mobile/.maestro/scripts/performance/tsconfig.json b/apps/mobile/.maestro/scripts/performance/tsconfig.json index 16205776611..93977c053ea 100644 --- a/apps/mobile/.maestro/scripts/performance/tsconfig.json +++ b/apps/mobile/.maestro/scripts/performance/tsconfig.json @@ -5,7 +5,6 @@ "lib": ["ES2020"], "outDir": "./dist", "rootDir": "./src", - "baseUrl": "./", "strict": true, "esModuleInterop": true, "skipLibCheck": true, diff --git a/apps/mobile/.maestro/shared-flows/copy-wallet-address.yaml b/apps/mobile/.maestro/shared-flows/copy-wallet-address.yaml new file mode 100644 index 00000000000..cb610e36f1a --- /dev/null +++ b/apps/mobile/.maestro/shared-flows/copy-wallet-address.yaml @@ -0,0 +1,42 @@ +appId: com.uniswap.mobile.dev +--- +# Shared flow to copy the wallet's own address from the Receive QR modal +# Must run from the home screen +# After running this flow, use `pasteText` to paste the address into an input field +# The address is stored in Maestro's internal clipboard via copyTextFrom + +# Step 1: Ensure the Receive button is visible and tap it +- assertVisible: + id: ${output.testIds.Receive} +- tapOn: + id: ${output.testIds.Receive} +- waitForAnimationToEnd + +# Wait for either the ReceiveCryptoModal (with WalletReceiveCrypto) or the QR modal (with AddressDisplay) +# The app may show ReceiveCryptoModal if CEX providers are available, or skip directly to QR modal +- extendedWaitUntil: + visible: + id: ${output.testIds.WalletReceiveCrypto} + timeout: 8000 + optional: true + +# Step 2: If ReceiveCryptoModal appeared, tap on the wallet card to open QR modal +- tapOn: + id: ${output.testIds.WalletReceiveCrypto} + optional: true +- waitForAnimationToEnd + +# Wait for the QR modal with full address to appear +- extendedWaitUntil: + visible: + id: ${output.testIds.AddressDisplay} + timeout: 8000 + +# Step 3: Copy the full wallet address from the display +- copyTextFrom: + id: ${output.testIds.AddressDisplay} + +# Step 4: Swipe down to dismiss the modal +- swipe: + direction: DOWN + duration: 300 diff --git a/apps/mobile/.maestro/shared-flows/navigate-to-explore.yaml b/apps/mobile/.maestro/shared-flows/navigate-to-explore.yaml index 3431fe0cb24..200f366aeaf 100644 --- a/apps/mobile/.maestro/shared-flows/navigate-to-explore.yaml +++ b/apps/mobile/.maestro/shared-flows/navigate-to-explore.yaml @@ -1,20 +1,37 @@ appId: com.uniswap.mobile.dev --- # This flow handles the common action of navigating to the Explore tab -# from the main wallet screen. +# from the main wallet screen. It supports both bottom tabs navigation (new) +# and the legacy floating navigation bar. # Wait for the home screen to be visible - extendedWaitUntil: - visible: 'noop' + visible: "noop" timeout: 2000 optional: true -# Tap on the Explore/Search button in the navigation bar +# OPTION 1: Try bottom tabs navigation first (new UI pattern) +# This will be available when BottomTabs feature flag is enabled +- tapOn: + id: ${output.testIds.ExploreTab} + optional: true + +# TODO: INFRA-1074 - Remove this fallback when we no longer support the legacy navigation bar +# OPTION 2: Fallback to legacy floating navigation bar +# This will be used when BottomTabs feature flag is disabled - tapOn: id: ${output.testIds.SearchTokensAndWallets} + optional: true +# Wait for tab animations to complete (200ms animation duration) - waitForAnimationToEnd # Verify we're in the Explore screen by checking for the search input +# This verification works for both navigation patterns +- extendedWaitUntil: + visible: + id: ${output.testIds.ExploreSearchInput} + timeout: 3000 + - assertVisible: id: ${output.testIds.ExploreSearchInput} diff --git a/apps/mobile/.maestro/shared-flows/start.yaml b/apps/mobile/.maestro/shared-flows/start.yaml index dea882ba843..cfff9954f69 100644 --- a/apps/mobile/.maestro/shared-flows/start.yaml +++ b/apps/mobile/.maestro/shared-flows/start.yaml @@ -8,30 +8,52 @@ appId: com.uniswap.mobile.dev - runScript: file: ../scripts/performance/dist/actions/start-sub-flow.js env: - SUB_FLOW_NAME: 'shared-start' + SUB_FLOW_NAME: "shared-start" # Track app launch - runScript: file: ../scripts/performance/dist/actions/track-action.js env: - ACTION: 'launchApp' - TARGET: 'app' - PHASE: 'start' + ACTION: "launchApp" + TARGET: "app" + PHASE: "start" - launchApp: permissions: contacts: unset notifications: unset clearState: true clearKeychain: true # optional: clear *entire* iOS keychain +# Handle Expo Dev Client screens (only present in development builds, not CI/prod builds) +- runFlow: + when: + true: ${CI != 'true'} + commands: + # Launch dev client with disableOnboarding to skip onboarding popup (only works for iOS) + - openLink: "uniswap://expo-development-client/?url=http://localhost:8081&disableOnboarding=1" + # Wait for app to fully load after deep link + - waitForAnimationToEnd + # On Android, disableOnboarding doesn't work - tap the Metro server URL to continue + - tapOn: + text: ".*:8081" + optional: true + - waitForAnimationToEnd + # Dismiss the expo dev menu + - tapOn: + text: "continue" + - tapOn: + point: "10%,10%" + retryTapIfNoChange: false + optional: true + - waitForAnimationToEnd - runScript: file: ../scripts/performance/dist/actions/track-action.js env: - ACTION: 'launchApp' - TARGET: 'app' - PHASE: 'end' + ACTION: "launchApp" + TARGET: "app" + PHASE: "end" # End tracking shared flow - runScript: file: ../scripts/performance/dist/actions/end-sub-flow.js env: - SUB_FLOW_NAME: 'shared-start' + SUB_FLOW_NAME: "shared-start" diff --git a/apps/mobile/.oxlintrc.fast.json b/apps/mobile/.oxlintrc.fast.json new file mode 100644 index 00000000000..4ab02352d27 --- /dev/null +++ b/apps/mobile/.oxlintrc.fast.json @@ -0,0 +1,13 @@ +{ + "extends": ["./.oxlintrc.json", "../../.oxlintrc.fast.json"], + "ignorePatterns": [ + ".maestro/**", + "scripts/**", + "ReactotronConfig.ts", + "index.js", + ".storybook/**", + "src/polyfills/**", + "jest*.js", + "__mocks__/**" + ] +} diff --git a/apps/mobile/.oxlintrc.json b/apps/mobile/.oxlintrc.json new file mode 100644 index 00000000000..0b8e1e817fb --- /dev/null +++ b/apps/mobile/.oxlintrc.json @@ -0,0 +1,94 @@ +{ + "extends": ["../../.oxlintrc.json"], + "ignorePatterns": [ + ".maestro/**", + "scripts/**", + "ReactotronConfig.ts", + "index.js", + ".storybook/**", + "src/polyfills/**", + "jest*.js", + "__mocks__/**" + ], + "rules": { + "universe-custom/enum-member-naming": "error", + "no-shadow": "error", + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "react-router", + "message": "Do not import react-router in native code. Use react-navigation instead." + } + ] + } + ], + "universe-custom/no-transform-percentage-strings": "error", + "complexity": ["error", 20], + "max-depth": ["error", 4], + "max-nested-callbacks": ["error", 3], + "no-restricted-syntax": [ + "error", + { + "selector": "CallExpression[callee.object.name='z'][callee.property.name='any']", + "message": "Avoid using z.any() in favor of more precise custom types." + } + ] + }, + "overrides": [ + { + "files": ["**/*.ts", "**/*.tsx"], + "rules": { + "universe-custom/no-relative-import-paths": [ + "error", + { + "allowSameFolder": false, + "prefix": "src" + } + ], + "typescript/explicit-function-return-type": "off" + } + }, + { + "files": ["**/*.ts", "**/*.tsx"], + "rules": { + "universe-custom/no-nested-component-definitions": "error", + "universe-custom/jsx-prop-order": [ + "error", + { + "groups": ["reserved", "shorthand-prop", "unknown", "callback"], + "reservedPattern": "^(key|ref)$", + "callbackPattern": "^on[A-Z].+" + } + ] + } + }, + { + "files": ["**/*saga*.ts", "**/*Saga.ts", "**/handleDeepLink.ts"], + "rules": { + "typescript/prefer-enum-initializers": "off" + } + }, + { + "files": ["migrations.ts"], + "rules": { + "typescript/prefer-enum-initializers": "off", + "typescript/no-non-null-assertion": "off", + "typescript/no-empty-interface": "off", + "typescript/no-explicit-any": "off" + } + }, + { + "files": [".maestro/scripts/**"], + "rules": { + "no-restricted-imports": "off", + "curly": "off", + "no-console": "off", + "no-lone-blocks": "off", + "no-case-declarations": "off", + "no-unused-vars": "off" + } + } + ] +} diff --git a/apps/mobile/.storybook/index.tsx b/apps/mobile/.storybook/index.tsx index ae5a53a6d88..b583e00c6a0 100644 --- a/apps/mobile/.storybook/index.tsx +++ b/apps/mobile/.storybook/index.tsx @@ -1,4 +1,5 @@ import { MMKV } from 'react-native-mmkv' +// oxlint-disable-next-line universe-custom/no-relative-import-paths -- biome-parity: oxlint is stricter here import { view } from './storybook.requires' const mmkv = new MMKV({ diff --git a/apps/mobile/.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch b/apps/mobile/.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch deleted file mode 100644 index 5187d9c0e67..00000000000 --- a/apps/mobile/.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch +++ /dev/null @@ -1,12 +0,0 @@ -diff --git a/RNFastImage.podspec b/RNFastImage.podspec -index db0fada63fc06191f8620d336d244edde6c3dba3..286fa816e47996fdff9f25261644d612c682ae0b 100644 ---- a/RNFastImage.podspec -+++ b/RNFastImage.podspec -@@ -16,6 +16,6 @@ Pod::Spec.new do |s| - s.source_files = "ios/**/*.{h,m}" - - s.dependency 'React-Core' -- s.dependency 'SDWebImage', '~> 5.11.1' -+ s.dependency 'SDWebImage', '~> 5.15.5' - s.dependency 'SDWebImageWebPCoder', '~> 0.8.4' - end diff --git a/apps/mobile/CLAUDE.md b/apps/mobile/CLAUDE.md deleted file mode 100644 index 45e6ba25bba..00000000000 --- a/apps/mobile/CLAUDE.md +++ /dev/null @@ -1,174 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Build and Development Commands - -### Running the App - -```bash -# Install dependencies (run from mobile directory) -bun install - -# Install iOS pods -bun pod - -# Run iOS app (development) -bun ios - -# Run iOS app with specific configuration -bun ios:dev:release # Dev release build -bun ios:beta # Beta debug build -bun ios:release # Release build - -# Run Android app (development) -bun android - -# Run Android app with specific variant -bun android:release # Dev release build -bun android:build:release # Dev release build -bun android:prod # Production debug build - -# Start Metro bundler (usually starts automatically) -bun start -``` - -### Testing - -```bash -# Run all tests -bun run test - -# Run tests for a specific file/pattern -bun run test path/to/test.ts - -# Update snapshots -bun snapshots - -# Type checking -bun typecheck - -# Linting -bun lint -bun lint:fix - -# Format code -bun format -``` - -### iOS-Specific Commands - -```bash -# Interactive iOS build selector -bun ios:interactive - -# Build iOS bundle for production -bun ios:bundle - -# Update pods -bun pod:update -``` - -### Environment Setup - -```bash -# Download environment variables (requires 1password CLI) -bun env:local:download - -# Reset development environment -bun softreset # Soft reset -bun hardreset # Full reset including node_modules -``` - -## Architecture Overview - -### State Management - -The app uses **Redux with Redux-Saga** for state management: - -- **Store**: Configured in `src/app/store.ts` with redux-persist using MMKV storage -- **Reducers**: Combined in `src/app/mobileReducer.ts` - includes both shared wallet reducers and mobile-specific reducers -- **Sagas**: Root saga in `src/app/saga.ts` orchestrates all side effects -- **Persistence**: State is persisted between sessions with migrations support - -### Navigation Structure - -React Navigation is used with the following stack structure: - -- **AppStack**: Main app screens (HomeScreen, TokenDetailsScreen, etc.) -- **OnboardingStack**: New user onboarding flow -- **SettingsStack**: Settings hierarchy -- **FiatOnRampStack**: Fiat on-ramp flow (separate navigation tree) -- **UnitagStack**: Unitag-related screens - -Navigation is configured in `src/app/navigation/navigation.tsx`. - -### Feature Organization - -Features are organized in `src/features/` with self-contained functionality: - -- Each feature typically has its own slice, saga, and selectors -- Key features: wallet, CloudBackup, biometrics, walletConnect, notifications, deepLinking -- Features can have platform-specific implementations - -### Screen Organization - -Screens are in `src/screens/` organized by functionality: - -- Main screens like HomeScreen have their own directories -- Related screens are grouped (e.g., all onboarding screens in Onboarding/) -- Modal screens use either React Navigation modals or custom modal system - -### Native Modules - -Platform-specific functionality is implemented via native modules: - -- iOS: Swift modules in `ios/Uniswap/` -- Android: Kotlin/Java modules in `android/app/src/main/` -- Key modules: RNEthersRS (key management), RNCloudStorageBackupsManager, RNWalletConnect - -### Shared Code - -The app is part of a monorepo and shares code via packages: - -- `wallet`: Core wallet functionality shared with extension -- `uniswap`: Shared utilities and constants -- `utilities`: Common utility functions - -### Data Flow - -1. User actions → Redux actions -2. Reducers update state synchronously -3. Sagas handle async operations (API calls, native modules) -4. Apollo Client manages GraphQL data with cache -5. Components subscribe via hooks/selectors - -## Development Tips - -### Testing Approach - -- Unit tests for utilities and reducers -- Integration tests for sagas using redux-saga-test-plan -- Component tests using React Native Testing Library -- E2E tests using Maestro (see docs/e2e-testing.md) - -### Performance Considerations - -- Heavy use of React.memo and useMemo for optimization -- FlashList used instead of FlatList for better performance -- Performance monitoring via Datadog integration - -### Common Patterns - -- Feature-based file organization -- Saga pattern for side effects -- Selectors for derived state -- Custom hooks for shared logic -- TypeScript for type safety throughout - -### Debugging - -- Reactotron support for development debugging -- Redux DevTools via Reactotron -- Native debugging via Xcode/Android Studio -- Performance profiling with React DevTools diff --git a/apps/mobile/Gemfile b/apps/mobile/Gemfile index a07e6ca69f3..e1d5aa1dd5b 100644 --- a/apps/mobile/Gemfile +++ b/apps/mobile/Gemfile @@ -1,11 +1,23 @@ source "https://rubygems.org" -gem 'fastlane', '2.214.0' -# Exclude problematic versions of cocoapods and activesupport that causes build failures. -gem 'cocoapods', '1.14.3' +gem 'fastlane', '2.228.0' +gem 'cocoapods', '1.16.2' gem 'activesupport', '7.1.2' gem 'xcodeproj', '1.27.0' gem 'concurrent-ruby', '1.3.4' +# Ruby 3.4.0 removed these from the standard library. +# See: https://github.com/fastlane/fastlane/issues/29183 +# See: https://www.ruby-lang.org/en/news/2024/12/25/ruby-3-4-0-released/ +gem 'abbrev' +gem 'base64' +gem 'bigdecimal' +gem 'benchmark' +gem 'drb' +gem 'logger' +gem 'mutex_m' +gem 'nkf' +gem 'ostruct' + plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/apps/mobile/Gemfile.lock b/apps/mobile/Gemfile.lock index b5891798a1a..315476a5e86 100644 --- a/apps/mobile/Gemfile.lock +++ b/apps/mobile/Gemfile.lock @@ -3,6 +3,7 @@ GEM specs: CFPropertyList (3.0.6) rexml + abbrev (0.1.2) activesupport (7.1.2) base64 bigdecimal @@ -18,32 +19,36 @@ GEM algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.3.0) - aws-partitions (1.877.0) - aws-sdk-core (3.190.1) + aws-eventstream (1.4.0) + aws-partitions (1.1162.0) + aws-sdk-core (3.232.0) aws-eventstream (~> 1, >= 1.3.0) - aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.8) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + base64 + bigdecimal jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.75.0) - aws-sdk-core (~> 3, >= 3.188.0) - aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.142.0) - aws-sdk-core (~> 3, >= 3.189.0) + logger + aws-sdk-kms (1.112.0) + aws-sdk-core (~> 3, >= 3.231.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.199.0) + aws-sdk-core (~> 3, >= 3.231.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.8) - aws-sigv4 (1.8.0) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.12.1) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) + benchmark (0.4.1) bigdecimal (3.1.9) claide (1.1.0) - cocoapods (1.14.3) + cocoapods (1.16.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.14.3) + cocoapods-core (= 1.16.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) @@ -57,8 +62,8 @@ GEM molinillo (~> 0.8.0) nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.14.3) + xcodeproj (>= 1.27.0, < 2.0) + cocoapods-core (1.16.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -84,17 +89,17 @@ GEM concurrent-ruby (1.3.4) connection_pool (2.5.0) declarative (0.0.20) - digest-crc (0.6.5) + digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20231109) + domain_name (0.6.20240107) dotenv (2.8.1) drb (2.2.1) emoji_regex (3.2.3) escape (0.0.4) - ethon (0.16.0) + ethon (0.15.0) ffi (>= 1.15.0) - excon (0.109.0) - faraday (1.10.3) + excon (0.112.0) + faraday (1.10.4) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) faraday-excon (~> 1.1) @@ -110,27 +115,27 @@ GEM faraday (>= 0.8.0) http-cookie (~> 1.0.0) faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.0) + faraday-em_synchrony (1.0.1) faraday-excon (1.1.0) faraday-httpclient (1.0.1) - faraday-multipart (1.0.4) - multipart-post (~> 2) - faraday-net_http (1.0.1) + faraday-multipart (1.1.1) + multipart-post (~> 2.0) + faraday-net_http (1.0.2) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) faraday-rack (1.0.0) faraday-retry (1.0.3) - faraday_middleware (1.2.0) + faraday_middleware (1.2.1) faraday (~> 1.0) - fastimage (2.3.0) - fastlane (2.214.0) + fastimage (2.4.1) + fastlane (2.228.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) - colored + colored (~> 1.2) commander (~> 4.6) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) @@ -139,38 +144,43 @@ GEM faraday-cookie_jar (~> 0.0.6) faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) + http-cookie (~> 1.0.5) json (< 3.0.0) jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (~> 0.1.1) + optparse (>= 0.1.1, < 1.0.0) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.3) + security (= 0.1.5) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (>= 1.4.5, < 2.0.0) + terminal-table (~> 3) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) xcodeproj (>= 1.13.0, < 2.0.0) - xcpretty (~> 0.3.0) - xcpretty-travis-formatter (>= 0.0.3) + xcpretty (~> 0.4.1) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-plugin-get_version_name (0.2.2) fastlane-plugin-versioning_android (0.1.1) - ffi (1.17.1) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + ffi (1.17.2-arm64-darwin) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.2) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -178,93 +188,95 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.29.0) + google-apis-storage_v1 (0.31.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.1) + google-cloud-core (1.8.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (2.1.0) - faraday (>= 1.0, < 3.a) - google-cloud-errors (1.3.1) - google-cloud-storage (1.45.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.5.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.29.0) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.9.1) - faraday (>= 1.0, < 3.a) - google-cloud-env (~> 2.1) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.5) + http-cookie (1.0.8) domain_name (~> 0.5) httpclient (2.8.3) i18n (1.14.7) concurrent-ruby (~> 1.0) jmespath (1.6.2) json (2.7.1) - jwt (2.7.1) - mini_magick (4.12.0) + jwt (2.10.2) + base64 + logger (1.7.0) + mini_magick (4.13.2) mini_mime (1.1.5) minitest (5.25.4) molinillo (0.8.0) - multi_json (1.15.0) - multipart-post (2.3.0) + multi_json (1.17.0) + multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) nap (1.1.0) - naturally (2.2.1) + naturally (2.3.0) netrc (0.11.0) - optparse (0.1.1) + nkf (0.2.0) + optparse (0.6.0) os (1.1.4) - plist (3.7.1) + ostruct (0.6.3) + plist (3.7.2) public_suffix (4.0.7) - rake (13.1.0) + rake (13.3.0) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) rexml (3.4.1) - rouge (2.0.7) + rouge (3.28.0) ruby-macho (2.5.1) ruby2_keywords (0.0.5) - rubyzip (2.3.2) - security (0.1.3) - signet (0.18.0) + rubyzip (2.4.1) + security (0.1.5) + signet (0.21.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) - jwt (>= 1.5, < 3.0) + jwt (>= 1.5, < 4.0) multi_json (~> 1.10) simctl (1.6.10) CFPropertyList naturally + sysrandom (1.0.5) terminal-notifier (2.0.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) - typhoeus (1.4.1) - ethon (>= 0.9.0) + typhoeus (1.5.0) + ethon (>= 0.9.0, < 0.16.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unicode-display_width (1.8.0) - webrick (1.8.1) + unicode-display_width (2.6.0) word_wrap (1.0.0) xcodeproj (1.27.0) CFPropertyList (>= 2.3.3, < 4.0) @@ -273,21 +285,32 @@ GEM colored2 (~> 3.1) nanaimo (~> 0.4.0) rexml (>= 3.3.6, < 4.0) - xcpretty (0.3.0) - rouge (~> 2.0.7) + xcpretty (0.4.1) + rouge (~> 3.28.0) xcpretty-travis-formatter (1.0.1) xcpretty (~> 0.2, >= 0.0.7) PLATFORMS - ruby + arm64-darwin-23 + arm64-darwin-24 + arm64-darwin-25 DEPENDENCIES + abbrev activesupport (= 7.1.2) - cocoapods (= 1.14.3) + base64 + benchmark + bigdecimal + cocoapods (= 1.16.2) concurrent-ruby (= 1.3.4) - fastlane (= 2.214.0) + drb + fastlane (= 2.228.0) fastlane-plugin-get_version_name fastlane-plugin-versioning_android + logger + mutex_m + nkf + ostruct xcodeproj (= 1.27.0) BUNDLED WITH diff --git a/apps/mobile/README.md b/apps/mobile/README.md index 8ab733c1d46..69a8e1a9548 100644 --- a/apps/mobile/README.md +++ b/apps/mobile/README.md @@ -23,7 +23,7 @@ If you have suggestions on how we can improve the app, or would like to report a - [Migrations](#migrations) - [Testing & Performance](#testing--performance) - [Build local app files](./docs/build-app-files.md) - - [E2E testing](./docs/e2e-testing.md) + - [E2E testing](./docs/e2e-mobile.md) - [Performance monitoring](./docs/perf-monitoring.md) - [Troubleshooting](#troubleshooting) - [Common issues](#common-issues) @@ -54,18 +54,18 @@ Note: If you are indeed using an Apple Silicon Mac, we recommend setting up your 3. Install `node` - Run the following command in your terminal: + Look at the `.nvmrc` file in your workspace to determine which version to install. Then run the following command in your terminal with that version: ```bash - nvm install 18 - nvm use 18 + nvm install 22.13.1 + nvm use 22.13.1 ``` - Quit and re-open the terminal, and then run to confirm that v18 is running + Quit and re-open the terminal, and then run to confirm that v22 is running ```bash > node -v - v18.20.4 + v22.13.1 ``` Alternatively, to automatically try to find and use an `.nvmrc` file in your workspace, per the [official nvm docs for zsh](https://github.com/nvm-sh/nvm?tab=readme-ov-file#zsh), add the following script to your shell (typically `~/.zshrc` on mac): @@ -100,17 +100,17 @@ Note: If you are indeed using an Apple Silicon Mac, we recommend setting up your 4. Install `bun`. We use bun as our package manager and to run scripts. - Run the following command to install it (npm comes with node, so it should work if the above step has been completed correctly) + Look at the `.bun-version` file in your workspace to determine which version to install. Run the following command to install it, being mindful of the version string here (npm comes with node, so it should work if the above step has been completed correctly) ```bash - curl -fsSL https://bun.sh/install | bash + curl -fsSL https://bun.sh/install | bash -s "bun-v1.3.11" ``` Check version to verify installation ```bash > bun -v - 3.2.3 + 1.3.11 ``` 5. Install `ruby` @@ -150,7 +150,7 @@ Note: If you are indeed using an Apple Silicon Mac, we recommend setting up your You should start with downloading Xcode if you don't already have it installed, since the file is so large. You can find it here: [developer.apple.com/xcode](https://developer.apple.com/xcode/) -You must use the [Required Xcode Version](https://github.com/Uniswap/universe/blob/main/apps/mobile/scripts/podinstall.sh#L5) to compile the app. [Older versions of xCode can be found here](https://developer.apple.com/download/all/?q=xcode). +You must use the [Required Xcode Version](https://github.com/Uniswap/universe/blob/main/.xcode-version) to compile the app. [Older versions of xCode can be found here](https://developer.apple.com/download/all/?q=xcode). #### Add Xcode Command Line Tools @@ -284,7 +284,7 @@ We use `redux-persist` to persist the Redux state between user sessions. Most of ## Testing & Performance - [Build local app files](./docs/build-app-files.md) -- [E2E testing](./docs/e2e-testing.md) +- [E2E testing](./docs/e2e-mobile.md) - [Performance monitoring](./docs/perf-monitoring.md) diff --git a/apps/mobile/__mocks__/@react-native-firebase/app.ts b/apps/mobile/__mocks__/@react-native-firebase/app.ts index bf28b89561c..d3a5a0625e9 100644 --- a/apps/mobile/__mocks__/@react-native-firebase/app.ts +++ b/apps/mobile/__mocks__/@react-native-firebase/app.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* oxlint-disable typescript/explicit-function-return-type */ export default { app: () => ({ diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index cf470808dde..febcfcd3138 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -1,40 +1,35 @@ -import com.android.build.OutputFile - -plugins { - id 'com.android.application' - id 'com.facebook.react' - id 'com.google.gms.google-services' - id 'maven-publish' - id 'kotlin-android' - id 'org.jetbrains.kotlin.plugin.compose' -} +apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "com.google.gms.google-services" +apply plugin: "maven-publish" +apply plugin: "kotlin-android" +apply plugin: "org.jetbrains.kotlin.plugin.compose" +apply plugin: "com.facebook.react" -def nodeModulesPath = "../../../../node_modules" -def rnRoot = "../.." +def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath() -def keystorePropertiesFile = rootProject.file("keystore.properties"); -def keystoreProperties = new Properties() -if (keystorePropertiesFile.exists()) { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) -} +def nodeModulesPath = "../../../../node_modules" react { - root = file("$rnRoot/") + // From expo docs: https://docs.expo.dev/brownfield/get-started + entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", rootDir.getAbsoluteFile().getParentFile().getAbsolutePath(), "android", "absolute"].execute(null, rootDir).text.trim()) + reactNativeDir = file("$nodeModulesPath/react-native") - codegenDir = file("$nodeModulesPath/react-native-codegen") - cliFile = file("$nodeModulesPath/@rnef/cli/dist/src/bin.js") + + hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc" + + codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() + + enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean() + + cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim()) + bundleCommand = "export:embed" + debuggableVariants = ["devDebug", "betaDebug", "prodDebug"] - hermesCommand = "../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc" // This is relative to the project root. + /* Autolinking */ autolinkLibrariesWithApp() } -/** - * Set this to true to create four separate APKs instead of one, - * one for each native architecture. This is useful if you don't - * use App Bundles (https://developer.android.com/guide/app-bundle/) - * and want to have separate APKs to upload to the Play Store. - */ -def enableSeparateBuildPerCPUArchitecture = false /** * Set this to true to Run Proguard on Release builds to minify the Java bytecode. @@ -45,14 +40,14 @@ def enableProguardInReleaseBuilds = false * The preferred build flavor of JavaScriptCore (JSC) * * For example, to use the international variant, you can use: - * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+` * * The international variant includes ICU i18n library and necessary data * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that * give correct results when using with locales other than en-US. Note that * this variant is about 6MiB larger per architecture than default. */ -def jscFlavor = 'org.webkit:android-jsc-intl:+' +def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' /** * Private function to get the list of Native Architectures you want to build. @@ -72,9 +67,9 @@ if (isCI && datadogPropertiesAvailable) { apply from: "../../../../node_modules/@datadog/mobile-react-native/datadog-sourcemaps.gradle" } -def devVersionName = "1.61" -def betaVersionName = "1.61" -def prodVersionName = "1.61" +def devVersionName = "1.70" +def betaVersionName = "1.70" +def prodVersionName = "1.70" android { ndkVersion rootProject.ext.ndkVersion @@ -91,7 +86,7 @@ android { splits { abi { reset() - enable enableSeparateBuildPerCPUArchitecture + enable false universalApk false // If true, also generate a universal APK include (*reactNativeArchitectures()) } @@ -107,10 +102,18 @@ android { keyPassword 'android' } release { - storeFile file(System.getenv("ANDROID_KEYSTORE_FILE") ?: 'keystore.jks') - storePassword System.getenv("ANDROID_STORE_PASSWORD") ?: keystoreProperties.getProperty("STORE_PASSWORD") - keyAlias System.getenv("ANDROID_KEYSTORE_ALIAS") ?: keystoreProperties.getProperty("KEYSTORE_ALIAS") - keyPassword System.getenv("ANDROID_KEY_PASSWORD") ?: keystoreProperties.getProperty("KEY_PASSWORD") + def useDebugKeystore = System.getenv("ANDROID_USE_DEBUG_KEYSTORE") == "true" + if (useDebugKeystore) { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } else { + storeFile file(System.getenv("ANDROID_KEYSTORE_FILE") ?: 'keystore.jks') + storePassword System.getenv("ANDROID_STORE_PASSWORD") ?: "" + keyAlias System.getenv("ANDROID_KEYSTORE_ALIAS") ?: "" + keyPassword System.getenv("ANDROID_KEY_PASSWORD") ?: "" + } } } @@ -119,12 +122,12 @@ android { productFlavors { dev { isDefault(true) - applicationIdSuffix ".dev" + applicationId "com.uniswap.mobile.dev" versionName devVersionName dimension "variant" } beta { - applicationIdSuffix ".beta" + applicationId "com.uniswap.mobile.beta" versionName betaVersionName dimension "variant" } @@ -146,18 +149,33 @@ android { } // applicationVariants are e.g. debug, release - applicationVariants.all { variant -> + applicationVariants.configureEach { variant -> + // Prevent using debug keystore for production builds + if (variant.flavorName == "prod" && variant.buildType.name == "release") { + def useDebugKeystore = System.getenv("ANDROID_USE_DEBUG_KEYSTORE") == "true" + if (useDebugKeystore) { + def blockTask = tasks.register("blockDebugKeystoreFor${variant.name.capitalize()}") { + doLast { + throw new GradleException( + "ANDROID_USE_DEBUG_KEYSTORE cannot be used for production builds.\n" + + "This prevents accidentally publishing an improperly signed APK." + ) + } + } + variant.assembleProvider.configure { dependsOn blockTask } + } + } + variant.outputs.each { output -> // For each separate APK per architecture, set a unique version code as described here: // https://developer.android.com/studio/build/configure-apk-splits.html // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc. def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] - def abi = output.getFilter(OutputFile.ABI) + def abi = output.getFilter(com.android.build.VariantOutput.FilterType.ABI) if (abi != null) { // null for the universal-debug, universal-release variants output.versionCodeOverride = defaultConfig.versionCode * 1000 + versionCodes.get(abi) } - } } @@ -184,7 +202,7 @@ android { sourceSets { main { jniLibs { - srcDir '../../../../node_modules/@uniswap/ethers-rs-mobile/android/jniLibs' + srcDir '../jniLibs' } } } @@ -193,6 +211,7 @@ android { dependencies { // The version of react-native is set by the React Native Gradle Plugin implementation "com.facebook.react:react-android" + implementation("com.facebook.react:hermes-android") implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" @@ -232,7 +251,7 @@ dependencies { implementation("androidx.compose.ui:ui-tooling-preview:$compose") implementation "androidx.security:security-crypto:1.0.0" - implementation 'com.lambdapioneer.argon2kt:argon2kt:1.3.0' + implementation 'com.lambdapioneer.argon2kt:argon2kt:1.6.0' implementation "com.google.accompanist:accompanist-flowlayout:$flowlayout" @@ -247,10 +266,4 @@ dependencies { implementation 'androidx.compose.ui:ui-tooling-preview-android:1.8.1' implementation project(':react-native-video') - - if (hermesEnabled.toBoolean()) { - implementation("com.facebook.react:hermes-android") - } else { - implementation jscFlavor - } } diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml index 66bdd248726..ac30f3d72bf 100644 --- a/apps/mobile/android/app/src/main/AndroidManifest.xml +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -7,7 +7,7 @@ - + + + - + - - + + - - + + @@ -77,39 +81,39 @@ + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/nfts/asset/" /> + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/nfts/collection/" /> + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/tokens" /> + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/portfolio/" /> + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/explore/tokens" /> + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/swap" /> + android:scheme="https" + android:host="app.uniswap.org" + android:pathPrefix="/buy" /> @@ -118,14 +122,14 @@ + android:scheme="https" + android:host="uniswap.org" + android:pathPrefix="/app" /> + android:scheme="https" + android:host="uniswap.org" + android:pathPrefix="/app/wc" /> diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/MainActivity.kt b/apps/mobile/android/app/src/main/java/com/uniswap/MainActivity.kt index 370ffb03d1f..ee40024b0a3 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/MainActivity.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/MainActivity.kt @@ -44,10 +44,6 @@ class MainActivity : ReactActivity() { * (aka React 18) with two boolean flags. */ override fun createReactActivityDelegate(): ReactActivityDelegate? { - return ReactActivityDelegateWrapper( - this, - BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, - DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) - ) + return ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)) } } diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/MainApplication.kt b/apps/mobile/android/app/src/main/java/com/uniswap/MainApplication.kt index b914ee2c34a..21c5c32714a 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/MainApplication.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/MainApplication.kt @@ -19,7 +19,7 @@ import expo.modules.ReactNativeHostWrapper class MainApplication : Application(), ReactApplication { override val reactNativeHost: ReactNativeHost = - ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) { + ReactNativeHostWrapper(this, object : DefaultReactNativeHost(this) { override fun getPackages(): List = PackageList(this).packages.apply { // Packages that cannot be autolinked yet can be added manually here, for example: @@ -29,9 +29,7 @@ class MainApplication : Application(), ReactApplication { add(ScantasticEncryptionModule()) add(RedirectToSourceAppPackage()) } - override fun getJSMainModuleName(): String { - return "index" - } + override fun getJSMainModuleName(): String = ".expo/.virtual-metro-entry" override fun getUseDeveloperSupport(): Boolean { return BuildConfig.DEBUG @@ -39,22 +37,23 @@ class MainApplication : Application(), ReactApplication { override val isNewArchEnabled: Boolean get() = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED - - override val isHermesEnabled: Boolean - get() = BuildConfig.IS_HERMES_ENABLED }) override val reactHost: ReactHost - get() = getDefaultReactHost(applicationContext, reactNativeHost) + get() = ReactNativeHostWrapper.createReactHost(applicationContext, reactNativeHost) override fun onCreate() { ReactNativePerformance.onAppStarted() super.onCreate() + + // Initialize SoLoader before any code that might load native libraries SoLoader.init(this, OpenSourceMergedSoMapping) if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { // If you opted-in for the New Architecture, we load the native entry point for this app. load() } + + // Initialize Expo modules after SoLoader ApplicationLifecycleDispatcher.onApplicationCreate(this) } diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/UniswapPackage.kt b/apps/mobile/android/app/src/main/java/com/uniswap/UniswapPackage.kt index 5463faf66a8..16776573e76 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/UniswapPackage.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/UniswapPackage.kt @@ -6,6 +6,7 @@ import com.facebook.react.bridge.NativeModule import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.uimanager.ReactShadowNode import com.facebook.react.uimanager.ViewManager +import com.uniswap.notifications.SilentPushEventEmitterModule import com.uniswap.onboarding.backup.MnemonicConfirmationViewManager import com.uniswap.onboarding.backup.MnemonicDisplayViewManager import com.uniswap.onboarding.import.SeedPhraseInputViewManager @@ -28,5 +29,6 @@ class UniswapPackage : ReactPackage { RNEthersRSModule(reactContext), EmbeddedWalletModule(reactContext), ThemeModule(reactContext), + SilentPushEventEmitterModule(reactContext), ) } diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushEventEmitterModule.kt b/apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushEventEmitterModule.kt new file mode 100644 index 00000000000..9c2504ec875 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushEventEmitterModule.kt @@ -0,0 +1,129 @@ +package com.uniswap.notifications + +import android.util.Log +import androidx.annotation.Keep +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.modules.core.DeviceEventManagerModule +import com.uniswap.utils.toWritableMap +import org.json.JSONObject + +@Keep +@ReactModule(name = SilentPushEventEmitterModule.MODULE_NAME) +class SilentPushEventEmitterModule( + reactContext: ReactApplicationContext +) : ReactContextBaseJavaModule(reactContext) { + + override fun getName() = MODULE_NAME + + override fun initialize() { + super.initialize() + instance = this + listenerCount = 0 + Log.d(TAG, "SilentPushEventEmitter initialized") + flushPendingEvents() + } + + override fun onCatalystInstanceDestroy() { + super.onCatalystInstanceDestroy() + if (instance === this) { + instance = null + } + listenerCount = 0 + } + + @ReactMethod + fun addListener(eventName: String) { + if (eventName != EVENT_NAME) { + return + } + listenerCount += 1 + flushPendingEvents() + } + + @ReactMethod + fun removeListeners(count: Int) { + if (count <= 0) { + return + } + listenerCount = (listenerCount - count).coerceAtLeast(0) + } + + private fun flushPendingEvents() { + if (!hasListeners()) { + return + } + + val events = synchronized(pendingPayloads) { + if (pendingPayloads.isEmpty()) { + null + } else { + val copy = ArrayList(pendingPayloads) + pendingPayloads.clear() + copy + } + } ?: return + + Log.d(TAG, "Flushing ${events.size} queued silent push events") + events.forEach { sendEvent(it) } + } + + private fun sendEvent(payload: JSONObject) { + if (!reactApplicationContext.hasActiveCatalystInstance()) { + synchronized(pendingPayloads) { + Log.d(TAG, "No active Catalyst instance; queueing payload: ${payload.toString()}") + pendingPayloads.add(JSONObject(payload.toString())) + } + return + } + + val map = payload.toWritableMap() + reactApplicationContext.runOnUiQueueThread { + if (!reactApplicationContext.hasActiveCatalystInstance()) { + synchronized(pendingPayloads) { + Log.d(TAG, "Catalyst inactive on UI thread; re-queueing payload: ${payload.toString()}") + pendingPayloads.add(JSONObject(payload.toString())) + } + return@runOnUiQueueThread + } + + Log.d(TAG, "Emitting silent push payload to JS: ${payload.toString()}") + reactApplicationContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(EVENT_NAME, map) + } + } + + private fun hasListeners(): Boolean = instance != null && listenerCount > 0 + + companion object { + const val MODULE_NAME = "SilentPushEventEmitter" + private const val EVENT_NAME = "SilentPushReceived" + private const val TAG = "SilentPushEmitter" + private val pendingPayloads = mutableListOf() + + @Volatile + private var instance: SilentPushEventEmitterModule? = null + + @Volatile + private var listenerCount: Int = 0 + + fun emitEvent(payload: JSONObject?) { + val eventPayload = payload?.let { JSONObject(it.toString()) } ?: JSONObject() + val currentInstance = instance + + if (currentInstance != null && currentInstance.hasListeners()) { + Log.d(TAG, "Sending silent push event to JS immediately: $eventPayload") + currentInstance.sendEvent(eventPayload) + return + } + + synchronized(pendingPayloads) { + Log.d(TAG, "Queueing silent push payload until listeners attach: $eventPayload") + pendingPayloads.add(eventPayload) + } + } + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushNotificationServiceExtension.kt b/apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushNotificationServiceExtension.kt new file mode 100644 index 00000000000..90321dc944d --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/notifications/SilentPushNotificationServiceExtension.kt @@ -0,0 +1,123 @@ +package com.uniswap.notifications + +import android.util.Log +import androidx.annotation.Keep +import com.onesignal.notifications.INotification +import com.onesignal.notifications.INotificationReceivedEvent +import com.onesignal.notifications.INotificationServiceExtension +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.json.JSONException +import org.json.JSONObject + +@Keep +class SilentPushNotificationServiceExtension : INotificationServiceExtension { + override fun onNotificationReceived(event: INotificationReceivedEvent) { + val notification = event.notification + val payload = buildPayload(notification) + + val hasContentAvailableFlag = hasContentAvailable(payload) + val isMissingVisibleContent = notification.isMissingVisibleContent() + + Log.d( + TAG, + "Notification received. hasContentAvailable=$hasContentAvailableFlag, " + + "missingVisibleContent=$isMissingVisibleContent, payload=$payload", + ) + + if (!hasContentAvailableFlag && !isMissingVisibleContent) { + return + } + + Log.d(TAG, "Emitting silent push event: $payload") + val payloadForEmission = try { + JSONObject(payload.toString()) + } catch (error: JSONException) { + Log.w(TAG, "Failed to clone payload for emission: ${error.message}") + payload + } + + CoroutineScope(Dispatchers.Default).launch { + withContext(Dispatchers.Main) { + SilentPushEventEmitterModule.emitEvent(payloadForEmission) + } + } + + if (isMissingVisibleContent) { + event.preventDefault() + } + } + + private fun INotification.isMissingVisibleContent(): Boolean { + val title: String? = this.title + val body: String? = this.body + return title.isNullOrBlank() && body.isNullOrBlank() + } + + private fun buildPayload(notification: INotification): JSONObject { + val rawPayload = notification.rawPayload + val payload = try { + if (rawPayload.isNullOrBlank()) JSONObject() else JSONObject(rawPayload) + } catch (error: JSONException) { + Log.w(TAG, "Failed parsing raw payload: ${error.message}") + JSONObject() + } + + notification.additionalData?.let { additionalData -> + try { + payload.put("additionalData", additionalData) + } catch (error: JSONException) { + Log.w(TAG, "Failed to append additional data: ${error.message}") + } + } + + return payload + } + + private fun hasContentAvailable(payload: JSONObject?): Boolean { + if (payload == null) { + return false + } + + if (payload.hasContentAvailableFlag()) { + return true + } + + val aps = payload.optJSONObject("aps") + if (aps != null && aps.hasContentAvailableFlag()) { + return true + } + + val additionalData = payload.optJSONObject("additionalData") + if (additionalData != null && additionalData.hasContentAvailableFlag()) { + return true + } + + return false + } + + private fun JSONObject.hasContentAvailableFlag(): Boolean { + return opt(CONTENT_AVAILABLE_UNDERSCORE).isTruthy() || opt(CONTENT_AVAILABLE_HYPHEN).isTruthy() + } + + private fun Any?.isTruthy(): Boolean { + return when (this) { + null, JSONObject.NULL -> false + is Boolean -> this + is Int -> this == 1 + is Long -> this == 1L + is Double -> this == 1.0 + is Float -> this == 1f + is String -> equals("1", ignoreCase = true) || equals("true", ignoreCase = true) + else -> false + } + } + + companion object { + private const val TAG = "SilentPushExt" + private const val CONTENT_AVAILABLE_UNDERSCORE = "content_available" + private const val CONTENT_AVAILABLE_HYPHEN = "content-available" + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmation.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmation.kt index 995266bf9a7..351b942d10f 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmation.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmation.kt @@ -1,5 +1,6 @@ package com.uniswap.onboarding.backup.ui +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -41,20 +42,23 @@ fun MnemonicConfirmation( } } - BoxWithConstraints { + Column( + modifier = Modifier + .fillMaxSize() + ) { Column( modifier = Modifier - .fillMaxSize() + .weight(1f, fill = true) .verticalScroll(rememberScrollState()) ) { MnemonicWordsGroup( words = displayedWords, shouldShowSmallText = shouldShowSmallText, ) - Spacer(modifier = Modifier.height(UniswapTheme.spacing.spacing24)) - MnemonicWordBank(words = wordBankList, shouldShowSmallText = shouldShowSmallText) { - viewModel.handleWordBankClick(it.index) - } + } + + MnemonicWordBank(words = wordBankList, shouldShowSmallText = shouldShowSmallText) { + viewModel.handleWordBankClick(it.index) } } } diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmationViewModel.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmationViewModel.kt index 840219c2eb0..a1cade2dbd7 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmationViewModel.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmationViewModel.kt @@ -28,9 +28,9 @@ class MnemonicConfirmationViewModel( private val selectedWordPlaceholderFlow = MutableStateFlow("") val selectedWords: StateFlow> = - selectedWordsIndexes.combine(selectedWordPlaceholderFlow) { _, placeholder -> + combine(selectedWordsIndexes, selectedWordPlaceholderFlow, focusedIndex) { _, placeholder, focusedIndex -> List(sourceWords.size) { index -> - getMnemonicWordUiState(index, placeholder) + getMnemonicWordUiState(index, placeholder, focusedIndex) } }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) @@ -77,6 +77,7 @@ class MnemonicConfirmationViewModel( } fun handleWordBankClick(wordBankIndex: Int) { + selectedWordsIndexes.update { indexes -> val updatedIndexes = indexes.toMutableList() updatedIndexes[focusedIndex.value] = wordBankIndex @@ -112,7 +113,8 @@ class MnemonicConfirmationViewModel( private fun getMnemonicWordUiState( displayIndex: Int, - placeholderText: String + placeholderText: String, + focusedIndex: Int, ): MnemonicWordUiState { val selectedWord = getSelectedWord(displayIndex) var status = MnemonicInputStatus.CORRECT_INPUT @@ -127,6 +129,7 @@ class MnemonicConfirmationViewModel( num = displayIndex + 1, text = selectedWord.ifEmpty { placeholderText }, status = status, + isActive = displayIndex == focusedIndex, ) } } diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordBank.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordBank.kt index 3c66d008f44..b6b63a75c40 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordBank.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordBank.kt @@ -30,7 +30,11 @@ fun MnemonicWordBank( FlowRow( modifier = Modifier .fillMaxWidth() - .wrapContentHeight(), + .wrapContentHeight() + .padding( + vertical = UniswapTheme.spacing.spacing16, + horizontal = UniswapTheme.spacing.spacing8 + ), mainAxisSpacing = if (shouldShowSmallText) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8, crossAxisSpacing = if (shouldShowSmallText) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8, mainAxisAlignment = MainAxisAlignment.Center, diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordCell.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordCell.kt index a92ae99e712..20bbd77dc87 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordCell.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordCell.kt @@ -1,13 +1,19 @@ package com.uniswap.onboarding.backup.ui +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.width +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.style.TextOverflow import com.uniswap.onboarding.backup.ui.model.MnemonicInputStatus import com.uniswap.onboarding.backup.ui.model.MnemonicWordUiState import com.uniswap.theme.UniswapTheme @@ -15,11 +21,21 @@ import com.uniswap.theme.UniswapTheme /** * Component used to display a single word as part of an overall seed phrase */ +@OptIn(ExperimentalFoundationApi::class) @Composable fun MnemonicWordCell( word: MnemonicWordUiState, shouldShowSmallText: Boolean = false, ) { + val bringIntoViewRequester = remember { BringIntoViewRequester() } + + LaunchedEffect(word.isActive) { + // When a cell status changes, request to bring it into view in the parent scroll container + if (word.isActive){ + bringIntoViewRequester.bringIntoView() + } + } + val textStyle = if (shouldShowSmallText) UniswapTheme.typography.body3 else UniswapTheme.typography.body2 @@ -29,7 +45,7 @@ fun MnemonicWordCell( MnemonicInputStatus.WRONG_INPUT -> UniswapTheme.colors.statusCritical } - Row { + Row(modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester)) { Text( text = "${word.num}", color = UniswapTheme.colors.neutral2, @@ -38,10 +54,11 @@ fun MnemonicWordCell( ) Spacer(modifier = Modifier.width(UniswapTheme.spacing.spacing16)) Text( - modifier = Modifier.weight(1f), text = word.text, style = textStyle, - color = textColor + color = textColor, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) } } diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/model/MnemonicWordUiState.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/model/MnemonicWordUiState.kt index a08f4cbf7d2..78766f0ecb0 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/model/MnemonicWordUiState.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/model/MnemonicWordUiState.kt @@ -3,11 +3,12 @@ package com.uniswap.onboarding.backup.ui.model enum class MnemonicInputStatus { NO_INPUT, CORRECT_INPUT, - WRONG_INPUT + WRONG_INPUT, } data class MnemonicWordUiState( val num: Int, val text: String, - val status: MnemonicInputStatus = MnemonicInputStatus.CORRECT_INPUT + val status: MnemonicInputStatus = MnemonicInputStatus.CORRECT_INPUT, + val isActive: Boolean = false ) diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewModel.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewModel.kt index ae50f3523b1..2a12ac22e57 100644 --- a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewModel.kt +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewModel.kt @@ -98,27 +98,31 @@ class SeedPhraseInputViewModel( val mnemonic = normalizedInput.trim() val words = mnemonic.split(" ") - if (words.isEmpty()) { - status = Status.None - return - } - + val prevStatus = status val isValidLength = words.size in MIN_LENGTH..MAX_LENGTH val firstInvalidWord = EthersRs.findInvalidWord(mnemonic) + val isFirstWordInvalid = firstInvalidWord == words.last() && skipInvalidWord + val isInvalidLengthError = + prevStatus is Status.Error && (prevStatus.error == MnemonicError.NotEnoughWords || prevStatus.error == MnemonicError.TooManyWords) && !isValidLength - status = if (firstInvalidWord == words.last() && skipInvalidWord) { - Status.None - } else if (firstInvalidWord.isEmpty() && isValidLength) { - Status.Valid - } else if (isAddress(mnemonic)) { - Status.Error(MnemonicError.WordIsAddress) - } else if (firstInvalidWord.isNotEmpty()) { - Status.Error(MnemonicError.InvalidWord(firstInvalidWord)) - } else { - Status.None + if (isFirstWordInvalid) { + return } - val canSubmit = status !is Status.Error && mnemonic != "" && firstInvalidWord.isEmpty() + status = + if (isAddress(mnemonic)) { + Status.Error(MnemonicError.WordIsAddress) + } else if (firstInvalidWord.isNotEmpty()) { + Status.Error(MnemonicError.InvalidWord(firstInvalidWord)) + } else if (isInvalidLengthError) { + prevStatus + } else if (firstInvalidWord.isEmpty() && isValidLength) { + Status.Valid + } else { + Status.None + } + + val canSubmit = status !is Status.Error && mnemonic != "" onInputValidated(canSubmit) } @@ -129,7 +133,7 @@ class SeedPhraseInputViewModel( fun handleSubmit() { validateLastWordJob?.cancel() - + try { val normalized = normalizeInput(input) val mnemonic = normalized.trim() diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/utils/JsonWritableExtensions.kt b/apps/mobile/android/app/src/main/java/com/uniswap/utils/JsonWritableExtensions.kt new file mode 100644 index 00000000000..81415055e6d --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/utils/JsonWritableExtensions.kt @@ -0,0 +1,61 @@ +package com.uniswap.utils + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import org.json.JSONArray +import org.json.JSONObject + +fun JSONObject.toWritableMap(): WritableMap { + val map = Arguments.createMap() + val iterator = keys() + while (iterator.hasNext()) { + val key = iterator.next() + when (val value = opt(key)) { + null, JSONObject.NULL -> map.putNull(key) + is JSONObject -> map.putMap(key, value.toWritableMap()) + is JSONArray -> map.putArray(key, value.toWritableArray()) + is Boolean -> map.putBoolean(key, value) + is Int -> map.putInt(key, value) + is Long -> { + if (value in Int.MIN_VALUE..Int.MAX_VALUE) { + map.putInt(key, value.toInt()) + } else { + map.putDouble(key, value.toDouble()) + } + } + is Double -> map.putDouble(key, value) + is Float -> map.putDouble(key, value.toDouble()) + is Number -> map.putDouble(key, value.toDouble()) + is String -> map.putString(key, value) + else -> map.putString(key, value.toString()) + } + } + return map +} + +fun JSONArray.toWritableArray(): WritableArray { + val array = Arguments.createArray() + for (index in 0 until length()) { + when (val value = opt(index)) { + null, JSONObject.NULL -> array.pushNull() + is JSONObject -> array.pushMap(value.toWritableMap()) + is JSONArray -> array.pushArray(value.toWritableArray()) + is Boolean -> array.pushBoolean(value) + is Int -> array.pushInt(value) + is Long -> { + if (value in Int.MIN_VALUE..Int.MAX_VALUE) { + array.pushInt(value.toInt()) + } else { + array.pushDouble(value.toDouble()) + } + } + is Double -> array.pushDouble(value) + is Float -> array.pushDouble(value.toDouble()) + is Number -> array.pushDouble(value.toDouble()) + is String -> array.pushString(value) + else -> array.pushString(value.toString()) + } + } + return array +} diff --git a/apps/mobile/android/build.gradle b/apps/mobile/android/build.gradle index 9ca0ef7eecb..19a02d6b52a 100644 --- a/apps/mobile/android/build.gradle +++ b/apps/mobile/android/build.gradle @@ -2,11 +2,6 @@ buildscript { ext { - buildToolsVersion = "35.0.0" - minSdkVersion = 28 - compileSdkVersion = 35 - targetSdkVersion = 35 - ndkVersion = "27.1.12297006" kotlinVersion = "2.0.21" appCompat = "1.6.1" @@ -36,6 +31,14 @@ plugins { id 'org.jetbrains.kotlin.plugin.compose' version "$kotlinVersion" apply false } +def reactNativeAndroidDir = new File( + providers.exec { + workingDir(rootDir) + commandLine("node", "--print", "require.resolve('react-native/package.json')") + }.standardOutput.asText.get().trim(), + "../android" +) + allprojects { project.pluginManager.withPlugin("com.facebook.react") { react { @@ -44,12 +47,29 @@ allprojects { } } - repositories { - maven { - // expo-camera bundles a custom com.google.android:cameraview - url "$rootDir/../../../node_modules/expo-camera/android/maven" + // Ensure all native libraries support 16KB page sizes (required for Android 15+) + pluginManager.withPlugin("com.android.library") { + project.android { + defaultConfig { + externalNativeBuild { + cmake { + arguments "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" + } + } + } } } + + repositories { + google() + mavenCentral() + maven { url 'https://www.jitpack.io' } + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + maven { url(reactNativeAndroidDir) } + // expo-camera bundles a custom com.google.android:cameraview + maven { url "$rootDir/../../../node_modules/expo-camera/android/maven" } + } } +apply plugin: "expo-root-project" apply plugin: "com.facebook.react.rootproject" diff --git a/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties b/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties index d5edd72a874..4dd5cf37e34 100644 --- a/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties +++ b/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ #Wed April 10 16:30:26 EDT 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/apps/mobile/android/gradlew b/apps/mobile/android/gradlew index 98d9216fb08..faf93008b77 100755 --- a/apps/mobile/android/gradlew +++ b/apps/mobile/android/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,11 +15,53 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME @@ -29,158 +71,181 @@ app_path=$0 # Need this for daisy-chained symlinks. while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] do - ls=$(ls -ld "$app_path") - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum -warn() { - echo "$*" -} +warn () { + echo "$*" +} >&2 -die() { - echo - echo "$*" - echo - exit 1 -} +die () { + echo + echo "$*" + echo + exit 1 +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "$(uname)" in -CYGWIN*) - cygwin=true - ;; -Darwin*) - darwin=true - ;; -MINGW*) - msys=true - ;; -NONSTOP*) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ]; then - if [ -x "$JAVA_HOME/jre/sh/java" ]; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ]; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." - fi + fi else - JAVACMD="java" - if ! command -v java >/dev/null 2>&1; then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." - fi + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ]; then - MAX_FD_LIMIT=$(ulimit -H -n) - if [ $? -eq 0 ]; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ]; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ]; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ]; then - APP_HOME=$(cygpath --path --mixed "$APP_HOME") - CLASSPATH=$(cygpath --path --mixed "$CLASSPATH") - - JAVACMD=$(cygpath --unix "$JAVACMD") - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=$(find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null) - SEP="" - for dir in $ROOTDIRSRAW; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ]; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@"; do - CHECK=$(echo "$arg" | egrep -c "$OURCYGPATTERN" -) - CHECK2=$(echo "$arg" | egrep -c "^-") ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ]; then ### Added a condition - eval $(echo args$i)=$(cygpath --path --ignore --mixed "$arg") - else - eval $(echo args$i)="\"$arg\"" - fi - i=$(expr $i + 1) - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi -# Escape application args -save() { - for i; do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"; done - echo " " -} -APP_ARGS=$(save "$@") +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/apps/mobile/android/settings.gradle b/apps/mobile/android/settings.gradle index 663fcfbe17b..8d322bc3be2 100644 --- a/apps/mobile/android/settings.gradle +++ b/apps/mobile/android/settings.gradle @@ -1,10 +1,35 @@ -pluginManagement { includeBuild("../../../node_modules/@react-native/gradle-plugin") } -plugins { id("com.facebook.react.settings") } -extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand(['bunx', 'rnef', 'config', '-p', 'android']) } +pluginManagement { + def reactNativeGradlePlugin = new File( + providers.exec { + workingDir(rootDir) + commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })") + }.standardOutput.asText.get().trim() + ).getParentFile().absolutePath + includeBuild(reactNativeGradlePlugin) + + def expoPluginsPath = new File( + providers.exec { + workingDir(rootDir) + commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })") + }.standardOutput.asText.get().trim(), + "../android/expo-gradle-plugin" + ).absolutePath + includeBuild(expoPluginsPath) +} + +plugins { + id("com.facebook.react.settings") + id("expo-autolinking-settings") +} + +extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> + ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand) +} + +expoAutolinking.useExpoModules() rootProject.name = 'Uniswap' -include ':app' -includeBuild('../../../node_modules/@react-native/gradle-plugin') -apply from: new File(["node", "--print", "require.resolve('../../../node_modules/expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle") -useExpoModules() +expoAutolinking.useExpoVersionCatalog() +includeBuild(expoAutolinking.reactNativeGradlePlugin) +include ':app' diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts new file mode 100644 index 00000000000..03aa4e6f462 --- /dev/null +++ b/apps/mobile/app.config.ts @@ -0,0 +1,18 @@ +import { ExpoConfig } from 'expo/config' + +const config: ExpoConfig = { + name: 'Uniswap', + slug: 'uniswapmobile', + scheme: 'uniswap', + owner: 'uniswap', + extra: { + eas: { + projectId: 'f1be3813-43d7-49ac-a792-7f42cf8500f5', + }, + }, + experiments: { + buildCacheProvider: 'eas', + }, +} + +export default config diff --git a/apps/mobile/app.json b/apps/mobile/app.json deleted file mode 100644 index b9b05c82db4..00000000000 --- a/apps/mobile/app.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "Uniswap", - "displayName": "Uniswap", - "appStoreUrl": "https://apps.apple.com/app/apple-store/id6443944476", - "playStoreUrl": "https://play.google.com/store/apps/details?id=com.uniswap.mobile", - "expo": { - "ios": { - "infoPlist": { - "CFBundleAllowMixedLocalizations": true - } - }, - "plugins": ["expo-localization", "expo-camera", "expo-local-authentication", "expo-secure-store"] - } -} diff --git a/apps/mobile/eas.json b/apps/mobile/eas.json new file mode 100644 index 00000000000..4068f674da3 --- /dev/null +++ b/apps/mobile/eas.json @@ -0,0 +1,90 @@ +{ + "cli": { + "version": ">= 15.0.15", + "appVersionSource": "remote" + }, + "build": { + "development": { + "bun": "1.3.11", + "node": "22.13.1", + "developmentClient": true, + "distribution": "internal", + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleDevDebug" + } + }, + "development-simulator": { + "bun": "1.3.11", + "node": "22.13.1", + "developmentClient": true, + "distribution": "internal", + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleDevDebug", + "withoutCredentials": true + }, + "ios": { + "simulator": true, + "withoutCredentials": true + } + }, + "e2e-simulator": { + "bun": "1.3.11", + "node": "22.13.1", + "developmentClient": false, + "distribution": "internal", + "env": { + "IS_E2E_TEST": "true" + }, + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleDevDebug", + "withoutCredentials": true + }, + "ios": { + "simulator": true, + "withoutCredentials": true + } + }, + "development-release": { + "bun": "1.3.11", + "node": "22.13.1", + "distribution": "internal", + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleDevRelease" + } + }, + "beta": { + "bun": "1.3.11", + "node": "22.13.1", + "distribution": "internal", + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleBetaDebug" + } + }, + "beta-release": { + "bun": "1.3.11", + "node": "22.13.1", + "distribution": "internal", + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleBetaRelease" + } + }, + "production": { + "bun": "1.3.11", + "node": "22.13.1", + "autoIncrement": true, + "android": { + "buildType": "apk", + "gradleCommand": ":app:assembleProdRelease" + } + } + }, + "submit": { + "production": {} + } +} diff --git a/apps/mobile/env.d.ts b/apps/mobile/env.d.ts new file mode 100644 index 00000000000..35eb9b9854e --- /dev/null +++ b/apps/mobile/env.d.ts @@ -0,0 +1,14 @@ +/* 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' + INCLUDE_PROTOTYPE_FEATURES?: string + IS_E2E_TEST?: string + } + } +} + +export {} diff --git a/apps/mobile/fingerprint.config.js b/apps/mobile/fingerprint.config.js new file mode 100644 index 00000000000..0f3617ae437 --- /dev/null +++ b/apps/mobile/fingerprint.config.js @@ -0,0 +1,8 @@ +/** @type {import('@expo/fingerprint').Config} */ +const config = { + sourceSkips: [ + 'PackageJsonScriptsAll', // Skip all package.json scripts + 'ExpoConfigVersions', // Skip version bumps if you want + ], +} +module.exports = config diff --git a/apps/mobile/global.d.ts b/apps/mobile/global.d.ts index b2cdce5dea8..f1bbb762661 100644 --- a/apps/mobile/global.d.ts +++ b/apps/mobile/global.d.ts @@ -1,9 +1,19 @@ -// biome-ignore-all lint/suspicious/noExplicitAny: required here +/* oxlint-disable typescript/no-explicit-any -- required here */ /** * The global chrome object is not available at runtime in mobile but is * required for TypeScript compilation due to its use in the utilities package */ declare let chrome: { + // oxlint-disable-next-line typescript/no-explicit-any -- biome-parity: oxlint is stricter here runtime: any + // oxlint-disable-next-line typescript/no-explicit-any -- biome-parity: oxlint is stricter here [key: string]: any } + +/** + * Module augmentation to @datadog deep import for tsgo compatibility + */ +declare module '@datadog/mobile-react-native/lib/typescript/rum/eventMappers/errorEventMapper' { + // oxlint-disable-next-line typescript/no-explicit-any -- biome-parity: oxlint is stricter here + export type ErrorEventMapper = (event: any) => any | null +} diff --git a/apps/mobile/hardhat.config.js b/apps/mobile/hardhat.config.js deleted file mode 100644 index 5fe6674925c..00000000000 --- a/apps/mobile/hardhat.config.js +++ /dev/null @@ -1,19 +0,0 @@ -const { ALCHEMY_API_KEY } = require('react-native-dotenv') - -const mainnetFork = { - url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_API_KEY}`, - blockNumber: 13582625, -} - -/** - * Hardhat config to fork mainnet at a specific block. - * @type import('hardhat/config').HardhatUserConfig - */ -module.exports = { - networks: { - hardhat: { - chainId: 1, - forking: mainnetFork, - }, - }, -} diff --git a/apps/mobile/index.js b/apps/mobile/index.js index 46c83394bd3..fbd0366b866 100644 --- a/apps/mobile/index.js +++ b/apps/mobile/index.js @@ -1,19 +1,16 @@ -// biome-ignore assist/source/organizeImports: we want to keep the import order import './wdyr' -// biome-ignore assist/source/organizeImports: we want to keep the import order import { isNonTestDev } from 'utilities/src/environment/constants' if (isNonTestDev) { require('./ReactotronConfig') } -import { AppRegistry } from 'react-native' import 'react-native-gesture-handler' import 'react-native-reanimated' import 'src/logbox' import 'src/polyfills' -// biome-ignore assist/source/organizeImports: we want to keep the import order +import { AppRegistry } from 'react-native' import App from 'src/app/App' -import { name as appName } from './app.json' +import AppConfig from './app.config' -AppRegistry.registerComponent(appName, () => App) +AppRegistry.registerComponent(AppConfig.name, () => App) diff --git a/apps/mobile/ios/Assets.xcassets/Contents.json b/apps/mobile/ios/Assets.xcassets/Contents.json index 73c00596a7f..74d6a722cf3 100644 --- a/apps/mobile/ios/Assets.xcassets/Contents.json +++ b/apps/mobile/ios/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Podfile b/apps/mobile/ios/Podfile index 7add18aba38..ffe34652706 100644 --- a/apps/mobile/ios/Podfile +++ b/apps/mobile/ios/Podfile @@ -16,6 +16,13 @@ setup_permissions([ $RNFirebaseAsStaticFramework = true $RNFirebaseAnalyticsWithoutAdIdSupport=true +project 'Uniswap', + 'Debug' => :debug, + 'DebugOptimized' => :debug, + 'Release' => :release, + 'Dev' => :release, + 'Beta' => :release + target 'Uniswap' do use_frameworks! :linkage => :static use_expo_modules! @@ -26,11 +33,27 @@ target 'Uniswap' do Pod::UI.warn e end end - use_expo_modules!(exclude: ['expo-constants','expo-file-system', 'expo-font', 'expo-keep-awake', 'expo-error-recovery']) - config = use_native_modules!(['bunx', 'rnef', 'config', '-p', 'ios']) + + if ENV['EXPO_USE_COMMUNITY_AUTOLINKING'] == '1' + config_command = ['node', '-e', "process.argv=['', '', 'config'];require('@react-native-community/cli').run()"]; + else + config_command = [ + 'node', + '--no-warnings', + '--eval', + 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))', + 'react-native-config', + '--json', + '--platform', + 'ios' + ] + end + + config = use_native_modules!(config_command) use_react_native!( :path => config[:reactNativePath], + :fabric_enabled => false, # to enable hermes on iOS, change `false` to `true` and then install pods :hermes_enabled => true ) @@ -50,6 +73,38 @@ target 'Uniswap' do target.build_configurations.each do |config| config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'No' + + # Optimize native code in DebugOptimized for faster Simulator runtime. + # Hermes ships pre-compiled and is unaffected. Source-compiled pods + # (Yoga, React-Core, RNScreens, RNReanimated, RNGestureHandler, etc.) + # run dramatically faster with optimization enabled. + # RN dev tools (Metro, React DevTools) work over the network and are unaffected. + if config.name == 'DebugOptimized' + config.build_settings['GCC_OPTIMIZATION_LEVEL'] = 's' + config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-O' + config.build_settings['MTL_ENABLE_DEBUG_INFO'] = 'NO' + end + end + end + + # Xcode 26+ has a stricter module verifier that requires the consuming target + # to resolve transitive C module dependencies. Argon2Swift depends on an internal + # 'argon2' C module — add its modulemap path so the Uniswap target (the only + # target declaring Argon2Swift) can find it. + argon2_srcroot = installer.sandbox.pod_dir('Argon2Swift') + argon2_module_paths = [ + "#{argon2_srcroot}/Sources/Modules", + "#{argon2_srcroot}/Sources/Argon2", + "#{argon2_srcroot}/Sources/Argon2/include", + ] + uniswap_aggregate = installer.aggregate_targets.find { |t| t.label == 'Pods-Uniswap' } + if uniswap_aggregate + uniswap_aggregate.xcconfigs.each do |config_name, xcconfig| + existing = xcconfig.attributes['SWIFT_INCLUDE_PATHS'] || '' + new_paths = argon2_module_paths.map { |p| "\"#{p}\"" }.join(' ') + xcconfig.attributes['SWIFT_INCLUDE_PATHS'] = "$(inherited) #{new_paths} #{existing}".strip + xcconfig_path = uniswap_aggregate.xcconfig_path(config_name) + xcconfig.save_as(xcconfig_path) end end end diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock index 41bed31112c..4078a244396 100644 --- a/apps/mobile/ios/Podfile.lock +++ b/apps/mobile/ios/Podfile.lock @@ -1161,60 +1161,333 @@ PODS: - BoringSSL-GRPC/Implementation (0.0.36): - BoringSSL-GRPC/Interface (= 0.0.36) - BoringSSL-GRPC/Interface (0.0.36) - - DatadogCore (2.27.0): - - DatadogInternal (= 2.27.0) - - DatadogCrashReporting (2.27.0): - - DatadogInternal (= 2.27.0) + - DatadogCore (2.30.0): + - DatadogInternal (= 2.30.0) + - DatadogCrashReporting (2.30.0): + - DatadogInternal (= 2.30.0) - PLCrashReporter (~> 1.12.0) - - DatadogInternal (2.27.0) - - DatadogLogs (2.27.0): - - DatadogInternal (= 2.27.0) - - DatadogRUM (2.27.0): - - DatadogInternal (= 2.27.0) - - DatadogSDKReactNative (2.8.2): - - DatadogCore (~> 2.27.0) - - DatadogCrashReporting (~> 2.27.0) - - DatadogLogs (~> 2.27.0) - - DatadogRUM (~> 2.27.0) - - DatadogTrace (~> 2.27.0) - - DatadogWebViewTracking (~> 2.27.0) - - React-Core - - DatadogTrace (2.27.0): - - DatadogInternal (= 2.27.0) + - DatadogInternal (2.30.0) + - DatadogLogs (2.30.0): + - DatadogInternal (= 2.30.0) + - DatadogRUM (2.30.0): + - DatadogInternal (= 2.30.0) + - DatadogSDKReactNative (2.12.2): + - DatadogCore (= 2.30.0) + - DatadogCrashReporting (= 2.30.0) + - DatadogLogs (= 2.30.0) + - DatadogRUM (= 2.30.0) + - DatadogTrace (= 2.30.0) + - DatadogWebViewTracking (= 2.30.0) + - React-Core + - DatadogTrace (2.30.0): + - DatadogInternal (= 2.30.0) - OpenTelemetrySwiftApi (= 1.13.1) - - DatadogWebViewTracking (2.27.0): - - DatadogInternal (= 2.27.0) + - DatadogWebViewTracking (2.30.0): + - DatadogInternal (= 2.30.0) - DoubleConversion (1.1.6) - EthersRS (0.0.5) - - Expo (52.0.46): + - EXConstants (17.1.8): + - ExpoModulesCore + - EXJSONUtils (0.15.0) + - EXManifests (0.16.6): + - ExpoModulesCore + - Expo (53.0.22): + - DoubleConversion + - ExpoModulesCore + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-client (5.2.4): + - EXManifests + - expo-dev-launcher + - expo-dev-menu + - expo-dev-menu-interface + - EXUpdatesInterface + - expo-dev-launcher (5.1.16): + - DoubleConversion + - EXManifests + - expo-dev-launcher/Main (= 5.1.16) + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-launcher/Main (5.1.16): + - DoubleConversion + - EXManifests + - expo-dev-launcher/Unsafe + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-launcher/Unsafe (5.1.16): + - DoubleConversion + - EXManifests + - expo-dev-menu + - expo-dev-menu-interface + - ExpoModulesCore + - EXUpdatesInterface + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTAppDelegate + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu (6.1.14): + - DoubleConversion + - expo-dev-menu/Main (= 6.1.14) + - expo-dev-menu/ReactNativeCompatibles (= 6.1.14) + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu-interface (1.10.0) + - expo-dev-menu/Main (6.1.14): + - DoubleConversion + - EXManifests + - expo-dev-menu-interface + - expo-dev-menu/Vendored - ExpoModulesCore - - ExpoAsset (11.0.5): + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-jsinspector + - React-NativeModulesApple + - React-RCTFabric + - React-rendererconsistency + - React-renderercss + - React-rendererdebug + - React-utils + - ReactAppDependencyProvider + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/ReactNativeCompatibles (6.1.14): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/SafeAreaView (6.1.14): + - DoubleConversion + - ExpoModulesCore + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - expo-dev-menu/Vendored (6.1.14): + - DoubleConversion + - expo-dev-menu/SafeAreaView + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - ExpoAsset (11.1.7): - ExpoModulesCore - - ExpoBlur (14.0.3): + - ExpoBlur (14.1.5): - ExpoModulesCore - - ExpoCamera (16.0.18): + - ExpoCamera (16.1.11): - ExpoModulesCore - ZXingObjC/OneD - ZXingObjC/PDF417 - - ExpoClipboard (7.0.1): + - ExpoClipboard (7.1.5): - ExpoModulesCore - - ExpoFileSystem (18.0.12): + - ExpoFileSystem (18.1.11): - ExpoModulesCore - - ExpoFont (13.0.4): + - ExpoFont (13.3.2): - ExpoModulesCore - ExpoHaptics (14.0.1): - ExpoModulesCore - - ExpoKeepAwake (14.0.3): + - ExpoImage (2.4.1): + - ExpoModulesCore + - libavif/libdav1d + - SDWebImage (~> 5.21.0) + - SDWebImageAVIFCoder (~> 0.11.0) + - SDWebImageSVGCoder (~> 1.7.0) + - SDWebImageWebPCoder (~> 0.14.6) + - ExpoKeepAwake (14.1.4): - ExpoModulesCore - - ExpoLinearGradient (14.0.2): + - ExpoLinearGradient (14.1.5): - ExpoModulesCore - - ExpoLinking (7.0.5): + - ExpoLinking (7.1.7): - ExpoModulesCore - - ExpoLocalAuthentication (15.0.2): + - ExpoLocalAuthentication (16.0.5): - ExpoModulesCore - - ExpoLocalization (16.0.1): + - ExpoLocalization (16.1.6): - ExpoModulesCore - - ExpoModulesCore (2.2.3): + - ExpoModulesCore (2.5.0): - DoubleConversion - glog - hermes-engine @@ -1226,28 +1499,31 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-jsinspector - React-NativeModulesApple - - React-RCTAppDelegate - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - - ReactAppDependencyProvider - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - ExpoScreenCapture (7.0.1): + - ExpoScreenCapture (7.2.0): - ExpoModulesCore - ExpoSecureStore (14.0.1): - ExpoModulesCore - - ExpoStoreReview (8.0.1): + - ExpoStoreReview (8.1.5): + - ExpoModulesCore + - ExpoWebBrowser (14.2.0): - ExpoModulesCore - - ExpoWebBrowser (14.0.2): + - EXUpdatesInterface (1.1.0): - ExpoModulesCore - fast_float (6.1.4) - - FBLazyVector (0.77.2) + - FBLazyVector (0.79.5) - Firebase/Auth (11.2.0): - Firebase/CoreOnly - FirebaseAuth (~> 11.2.0) @@ -1256,7 +1532,7 @@ PODS: - Firebase/Firestore (11.2.0): - Firebase/CoreOnly - FirebaseFirestore (~> 11.2.0) - - FirebaseAppCheckInterop (11.14.0) + - FirebaseAppCheckInterop (11.15.0) - FirebaseAuth (11.2.0): - FirebaseAppCheckInterop (~> 11.0) - FirebaseAuthInterop (~> 11.0) @@ -1266,14 +1542,14 @@ PODS: - GoogleUtilities/Environment (~> 8.0) - GTMSessionFetcher/Core (~> 3.4) - RecaptchaInterop (~> 100.0) - - FirebaseAuthInterop (11.14.0) + - FirebaseAuthInterop (11.15.0) - FirebaseCore (11.2.0): - FirebaseCoreInternal (~> 11.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Logger (~> 8.0) - FirebaseCoreExtension (11.4.1): - FirebaseCore (~> 11.0) - - FirebaseCoreInternal (11.14.0): + - FirebaseCoreInternal (11.15.0): - "GoogleUtilities/NSData+zlib (~> 8.1)" - FirebaseFirestore (11.2.0): - FirebaseCore (~> 11.0) @@ -1295,7 +1571,7 @@ PODS: - gRPC-Core (~> 1.65.0) - leveldb-library (~> 1.22) - nanopb (~> 3.30910.0) - - FirebaseSharedSwift (11.14.0) + - FirebaseSharedSwift (11.15.0) - fmt (11.0.2) - glog (0.3.5) - GoogleUtilities/AppDelegateSwizzler (8.1.0): @@ -1410,10 +1686,15 @@ PODS: - gRPC-Core/Interface (1.65.5) - gRPC-Core/Privacy (1.65.5) - GTMSessionFetcher/Core (3.5.0) - - hermes-engine (0.77.2): - - hermes-engine/Pre-built (= 0.77.2) - - hermes-engine/Pre-built (0.77.2) + - hermes-engine (0.79.5): + - hermes-engine/Pre-built (= 0.79.5) + - hermes-engine/Pre-built (0.79.5) - leveldb-library (1.22.6) + - libavif/core (1.0.0) + - libavif/libdav1d (1.0.0): + - libavif/core + - libdav1d (>= 0.6.0) + - libdav1d (1.2.0) - libwebp (1.5.0): - libwebp/demux (= 1.5.0) - libwebp/mux (= 1.5.0) @@ -1426,14 +1707,65 @@ PODS: - libwebp/sharpyuv (1.5.0) - libwebp/webp (1.5.0): - libwebp/sharpyuv - - MMKV (2.2.2): - - MMKVCore (~> 2.2.2) - - MMKVCore (2.2.2) + - MMKV (2.3.0): + - MMKVCore (~> 2.3.0) + - MMKVCore (2.3.0) - nanopb (3.30910.0): - nanopb/decode (= 3.30910.0) - nanopb/encode (= 3.30910.0) - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) + - NitroHashcashNative (0.0.1): + - DoubleConversion + - glog + - hermes-engine + - NitroModules + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - NitroModules (0.31.10): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - OneSignalXCFramework (5.2.10): - OneSignalXCFramework/OneSignalComplete (= 5.2.10) - OneSignalXCFramework/OneSignal (5.2.10): @@ -1501,44 +1833,45 @@ PODS: - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - - RCTDeprecation (0.77.2) - - RCTRequired (0.77.2) - - RCTTypeSafety (0.77.2): - - FBLazyVector (= 0.77.2) - - RCTRequired (= 0.77.2) - - React-Core (= 0.77.2) - - React (0.77.2): - - React-Core (= 0.77.2) - - React-Core/DevSupport (= 0.77.2) - - React-Core/RCTWebSocket (= 0.77.2) - - React-RCTActionSheet (= 0.77.2) - - React-RCTAnimation (= 0.77.2) - - React-RCTBlob (= 0.77.2) - - React-RCTImage (= 0.77.2) - - React-RCTLinking (= 0.77.2) - - React-RCTNetwork (= 0.77.2) - - React-RCTSettings (= 0.77.2) - - React-RCTText (= 0.77.2) - - React-RCTVibration (= 0.77.2) - - React-callinvoker (0.77.2) - - React-Core (0.77.2): + - RCTDeprecation (0.79.5) + - RCTRequired (0.79.5) + - RCTTypeSafety (0.79.5): + - FBLazyVector (= 0.79.5) + - RCTRequired (= 0.79.5) + - React-Core (= 0.79.5) + - React (0.79.5): + - React-Core (= 0.79.5) + - React-Core/DevSupport (= 0.79.5) + - React-Core/RCTWebSocket (= 0.79.5) + - React-RCTActionSheet (= 0.79.5) + - React-RCTAnimation (= 0.79.5) + - React-RCTBlob (= 0.79.5) + - React-RCTImage (= 0.79.5) + - React-RCTLinking (= 0.79.5) + - React-RCTNetwork (= 0.79.5) + - React-RCTSettings (= 0.79.5) + - React-RCTText (= 0.79.5) + - React-RCTVibration (= 0.79.5) + - React-callinvoker (0.79.5) + - React-Core (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - RCTDeprecation - - React-Core/Default (= 0.77.2) + - React-Core/Default (= 0.79.5) - React-cxxreact - React-featureflags - React-hermes - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/CoreModulesHeaders (0.77.2): + - React-Core/CoreModulesHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1550,12 +1883,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/Default (0.77.2): + - React-Core/Default (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1566,30 +1900,32 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/DevSupport (0.77.2): + - React-Core/DevSupport (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - RCTDeprecation - - React-Core/Default (= 0.77.2) - - React-Core/RCTWebSocket (= 0.77.2) + - React-Core/Default (= 0.79.5) + - React-Core/RCTWebSocket (= 0.79.5) - React-cxxreact - React-featureflags - React-hermes - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTActionSheetHeaders (0.77.2): + - React-Core/RCTActionSheetHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1601,12 +1937,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTAnimationHeaders (0.77.2): + - React-Core/RCTAnimationHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1618,12 +1955,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTBlobHeaders (0.77.2): + - React-Core/RCTBlobHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1635,12 +1973,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTImageHeaders (0.77.2): + - React-Core/RCTImageHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1652,12 +1991,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTLinkingHeaders (0.77.2): + - React-Core/RCTLinkingHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1669,12 +2009,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTNetworkHeaders (0.77.2): + - React-Core/RCTNetworkHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1686,12 +2027,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTSettingsHeaders (0.77.2): + - React-Core/RCTSettingsHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1703,12 +2045,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTTextHeaders (0.77.2): + - React-Core/RCTTextHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1720,12 +2063,13 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTVibrationHeaders (0.77.2): + - React-Core/RCTVibrationHeaders (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -1737,44 +2081,47 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-Core/RCTWebSocket (0.77.2): + - React-Core/RCTWebSocket (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - RCTDeprecation - - React-Core/Default (= 0.77.2) + - React-Core/Default (= 0.79.5) - React-cxxreact - React-featureflags - React-hermes - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-perflogger - React-runtimescheduler - React-utils - SocketRocket (= 0.7.1) - Yoga - - React-CoreModules (0.77.2): + - React-CoreModules (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - RCT-Folly (= 2024.11.18.00) - - RCTTypeSafety (= 0.77.2) - - React-Core/CoreModulesHeaders (= 0.77.2) - - React-jsi (= 0.77.2) + - RCTTypeSafety (= 0.79.5) + - React-Core/CoreModulesHeaders (= 0.79.5) + - React-jsi (= 0.79.5) - React-jsinspector + - React-jsinspectortracing - React-NativeModulesApple - React-RCTBlob - React-RCTFBReactNativeSpec - - React-RCTImage (= 0.77.2) + - React-RCTImage (= 0.79.5) - ReactCommon - SocketRocket (= 0.7.1) - - React-cxxreact (0.77.2): + - React-cxxreact (0.79.5): - boost - DoubleConversion - fast_float (= 6.1.4) @@ -1782,37 +2129,40 @@ PODS: - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-callinvoker (= 0.77.2) - - React-debug (= 0.77.2) - - React-jsi (= 0.77.2) + - React-callinvoker (= 0.79.5) + - React-debug (= 0.79.5) + - React-jsi (= 0.79.5) - React-jsinspector - - React-logger (= 0.77.2) - - React-perflogger (= 0.77.2) - - React-runtimeexecutor (= 0.77.2) - - React-timing (= 0.77.2) - - React-debug (0.77.2) - - React-defaultsnativemodule (0.77.2): + - React-jsinspectortracing + - React-logger (= 0.79.5) + - React-perflogger (= 0.79.5) + - React-runtimeexecutor (= 0.79.5) + - React-timing (= 0.79.5) + - React-debug (0.79.5) + - React-defaultsnativemodule (0.79.5): - hermes-engine - RCT-Folly - React-domnativemodule - React-featureflagsnativemodule + - React-hermes - React-idlecallbacksnativemodule - React-jsi - React-jsiexecutor - React-microtasksnativemodule - React-RCTFBReactNativeSpec - - React-domnativemodule (0.77.2): + - React-domnativemodule (0.79.5): - hermes-engine - RCT-Folly - React-Fabric - React-FabricComponents - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - Yoga - - React-Fabric (0.77.2): + - React-Fabric (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1824,23 +2174,25 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/animations (= 0.77.2) - - React-Fabric/attributedstring (= 0.77.2) - - React-Fabric/componentregistry (= 0.77.2) - - React-Fabric/componentregistrynative (= 0.77.2) - - React-Fabric/components (= 0.77.2) - - React-Fabric/core (= 0.77.2) - - React-Fabric/dom (= 0.77.2) - - React-Fabric/imagemanager (= 0.77.2) - - React-Fabric/leakchecker (= 0.77.2) - - React-Fabric/mounting (= 0.77.2) - - React-Fabric/observers (= 0.77.2) - - React-Fabric/scheduler (= 0.77.2) - - React-Fabric/telemetry (= 0.77.2) - - React-Fabric/templateprocessor (= 0.77.2) - - React-Fabric/uimanager (= 0.77.2) + - React-Fabric/animations (= 0.79.5) + - React-Fabric/attributedstring (= 0.79.5) + - React-Fabric/componentregistry (= 0.79.5) + - React-Fabric/componentregistrynative (= 0.79.5) + - React-Fabric/components (= 0.79.5) + - React-Fabric/consistency (= 0.79.5) + - React-Fabric/core (= 0.79.5) + - React-Fabric/dom (= 0.79.5) + - React-Fabric/imagemanager (= 0.79.5) + - React-Fabric/leakchecker (= 0.79.5) + - React-Fabric/mounting (= 0.79.5) + - React-Fabric/observers (= 0.79.5) + - React-Fabric/scheduler (= 0.79.5) + - React-Fabric/telemetry (= 0.79.5) + - React-Fabric/templateprocessor (= 0.79.5) + - React-Fabric/uimanager (= 0.79.5) - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1848,7 +2200,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/animations (0.77.2): + - React-Fabric/animations (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1862,6 +2214,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1869,7 +2222,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/attributedstring (0.77.2): + - React-Fabric/attributedstring (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1883,6 +2236,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1890,7 +2244,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/componentregistry (0.77.2): + - React-Fabric/componentregistry (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1904,6 +2258,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1911,7 +2266,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/componentregistrynative (0.77.2): + - React-Fabric/componentregistrynative (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1925,6 +2280,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1932,7 +2288,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components (0.77.2): + - React-Fabric/components (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1944,11 +2300,13 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/components/legacyviewmanagerinterop (= 0.77.2) - - React-Fabric/components/root (= 0.77.2) - - React-Fabric/components/view (= 0.77.2) + - React-Fabric/components/legacyviewmanagerinterop (= 0.79.5) + - React-Fabric/components/root (= 0.79.5) + - React-Fabric/components/scrollview (= 0.79.5) + - React-Fabric/components/view (= 0.79.5) - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1956,7 +2314,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/legacyviewmanagerinterop (0.77.2): + - React-Fabric/components/legacyviewmanagerinterop (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1970,6 +2328,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1977,7 +2336,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/root (0.77.2): + - React-Fabric/components/root (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -1991,6 +2350,29 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/components/scrollview (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -1998,7 +2380,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/components/view (0.77.2): + - React-Fabric/components/view (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2012,15 +2394,39 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger + - React-renderercss - React-rendererdebug - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - Yoga - - React-Fabric/core (0.77.2): + - React-Fabric/consistency (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-cxxreact + - React-debug + - React-featureflags + - React-graphics + - React-hermes + - React-jsi + - React-jsiexecutor + - React-logger + - React-rendererdebug + - React-runtimescheduler + - React-utils + - ReactCommon/turbomodule/core + - React-Fabric/core (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2034,6 +2440,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2041,7 +2448,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/dom (0.77.2): + - React-Fabric/dom (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2055,6 +2462,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2062,7 +2470,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/imagemanager (0.77.2): + - React-Fabric/imagemanager (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2076,6 +2484,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2083,7 +2492,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/leakchecker (0.77.2): + - React-Fabric/leakchecker (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2097,6 +2506,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2104,7 +2514,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/mounting (0.77.2): + - React-Fabric/mounting (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2118,6 +2528,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2125,7 +2536,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/observers (0.77.2): + - React-Fabric/observers (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2137,9 +2548,10 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/observers/events (= 0.77.2) + - React-Fabric/observers/events (= 0.79.5) - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2147,7 +2559,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/observers/events (0.77.2): + - React-Fabric/observers/events (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2161,6 +2573,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2168,7 +2581,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/scheduler (0.77.2): + - React-Fabric/scheduler (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2183,6 +2596,7 @@ PODS: - React-Fabric/observers/events - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2191,7 +2605,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/telemetry (0.77.2): + - React-Fabric/telemetry (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2205,6 +2619,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2212,7 +2627,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/templateprocessor (0.77.2): + - React-Fabric/templateprocessor (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2226,6 +2641,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2233,7 +2649,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/uimanager (0.77.2): + - React-Fabric/uimanager (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2245,9 +2661,10 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/uimanager/consistency (= 0.77.2) + - React-Fabric/uimanager/consistency (= 0.79.5) - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2256,7 +2673,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-Fabric/uimanager/consistency (0.77.2): + - React-Fabric/uimanager/consistency (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2270,6 +2687,7 @@ PODS: - React-debug - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2278,7 +2696,7 @@ PODS: - React-runtimescheduler - React-utils - ReactCommon/turbomodule/core - - React-FabricComponents (0.77.2): + - React-FabricComponents (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2291,10 +2709,11 @@ PODS: - React-cxxreact - React-debug - React-Fabric - - React-FabricComponents/components (= 0.77.2) - - React-FabricComponents/textlayoutmanager (= 0.77.2) + - React-FabricComponents/components (= 0.79.5) + - React-FabricComponents/textlayoutmanager (= 0.79.5) - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2303,7 +2722,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components (0.77.2): + - React-FabricComponents/components (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2316,17 +2735,18 @@ PODS: - React-cxxreact - React-debug - React-Fabric - - React-FabricComponents/components/inputaccessory (= 0.77.2) - - React-FabricComponents/components/iostextinput (= 0.77.2) - - React-FabricComponents/components/modal (= 0.77.2) - - React-FabricComponents/components/rncore (= 0.77.2) - - React-FabricComponents/components/safeareaview (= 0.77.2) - - React-FabricComponents/components/scrollview (= 0.77.2) - - React-FabricComponents/components/text (= 0.77.2) - - React-FabricComponents/components/textinput (= 0.77.2) - - React-FabricComponents/components/unimplementedview (= 0.77.2) + - React-FabricComponents/components/inputaccessory (= 0.79.5) + - React-FabricComponents/components/iostextinput (= 0.79.5) + - React-FabricComponents/components/modal (= 0.79.5) + - React-FabricComponents/components/rncore (= 0.79.5) + - React-FabricComponents/components/safeareaview (= 0.79.5) + - React-FabricComponents/components/scrollview (= 0.79.5) + - React-FabricComponents/components/text (= 0.79.5) + - React-FabricComponents/components/textinput (= 0.79.5) + - React-FabricComponents/components/unimplementedview (= 0.79.5) - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2335,7 +2755,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/inputaccessory (0.77.2): + - React-FabricComponents/components/inputaccessory (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2350,6 +2770,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2358,7 +2779,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/iostextinput (0.77.2): + - React-FabricComponents/components/iostextinput (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2373,6 +2794,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2381,7 +2803,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/modal (0.77.2): + - React-FabricComponents/components/modal (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2396,6 +2818,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2404,7 +2827,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/rncore (0.77.2): + - React-FabricComponents/components/rncore (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2419,6 +2842,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2427,7 +2851,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/safeareaview (0.77.2): + - React-FabricComponents/components/safeareaview (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2442,6 +2866,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2450,7 +2875,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/scrollview (0.77.2): + - React-FabricComponents/components/scrollview (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2465,6 +2890,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2473,7 +2899,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/text (0.77.2): + - React-FabricComponents/components/text (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2488,6 +2914,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2496,7 +2923,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/textinput (0.77.2): + - React-FabricComponents/components/textinput (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2511,6 +2938,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2519,7 +2947,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/components/unimplementedview (0.77.2): + - React-FabricComponents/components/unimplementedview (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2534,6 +2962,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2542,7 +2971,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricComponents/textlayoutmanager (0.77.2): + - React-FabricComponents/textlayoutmanager (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2557,6 +2986,7 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-logger @@ -2565,66 +2995,74 @@ PODS: - React-utils - ReactCommon/turbomodule/core - Yoga - - React-FabricImage (0.77.2): + - React-FabricImage (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) - - RCTRequired (= 0.77.2) - - RCTTypeSafety (= 0.77.2) + - RCTRequired (= 0.79.5) + - RCTTypeSafety (= 0.79.5) - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager - React-jsi - - React-jsiexecutor (= 0.77.2) + - React-jsiexecutor (= 0.79.5) - React-logger - React-rendererdebug - React-utils - ReactCommon - Yoga - - React-featureflags (0.77.2) - - React-featureflagsnativemodule (0.77.2): + - React-featureflags (0.79.5): + - RCT-Folly (= 2024.11.18.00) + - React-featureflagsnativemodule (0.79.5): - hermes-engine - RCT-Folly - React-featureflags + - React-hermes - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - - React-graphics (0.77.2): + - React-graphics (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog + - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) + - React-hermes - React-jsi - React-jsiexecutor - React-utils - - React-hermes (0.77.2): + - React-hermes (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-cxxreact (= 0.77.2) + - React-cxxreact (= 0.79.5) - React-jsi - - React-jsiexecutor (= 0.77.2) + - React-jsiexecutor (= 0.79.5) - React-jsinspector - - React-perflogger (= 0.77.2) + - React-jsinspectortracing + - React-perflogger (= 0.79.5) - React-runtimeexecutor - - React-idlecallbacksnativemodule (0.77.2): + - React-idlecallbacksnativemodule (0.79.5): + - glog - hermes-engine - RCT-Folly + - React-hermes - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec - React-runtimescheduler - ReactCommon/turbomodule/core - - React-ImageManager (0.77.2): + - React-ImageManager (0.79.5): - glog - RCT-Folly/Fabric - React-Core/Default @@ -2633,7 +3071,7 @@ PODS: - React-graphics - React-rendererdebug - React-utils - - React-jserrorhandler (0.77.2): + - React-jserrorhandler (0.79.5): - glog - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) @@ -2642,7 +3080,7 @@ PODS: - React-featureflags - React-jsi - ReactCommon/turbomodule/bridging - - React-jsi (0.77.2): + - React-jsi (0.79.5): - boost - DoubleConversion - fast_float (= 6.1.4) @@ -2650,36 +3088,52 @@ PODS: - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-jsiexecutor (0.77.2): + - React-jsiexecutor (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-cxxreact (= 0.77.2) - - React-jsi (= 0.77.2) + - React-cxxreact (= 0.79.5) + - React-jsi (= 0.79.5) - React-jsinspector - - React-perflogger (= 0.77.2) - - React-jsinspector (0.77.2): + - React-jsinspectortracing + - React-perflogger (= 0.79.5) + - React-jsinspector (0.79.5): - DoubleConversion - glog - hermes-engine - - RCT-Folly (= 2024.11.18.00) + - RCT-Folly - React-featureflags - React-jsi - - React-perflogger (= 0.77.2) - - React-runtimeexecutor (= 0.77.2) - - React-jsitracing (0.77.2): + - React-jsinspectortracing + - React-perflogger (= 0.79.5) + - React-runtimeexecutor (= 0.79.5) + - React-jsinspectortracing (0.79.5): + - RCT-Folly + - React-oscompat + - React-jsitooling (0.79.5): + - DoubleConversion + - fast_float (= 6.1.4) + - fmt (= 11.0.2) + - glog + - RCT-Folly (= 2024.11.18.00) + - React-cxxreact (= 0.79.5) + - React-jsi (= 0.79.5) + - React-jsinspector + - React-jsinspectortracing + - React-jsitracing (0.79.5): - React-jsi - - React-logger (0.77.2): + - React-logger (0.79.5): - glog - - React-Mapbuffer (0.77.2): + - React-Mapbuffer (0.79.5): - glog - React-debug - - React-microtasksnativemodule (0.77.2): + - React-microtasksnativemodule (0.79.5): - hermes-engine - RCT-Folly + - React-hermes - React-jsi - React-jsiexecutor - React-RCTFBReactNativeSpec @@ -2687,7 +3141,7 @@ PODS: - react-native-appsflyer (6.13.1): - AppsFlyerFramework (= 6.13.1) - React - - react-native-compat (2.21.4): + - react-native-compat (2.23.0): - DoubleConversion - glog - hermes-engine @@ -2699,9 +3153,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2724,9 +3181,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2745,24 +3205,27 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-mmkv (2.10.1): - - MMKV (>= 1.2.13) + - react-native-mmkv (2.12.0): + - MMKV (>= 1.3.3) - React-Core - react-native-netinfo (11.4.1): - React-Core - react-native-onesignal (5.2.9): - OneSignalXCFramework (= 5.2.10) - React (< 1.0.0, >= 0.13.0) - - react-native-pager-view (6.5.1): + - react-native-pager-view (6.7.1): - DoubleConversion - glog - hermes-engine @@ -2774,9 +3237,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2787,9 +3253,9 @@ PODS: - React-Core - react-native-restart (0.0.27): - React-Core - - react-native-safe-area-context (5.1.0): + - react-native-safe-area-context (5.4.0): - React-Core - - react-native-skia (1.12.4): + - react-native-skia (2.2.20): - DoubleConversion - glog - hermes-engine @@ -2803,16 +3269,19 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-slider (4.5.5): + - react-native-slider (4.5.6): - DoubleConversion - glog - hermes-engine @@ -2824,9 +3293,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2845,10 +3317,13 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - react-native-video/Video (= 6.13.0) - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2867,9 +3342,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2888,9 +3366,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -2899,29 +3380,33 @@ PODS: - Yoga - react-native-widgetkit (1.0.9): - React - - React-nativeconfig (0.77.2) - - React-NativeModulesApple (0.77.2): + - React-NativeModulesApple (0.79.5): - glog - hermes-engine - React-callinvoker - React-Core - React-cxxreact + - React-featureflags + - React-hermes - React-jsi - React-jsinspector - React-runtimeexecutor - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - React-perflogger (0.77.2): + - React-oscompat (0.79.5) + - React-perflogger (0.79.5): - DoubleConversion - RCT-Folly (= 2024.11.18.00) - - React-performancetimeline (0.77.2): + - React-performancetimeline (0.79.5): - RCT-Folly (= 2024.11.18.00) - React-cxxreact - React-featureflags + - React-jsinspectortracing + - React-perflogger - React-timing - - React-RCTActionSheet (0.77.2): - - React-Core/RCTActionSheetHeaders (= 0.77.2) - - React-RCTAnimation (0.77.2): + - React-RCTActionSheet (0.79.5): + - React-Core/RCTActionSheetHeaders (= 0.79.5) + - React-RCTAnimation (0.79.5): - RCT-Folly (= 2024.11.18.00) - RCTTypeSafety - React-Core/RCTAnimationHeaders @@ -2929,7 +3414,8 @@ PODS: - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - React-RCTAppDelegate (0.77.2): + - React-RCTAppDelegate (0.79.5): + - hermes-engine - RCT-Folly (= 2024.11.18.00) - RCTRequired - RCTTypeSafety @@ -2941,20 +3427,20 @@ PODS: - React-featureflags - React-graphics - React-hermes - - React-nativeconfig + - React-jsitooling - React-NativeModulesApple - React-RCTFabric - React-RCTFBReactNativeSpec - React-RCTImage - React-RCTNetwork + - React-RCTRuntime - React-rendererdebug - React-RuntimeApple - React-RuntimeCore - - React-RuntimeHermes - React-runtimescheduler - React-utils - ReactCommon - - React-RCTBlob (0.77.2): + - React-RCTBlob (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) @@ -2968,7 +3454,7 @@ PODS: - React-RCTFBReactNativeSpec - React-RCTNetwork - ReactCommon - - React-RCTFabric (0.77.2): + - React-RCTFabric (0.79.5): - glog - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) @@ -2979,29 +3465,33 @@ PODS: - React-FabricImage - React-featureflags - React-graphics + - React-hermes - React-ImageManager - React-jsi - React-jsinspector - - React-nativeconfig + - React-jsinspectortracing - React-performancetimeline + - React-RCTAnimation - React-RCTImage - React-RCTText - React-rendererconsistency + - React-renderercss - React-rendererdebug - React-runtimescheduler - React-utils - Yoga - - React-RCTFBReactNativeSpec (0.77.2): + - React-RCTFBReactNativeSpec (0.79.5): - hermes-engine - RCT-Folly - RCTRequired - RCTTypeSafety - React-Core + - React-hermes - React-jsi - React-jsiexecutor - React-NativeModulesApple - ReactCommon - - React-RCTImage (0.77.2): + - React-RCTImage (0.79.5): - RCT-Folly (= 2024.11.18.00) - RCTTypeSafety - React-Core/RCTImageHeaders @@ -3010,14 +3500,14 @@ PODS: - React-RCTFBReactNativeSpec - React-RCTNetwork - ReactCommon - - React-RCTLinking (0.77.2): - - React-Core/RCTLinkingHeaders (= 0.77.2) - - React-jsi (= 0.77.2) + - React-RCTLinking (0.79.5): + - React-Core/RCTLinkingHeaders (= 0.79.5) + - React-jsi (= 0.79.5) - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - ReactCommon/turbomodule/core (= 0.77.2) - - React-RCTNetwork (0.77.2): + - ReactCommon/turbomodule/core (= 0.79.5) + - React-RCTNetwork (0.79.5): - RCT-Folly (= 2024.11.18.00) - RCTTypeSafety - React-Core/RCTNetworkHeaders @@ -3025,7 +3515,20 @@ PODS: - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - React-RCTSettings (0.77.2): + - React-RCTRuntime (0.79.5): + - glog + - hermes-engine + - RCT-Folly/Fabric (= 2024.11.18.00) + - React-Core + - React-hermes + - React-jsi + - React-jsinspector + - React-jsinspectortracing + - React-jsitooling + - React-RuntimeApple + - React-RuntimeCore + - React-RuntimeHermes + - React-RCTSettings (0.79.5): - RCT-Folly (= 2024.11.18.00) - RCTTypeSafety - React-Core/RCTSettingsHeaders @@ -3033,25 +3536,28 @@ PODS: - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - React-RCTText (0.77.2): - - React-Core/RCTTextHeaders (= 0.77.2) + - React-RCTText (0.79.5): + - React-Core/RCTTextHeaders (= 0.79.5) - Yoga - - React-RCTVibration (0.77.2): + - React-RCTVibration (0.79.5): - RCT-Folly (= 2024.11.18.00) - React-Core/RCTVibrationHeaders - React-jsi - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - React-rendererconsistency (0.77.2) - - React-rendererdebug (0.77.2): + - React-rendererconsistency (0.79.5) + - React-renderercss (0.79.5): + - React-debug + - React-utils + - React-rendererdebug (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - RCT-Folly (= 2024.11.18.00) - React-debug - - React-rncore (0.77.2) - - React-RuntimeApple (0.77.2): + - React-rncore (0.79.5) + - React-RuntimeApple (0.79.5): - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) - React-callinvoker @@ -3063,6 +3569,7 @@ PODS: - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-Mapbuffer - React-NativeModulesApple - React-RCTFabric @@ -3072,35 +3579,38 @@ PODS: - React-RuntimeHermes - React-runtimescheduler - React-utils - - React-RuntimeCore (0.77.2): + - React-RuntimeCore (0.79.5): - glog - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) - React-cxxreact - React-Fabric - React-featureflags + - React-hermes - React-jserrorhandler - React-jsi - React-jsiexecutor - React-jsinspector + - React-jsitooling - React-performancetimeline - React-runtimeexecutor - React-runtimescheduler - React-utils - - React-runtimeexecutor (0.77.2): - - React-jsi (= 0.77.2) - - React-RuntimeHermes (0.77.2): + - React-runtimeexecutor (0.79.5): + - React-jsi (= 0.79.5) + - React-RuntimeHermes (0.79.5): - hermes-engine - RCT-Folly/Fabric (= 2024.11.18.00) - React-featureflags - React-hermes - React-jsi - React-jsinspector + - React-jsinspectortracing + - React-jsitooling - React-jsitracing - - React-nativeconfig - React-RuntimeCore - React-utils - - React-runtimescheduler (0.77.2): + - React-runtimescheduler (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) @@ -3108,23 +3618,26 @@ PODS: - React-cxxreact - React-debug - React-featureflags + - React-hermes - React-jsi + - React-jsinspectortracing - React-performancetimeline - React-rendererconsistency - React-rendererdebug - React-runtimeexecutor - React-timing - React-utils - - React-timing (0.77.2) - - React-utils (0.77.2): + - React-timing (0.79.5) + - React-utils (0.79.5): - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - React-debug - - React-jsi (= 0.77.2) - - ReactAppDependencyProvider (0.77.2): + - React-hermes + - React-jsi (= 0.79.5) + - ReactAppDependencyProvider (0.79.5): - ReactCodegen - - ReactCodegen (0.77.2): + - ReactCodegen (0.79.5): - DoubleConversion - glog - hermes-engine @@ -3137,6 +3650,7 @@ PODS: - React-FabricImage - React-featureflags - React-graphics + - React-hermes - React-jsi - React-jsiexecutor - React-NativeModulesApple @@ -3145,55 +3659,55 @@ PODS: - React-utils - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - ReactCommon (0.77.2): - - ReactCommon/turbomodule (= 0.77.2) - - ReactCommon/turbomodule (0.77.2): + - ReactCommon (0.79.5): + - ReactCommon/turbomodule (= 0.79.5) + - ReactCommon/turbomodule (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-callinvoker (= 0.77.2) - - React-cxxreact (= 0.77.2) - - React-jsi (= 0.77.2) - - React-logger (= 0.77.2) - - React-perflogger (= 0.77.2) - - ReactCommon/turbomodule/bridging (= 0.77.2) - - ReactCommon/turbomodule/core (= 0.77.2) - - ReactCommon/turbomodule/bridging (0.77.2): + - React-callinvoker (= 0.79.5) + - React-cxxreact (= 0.79.5) + - React-jsi (= 0.79.5) + - React-logger (= 0.79.5) + - React-perflogger (= 0.79.5) + - ReactCommon/turbomodule/bridging (= 0.79.5) + - ReactCommon/turbomodule/core (= 0.79.5) + - ReactCommon/turbomodule/bridging (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-callinvoker (= 0.77.2) - - React-cxxreact (= 0.77.2) - - React-jsi (= 0.77.2) - - React-logger (= 0.77.2) - - React-perflogger (= 0.77.2) - - ReactCommon/turbomodule/core (0.77.2): + - React-callinvoker (= 0.79.5) + - React-cxxreact (= 0.79.5) + - React-jsi (= 0.79.5) + - React-logger (= 0.79.5) + - React-perflogger (= 0.79.5) + - ReactCommon/turbomodule/core (0.79.5): - DoubleConversion - fast_float (= 6.1.4) - fmt (= 11.0.2) - glog - hermes-engine - RCT-Folly (= 2024.11.18.00) - - React-callinvoker (= 0.77.2) - - React-cxxreact (= 0.77.2) - - React-debug (= 0.77.2) - - React-featureflags (= 0.77.2) - - React-jsi (= 0.77.2) - - React-logger (= 0.77.2) - - React-perflogger (= 0.77.2) - - React-utils (= 0.77.2) + - React-callinvoker (= 0.79.5) + - React-cxxreact (= 0.79.5) + - React-debug (= 0.79.5) + - React-featureflags (= 0.79.5) + - React-jsi (= 0.79.5) + - React-logger (= 0.79.5) + - React-perflogger (= 0.79.5) + - React-utils (= 0.79.5) - ReactNativePerformance (4.1.2): - React-Core - RecaptchaInterop (100.0.0) - - RNBootSplash (6.3.1): + - RNBootSplash (6.3.10): - React-Core - - RNCAsyncStorage (1.23.1): + - RNCAsyncStorage (2.1.2): - React-Core - RNCMaskedView (0.3.2): - DoubleConversion @@ -3207,23 +3721,22 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNDateTimePicker (8.2.0): + - RNDateTimePicker (8.4.1): - React-Core - RNDeviceInfo (10.11.0): - React-Core - - RNFastImage (8.6.3): - - React-Core - - SDWebImage (~> 5.21.0) - - SDWebImageWebPCoder (~> 0.14.6) - RNFBApp (21.0.0): - Firebase/CoreOnly (= 11.2.0) - React-Core @@ -3235,7 +3748,7 @@ PODS: - Firebase/Firestore (= 11.2.0) - React-Core - RNFBApp - - RNFlashList (1.7.3): + - RNFlashList (1.7.6): - DoubleConversion - glog - hermes-engine @@ -3247,16 +3760,19 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNGestureHandler (2.22.1): + - RNGestureHandler (2.24.0): - DoubleConversion - glog - hermes-engine @@ -3268,9 +3784,12 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen @@ -3286,7 +3805,33 @@ PODS: - RNQrGenerator (1.4.3): - React - ZXingObjC - - RNReanimated (3.16.7): + - RNReanimated (3.19.3): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNReanimated/reanimated (= 3.19.3) + - RNReanimated/worklets (= 3.19.3) + - Yoga + - RNReanimated/reanimated (3.19.3): - DoubleConversion - glog - hermes-engine @@ -3298,18 +3843,20 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated (= 3.16.7) - - RNReanimated/worklets (= 3.16.7) + - RNReanimated/reanimated/apple (= 3.19.3) - Yoga - - RNReanimated/reanimated (3.16.7): + - RNReanimated/reanimated/apple (3.19.3): - DoubleConversion - glog - hermes-engine @@ -3321,17 +3868,19 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - - RNReanimated/reanimated/apple (= 3.16.7) - Yoga - - RNReanimated/reanimated/apple (3.16.7): + - RNReanimated/worklets (3.19.3): - DoubleConversion - glog - hermes-engine @@ -3343,16 +3892,20 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core + - RNReanimated/worklets/apple (= 3.19.3) - Yoga - - RNReanimated/worklets (3.16.7): + - RNReanimated/worklets/apple (3.19.3): - DoubleConversion - glog - hermes-engine @@ -3364,16 +3917,19 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNScreens (4.11.0): + - RNScreens (4.11.1): - DoubleConversion - glog - hermes-engine @@ -3385,21 +3941,29 @@ PODS: - React-Fabric - React-featureflags - React-graphics + - React-hermes - React-ImageManager + - React-jsi - React-NativeModulesApple - React-RCTFabric - React-RCTImage + - React-renderercss - React-rendererdebug - React-utils - ReactCodegen - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNSVG (15.11.2): + - RNSVG (15.13.0): - React-Core - - SDWebImage (5.21.1): - - SDWebImage/Core (= 5.21.1) - - SDWebImage/Core (5.21.1) + - SDWebImage (5.21.6): + - SDWebImage/Core (= 5.21.6) + - SDWebImage/Core (5.21.6) + - SDWebImageAVIFCoder (0.11.1): + - libavif/core (>= 0.11.0) + - SDWebImage (~> 5.10) + - SDWebImageSVGCoder (1.7.0): + - SDWebImage/Core (~> 5.6) - SDWebImageWebPCoder (0.14.6): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) @@ -3425,7 +3989,14 @@ DEPENDENCIES: - "DatadogSDKReactNative (from `../../../node_modules/@datadog/mobile-react-native`)" - DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - "EthersRS (from `../../../node_modules/@uniswap/ethers-rs-mobile`)" + - EXConstants (from `../../../node_modules/expo-constants/ios`) + - EXJSONUtils (from `../../../node_modules/expo-json-utils/ios`) + - EXManifests (from `../../../node_modules/expo-manifests/ios`) - Expo (from `../../../node_modules/expo`) + - expo-dev-client (from `../../../node_modules/expo-dev-client/ios`) + - expo-dev-launcher (from `../../../node_modules/expo-dev-launcher`) + - expo-dev-menu (from `../../../node_modules/expo-dev-menu`) + - expo-dev-menu-interface (from `../../../node_modules/expo-dev-menu-interface/ios`) - ExpoAsset (from `../../../node_modules/expo-asset/ios`) - ExpoBlur (from `../../../node_modules/expo-blur/ios`) - ExpoCamera (from `../../../node_modules/expo-camera/ios`) @@ -3433,6 +4004,7 @@ DEPENDENCIES: - ExpoFileSystem (from `../../../node_modules/expo-file-system/ios`) - ExpoFont (from `../../../node_modules/expo-font/ios`) - ExpoHaptics (from `../../../node_modules/expo-haptics/ios`) + - ExpoImage (from `../../../node_modules/expo-image/ios`) - ExpoKeepAwake (from `../../../node_modules/expo-keep-awake/ios`) - ExpoLinearGradient (from `../../../node_modules/expo-linear-gradient/ios`) - ExpoLinking (from `../../../node_modules/expo-linking/ios`) @@ -3443,11 +4015,14 @@ DEPENDENCIES: - ExpoSecureStore (from `../../../node_modules/expo-secure-store/ios`) - ExpoStoreReview (from `../../../node_modules/expo-store-review/ios`) - ExpoWebBrowser (from `../../../node_modules/expo-web-browser/ios`) + - EXUpdatesInterface (from `../../../node_modules/expo-updates-interface/ios`) - fast_float (from `../../../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`) - fmt (from `../../../node_modules/react-native/third-party-podspecs/fmt.podspec`) - glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`) - hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - "NitroHashcashNative (from `../../../node_modules/@universe/hashcash-native`)" + - NitroModules (from `../../../node_modules/react-native-nitro-modules`) - OneSignalXCFramework (< 6.0, >= 5.0.0) - RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCT-Folly/Fabric (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) @@ -3476,6 +4051,8 @@ DEPENDENCIES: - React-jsi (from `../../../node_modules/react-native/ReactCommon/jsi`) - React-jsiexecutor (from `../../../node_modules/react-native/ReactCommon/jsiexecutor`) - React-jsinspector (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern`) + - React-jsinspectortracing (from `../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing`) + - React-jsitooling (from `../../../node_modules/react-native/ReactCommon/jsitooling`) - React-jsitracing (from `../../../node_modules/react-native/ReactCommon/hermes/executor/`) - React-logger (from `../../../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../../../node_modules/react-native/ReactCommon`) @@ -3498,8 +4075,8 @@ DEPENDENCIES: - react-native-video (from `../../../node_modules/react-native-video`) - react-native-webview (from `../../../node_modules/react-native-webview`) - react-native-widgetkit (from `../../../node_modules/react-native-widgetkit`) - - React-nativeconfig (from `../../../node_modules/react-native/ReactCommon`) - React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) + - React-oscompat (from `../../../node_modules/react-native/ReactCommon/oscompat`) - React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`) - React-performancetimeline (from `../../../node_modules/react-native/ReactCommon/react/performance/timeline`) - React-RCTActionSheet (from `../../../node_modules/react-native/Libraries/ActionSheetIOS`) @@ -3511,10 +4088,12 @@ DEPENDENCIES: - React-RCTImage (from `../../../node_modules/react-native/Libraries/Image`) - React-RCTLinking (from `../../../node_modules/react-native/Libraries/LinkingIOS`) - React-RCTNetwork (from `../../../node_modules/react-native/Libraries/Network`) + - React-RCTRuntime (from `../../../node_modules/react-native/React/Runtime`) - React-RCTSettings (from `../../../node_modules/react-native/Libraries/Settings`) - React-RCTText (from `../../../node_modules/react-native/Libraries/Text`) - React-RCTVibration (from `../../../node_modules/react-native/Libraries/Vibration`) - React-rendererconsistency (from `../../../node_modules/react-native/ReactCommon/react/renderer/consistency`) + - React-renderercss (from `../../../node_modules/react-native/ReactCommon/react/renderer/css`) - React-rendererdebug (from `../../../node_modules/react-native/ReactCommon/react/renderer/debug`) - React-rncore (from `../../../node_modules/react-native/ReactCommon`) - React-RuntimeApple (from `../../../node_modules/react-native/ReactCommon/react/runtime/platform/ios`) @@ -3533,7 +4112,6 @@ DEPENDENCIES: - "RNCMaskedView (from `../../../node_modules/@react-native-masked-view/masked-view`)" - "RNDateTimePicker (from `../../../node_modules/@react-native-community/datetimepicker`)" - RNDeviceInfo (from `../../../node_modules/react-native-device-info`) - - RNFastImage (from `../../../node_modules/react-native-fast-image`) - "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)" - "RNFBAuth (from `../../../node_modules/@react-native-firebase/auth`)" - "RNFBFirestore (from `../../../node_modules/@react-native-firebase/firestore`)" @@ -3579,6 +4157,8 @@ SPEC REPOS: - gRPC-Core - GTMSessionFetcher - leveldb-library + - libavif + - libdav1d - libwebp - MMKV - MMKVCore @@ -3588,6 +4168,8 @@ SPEC REPOS: - PLCrashReporter - RecaptchaInterop - SDWebImage + - SDWebImageAVIFCoder + - SDWebImageSVGCoder - SDWebImageWebPCoder - SocketRocket - UIImageColors @@ -3604,8 +4186,22 @@ EXTERNAL SOURCES: :podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" EthersRS: :path: "../../../node_modules/@uniswap/ethers-rs-mobile" + EXConstants: + :path: "../../../node_modules/expo-constants/ios" + EXJSONUtils: + :path: "../../../node_modules/expo-json-utils/ios" + EXManifests: + :path: "../../../node_modules/expo-manifests/ios" Expo: :path: "../../../node_modules/expo" + expo-dev-client: + :path: "../../../node_modules/expo-dev-client/ios" + expo-dev-launcher: + :path: "../../../node_modules/expo-dev-launcher" + expo-dev-menu: + :path: "../../../node_modules/expo-dev-menu" + expo-dev-menu-interface: + :path: "../../../node_modules/expo-dev-menu-interface/ios" ExpoAsset: :path: "../../../node_modules/expo-asset/ios" ExpoBlur: @@ -3620,6 +4216,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/expo-font/ios" ExpoHaptics: :path: "../../../node_modules/expo-haptics/ios" + ExpoImage: + :path: "../../../node_modules/expo-image/ios" ExpoKeepAwake: :path: "../../../node_modules/expo-keep-awake/ios" ExpoLinearGradient: @@ -3640,6 +4238,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/expo-store-review/ios" ExpoWebBrowser: :path: "../../../node_modules/expo-web-browser/ios" + EXUpdatesInterface: + :path: "../../../node_modules/expo-updates-interface/ios" fast_float: :podspec: "../../../node_modules/react-native/third-party-podspecs/fast_float.podspec" FBLazyVector: @@ -3650,7 +4250,11 @@ EXTERNAL SOURCES: :podspec: "../../../node_modules/react-native/third-party-podspecs/glog.podspec" hermes-engine: :podspec: "../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" - :tag: hermes-2024-11-25-RNv0.77.0-d4f25d534ab744866448b36ca3bf3d97c08e638c + :tag: hermes-2025-06-04-RNv0.79.3-7f9a871eefeb2c3852365ee80f0b6733ec12ac3b + NitroHashcashNative: + :path: "../../../node_modules/@universe/hashcash-native" + NitroModules: + :path: "../../../node_modules/react-native-nitro-modules" RCT-Folly: :podspec: "../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" RCTDeprecation: @@ -3701,6 +4305,10 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native/ReactCommon/jsiexecutor" React-jsinspector: :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern" + React-jsinspectortracing: + :path: "../../../node_modules/react-native/ReactCommon/jsinspector-modern/tracing" + React-jsitooling: + :path: "../../../node_modules/react-native/ReactCommon/jsitooling" React-jsitracing: :path: "../../../node_modules/react-native/ReactCommon/hermes/executor/" React-logger: @@ -3745,10 +4353,10 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native-webview" react-native-widgetkit: :path: "../../../node_modules/react-native-widgetkit" - React-nativeconfig: - :path: "../../../node_modules/react-native/ReactCommon" React-NativeModulesApple: :path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios" + React-oscompat: + :path: "../../../node_modules/react-native/ReactCommon/oscompat" React-perflogger: :path: "../../../node_modules/react-native/ReactCommon/reactperflogger" React-performancetimeline: @@ -3771,6 +4379,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native/Libraries/LinkingIOS" React-RCTNetwork: :path: "../../../node_modules/react-native/Libraries/Network" + React-RCTRuntime: + :path: "../../../node_modules/react-native/React/Runtime" React-RCTSettings: :path: "../../../node_modules/react-native/Libraries/Settings" React-RCTText: @@ -3779,6 +4389,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native/Libraries/Vibration" React-rendererconsistency: :path: "../../../node_modules/react-native/ReactCommon/react/renderer/consistency" + React-renderercss: + :path: "../../../node_modules/react-native/ReactCommon/react/renderer/css" React-rendererdebug: :path: "../../../node_modules/react-native/ReactCommon/react/renderer/debug" React-rncore: @@ -3815,8 +4427,6 @@ EXTERNAL SOURCES: :path: "../../../node_modules/@react-native-community/datetimepicker" RNDeviceInfo: :path: "../../../node_modules/react-native-device-info" - RNFastImage: - :path: "../../../node_modules/react-native-fast-image" RNFBApp: :path: "../../../node_modules/@react-native-firebase/app" RNFBAuth: @@ -3854,165 +4464,183 @@ SPEC CHECKSUMS: Argon2Swift: 99482c1b8122a03524b61e41c4903a9548e7c33b boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 BoringSSL-GRPC: ca6a8e5d04812fce8ffd6437810c2d46f925eaeb - DatadogCore: 68aee4ffcc3ea17a3b0aa527907757883fc72c84 - DatadogCrashReporting: e6a83b143394e28c9c1cb48c5cfb18eff507b3be - DatadogInternal: 3c5cae6772295fd175a9de11e4747a9322aaa4e7 - DatadogLogs: 09d6358dc7682f9d3eaea85dd418f82d2db3560c - DatadogRUM: 0f267df8c9c8579a291870c2bce4549587391a07 - DatadogSDKReactNative: 55c5868f9321a483bb6f592c1b2948345137a394 - DatadogTrace: f46c8220c73463d09741013f385a6e27cd39185b - DatadogWebViewTracking: dc8376420c8686efd09d00752bc1034b639d180b + DatadogCore: 5c01290a3b60b27bf49aa958f2e339c738364d9e + DatadogCrashReporting: 11286d48ab61baeb2b41b945c7c0d4ef23db317d + DatadogInternal: 7aeb48e254178a0c462c3953dc0a8a8d64499a93 + DatadogLogs: 4324739de62a6059e07d70bf6ceceed78764edeb + DatadogRUM: f36949a38285f3b240a7be577d425f8518e087d4 + DatadogSDKReactNative: 58d9a3f2005f0f9b47c057929c021fcb3b5201e4 + DatadogTrace: bfea32b6ed2870829629a9296cf526221493cc3e + DatadogWebViewTracking: 78c20d8e5f1ade506f4aadaec5690c1a63283fe2 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb EthersRS: 56b70e73d22d4e894b7e762eef1129159bcd3135 - Expo: 3e53243e3281214a7d613f8a875c0b732d7512c2 - ExpoAsset: 0687fe05f5d051c4a34dd1f9440bd00858413cfe - ExpoBlur: 567af66164e3043a9a30069594aed1ddf0a88d97 - ExpoCamera: 173e000631122854b87c20310513981e89030bc6 - ExpoClipboard: 5250b207b6d545f4e9aac5ea3c6e61c4f16d0aed - ExpoFileSystem: c8c19bf80d914c83dda3beb8569d7fb603be0970 - ExpoFont: 773955186469acc5108ff569712a2d243857475f + EXConstants: 7b84e6fc99a2bc8c589b647336b6005b419b280b + EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd + EXManifests: f4cc4a62ee4f1c8a9cf2bb79d325eac6cb9f5684 + Expo: 2a8d20c4498052d30c3b198e98cf8c19a137ecd7 + expo-dev-client: f1b99dfea0c9174d2e4ec96c2c5461587dda1e86 + expo-dev-launcher: 73e0cc1a270486501011fd8bed4cb096cc431a43 + expo-dev-menu: b2554d3971b251b2c1f0f5c9c3da50855150f195 + expo-dev-menu-interface: 609c35ae8b97479cdd4c9e23c8cf6adc44beea0e + ExpoAsset: 7bdbbacf4e6752ae6e3cf70555cee076f6229e6e + ExpoBlur: 846780b2c90f59e964b9a50385d4deb67174ebfb + ExpoCamera: fc1ab0e1c665b543a307c577df107e37cc2edc8e + ExpoClipboard: 6b9aae54fd48a579473fb101051ad693435b9294 + ExpoFileSystem: 9681caebda23fa1b38a12a9c68b2bade7072ce20 + ExpoFont: 091a47eeaa1b30b0b760aa1d0a2e7814e8bf6fe6 ExpoHaptics: e01cce0741d68c281853118eb0267f88d42c6b7a - ExpoKeepAwake: 2a5f15dd4964cba8002c9a36676319a3394c85c7 - ExpoLinearGradient: ee9efc5acb988b911320e964fab9b4cbdeb198c4 - ExpoLinking: 0381341519ca7180a3a057d20edb1cf6a908aaf4 - ExpoLocalAuthentication: 64bf2cbee456f5639d69a853684c285afc0602d8 - ExpoLocalization: e36b911e04d371c6c6624ef818e56229bf51c498 - ExpoModulesCore: 87f0b8b38f9d4c8a983212ba54119f11f3fcb615 - ExpoScreenCapture: 29ab5480e0d2b7849691d17f00a70b279cbe6a65 + ExpoImage: b5d08ac8a79f798c8306b53165415c15986f9764 + ExpoKeepAwake: e8dedc115d9f6f24b153ccd2d1d8efcdfd68a527 + ExpoLinearGradient: ce334cff9859da4635c1d8eff6e291b11b04ccbb + ExpoLinking: 343a89ea864a851831fd4495e8aea01cf0f6a36f + ExpoLocalAuthentication: 78f74d187ee51126e1a789d73fee32d6d7e60f1f + ExpoLocalization: 677e45c2536bf918119962f78d7ffeeea317e07d + ExpoModulesCore: 8030601b6028c50a3adf8864dabf43c84c913f43 + ExpoScreenCapture: 329c26be22741077b81612de1edaee8648fb209e ExpoSecureStore: d006eea5e316283099d46f80a6b10055b89a6008 - ExpoStoreReview: 32f7186925fdecacddf3c1bc9628dd11b10c3ddd - ExpoWebBrowser: 6890a769e6c9d83da938dceb9a03e764afc3ec9c + ExpoStoreReview: bed43bea90a5876a6a480504f95fea1521dacaa7 + ExpoWebBrowser: eeb47f52e85b2686b56178749675cf90d0822f86 + EXUpdatesInterface: 64f35449b8ef89ce08cdd8952a4d119b5de6821d fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 - FBLazyVector: 4c16dde959a9d6b24f2aa32cb87cb919a1ace3f3 + FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52 Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c - FirebaseAppCheckInterop: a92ba81d0ee3c4cddb1a2e52c668ea51dc63c3ae + FirebaseAppCheckInterop: 06fe5a3799278ae4667e6c432edd86b1030fa3df FirebaseAuth: 2a198b8cdbbbd457f08d74df7040feb0a0e7777a - FirebaseAuthInterop: e25b58ecb90f3285085fa2118861a3c9dfdc62ad + FirebaseAuthInterop: 7087d7a4ee4bc4de019b2d0c240974ed5d89e2fd FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da FirebaseCoreExtension: f1bc67a4702931a7caa097d8e4ac0a1b0d16720e - FirebaseCoreInternal: 6a3b668197644aa858fc4127578637c6767ba123 + FirebaseCoreInternal: 9afa45b1159304c963da48addb78275ef701c6b4 FirebaseFirestore: 62708adbc1dfcd6d165a7c0a202067b441912dc9 FirebaseFirestoreInternal: ad9b9ee2d3d430c8f31333a69b3b6737a7206232 - FirebaseSharedSwift: bdd5c8674c4712a98e70287c936bc5cca5d640f6 + FirebaseSharedSwift: e17c654ef1f1a616b0b33054e663ad1035c8fd40 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd - glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8 + glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 "gRPC-C++": 2fa52b3141e7789a28a737f251e0c45b4cb20a87 gRPC-Core: a27c294d6149e1c39a7d173527119cfbc3375ce4 GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6 - hermes-engine: 8eb265241fa1d7095d3a40d51fd90f7dce68217c + hermes-engine: f03b0e06d3882d71e67e45b073bb827da1a21aae leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19 + libavif: 5f8e715bea24debec477006f21ef9e95432e254d + libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - MMKV: b4802ebd5a7c68fc0c4a5ccb4926fbdfb62d68e0 - MMKVCore: a255341a3746955f50da2ad9121b18cb2b346e61 + MMKV: c953dbaac0da392c24b005e763c03ce2638b4ed7 + MMKVCore: d078dce7d6586a888b2c2ef5343b6242678e3ee8 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + NitroHashcashNative: cc2cb81a1c6ef47ba928e1b217c670ca33c7c56c + NitroModules: 3657718d49e12e1954f7c4bb82f6f2a3ee567253 OneSignalXCFramework: 1a3b28dfbff23aabce585796d23c1bef37772774 OpenTelemetrySwiftApi: aaee576ed961e0c348af78df58b61300e95bd104 PLCrashReporter: db59ef96fa3d25f3650040d02ec2798cffee75f2 RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809 - RCTDeprecation: 85b72250b63cfb54f29ca96ceb108cb9ef3c2079 - RCTRequired: 567cb8f5d42b990331bfd93faad1d8999b1c1736 - RCTTypeSafety: 5e57924492a5e0a762654f814dd018953274eca9 - React: 53c9bd6f974c5dd019ee466e46477eb679149c38 - React-callinvoker: d6484472c1c742917b51338525336d6a74ab8a9f - React-Core: 043aaf319142ecc02db6fffccb780186e6e7462a - React-CoreModules: abe0b2089368e420b7beaa5e140771181e2f6edb - React-cxxreact: 8b678dd36228089b6ee19d62f2bfb8935ea1c63b - React-debug: af25f71a2ea800559f591ef9e9e6495a206c4f7c - React-defaultsnativemodule: 7883e6ef963ee6a6346eb43b73967919029f1033 - React-domnativemodule: eafabfac38103187bfd0bc8b4c64d1ebc35444c2 - React-Fabric: 72038b554a0e4791432a7c161e6ea52bdc854d7c - React-FabricComponents: 47e25c62a3fdc4d6630dc44f41c5552a80202a2a - React-FabricImage: 95c3a49e22cc8c5c33cf282287c73cf261edd104 - React-featureflags: 05545ed41078babfec20095fd7825f029709cde6 - React-featureflagsnativemodule: 75f805793e3feb0dea7055e92540624965543de9 - React-graphics: e33b1bff03c62a7293991bcc28ceb946740bae0f - React-hermes: c07c778ab9cb80a943116ef4574087e8570206cf - React-idlecallbacksnativemodule: f800ae00e3427cbf0ae1f7d880ccbbe8ffea5e16 - React-ImageManager: ca548168b2efedd1e017dc6d715f5e0028eb446a - React-jserrorhandler: 8a7c11b5691b798c9b25b8e4cfbda02742828602 - React-jsi: 1c901c8f6e8d4555b6a5747c5a5b7c7cf757da71 - React-jsiexecutor: d59faf2904bb0bd1daec18bc59731623e79a74eb - React-jsinspector: eb7486a37a90aa2e896d7c67d7ea04c1c466b4ef - React-jsitracing: a417d71b554e891ccab72510f9482e6c53d0b09a - React-logger: 5320a2acb25baa566cda59b611231929dd3ed7fc - React-Mapbuffer: 9af3695e354816d30d4429992714e0c8cefac25f - React-microtasksnativemodule: ab4c1c9d4841e71a4116f167a4cd3f8dc1ab50ba + RCTDeprecation: 5f638f65935e273753b1f31a365db6a8d6dc53b5 + RCTRequired: 8b46a520ea9071e2bc47d474aa9ca31b4a935bd8 + RCTTypeSafety: cc4740278c2a52cbf740592b0a0a40df1587c9ab + React: 6393ae1807614f017a84805bf2417e3497f518a6 + React-callinvoker: c34f666f551f05a325b87e7e3e6df0e082fa3d99 + React-Core: fc07a4b69a963880b25142c51178f4cb75628c7d + React-CoreModules: 94d39315cfa791f6c477712fea47c34f8ecb26c6 + React-cxxreact: 628c28cdb3fdef93ee3bfc2bec8e2d776e81ae49 + React-debug: c1b10e5982b961738eab5b1d66fa31572ca28b5e + React-defaultsnativemodule: dd13932a4a4b0f1d556c9a4e76cc00fa05207126 + React-domnativemodule: 44bd8074cffa6a8ed298a45e2f7d1b519fb15a23 + React-Fabric: 5efe9e2171352089ded33d73e177345e8eb74e00 + React-FabricComponents: 359ea9205fc116ec52c90186ace048391ba477f7 + React-FabricImage: e536ff5e1c1f081dc5953696287e33d4d3b543bd + React-featureflags: 1e3a098a98c63a339a8b5ef4014ba4c4b43fb1f6 + React-featureflagsnativemodule: c926c24fda31daab491e25f4003413b49a5dfaec + React-graphics: 1476634e2deaf13dad3ab7ddfa33f78933445e07 + React-hermes: af1b3d79491295abc9d1b11f84e77d5dc00095b6 + React-idlecallbacksnativemodule: 43fc456e78c7dd7a342a9f185ef7c931d8c44ab0 + React-ImageManager: bd97427edf2df85e7e162e2161c9c981a6185915 + React-jserrorhandler: 44ebfb576a9ce098205b246a4bb81c9ad55ffbb6 + React-jsi: e9c3019e00db5d144e0a660616a52a605e12c39a + React-jsiexecutor: 3ed70a394b76f33e6c4ec4b382a457df7309d96c + React-jsinspector: d0c7ef76573b8e2b362ebc570aecd43b0c88a282 + React-jsinspectortracing: 551b7981d2a0b6a7829fd8c8c310ca51b5b323f8 + React-jsitooling: 5d06fc7c61ac1d260a553a9a9cffcd78865e430c + React-jsitracing: 2ecfa3ccc58e876a8c4f76a2cbdd920fc1ccfbb0 + React-logger: e6e6164f1753e46d1b7e2c8f0949cd7937eaf31b + React-Mapbuffer: a83853bd80bb31a4451e64af91a86c02f7df4f80 + React-microtasksnativemodule: 964a2c1213bb39fa5cc8d5ee4f55846d67747f32 react-native-appsflyer: 2cc1f96348065fc23e976fc7a27e371789fb349e - react-native-compat: 17cc6a63937e3fc291b92250f56ce5e9c0c3aa53 + react-native-compat: 1e09d1a14355b6a0383fb371fe98509fa8c3f4fd react-native-context-menu-view: dcec18eb8882e20596dbb75802e7d19cb87dac02 react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06 - react-native-image-picker: 2064a7a43d1e204c3b42ce6d2208df32e881fc9d - react-native-keyboard-controller: 2fdaf70d94da51a982c702720fdc7b051db8140b - react-native-mmkv: dea675cf9697ad35940f1687e98e133e1358ef9f + react-native-image-picker: 75cc6db21e264e573456725c71ad21828a82c455 + react-native-keyboard-controller: 8698f5ff79ab0a4a8f0259a40b4c3c7f23cae7fd + react-native-mmkv: d881794b39391fb831de1b6993ecb3c9c57bae51 react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac react-native-onesignal: 33ade92bd91578374c31c5a5a91f45f49c2d6614 - react-native-pager-view: b8b7c09ce10ed0ca632689570aa1020271b44156 + react-native-pager-view: d6f91626b36fcca51d28a9c5ec109a9309242089 react-native-passkey: 69bede03f6bb35fad8117cad73155231cc31066c react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162 - react-native-safe-area-context: 04803a01f39f31cc6605a5531280b477b48f8a88 - react-native-skia: 628fde753bb219b462d97d761f028076d289b1a1 - react-native-slider: bf50824dd00db1e7b66eaec12598883cea3c79e7 - react-native-video: 85e6571bb240a1f084cc7fc9b2f2f0c27b51b622 - react-native-webview: 6c92617eff2519f6359440508c603d3ecda50984 + react-native-safe-area-context: 8870dc3e45c8d241336cd8ee3fa3fc76f3a040ac + react-native-skia: ada93c6118964447045cb8a0c04bacc7400373d3 + react-native-slider: d3ddeb61d8c4c4d99f19194338d8d2c33957e717 + react-native-video: a6a2ad5d778133dee45875faf44c6ce0d61cac0e + react-native-webview: 94294e5a5d8cf4f53793aee7ef69ebd14c79e397 react-native-widgetkit: efb6680df237463bbe1be3a4d1a1578a1b0bb08f - React-nativeconfig: 75658bde8f977492f668e94ae8eb9c0dfef7ed94 - React-NativeModulesApple: 1bd9fa09c40204ac489acbe7da239efa4aa47244 - React-perflogger: a0f49e229d1252d683102df60d2392229ece5837 - React-performancetimeline: d0fb47dfc5d55ec6b7c4a79c83912ff59bb8df51 - React-RCTActionSheet: 150cfe1df4275db2251a2a4a1b22be3294e94ef7 - React-RCTAnimation: 67a303df28e0981f58a26968b3c15252a6b277b8 - React-RCTAppDelegate: 5ffb34a2bc043815e470d64feb8510d8930d7e40 - React-RCTBlob: 2ccb60fd765ed41292c495e4374eaf7c23a3c81c - React-RCTFabric: ab786f111eb621bcf1e5b3e6f91eaecaf99eadeb - React-RCTFBReactNativeSpec: e943ce6a5952941d730758fcd5b4a603c6b5ea0a - React-RCTImage: f235db428b4c98cbb1ad6304f826695f19e82c57 - React-RCTLinking: 523a01769de55660743d6332157dfb4dbad817b8 - React-RCTNetwork: d99ee5bf1f15ad8521d30c9da477906d7fb00110 - React-RCTSettings: 4165a44c6f51787980634bd522f3b379ee8531a6 - React-RCTText: 462ab8d4f2f180be83e3983307ce5ef5fa58210e - React-RCTVibration: ca784cb7e30c21852d4b78b1621bfa11940ab061 - React-rendererconsistency: 28f87593201bca785e0bbdc94bff4d92ee2d32ee - React-rendererdebug: c3121b6c4f0873ad2cf1bb63947c0a8a0ae8a1ab - React-rncore: df9c0360d3f28371a103921890e20c309c906407 - React-RuntimeApple: 4b8060742249e0ede1e186e3df656cea119ac9da - React-RuntimeCore: c622d85e3fd6f60ccb7f54bb0d583a85dc1dff25 - React-runtimeexecutor: e6e7af01f9989f931289250ee9060604bc0f0144 - React-RuntimeHermes: 5062f63b39a375c63909c16b33bbc24e2c8e7cf6 - React-runtimescheduler: b6788737ee06eec93c63a2cda4b20c5ca916f5b5 - React-timing: dfac299d2afa69272d469c8e5fd4d4328fe41d1e - React-utils: 280b2ed61cfb45ad94fc8e86d7be402689714962 - ReactAppDependencyProvider: 8955603808eb24bbfde3511d2bcc8362d1d5860e - ReactCodegen: 1e8981b03b6389301f819833f8b3b498d9d4da8d - ReactCommon: ad39e4549e2920b3b065a28603921a75619d0639 + React-NativeModulesApple: d2b9bd7d55dfd864e56c76592777ad32e8ab1f3d + React-oscompat: 0592889a9fcf0eacb205532028e4a364e22907dd + React-perflogger: 634408a9a0f5753faa577dfa81bc009edca01062 + React-performancetimeline: b58a6e65c9fbe1aa02b97152edd6ad5275aef36b + React-RCTActionSheet: ce67bdc050cc1d9ef673c7a93e9799288a183f24 + React-RCTAnimation: 12193c2092a78012c7f77457806dcc822cc40d2c + React-RCTAppDelegate: b0a8aa38e4791915673a7a3ae80b2840a81ec255 + React-RCTBlob: 923cf9b0098b9a641cb1e454c30a444d9d3cda70 + React-RCTFabric: 3f2f2980ad1d426f3a03c9183521b835e0bbbe83 + React-RCTFBReactNativeSpec: b7671d70d65f61326805725b24c7855aab0befb2 + React-RCTImage: 580a5d0a6fdf9b69629d0582e5fb5a173e152099 + React-RCTLinking: 4ed7c5667709099bfd6b2b6246b1dfd79c89f7cb + React-RCTNetwork: 06a22dd0088392694df4fd098634811aa0b3e166 + React-RCTRuntime: 38591d6246389f4f8b93f0f94f565f8448805581 + React-RCTSettings: 9dbf433f302c8ebe43b280453e74624098fbc706 + React-RCTText: 92fcd78d6c44dbe64d147bb63f53698bcba7c971 + React-RCTVibration: 513659394c92491e6c749e981424f6e1e0abdb3c + React-rendererconsistency: c9c28e3b0834d9be2e6aa0ba2d1fd77c76441658 + React-renderercss: 700c57db7fcb36a1e5a9b3645f4cc22f4de43899 + React-rendererdebug: 8ce5f50fd01160e1d1bfc9ec34dac0ca5411afc2 + React-rncore: 289894dda4ebcca06104070f1a9c9283f37dd123 + React-RuntimeApple: 5e5315e698c4fc4e5a3e6b610e4e0ae135f3718c + React-RuntimeCore: 23b6e0e4e2b1cb8a81a14db68814ade8998a5fb0 + React-runtimeexecutor: ebfd71307b3166c73ac0c441c1ea42e0f17f821d + React-RuntimeHermes: baef54b36a6623ea8cd7442027744b08ad06d01b + React-runtimescheduler: 4d9a1afaa16d7dd11a909a9103b18b63995c5683 + React-timing: 0f749e1c5ca1147b699b25ec79003950e6366056 + React-utils: 52ce70a20366bb7f1b416c57281f40ffafeb8a8a + ReactAppDependencyProvider: c42e7abdd2228ae583bdabc3dcd8e5cda6bef944 + ReactCodegen: 0851536ada69d85536f54a7239956a50b6dcd42e + ReactCommon: 3dbed0a44e9e5d7b77b0f35983633aa5d33c9694 ReactNativePerformance: ab7dee4c4862623d72c1530a9fc71b55458edf71 RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 - RNBootSplash: 66c8458007bda40cc25a3f25e4326244a71d9a73 - RNCAsyncStorage: 826b603ae9c0f88b5ac4e956801f755109fa4d5c - RNCMaskedView: 9953b54e4f488389ec45d8befd8687d71e7c121e - RNDateTimePicker: 40ffda97d071a98a10fdca4fa97e3977102ccd14 + RNBootSplash: 2c6b226e3ad3c97d16b6d53bd75d0cd281646bfb + RNCAsyncStorage: addfc2cb6511dbe199c56c6b26ede383b6c38919 + RNCMaskedView: 42f3684c136239957b410dbfa81978b25f2c0e18 + RNDateTimePicker: 279bad2682d9ebdd4151cb71d88f3b460f818fc8 RNDeviceInfo: bf8a32acbcb875f568217285d1793b0e8588c974 - RNFastImage: 074e3c1a0d65e2971f28299a85d0155c3b2c948e RNFBApp: 4122dd41d8d7ff017b6ecf777a6224f5b349ca04 RNFBAuth: 1632cefd787a43ba952fa52ff016e7b69fe355cb RNFBFirestore: 5f110e37b7f7f3d6e03c85044dd4cf3ebacec38b - RNFlashList: 4afe189d83616f240be187f717320ad966f6024f - RNGestureHandler: ab4058d59c000e7df387ad9a973e93f7e40de331 + RNFlashList: 7c43eac420e04bfa7798d40c7246c7067e8a4c2c + RNGestureHandler: eb5ad44465a546182d05aebae304e45c881d2f22 RNImageColors: 9ac05083b52d5c350e6972650ae3ba0e556466c1 RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 RNPermissions: 87aac13521bea6dcb6dfd60b03ac69741ccef2b4 RNQrGenerator: ac6a6c766e80dd3625038929ed2b13e2f3edcafb - RNReanimated: 9f96886ec1e1772ed7462cc80da82b7bf1ba984d - RNScreens: 567d3119f7d5d1041090eab25027667c505e4e75 - RNSVG: 4cbae6c6f0ef2b0aa277c5fce8447d3e4cd97cd0 - SDWebImage: f29024626962457f3470184232766516dee8dfea + RNReanimated: ad46062e119fcf93712dfe9dcf72b45ea16892e4 + RNScreens: edd4795b025d94f879e20cc346b844176d938f0c + RNSVG: 204b068da3a7416d22840cd3233bcf745443a455 + SDWebImage: 1bb6a1b84b6fe87b972a102bdc77dd589df33477 + SDWebImageAVIFCoder: afe194a084e851f70228e4be35ef651df0fc5c57 + SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 sparkfabrik-react-native-idfa-aaid: 1b72a6264a2175473e309ffa6434db87c58af264 UIImageColors: d2ef3b0877d203cbb06489eeb78ea8b7788caabe - Yoga: fdc0542faa3ba87e56f2030b3f3f2e21bc3ba01c + Yoga: bfcce202dba74007f8974ee9c5f903a9a286c445 ZXingObjC: 8898711ab495761b2dbbdec76d90164a6d7e14c5 -PODFILE CHECKSUM: 5438afe50eaeebf354579fd9c40fb3c93d56ce9b +PODFILE CHECKSUM: 407b7412f08ef053b16b402cacac69b31d41ef80 -COCOAPODS: 1.14.3 +COCOAPODS: 1.16.2 diff --git a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj index 6cdc18319f2..397a58d2467 100644 --- a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj @@ -7,16 +7,11 @@ objects = { /* Begin PBXBuildFile section */ - 0013F5F72C93399400D6EF09 /* ProtectionInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0013F5F62C93399400D6EF09 /* ProtectionInfo.graphql.swift */; }; - 00265F792C933CE300A5DA57 /* ProtectionResult.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00265F772C933CE300A5DA57 /* ProtectionResult.graphql.swift */; }; - 00265F7A2C933CE300A5DA57 /* ProtectionAttackType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 00265F782C933CE300A5DA57 /* ProtectionAttackType.graphql.swift */; }; - 0094F4FBBC1C0A2FFABF7157 /* TokenProject.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4981A14906A35E8A0B7F9457 /* TokenProject.graphql.swift */; }; 00E356F31AD99517003FC87E /* UniswapTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* UniswapTests.m */; }; - 03291E0DA448AF438F97EA5D /* DescriptionTranslations.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E1F288B7EDDE906C4C00C46 /* DescriptionTranslations.graphql.swift */; }; 037C5AAA2C04970B00B1D808 /* CopyIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 037C5AA92C04970B00B1D808 /* CopyIcon.swift */; }; + 038FC04C9385A04F3EA5EBF4 /* TokenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16F706375DFAFB58F8440CF9 /* TokenQuery.graphql.swift */; }; 03C788232C10E7390011E5DC /* ActionButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C788222C10E7390011E5DC /* ActionButtons.swift */; }; 03D2F3182C218D390030D987 /* RelativeOffsetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03D2F3172C218D380030D987 /* RelativeOffsetView.swift */; }; - 03E3515E38E247F459218CAA /* SwapOrderDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C955CE2592785712A11892DE /* SwapOrderDetails.graphql.swift */; }; 0703EE032A5734A600AED1DA /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0703EE022A5734A600AED1DA /* UserDefaults.swift */; }; 0703EE052A57351800AED1DA /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072F6C372A44BECC00DA720A /* Logging.swift */; }; 072E238E2A44D5BD006AD6C9 /* WidgetsCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */; }; @@ -33,86 +28,11 @@ 074086FA2A703B76006E3053 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074086F92A703B76006E3053 /* FormatTests.swift */; }; 0741433E2A588CCC00A157D3 /* TokenPriceWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 072F6C292A44A32E00DA720A /* TokenPriceWidget.intentdefinition */; }; 074143402A588F5800A157D3 /* Structs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0741433F2A588F5800A157D3 /* Structs.swift */; }; - 074321EB2A83E3CA00F8518D /* TokenDetailsScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321902A83E3C900F8518D /* TokenDetailsScreenQuery.graphql.swift */; }; - 074321EE2A83E3CA00F8518D /* NftsTabQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321932A83E3C900F8518D /* NftsTabQuery.graphql.swift */; }; - 074321EF2A83E3CA00F8518D /* TokenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321942A83E3C900F8518D /* TokenQuery.graphql.swift */; }; - 074321F12A83E3CA00F8518D /* TopTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321962A83E3C900F8518D /* TopTokensQuery.graphql.swift */; }; - 074321F22A83E3CA00F8518D /* TokenPriceHistoryQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321972A83E3C900F8518D /* TokenPriceHistoryQuery.graphql.swift */; }; - 074321F32A83E3CA00F8518D /* FavoriteTokenCardQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321982A83E3C900F8518D /* FavoriteTokenCardQuery.graphql.swift */; }; - 074321F42A83E3CA00F8518D /* NFTItemScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321992A83E3C900F8518D /* NFTItemScreenQuery.graphql.swift */; }; - 074321F52A83E3CA00F8518D /* NftCollectionScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0743219A2A83E3C900F8518D /* NftCollectionScreenQuery.graphql.swift */; }; - 074321F82A83E3CA00F8518D /* TokenProjectsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0743219D2A83E3C900F8518D /* TokenProjectsQuery.graphql.swift */; }; - 074321F92A83E3CA00F8518D /* SelectWalletScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0743219E2A83E3C900F8518D /* SelectWalletScreenQuery.graphql.swift */; }; - 074321FA2A83E3CA00F8518D /* PortfolioBalancesQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0743219F2A83E3C900F8518D /* PortfolioBalancesQuery.graphql.swift */; }; - 074321FB2A83E3CA00F8518D /* NftsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321A02A83E3C900F8518D /* NftsQuery.graphql.swift */; }; - 074321FC2A83E3CA00F8518D /* TransactionListQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321A12A83E3C900F8518D /* TransactionListQuery.graphql.swift */; }; - 074321FD2A83E3CA00F8518D /* TransactionHistoryUpdaterQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321A22A83E3C900F8518D /* TransactionHistoryUpdaterQuery.graphql.swift */; }; - 074321FE2A83E3CA00F8518D /* TopTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321A42A83E3C900F8518D /* TopTokenParts.graphql.swift */; }; - 074321FF2A83E3CA00F8518D /* MobileSchema.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321A52A83E3C900F8518D /* MobileSchema.graphql.swift */; }; - 074322002A83E3CA00F8518D /* AssetChange.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321A82A83E3C900F8518D /* AssetChange.graphql.swift */; }; - 074322012A83E3CA00F8518D /* HistoryDuration.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321AA2A83E3C900F8518D /* HistoryDuration.graphql.swift */; }; - 074322022A83E3CA00F8518D /* TokenStandard.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321AB2A83E3C900F8518D /* TokenStandard.graphql.swift */; }; - 074322032A83E3CA00F8518D /* Currency.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321AC2A83E3C900F8518D /* Currency.graphql.swift */; }; - 074322052A83E3CA00F8518D /* Chain.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321AE2A83E3C900F8518D /* Chain.graphql.swift */; }; - 074322062A83E3CA00F8518D /* TokenSortableField.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321AF2A83E3C900F8518D /* TokenSortableField.graphql.swift */; }; - 074322072A83E3CA00F8518D /* TransactionDirection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B02A83E3C900F8518D /* TransactionDirection.graphql.swift */; }; - 074322082A83E3CA00F8518D /* NftStandard.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B12A83E3C900F8518D /* NftStandard.graphql.swift */; }; - 074322092A83E3CA00F8518D /* NftActivityType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B22A83E3C900F8518D /* NftActivityType.graphql.swift */; }; - 0743220A2A83E3CA00F8518D /* SafetyLevel.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B32A83E3C900F8518D /* SafetyLevel.graphql.swift */; }; - 0743220B2A83E3CA00F8518D /* NftMarketplace.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B42A83E3C900F8518D /* NftMarketplace.graphql.swift */; }; - 0743220C2A83E3CA00F8518D /* TransactionStatus.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B52A83E3C900F8518D /* TransactionStatus.graphql.swift */; }; - 0743220D2A83E3CA00F8518D /* Image.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B72A83E3C900F8518D /* Image.graphql.swift */; }; - 0743220E2A83E3CA00F8518D /* NftOrderEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B82A83E3C900F8518D /* NftOrderEdge.graphql.swift */; }; - 0743220F2A83E3CA00F8518D /* NftApproveForAll.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B92A83E3C900F8518D /* NftApproveForAll.graphql.swift */; }; - 074322102A83E3CA00F8518D /* NftAssetTrait.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321BA2A83E3C900F8518D /* NftAssetTrait.graphql.swift */; }; - 074322112A83E3CA00F8518D /* NftBalanceConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321BB2A83E3C900F8518D /* NftBalanceConnection.graphql.swift */; }; - 074322122A83E3CA00F8518D /* NftActivityEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321BC2A83E3C900F8518D /* NftActivityEdge.graphql.swift */; }; - 074322132A83E3CA00F8518D /* NftCollection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321BD2A83E3C900F8518D /* NftCollection.graphql.swift */; }; - 074322142A83E3CA00F8518D /* TimestampedAmount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321BE2A83E3C900F8518D /* TimestampedAmount.graphql.swift */; }; - 074322152A83E3CA00F8518D /* NftAssetConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321BF2A83E3C900F8518D /* NftAssetConnection.graphql.swift */; }; - 074322162A83E3CA00F8518D /* TokenProject.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C02A83E3C900F8518D /* TokenProject.graphql.swift */; }; - 074322172A83E3CA00F8518D /* NftTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C12A83E3C900F8518D /* NftTransfer.graphql.swift */; }; - 074322182A83E3CA00F8518D /* TokenApproval.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C22A83E3C900F8518D /* TokenApproval.graphql.swift */; }; - 074322192A83E3CA00F8518D /* NftOrderConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C32A83E3C900F8518D /* NftOrderConnection.graphql.swift */; }; - 0743221A2A83E3CA00F8518D /* TokenMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C42A83E3C900F8518D /* TokenMarket.graphql.swift */; }; - 0743221B2A83E3CA00F8518D /* NftCollectionMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C52A83E3C900F8518D /* NftCollectionMarket.graphql.swift */; }; - 0743221C2A83E3CA00F8518D /* TokenBalance.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C62A83E3C900F8518D /* TokenBalance.graphql.swift */; }; - 0743221D2A83E3CA00F8518D /* NftOrder.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C72A83E3C900F8518D /* NftOrder.graphql.swift */; }; - 0743221E2A83E3CA00F8518D /* Portfolio.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C82A83E3C900F8518D /* Portfolio.graphql.swift */; }; - 0743221F2A83E3CA00F8518D /* PageInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C92A83E3C900F8518D /* PageInfo.graphql.swift */; }; - 074322202A83E3CA00F8518D /* NftAssetEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321CA2A83E3C900F8518D /* NftAssetEdge.graphql.swift */; }; - 074322212A83E3CA00F8518D /* NftCollectionConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321CB2A83E3C900F8518D /* NftCollectionConnection.graphql.swift */; }; - 074322222A83E3CA00F8518D /* NftContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321CC2A83E3C900F8518D /* NftContract.graphql.swift */; }; - 074322232A83E3CA00F8518D /* NftActivityConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321CD2A83E3C900F8518D /* NftActivityConnection.graphql.swift */; }; - 074322242A83E3CA00F8518D /* Amount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321CE2A83E3C900F8518D /* Amount.graphql.swift */; }; - 074322252A83E3CA00F8518D /* Query.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321CF2A83E3C900F8518D /* Query.graphql.swift */; }; - 074322262A83E3CA00F8518D /* NftAsset.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D02A83E3C900F8518D /* NftAsset.graphql.swift */; }; - 074322272A83E3CA00F8518D /* Dimensions.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D12A83E3C900F8518D /* Dimensions.graphql.swift */; }; - 074322282A83E3CA00F8518D /* TokenProjectMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D22A83E3C900F8518D /* TokenProjectMarket.graphql.swift */; }; - 074322292A83E3CA00F8518D /* AmountChange.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D32A83E3C900F8518D /* AmountChange.graphql.swift */; }; - 0743222A2A83E3CA00F8518D /* NftBalanceEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D42A83E3C900F8518D /* NftBalanceEdge.graphql.swift */; }; - 0743222B2A83E3CA00F8518D /* NftActivity.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D52A83E3C900F8518D /* NftActivity.graphql.swift */; }; - 0743222C2A83E3CA00F8518D /* AssetActivity.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D62A83E3C900F8518D /* AssetActivity.graphql.swift */; }; - 0743222D2A83E3CA00F8518D /* NftProfile.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D72A83E3C900F8518D /* NftProfile.graphql.swift */; }; - 0743222E2A83E3CA00F8518D /* NftCollectionEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D82A83E3C900F8518D /* NftCollectionEdge.graphql.swift */; }; - 074322302A83E3CA00F8518D /* TokenTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321DA2A83E3C900F8518D /* TokenTransfer.graphql.swift */; }; - 074322312A83E3CA00F8518D /* NftApproval.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321DB2A83E3C900F8518D /* NftApproval.graphql.swift */; }; - 074322322A83E3CA00F8518D /* NftBalance.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321DC2A83E3C900F8518D /* NftBalance.graphql.swift */; }; - 074322332A83E3CA00F8518D /* Token.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321DD2A83E3C900F8518D /* Token.graphql.swift */; }; 074322342A83E3CA00F8518D /* SchemaConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321DE2A83E3C900F8518D /* SchemaConfiguration.swift */; }; - 074322352A83E3CA00F8518D /* NftBalancesFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E02A83E3C900F8518D /* NftBalancesFilterInput.graphql.swift */; }; - 074322372A83E3CA00F8518D /* ContractInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E22A83E3C900F8518D /* ContractInput.graphql.swift */; }; - 074322382A83E3CA00F8518D /* NftActivityFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E32A83E3C900F8518D /* NftActivityFilterInput.graphql.swift */; }; - 074322392A83E3CA00F8518D /* NftAssetTraitInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E42A83E3C900F8518D /* NftAssetTraitInput.graphql.swift */; }; - 0743223B2A83E3CA00F8518D /* NftAssetsFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E62A83E3C900F8518D /* NftAssetsFilterInput.graphql.swift */; }; - 0743223C2A83E3CA00F8518D /* SchemaMetadata.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E72A83E3C900F8518D /* SchemaMetadata.graphql.swift */; }; - 0743223D2A83E3CA00F8518D /* IAmount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E92A83E3C900F8518D /* IAmount.graphql.swift */; }; - 0743223E2A83E3CA00F8518D /* IContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321EA2A83E3C900F8518D /* IContract.graphql.swift */; }; 074322402A841BBD00F8518D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0743223F2A841BBD00F8518D /* Constants.swift */; }; 0767E0382A65C8330042ADA2 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0767E0372A65C8330042ADA2 /* Colors.swift */; }; 0767E03B2A65D2550042ADA2 /* Styling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0767E03A2A65D2550042ADA2 /* Styling.swift */; }; - 077E60392A85587800ABC4B9 /* TokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077E60382A85587800ABC4B9 /* TokensQuery.graphql.swift */; }; - 077E603B2A86D06100ABC4B9 /* MultiplePortfolioBalancesQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077E603A2A86D06100ABC4B9 /* MultiplePortfolioBalancesQuery.graphql.swift */; }; + 0781032B76A7F58B5776207B /* FeedTransactionListQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21B5EF6BC4582DD2BB0DA13 /* FeedTransactionListQuery.graphql.swift */; }; 0783F7B42A619E7C009ED617 /* UIComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0783F7B32A619E7C009ED617 /* UIComponents.swift */; }; 078E79472A55EB3300F59CF2 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 078E79462A55EB3300F59CF2 /* Intents.framework */; }; 078E794E2A55EB3300F59CF2 /* WidgetIntentExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 078E79452A55EB3300F59CF2 /* WidgetIntentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; @@ -123,72 +43,60 @@ 07F136422A5763480067004F /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F136412A5763480067004F /* Network.swift */; }; 07F5CF712A6AD97D00C648A5 /* Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F5CF702A6AD97D00C648A5 /* Chart.swift */; }; 07F5CF752A7020FD00C648A5 /* Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F5CF742A7020FD00C648A5 /* Format.swift */; }; - 09E8C497051C37FE604FD40E /* OnRampTransactionsAuth.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315B8A905FA2C60C053F138B /* OnRampTransactionsAuth.graphql.swift */; }; - 09F9DEB33392F3051BEA8D52 /* TokenFeeDataParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018603D0FA4D05DBF16F1441 /* TokenFeeDataParts.graphql.swift */; }; - 0C6FC49E0FC9BCB2DF5B627E /* TokenSortableField.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32012AC135EBD991C8F02F92 /* TokenSortableField.graphql.swift */; }; - 0CEBEB8BE3AB95C3F584F6CE /* Image.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31ED613D63FE9C56D9270517 /* Image.graphql.swift */; }; + 0901AAD2DCDD615889B6680A /* TransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47148657FBA8DCC92DD69573 /* TransactionDetails.graphql.swift */; }; 0DB282262CDADB260014CF77 /* EmbeddedWallet.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DB282242CDADB260014CF77 /* EmbeddedWallet.m */; }; 0DB282272CDADB260014CF77 /* EmbeddedWallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DB282252CDADB260014CF77 /* EmbeddedWallet.swift */; }; - 0DC6ADF02B1E2C100092909C /* PortfolioValueModifier.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC6ADEF2B1E2C0F0092909C /* PortfolioValueModifier.graphql.swift */; }; - 0DE251432C13B674005F47F9 /* OnRampTransactionsAuth.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE251422C13B674005F47F9 /* OnRampTransactionsAuth.graphql.swift */; }; - 0DE251472C13B69D005F47F9 /* OnRampTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE251442C13B69D005F47F9 /* OnRampTransfer.graphql.swift */; }; - 0DE251482C13B69D005F47F9 /* OnRampServiceProvider.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE251452C13B69D005F47F9 /* OnRampServiceProvider.graphql.swift */; }; - 0DE251492C13B69D005F47F9 /* OnRampTransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DE251462C13B69D005F47F9 /* OnRampTransactionDetails.graphql.swift */; }; - 0F282C26344F1AE3622232B0 /* OffRampTransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 396B6AB3BB41898B56EB3828 /* OffRampTransactionDetails.graphql.swift */; }; - 126CC4BC99F3F15CC1E2F1C3 /* NftAssetTrait.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70851E3D1A4B296A1CE22A20 /* NftAssetTrait.graphql.swift */; }; + 0DF7B81A4727A8CA063CD5B5 /* SwapOrderDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32764E27B5D9FCF0AA4217CF /* SwapOrderDetails.graphql.swift */; }; + 100A336AFEC40D106F257664 /* OnRampTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60195493B9C71EDEECA46E80 /* OnRampTransfer.graphql.swift */; }; + 121CD8F92A5E382AD1A84AA9 /* NftBalanceAssetInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C394AC5DB58BDF1C33CC0927 /* NftBalanceAssetInput.graphql.swift */; }; + 12A7E560842C954098B3A558 /* NftsTabQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EA8FE9029A1E7C4EAD7F547 /* NftsTabQuery.graphql.swift */; }; 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; - 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; - 143C638C2CCAC689371BFC93 /* NftActivityType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D95FE36D2667E070345CDD0 /* NftActivityType.graphql.swift */; }; 1440B371A1C9A42F3E91DAAE /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF5F26A06553EFDD4D99214 /* ExpoModulesProvider.swift */; }; - 171DD1C966C7FBF9DD68CFAC /* TopTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04E5C2900254E7BA7F10CE51 /* TopTokensQuery.graphql.swift */; }; - 1B59968CF49C45B127E9C768 /* NftOrder.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E3A78386B3B779B688021FF /* NftOrder.graphql.swift */; }; - 1BC3A49161EB906542F8E23B /* ProtectionAttackType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40E034582FBD2C374CEF808E /* ProtectionAttackType.graphql.swift */; }; - 1C3559D557BC09C709104802 /* NftsTabQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 730CA356D6E1D498CC0C1472 /* NftsTabQuery.graphql.swift */; }; - 1CE49305114BF4792290DDC6 /* AmountChange.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63EA0C75D8011BCC182492C /* AmountChange.graphql.swift */; }; - 1DC34AE3E11264475E8B9F62 /* NftApproval.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E32BBB65A6DD9CFF608C8E8 /* NftApproval.graphql.swift */; }; - 1E2AF2C38C8FBEB2A95B644E /* OffRampTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9D5E6A07F404974045BD525 /* OffRampTransfer.graphql.swift */; }; - 252BD40CB9B46FE47B2440B1 /* NftCollection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63ACFF2A1D5AE3498731AC69 /* NftCollection.graphql.swift */; }; - 257C7A9F2C4AC3C99C6B7348 /* TransactionHistoryUpdaterQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF3D7A923E5E1FB504E1F64D /* TransactionHistoryUpdaterQuery.graphql.swift */; }; - 2B12DFE797E2CBF314565D81 /* TokenProtectionInfoParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0BB4A40E30E1D985519DB3CF /* TokenProtectionInfoParts.graphql.swift */; }; - 2B422D705C68BB51FBDA9895 /* NFTItemScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207F4DF7664454C31DE069F8 /* NFTItemScreenQuery.graphql.swift */; }; - 2E05037B51AF526A47701B09 /* TransactionDirection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2FBE9F353D3753FB94B8BF /* TransactionDirection.graphql.swift */; }; - 302E24504C4FCE674CF95984 /* TokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98F596250F55A3D6E9E1F499 /* TokenParts.graphql.swift */; }; - 30872BFB39EEC66944E3EFD7 /* MobileSchema.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4E0C448C5D45BF1071FA458 /* MobileSchema.graphql.swift */; }; - 31661D70B58410EA030E2C53 /* TimestampedAmount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B0DB67D6C438F623976727E9 /* TimestampedAmount.graphql.swift */; }; - 326CFFDBB89D95FA4CB95EBB /* HistoryDuration.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BF42F88002431BFDC9FCA9F /* HistoryDuration.graphql.swift */; }; - 3361FE5837059AEF4AA877BE /* TransactionListQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 224CA82F1871016F4173F69E /* TransactionListQuery.graphql.swift */; }; - 33928A7366AFE7F892CDC89F /* NftBalanceConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FC7B80EC0E0D2134B67575B /* NftBalanceConnection.graphql.swift */; }; - 35B8176433A98BA798BBEE79 /* PageInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E475EBABF7EA310435E2F19C /* PageInfo.graphql.swift */; }; - 36E601F269D40A67FC353947 /* AssetChange.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39F60B2B54796DC978765C99 /* AssetChange.graphql.swift */; }; - 3E338E14E33C721DAB708664 /* NftActivityFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43E2ECCEFBC3BE8D13BE6CAB /* NftActivityFilterInput.graphql.swift */; }; - 3FE25F715DFFD1D03DCDA57D /* NftContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B9EB43501127BE1B14F1A5 /* NftContract.graphql.swift */; }; + 19AB4E8D176A5656BBB2D797 /* TokenProjectsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1B3A0C152B683215291E3BD /* TokenProjectsQuery.graphql.swift */; }; + 1A4145B8CC5F5F5B9297B9D6 /* NftAsset.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD3573B4E6FD61F5DDBD9C75 /* NftAsset.graphql.swift */; }; + 1B9BE681A85AE55F18054F37 /* NftTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 792EF7A7820987EA9E99D792 /* NftTransfer.graphql.swift */; }; + 1E01B5593B54A53D822972C1 /* HomeScreenTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB3EEA677984132330D8D279 /* HomeScreenTokenParts.graphql.swift */; }; + 1F9BD005762B8BFA302E1B0C /* NftBalanceConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 56093E216B82288E1A4018F0 /* NftBalanceConnection.graphql.swift */; }; + 1FB2F652907A72BD28CF6DD4 /* OffRampTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3063061F7C203A1DA47B8FBB /* OffRampTransfer.graphql.swift */; }; + 213BE982C7E2CCE304B88A6E /* Image.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 284C5150E9B50FB5BE531572 /* Image.graphql.swift */; }; + 23FC1CDFE7B5E25CEDF338F2 /* TokenProjectMarketsParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BA467FB84503BB53B081C73 /* TokenProjectMarketsParts.graphql.swift */; }; + 29BC0E8E68D0365EB77456AF /* PortfolioBalancesQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8604CAA8F7E44D704A6F4AAF /* PortfolioBalancesQuery.graphql.swift */; }; + 2C7ED9DF09914F82D85DE7C9 /* NftBalanceEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F182567B705FC0E15DA8F34A /* NftBalanceEdge.graphql.swift */; }; + 2DE0A41ECCCE673BAE68F715 /* FeeData.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 290B66F38FEC486AF1709BE4 /* FeeData.graphql.swift */; }; + 2FF904EA65F2A7B960547609 /* NftBalancesFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70F4D05A45BC18EA88868A0 /* NftBalancesFilterInput.graphql.swift */; }; + 333C6D9C267BB7783F9CA56E /* TokenBalanceMainParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F0C115796D33739746778E3 /* TokenBalanceMainParts.graphql.swift */; }; + 340A73A44EC57C9376FF24E9 /* SwapOrderType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A7D41CB7AA0496123E2153D /* SwapOrderType.graphql.swift */; }; + 37A47FF4EEEB8E9D839D5DD6 /* TokenMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A02D64B698D203A7F3643C18 /* TokenMarket.graphql.swift */; }; + 3B27BBB18E152F19B68D6FAB /* TokenMarketParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6754D4BD9CE76AAC915F814 /* TokenMarketParts.graphql.swift */; }; + 44B612C45F986F8ACB66D366 /* Query.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 58641E30674C4C4241702902 /* Query.graphql.swift */; }; + 45FFF7DF2E8C2A8100362570 /* SilentPushEventEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 45FFF7DE2E8C2A6400362570 /* SilentPushEventEmitter.m */; }; + 45FFF7E12E8C2E6900362570 /* SilentPushEventEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45FFF7E02E8C2E6100362570 /* SilentPushEventEmitter.swift */; }; + 462433873C56042BDAA3D8E1 /* MultiplePortfolioBalancesQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3AF9944E853669C2BC9B5AB /* MultiplePortfolioBalancesQuery.graphql.swift */; }; 463BA791004B1B7AC1773914 /* Pods_Uniswap.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2226DF79BEAFECEE11A51347 /* Pods_Uniswap.framework */; }; - 4917B04DB81579CF4243A1DA /* Chain.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A8990A17F489F284E0DB1B3 /* Chain.graphql.swift */; }; - 4BB9E218D89B8AF5E4E27CCB /* NftOrderEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49BD6C776029FAF4FC711DE7 /* NftOrderEdge.graphql.swift */; }; - 4C187202229BDC4D8898E067 /* Dimensions.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B1C71AED738D17898103A6 /* Dimensions.graphql.swift */; }; - 4EF8D293BB1EBCFBFC65A330 /* TokenTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DA4E7C052A6C4AC3A9DCDA6 /* TokenTransfer.graphql.swift */; }; - 4FED6AAF896BA371C56D88B1 /* Portfolio.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1849039FD4A210DA990363C8 /* Portfolio.graphql.swift */; }; - 50C89DFAF2DBC80D6343B164 /* NftAssetTraitInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 556339705BD103365D4ED07B /* NftAssetTraitInput.graphql.swift */; }; - 553E6B467BEE49C9984691C5 /* OnRampTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2139ABF56EA067ED792A48C9 /* OnRampTransfer.graphql.swift */; }; - 5804A1B1BB144D9EFBBE177D /* NftAssetEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DF7C8888BE6AFA2F22889FE /* NftAssetEdge.graphql.swift */; }; + 498F01286C660E8241774216 /* TokenTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93783F96FA74CB9A1BEB32EE /* TokenTransfer.graphql.swift */; }; + 4AA5D71F770AA5E8A5AB8D6A /* TokenBalanceParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B97D41BC2760803FD1C7CB3C /* TokenBalanceParts.graphql.swift */; }; + 4D846E92BAEED7A4F5B72919 /* TokenStandard.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5E0F71E23968DEFCEB7A4B /* TokenStandard.graphql.swift */; }; + 4ECB63464100262E5A163345 /* TokenBalanceQuantityParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5FF4316461BD4FB5847D62 /* TokenBalanceQuantityParts.graphql.swift */; }; + 50F87796A463D0224875F0F4 /* TransactionHistoryUpdaterQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE6A0CAEDE1DC1D2FA14DEA /* TransactionHistoryUpdaterQuery.graphql.swift */; }; + 530FBF9EB2190828460BD496 /* AmountChange.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43AB42F3ED903CF49D7D4DE7 /* AmountChange.graphql.swift */; }; + 554AB3FB0D09B2DBC870E044 /* TokenDetailsScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7ED83FFCB410FCCA2C6B97B /* TokenDetailsScreenQuery.graphql.swift */; }; + 57D4FACDC13E20AD220D7AEC /* Currency.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5DDE9BCC6A539F6E9AE1664 /* Currency.graphql.swift */; }; + 5A9BA87096096C8BDC5FF210 /* SchemaMetadata.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D981A2D31B6F8822C19324 /* SchemaMetadata.graphql.swift */; }; 5B4398EC2DD3B22C00F6BE08 /* PrivateKeyDisplayManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5B4398E82DD3B22C00F6BE08 /* PrivateKeyDisplayManager.m */; }; 5B4398ED2DD3B22C00F6BE08 /* PrivateKeyDisplayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4398E92DD3B22C00F6BE08 /* PrivateKeyDisplayManager.swift */; }; 5B4398EE2DD3B22C00F6BE08 /* PrivateKeyDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4398EA2DD3B22C00F6BE08 /* PrivateKeyDisplayView.swift */; }; 5B4CEC5F2DD65DD4009F082B /* CopyIconOutline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B4CEC5E2DD65DD4009F082B /* CopyIconOutline.swift */; }; - 5C3958DA046B5A84FE90C1BD /* FeeData.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17819A49DE69723EC60D9D72 /* FeeData.graphql.swift */; }; - 5D06BAB366D14A5AFE2355E8 /* TransactionType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69F2FFD11FB5E462CC454A15 /* TransactionType.graphql.swift */; }; + 5CC5D4B276CC1E3BA1906120 /* NftContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A2AA83B4F9F4290A2BC7CCB /* NftContract.graphql.swift */; }; 5E5E0A632D380F5800E166AA /* Env.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E5E0A622D380F5700E166AA /* Env.swift */; }; - 5EFB78362B1E585000E77EAC /* ConvertQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFB78352B1E585000E77EAC /* ConvertQuery.graphql.swift */; }; - 614FCC3D8E9AA8175E950726 /* Currency.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25B4C7C7FEF32EAF10D030F9 /* Currency.graphql.swift */; }; - 61988A922656857278F7CBF9 /* TokenBalanceMainParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 137696908D703632CEE3AB28 /* TokenBalanceMainParts.graphql.swift */; }; - 62B32E9623DA0E939F062033 /* TokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3C3F0403FAEFDD632AC6CCE /* TokensQuery.graphql.swift */; }; - 63BDDE4499AC6B2375C9F795 /* FavoriteTokenCardQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F0EEAC1DADE827AE38EB8A6 /* FavoriteTokenCardQuery.graphql.swift */; }; - 648B919F00D069DE1F0040F6 /* NftMarketplace.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 600250B00B6C02AAB063818B /* NftMarketplace.graphql.swift */; }; + 5F14564B8F6814A85EF53C46 /* TokenSortableField.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D96819BA563CB218D29C657 /* TokenSortableField.graphql.swift */; }; + 5F581541EFD85236EF98D3F3 /* TokenApproval.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 009F4258CE111989223E99A4 /* TokenApproval.graphql.swift */; }; + 5FED383DD705AEFE8B7DA8DC /* TransactionListQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C27225607FACF638581E25B5 /* TransactionListQuery.graphql.swift */; }; + 63E84504F6D89EFBF97F4ACF /* SelectWalletScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB3CBF221D5D2E3C036A1BA0 /* SelectWalletScreenQuery.graphql.swift */; }; 649A7A782D9AE70B00B53589 /* KeychainUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A7A772D9AE70B00B53589 /* KeychainUtils.swift */; }; 649A7A792D9AE70B00B53589 /* KeychainConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 649A7A762D9AE70B00B53589 /* KeychainConstants.swift */; }; - 66D2765A00D8CE3136D28F1F /* SafetyLevel.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4ACFA5B4204741038C986C5D /* SafetyLevel.graphql.swift */; }; - 677FC15A05D9BD23930FAC95 /* TokenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74885865ED8CFEA875E40916 /* TokenQuery.graphql.swift */; }; - 6A60BDC9D46A710D871DEC6E /* NftProfile.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35E518AD7FBFEBEA9CAB95 /* NftProfile.graphql.swift */; }; + 6602483D8F6C6481FB8128E9 /* HistoryDuration.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC6517FF8972DA448CDD2DFC /* HistoryDuration.graphql.swift */; }; + 6682BC9B8E38430BE82FADDA /* NftBalance.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C8BAFEAABF78617095D44E32 /* NftBalance.graphql.swift */; }; + 6B8E2BD6B9A12FF05F826BDB /* TopTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C050FFEC067334CF05C17EA0 /* TopTokensQuery.graphql.swift */; }; 6BC7D07E2B5FF02400617C95 /* ScantasticEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BC7D07B2B5FF02400617C95 /* ScantasticEncryption.m */; }; 6BC7D07F2B5FF02400617C95 /* ScantasticEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC7D07C2B5FF02400617C95 /* ScantasticEncryption.swift */; }; 6BC7D0802B5FF02400617C95 /* EncryptionUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC7D07D2B5FF02400617C95 /* EncryptionUtils.swift */; }; @@ -198,21 +106,26 @@ 6CA91BE12A95226200C4063E /* RNCloudStorageBackupsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6CA91BDE2A95226200C4063E /* RNCloudStorageBackupsManager.m */; }; 6CA91BE22A95226200C4063E /* EncryptionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA91BDF2A95226200C4063E /* EncryptionHelper.swift */; }; 6CA91BE32A95226200C4063E /* RNCloudStorageBackupsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA91BE02A95226200C4063E /* RNCloudStorageBackupsManager.swift */; }; + 6E2E4593B2C40DBBA7C861BF /* IContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D88F29D12AB0D8B4C00D065 /* IContract.graphql.swift */; }; + 6FAEE2539C8AFB99445F29BE /* TokenFeeDataParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 413CEB2443D6DD1DA7CC7231 /* TokenFeeDataParts.graphql.swift */; }; 70EB8338CA39744B7DBD553E /* Pods_WidgetIntentExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1064E23E366D0C2C2B20C30E /* Pods_WidgetIntentExtension.framework */; }; - 71CF37F19F1C138B57CC135C /* TopTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5640432978873FB030467750 /* TopTokenParts.graphql.swift */; }; + 7154698A2773F05CAD639211 /* TokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A266B733FD2FA9C1C1A7731 /* TokenParts.graphql.swift */; }; + 72272D14F0DB24D05F1162FE /* NftCollection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ECFE86020AB7A252CDC9BB1 /* NftCollection.graphql.swift */; }; + 722E7F2ECE654320C2452CC8 /* TokenProtectionInfoParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93D1BA7F635F61FC7E62C6C1 /* TokenProtectionInfoParts.graphql.swift */; }; + 72567EB83861EBC04FAA0941 /* TokenProjectDescriptionQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C66DD1D03A824EB6A231DCE /* TokenProjectDescriptionQuery.graphql.swift */; }; + 74D0262298BD1BE5363444B5 /* TokenBasicProjectParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9FB14C8B68CE1D05643256 /* TokenBasicProjectParts.graphql.swift */; }; + 75FA3F2F18047B759472AEA8 /* NetworkFee.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FB70424D7C456B9B72536C6 /* NetworkFee.graphql.swift */; }; + 77206ACF669BD9DAAC096227 /* PageInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F2898E709490CC198B98228 /* PageInfo.graphql.swift */; }; 77CF6065C8A24FE48204A2C1 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF9176E944C84910B1C0B057 /* SplashScreen.storyboard */; }; - 7BE97D20155AE69FF36C4CB4 /* NftStandard.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B308EC3DB7C3C1DD5DDE03D /* NftStandard.graphql.swift */; }; - 818179F5724AA35E1B1D5A9C /* ProtectionInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0702203B17BDD858CED28F8B /* ProtectionInfo.graphql.swift */; }; + 7B49698C356931577828B41E /* TimestampedAmount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C9E8E39C13967E447FC193F /* TimestampedAmount.graphql.swift */; }; + 7BDC2CBE10DC34F9BCFF86EA /* TokenProjectTokensTvlParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2EE4B5A1180F03030E80A9 /* TokenProjectTokensTvlParts.graphql.swift */; }; 8273FC23FB1AE47B80C5E09F /* Pods_OneSignalNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 15092E550A1C78508ABA3280 /* Pods_OneSignalNotificationServiceExtension.framework */; }; 8385A47D3C765B841F450090 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26D739993D5C939C6FBB58A /* ExpoModulesProvider.swift */; }; - 8410B98A8D7974A941AAF299 /* WidgetTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA33683D2BA6BBE3251D67C /* WidgetTokensQuery.graphql.swift */; }; - 85ADD03923353DB3D6CD7301 /* SwapOrderStatus.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = D574AB5194929038A54B4D4B /* SwapOrderStatus.graphql.swift */; }; - 85D0E81B798D0F2D841386E9 /* NftCollectionMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F4F47D22B749A438B5BF674 /* NftCollectionMarket.graphql.swift */; }; - 869F3639FBC6D8156FFE3BD3 /* TokenProjectMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B9A505509D9A85BE1DB7D05 /* TokenProjectMarket.graphql.swift */; }; - 8950F8354810AA673E1E35DA /* SchemaMetadata.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C65195A1CC9894C6341385C3 /* SchemaMetadata.graphql.swift */; }; - 8ADDD2E2AB1FD9D1D9AF5627 /* BlockaidFees.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD35300F81746B2D4A23065A /* BlockaidFees.graphql.swift */; }; - 8CC06A8205186D0640F0BC55 /* NftAssetConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 218B8AB05C80D919671D3970 /* NftAssetConnection.graphql.swift */; }; - 8CEA6459A5B739C3A0382000 /* TokenProjectsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8A49D011C60137D4034CAC4 /* TokenProjectsQuery.graphql.swift */; }; + 890E98060D98F2F5617D1914 /* HomeScreenTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2240891823DAE7BF8BB88FF4 /* HomeScreenTokensQuery.graphql.swift */; }; + 8989D182DEC9682661D588F3 /* NftApproval.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42158AF500DDBEEF6B7E98E3 /* NftApproval.graphql.swift */; }; + 8B2A92172EB3E78E00990413 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B2A92162EB3E78E00990413 /* AppDelegate.swift */; }; + 8C19BAD465EA9DFEB20EFB24 /* ActivityDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68E391195D4587F61A33380 /* ActivityDetails.graphql.swift */; }; + 8D90B4306573344A1FFC4832 /* OnRampTransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 614A491FAB9659FA08CD9AE4 /* OnRampTransactionDetails.graphql.swift */; }; 8E89C3AE2AB8AAA400C84DE5 /* MnemonicConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E89C3A62AB8AAA400C84DE5 /* MnemonicConfirmationView.swift */; }; 8E89C3AF2AB8AAA400C84DE5 /* MnemonicDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E89C3A72AB8AAA400C84DE5 /* MnemonicDisplayView.swift */; }; 8E89C3B12AB8AAA400C84DE5 /* MnemonicConfirmationWordBankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E89C3A92AB8AAA400C84DE5 /* MnemonicConfirmationWordBankView.swift */; }; @@ -226,114 +139,73 @@ 8EA8AB412AB7ED76004E7EF3 /* AlertTriangleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EA8AB402AB7ED76004E7EF3 /* AlertTriangleIcon.swift */; }; 8EBFB1552ABA6AA6006B32A8 /* PasteIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EBFB1542ABA6AA6006B32A8 /* PasteIcon.swift */; }; 8ED0562C2AA78E2C009BD5A2 /* ScrollFadeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ED0562B2AA78E2C009BD5A2 /* ScrollFadeExtensions.swift */; }; - 8EE7C0582AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE7C0572AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift */; }; - 8FE0F30936E893297558F467 /* ProtectionResult.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD0FC2D534CB82B151C7F9B7 /* ProtectionResult.graphql.swift */; }; - 9127D1362CC2D3D00096F134 /* TokenBalanceMainParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9127D1342CC2D3D00096F134 /* TokenBalanceMainParts.graphql.swift */; }; - 9127D1372CC2D3D00096F134 /* TokenBalanceQuantityParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9127D1352CC2D3D00096F134 /* TokenBalanceQuantityParts.graphql.swift */; }; - 9173CEBC2D03C6F30036DA28 /* TokenBalanceParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9173CEBB2D03C6F30036DA28 /* TokenBalanceParts.graphql.swift */; }; - 91D501702CDBEAE700B09B7F /* TokenMarketParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D501652CDBEAE700B09B7F /* TokenMarketParts.graphql.swift */; }; - 91D501712CDBEAE700B09B7F /* TokenBalanceMainParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D501662CDBEAE700B09B7F /* TokenBalanceMainParts.graphql.swift */; }; - 91D501722CDBEAE700B09B7F /* TokenProjectMarketsParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D501672CDBEAE700B09B7F /* TokenProjectMarketsParts.graphql.swift */; }; - 91D501732CDBEAE700B09B7F /* TokenBasicInfoParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D501682CDBEAE700B09B7F /* TokenBasicInfoParts.graphql.swift */; }; - 91D501742CDBEAE700B09B7F /* TokenBasicProjectParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D501692CDBEAE700B09B7F /* TokenBasicProjectParts.graphql.swift */; }; - 91D501752CDBEAE700B09B7F /* TokenBalanceQuantityParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D5016A2CDBEAE700B09B7F /* TokenBalanceQuantityParts.graphql.swift */; }; - 91D501762CDBEAE700B09B7F /* TokenProtectionInfoParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D5016B2CDBEAE700B09B7F /* TokenProtectionInfoParts.graphql.swift */; }; - 91D501772CDBEAE700B09B7F /* TokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D5016C2CDBEAE700B09B7F /* TokenParts.graphql.swift */; }; - 91D501782CDBEAE700B09B7F /* TokenProjectUrlsParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D5016D2CDBEAE700B09B7F /* TokenProjectUrlsParts.graphql.swift */; }; - 91D501792CDBEAE700B09B7F /* TopTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D5016E2CDBEAE700B09B7F /* TopTokenParts.graphql.swift */; }; - 91D5017A2CDBEAE700B09B7F /* TokenFeeDataParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D5016F2CDBEAE700B09B7F /* TokenFeeDataParts.graphql.swift */; }; - 91D5017E2CDBEAF600B09B7F /* HomeScreenTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D5017C2CDBEAF600B09B7F /* HomeScreenTokenParts.graphql.swift */; }; - 9301D18644F3DFA9ABB8F0BE /* TokenApproval.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E975BEDBED5FE51D0DC6E96 /* TokenApproval.graphql.swift */; }; - 93566FBDE94E1A2D8CC5AB62 /* ConvertQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BD3F0794908635CDFA7DAB6 /* ConvertQuery.graphql.swift */; }; - 9381B5EA5839DA17D07A45F0 /* TokenDetailsScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7A33933670CCAD9E62A1A80 /* TokenDetailsScreenQuery.graphql.swift */; }; - 93AEECDBDB160B5E0C9E3149 /* TokenProjectDescriptionQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF8B660E9C2C0D8DDF7588A /* TokenProjectDescriptionQuery.graphql.swift */; }; - 97CA219E1FFD832D8FA02C20 /* TransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABE51731EB853137302A7B0D /* TransactionDetails.graphql.swift */; }; - 9822D243ED2F5C350404A375 /* HomeScreenTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F8B915042BD8D67D9627EA /* HomeScreenTokenParts.graphql.swift */; }; - 9A3E861F5D7B0F2CB0EFA7A6 /* AssetActivity.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4624A41EC1468712BD77653 /* AssetActivity.graphql.swift */; }; - 9AF0D1FDF2BFB17FB732C5FD /* SchemaConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E49E724432C70A2551FB2C7 /* SchemaConfiguration.swift */; }; - 9B6C88F7D8D542AAC353A3EA /* NftOrderConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39FC2A13550FA6754EC1A7FC /* NftOrderConnection.graphql.swift */; }; - 9F00A43A2B33894C0088A0D0 /* ApplicationContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F00A4392B33894C0088A0D0 /* ApplicationContract.graphql.swift */; }; - 9F29D4ED2B47126D004D003A /* NftBalanceAssetInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F29D4EC2B47126D004D003A /* NftBalanceAssetInput.graphql.swift */; }; + 90B93D7970186C8FC83F9292 /* TokenBasicInfoParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319AAC5377C60EB6579CC5C6 /* TokenBasicInfoParts.graphql.swift */; }; + 914DFEC13BB1853D25D23E34 /* ProtectionAttackType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE4AB3FCC91BE6C73557E0E3 /* ProtectionAttackType.graphql.swift */; }; + 939256080FE6141E53B49E90 /* Dimensions.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DCC7E203AB163C9C2D3D789 /* Dimensions.graphql.swift */; }; + 95AA27A2056B51265EE643F1 /* AssetActivity.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BB8B6395E3C86412FA793CB /* AssetActivity.graphql.swift */; }; + 99ED3ACCFE04709CFA7FD490 /* WidgetTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0C956C520AAB54BCE8C5CCC /* WidgetTokensQuery.graphql.swift */; }; + 9F1AE0C43E80AE592CA4AD7E /* ProtectionInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346B304DE3207797FD1FDE71 /* ProtectionInfo.graphql.swift */; }; 9F78980B2A819CC4004D5A98 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072F6C222A44A32E00DA720A /* SwiftUI.framework */; }; 9F78980E2A819D2B004D5A98 /* WidgetsCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */; }; 9F7898112A819D32004D5A98 /* WidgetsCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */; }; 9F7898142A819D62004D5A98 /* WidgetsCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */; }; 9F7898152A819D62004D5A98 /* WidgetsCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 9F8123832D2F33C4009C5D88 /* BlockaidFees.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F8123822D2F33C4009C5D88 /* BlockaidFees.graphql.swift */; }; - 9F813E9E2AA8FB5700438D89 /* ActivityDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F813E9D2AA8FB5700438D89 /* ActivityDetails.graphql.swift */; }; - 9F813EA02AA8FB7500438D89 /* TransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F813E9F2AA8FB7500438D89 /* TransactionDetails.graphql.swift */; }; - 9F813EA22AA8FB8C00438D89 /* SwapOrderDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F813EA12AA8FB8C00438D89 /* SwapOrderDetails.graphql.swift */; }; - 9F813EA42AA8FBCF00438D89 /* TransactionType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F813EA32AA8FBCF00438D89 /* TransactionType.graphql.swift */; }; 9FCEBF002A95A8E00079EDDB /* RNWalletConnect.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FCEBEFE2A95A8E00079EDDB /* RNWalletConnect.m */; }; 9FCEBF012A95A8E00079EDDB /* RNWalletConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCEBEFF2A95A8E00079EDDB /* RNWalletConnect.swift */; }; 9FCEBF042A95A99C0079EDDB /* RCTThemeModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FCEBF032A95A99B0079EDDB /* RCTThemeModule.m */; }; - 9FEC9B8B2A858CF1003CD019 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FEC9B8A2A858CF1003CD019 /* AppDelegate.m */; }; - A104024A1861354EC1DD53C1 /* PortfolioBalancesQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 985DDC8324B872DD44E87781 /* PortfolioBalancesQuery.graphql.swift */; }; + A0ACC9C3ABF174616E0CBCA4 /* OffRampTransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AFC48B9A573915BFA6FC6E2 /* OffRampTransactionDetails.graphql.swift */; }; A32F9FBD272343C9002CFCDB /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A32F9FBC272343C8002CFCDB /* GoogleService-Info.plist */; }; - A3318C676D7FBF78A1583B16 /* NftCollectionEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37038CCEB11E70E2027EF4EA /* NftCollectionEdge.graphql.swift */; }; A3551F2CAC134AD49D40927F /* Basel-Grotesk-Book.otf in Resources */ = {isa = PBXBuildFile; fileRef = 6F33E8069B7B40AFB313B8B0 /* Basel-Grotesk-Book.otf */; }; + A3EEE7EE3CC93DDBA3CE6A5C /* PortfolioValueModifier.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E13150074BB66C20E7EA271A /* PortfolioValueModifier.graphql.swift */; }; A3F0A5B1272B1DFA00895B25 /* KeychainSwiftDistrib.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3F0A5B0272B1DFA00895B25 /* KeychainSwiftDistrib.swift */; }; - A70E4DD42C25DA0A002D6D86 /* NetworkFee.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70E4DD32C25DA0A002D6D86 /* NetworkFee.graphql.swift */; }; - A70E4DD72C260416002D6D86 /* SwapOrderType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70E4DD52C260416002D6D86 /* SwapOrderType.graphql.swift */; }; - A70E4DD82C260416002D6D86 /* SwapOrderStatus.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A70E4DD62C260416002D6D86 /* SwapOrderStatus.graphql.swift */; }; - A7B8EFCB2BF68F0D00CA4A1C /* FeeData.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B8EFCA2BF68F0D00CA4A1C /* FeeData.graphql.swift */; }; - A974633048E27D5D23420F34 /* TokenBasicInfoParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3377F7BB38BD5A45AE31909D /* TokenBasicInfoParts.graphql.swift */; }; - AA3AE4E5C2AAC3F9460A3637 /* NftBalanceAssetInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E5FA43C5E31035775917C4E /* NftBalanceAssetInput.graphql.swift */; }; AC0EE0982BD826E700BCCF07 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = AC0EE0972BD826E700BCCF07 /* PrivacyInfo.xcprivacy */; }; AC2EF4032C914B1600EEEFDB /* fonts in Resources */ = {isa = PBXBuildFile; fileRef = AC2EF4022C914B1600EEEFDB /* fonts */; }; - AC70FF8207ED26561B634E30 /* NftsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9030AC59702756C39396FFE /* NftsQuery.graphql.swift */; }; - ADE104A101B3DFFBDB308189 /* Query.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED157D3DB73302C8A1B9522E /* Query.graphql.swift */; }; - AF83E7713BB625D787DD1A1D /* TokenMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBD833F6580F9B82A68D4A98 /* TokenMarket.graphql.swift */; }; + ADD898CA4B87F6E7C990E268 /* NftCollectionMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66697C913CF365C9460F42E6 /* NftCollectionMarket.graphql.swift */; }; + AEA0B1AC57BB6F11F5861BCE /* Token.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88B481513A2E780E6489DC4F /* Token.graphql.swift */; }; + AF467A9F1C200706537B24E5 /* TokenBalance.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AF8EE05291534B82ACC1105 /* TokenBalance.graphql.swift */; }; + B009D229E81544EA0F47C6DD /* TransactionStatus.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A182D5FF8C0919083F2E333C /* TransactionStatus.graphql.swift */; }; B193AD315CF844A3BDC3D11D /* Basel-Grotesk-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 3C606D2C81014A0A8898F38E /* Basel-Grotesk-Medium.otf */; }; - B377DB0418EA15695F208D9F /* NetworkFee.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BEADEB4AE05B860CF2232D1 /* NetworkFee.graphql.swift */; }; - B61E182CF4937B5169885C95 /* OnRampTransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12D3AB6C2FC9513E9F224B7C /* OnRampTransactionDetails.graphql.swift */; }; - B746C09DA19B4C7F9C700989 /* NftActivityConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F912AB9AB67808B740246798 /* NftActivityConnection.graphql.swift */; }; - BA83003638263D702D03C3C1 /* ActivityDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A4EB7BAD2C90E61FD6C30AF /* ActivityDetails.graphql.swift */; }; - BA869E372D56B0B600D7A718 /* WidgetTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA869E362D56B0B600D7A718 /* WidgetTokensQuery.graphql.swift */; }; + B2692B7521F4E7767DECE974 /* NftsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42013D777BAA40652AB28985 /* NftsQuery.graphql.swift */; }; + B4F84A94618C5A8D4F12007E /* ContractInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 957594D21A42B9D77F5AD685 /* ContractInput.graphql.swift */; }; + B5EF58A67FC42D684B96C0F0 /* TokenProjectMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3E638BE80543E905D072D7F /* TokenProjectMarket.graphql.swift */; }; + B83B63F900E565F5C3F11589 /* FavoriteTokenCardQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48DE72B77CD2D89B75BB934D /* FavoriteTokenCardQuery.graphql.swift */; }; + B8ADB9BF8BB2D4E456D80B23 /* NftStandard.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D973A66C23859D34E6FE175 /* NftStandard.graphql.swift */; }; BA8FC9627A40644259D9E2F9 /* Pods_WidgetsCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB29AC0C0907A833F23D2C30 /* Pods_WidgetsCore.framework */; }; - BD1FF76A8E50EFEA0720CC04 /* NftAsset.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2932B486DD7070DF226A27B /* NftAsset.graphql.swift */; }; - BD59A9C8414D6A4BFE9C9A2E /* NftActivity.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1A24D364C0D262A14BB7182 /* NftActivity.graphql.swift */; }; - C1FC1F8B4A2725A195F66D60 /* PortfolioValueModifier.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41003491EF939043CF9D0F4E /* PortfolioValueModifier.graphql.swift */; }; - C791C5505DBB3036B9D5066D /* SwapOrderType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 148617FD73F2CD7117960DBA /* SwapOrderType.graphql.swift */; }; - C7ABAADA504107D152A52FD4 /* NftBalancesFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41BA2129FFE961C8549C8A30 /* NftBalancesFilterInput.graphql.swift */; }; - C8338C5BE951EF9111C48217 /* TokenBasicProjectParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2F4BE3080D4129D4478528 /* TokenBasicProjectParts.graphql.swift */; }; - C8402101FF510BAA358DCA46 /* TokenBalanceParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0C34E94906CB9F0A016D566 /* TokenBalanceParts.graphql.swift */; }; - CA0EDABCA1B6C2B0936BF5CF /* NftBalanceEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B132543F80E3C9191219E6D /* NftBalanceEdge.graphql.swift */; }; - CCBC45FD4310D8153D859361 /* TransactionStatus.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ABA47847A73130D356EB6BF /* TransactionStatus.graphql.swift */; }; - CE1DF567D58943ACCBB8360C /* Amount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = D20E9461A6C9C1CD6A24BEED /* Amount.graphql.swift */; }; - CF2BAECCF9A2EC43AC3EC583 /* OnRampServiceProvider.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A84A192F750A0F1BC8D7A69 /* OnRampServiceProvider.graphql.swift */; }; - CF36F741187285B430EFE557 /* NftBalance.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 05BE2F49DD1FB9A692E21C13 /* NftBalance.graphql.swift */; }; + BD07375602C71B48961CD5A0 /* Portfolio.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A34E3A6E13A4066767AA6757 /* Portfolio.graphql.swift */; }; + C403639465231038D196D7B5 /* BridgedWithdrawalInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44D824154584CD39C82904E9 /* BridgedWithdrawalInfo.graphql.swift */; }; + CA4994AA5EF5F36F42117ECA /* TopTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D7F54953D864DC067FADD1 /* TopTokenParts.graphql.swift */; }; + CBFF07DDFB9C638D6E383290 /* SchemaConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D25B69F4A69D733A5DAEC42 /* SchemaConfiguration.swift */; }; + CFA408C9A92DB1F808BA57CD /* MobileSchema.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A24882D069B34B7DC9E8B4CE /* MobileSchema.graphql.swift */; }; + D0AC45D21734567F720877AD /* TokenProjectUrlsParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F1055EFA8467996754739A7 /* TokenProjectUrlsParts.graphql.swift */; }; + D1642682124F702EE2454A64 /* IAmount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A372929BEDB706B6144334F0 /* IAmount.graphql.swift */; }; + D179B805D7A77EA127F41F13 /* DescriptionTranslations.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F02DBC2C51EFC6B1AA9C1CD /* DescriptionTranslations.graphql.swift */; }; D3B63ACA9B0C42F68080B080 /* InputMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 1834199AFFB04D91B05FFB64 /* InputMono-Regular.ttf */; }; - D660CA59775C3634324037ED /* MultiplePortfolioBalancesQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4429CA6DA254ECC24A65DA14 /* MultiplePortfolioBalancesQuery.graphql.swift */; }; - D680C4844F2C5DACF5D0892E /* NftAssetsFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B477EBEA1EDA3914C77E8A6 /* NftAssetsFilterInput.graphql.swift */; }; + D420E10F9BA8C789530E70F4 /* ApplicationContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BF540E1110D0FD1C219C45C /* ApplicationContract.graphql.swift */; }; + D6B69EFB74ACEF485C289266 /* ConvertQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D3112654E5722A2F862D3FC /* ConvertQuery.graphql.swift */; }; D7926D4A878B2237137B300F /* Pods_WidgetsCoreTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 021E59CE7ECBD4FE0F3BFCFD /* Pods_WidgetsCoreTests.framework */; }; - D7E3642851C618D369D26B82 /* TokenMarketParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC36D963A2A52D1E7CA77E4 /* TokenMarketParts.graphql.swift */; }; - D81BAFFCC105668B88B607FC /* NftCollectionConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92DE390311705FF1EE65C0E6 /* NftCollectionConnection.graphql.swift */; }; - DC0641C5870F91D26C914F1D /* TokenPriceHistoryQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DFBD29C0BDFDB4464B8189C /* TokenPriceHistoryQuery.graphql.swift */; }; + D86DB22D27B00DA71F968324 /* TransactionType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37A6D5D76D31F8C5CDD29814 /* TransactionType.graphql.swift */; }; DE2F24512E7204C2CA255C50 /* Pods_Widgets.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2E8B7D36D2E14D9488F351EB /* Pods_Widgets.framework */; }; - E091F379106E92D3181103CB /* IAmount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AD8C2B1226EBE1524F2573 /* IAmount.graphql.swift */; }; - E0F63BA3A99DB10FA1CCAB5E /* TokenProjectUrlsParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 236126979349098B98C70F27 /* TokenProjectUrlsParts.graphql.swift */; }; - E24ED8688E9B18BBD543F8F0 /* TokenBalance.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4B149117182C6CC58DD570C /* TokenBalance.graphql.swift */; }; + DEBB37600A7C5C9A273DA38E /* BlockaidFees.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201FF7E30EC98ABC121BC8BF /* BlockaidFees.graphql.swift */; }; + DFBC904E6C0B818152912819 /* OnRampServiceProvider.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E5478334318BD5D6F8366D6 /* OnRampServiceProvider.graphql.swift */; }; E4B3067A930D2E57558E5229 /* Pods_Uniswap_UniswapTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0929C0B4AE1570B8C0B45D4D /* Pods_Uniswap_UniswapTests.framework */; }; - E54093DC857B8C634804D42B /* NftTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3317EBD4E3B11E2F2A1CE58 /* NftTransfer.graphql.swift */; }; - EF05F69E61EA8A16C6A66D53 /* IContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0D68CC8D1F13D864F069BB /* IContract.graphql.swift */; }; - EF3D92CA76DEE66090F18D0C /* NftCollectionScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE0973BB445AAD8FAA25757F /* NftCollectionScreenQuery.graphql.swift */; }; - F0D6A92BB4FCE19CDFA5BBE1 /* HomeScreenTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7E03AA02B39A7A90C96AA76 /* HomeScreenTokensQuery.graphql.swift */; }; - F2EB62621E64B57B07DE8B13 /* Token.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647933A5DC35BDFEEF3620A1 /* Token.graphql.swift */; }; + E6CC238CB9AF565DE89087B7 /* SafetyLevel.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41FFD02A227ACFB661A44813 /* SafetyLevel.graphql.swift */; }; + E774B10E7FB502BE97D3895B /* ProtectionResult.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA586C522ABE5DA5363D27C7 /* ProtectionResult.graphql.swift */; }; + ECB6546D9AA307163172BEA3 /* NftApproveForAll.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F99987D29B574C20584CE11A /* NftApproveForAll.graphql.swift */; }; + EEEE88236C7EBC4B67BBE858 /* TokenProject.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = A424FA93DB720DD7684C7674 /* TokenProject.graphql.swift */; }; F35AFD3E27EE49990011A725 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35AFD3D27EE49990011A725 /* NotificationService.swift */; }; F35AFD4227EE49990011A725 /* OneSignalNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F35AFD3B27EE49990011A725 /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - F53121FB1B070DB9A81347DF /* TokenProjectMarketsParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC055C3EDFDE2966FCA83C9E /* TokenProjectMarketsParts.graphql.swift */; }; - F5B577CA1201CB6FF04A9A58 /* ApplicationContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608B07304ED1387B5AEAA3BB /* ApplicationContract.graphql.swift */; }; - F772B09295DABC5E8895C1C5 /* SelectWalletScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = B033206DC974BCB6AF8CC613 /* SelectWalletScreenQuery.graphql.swift */; }; - FA8EB6A63AB3F56194C6CFB8 /* TokenBalanceQuantityParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2370BAD675DC8064C00AC037 /* TokenBalanceQuantityParts.graphql.swift */; }; - FBE634E2AEC7A62B89863366 /* ContractInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7B3C6DF5DED22E892851B10 /* ContractInput.graphql.swift */; }; - FC7C117CEA5EA63460E6A2C6 /* NftApproveForAll.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5BCBB5D46312050E4A5BD64 /* NftApproveForAll.graphql.swift */; }; - FD4E55146E046C7F5983A379 /* FeedTransactionListQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2BA0ACE5B9B2C41187CD41F3 /* FeedTransactionListQuery.graphql.swift */; }; + F36073C9BDFF611771D04683 /* TokenPriceHistoryQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDA02DA46906E4CDF827104A /* TokenPriceHistoryQuery.graphql.swift */; }; + F3E5BB84916B1F496A0C740D /* TokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5442480B3C91EDC515728606 /* TokensQuery.graphql.swift */; }; + F5FD688A33BCADBF601A2D7F /* TransactionDirection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EC9CABC0B4E73B25C7A2128 /* TransactionDirection.graphql.swift */; }; + F6EA6445BF4E0DEEDD076C7A /* Chain.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25DDE9DCAFA0EE2838508994 /* Chain.graphql.swift */; }; + F784F1CA9FCEB5FFE4C1DD62 /* NftProfile.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21EB4FB53B7B6B5848946D9 /* NftProfile.graphql.swift */; }; + F7B8B2FAEB30B3343CFF6F29 /* SwapOrderStatus.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 132FC1B05AE3D510C3B71EBA /* SwapOrderStatus.graphql.swift */; }; + F7CA47B62C91F3B0DA4106C0 /* AssetChange.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = F38DFE921F0467FF51A3D717 /* AssetChange.graphql.swift */; }; + F8E54B79B2742008CF1C0AA9 /* OnRampTransactionsAuth.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38EAF64134981DC76B6C7E90 /* OnRampTransactionsAuth.graphql.swift */; }; + FAB057109F187E5375D137FD /* Amount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74673620CE906C9AF34C6750 /* Amount.graphql.swift */; }; FD54D51D296C79A4007A37E9 /* GoogleServiceInfo in Resources */ = {isa = PBXBuildFile; fileRef = FD54D51C296C79A4007A37E9 /* GoogleServiceInfo */; }; FD7304CE28A364FC0085BDEA /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FD7304CD28A364FC0085BDEA /* Colors.xcassets */; }; FD7304D028A3650A0085BDEA /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7304CF28A3650A0085BDEA /* Colors.swift */; }; - FD84AA379C4562598807843D /* TokenStandard.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC419570F5A025E80AB10A72 /* TokenStandard.graphql.swift */; }; - FE9CE5C3B5A456B249FC34C5 /* NftActivityEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DB4379B5E222207CA7328D0 /* NftActivityEdge.graphql.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -430,22 +302,15 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0013F5F62C93399400D6EF09 /* ProtectionInfo.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProtectionInfo.graphql.swift; path = MobileSchema/Schema/Objects/ProtectionInfo.graphql.swift; sourceTree = ""; }; - 00265F772C933CE300A5DA57 /* ProtectionResult.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProtectionResult.graphql.swift; path = MobileSchema/Schema/Enums/ProtectionResult.graphql.swift; sourceTree = ""; }; - 00265F782C933CE300A5DA57 /* ProtectionAttackType.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProtectionAttackType.graphql.swift; path = MobileSchema/Schema/Enums/ProtectionAttackType.graphql.swift; sourceTree = ""; }; + 009F4258CE111989223E99A4 /* TokenApproval.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenApproval.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenApproval.graphql.swift; sourceTree = ""; }; 00E356EE1AD99517003FC87E /* UniswapTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UniswapTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 00E356F21AD99517003FC87E /* UniswapTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UniswapTests.m; sourceTree = ""; }; - 018603D0FA4D05DBF16F1441 /* TokenFeeDataParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenFeeDataParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenFeeDataParts.graphql.swift; sourceTree = ""; }; 021E59CE7ECBD4FE0F3BFCFD /* Pods_WidgetsCoreTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WidgetsCoreTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 037C5AA92C04970B00B1D808 /* CopyIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyIcon.swift; sourceTree = ""; }; - 03AD8C2B1226EBE1524F2573 /* IAmount.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = IAmount.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Interfaces/IAmount.graphql.swift; sourceTree = ""; }; 03C788222C10E7390011E5DC /* ActionButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtons.swift; sourceTree = ""; }; 03D2F3172C218D380030D987 /* RelativeOffsetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelativeOffsetView.swift; sourceTree = ""; }; - 04E5C2900254E7BA7F10CE51 /* TopTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TopTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TopTokensQuery.graphql.swift; sourceTree = ""; }; - 05BE2F49DD1FB9A692E21C13 /* NftBalance.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalance.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftBalance.graphql.swift; sourceTree = ""; }; 065A981F892F7A06A900FCD5 /* Pods-WidgetsCoreTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCoreTests.dev.xcconfig"; path = "Target Support Files/Pods-WidgetsCoreTests/Pods-WidgetsCoreTests.dev.xcconfig"; sourceTree = ""; }; - 0702203B17BDD858CED28F8B /* ProtectionInfo.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtectionInfo.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/ProtectionInfo.graphql.swift; sourceTree = ""; }; 0703EE022A5734A600AED1DA /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; 070480372A58A507009006CE /* WidgetIntentExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetIntentExtension.entitlements; sourceTree = ""; }; 0712B3629C74D1F958DF35FB /* Pods-Uniswap-UniswapTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap-UniswapTests.dev.xcconfig"; path = "Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests.dev.xcconfig"; sourceTree = ""; }; @@ -465,89 +330,10 @@ 072F6C372A44BECC00DA720A /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; 074086F92A703B76006E3053 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = ""; }; 0741433F2A588F5800A157D3 /* Structs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Structs.swift; sourceTree = ""; }; - 074321902A83E3C900F8518D /* TokenDetailsScreenQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenDetailsScreenQuery.graphql.swift; sourceTree = ""; }; - 074321932A83E3C900F8518D /* NftsTabQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftsTabQuery.graphql.swift; sourceTree = ""; }; - 074321942A83E3C900F8518D /* TokenQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenQuery.graphql.swift; sourceTree = ""; }; - 074321962A83E3C900F8518D /* TopTokensQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopTokensQuery.graphql.swift; sourceTree = ""; }; - 074321972A83E3C900F8518D /* TokenPriceHistoryQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenPriceHistoryQuery.graphql.swift; sourceTree = ""; }; - 074321982A83E3C900F8518D /* FavoriteTokenCardQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteTokenCardQuery.graphql.swift; sourceTree = ""; }; - 074321992A83E3C900F8518D /* NFTItemScreenQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NFTItemScreenQuery.graphql.swift; sourceTree = ""; }; - 0743219A2A83E3C900F8518D /* NftCollectionScreenQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionScreenQuery.graphql.swift; sourceTree = ""; }; - 0743219B2A83E3C900F8518D /* SearchTokensQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTokensQuery.graphql.swift; sourceTree = ""; }; - 0743219C2A83E3C900F8518D /* ExploreSearchQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExploreSearchQuery.graphql.swift; sourceTree = ""; }; - 0743219D2A83E3C900F8518D /* TokenProjectsQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenProjectsQuery.graphql.swift; sourceTree = ""; }; - 0743219E2A83E3C900F8518D /* SelectWalletScreenQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectWalletScreenQuery.graphql.swift; sourceTree = ""; }; - 0743219F2A83E3C900F8518D /* PortfolioBalancesQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PortfolioBalancesQuery.graphql.swift; sourceTree = ""; }; - 074321A02A83E3C900F8518D /* NftsQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftsQuery.graphql.swift; sourceTree = ""; }; - 074321A12A83E3C900F8518D /* TransactionListQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionListQuery.graphql.swift; sourceTree = ""; }; - 074321A22A83E3C900F8518D /* TransactionHistoryUpdaterQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionHistoryUpdaterQuery.graphql.swift; sourceTree = ""; }; - 074321A42A83E3C900F8518D /* TopTokenParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopTokenParts.graphql.swift; sourceTree = ""; }; - 074321A52A83E3C900F8518D /* MobileSchema.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MobileSchema.graphql.swift; sourceTree = ""; }; - 074321A82A83E3C900F8518D /* AssetChange.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetChange.graphql.swift; sourceTree = ""; }; - 074321AA2A83E3C900F8518D /* HistoryDuration.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryDuration.graphql.swift; sourceTree = ""; }; - 074321AB2A83E3C900F8518D /* TokenStandard.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenStandard.graphql.swift; sourceTree = ""; }; - 074321AC2A83E3C900F8518D /* Currency.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Currency.graphql.swift; sourceTree = ""; }; - 074321AE2A83E3C900F8518D /* Chain.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Chain.graphql.swift; sourceTree = ""; }; - 074321AF2A83E3C900F8518D /* TokenSortableField.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenSortableField.graphql.swift; sourceTree = ""; }; - 074321B02A83E3C900F8518D /* TransactionDirection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDirection.graphql.swift; sourceTree = ""; }; - 074321B12A83E3C900F8518D /* NftStandard.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftStandard.graphql.swift; sourceTree = ""; }; - 074321B22A83E3C900F8518D /* NftActivityType.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityType.graphql.swift; sourceTree = ""; }; - 074321B32A83E3C900F8518D /* SafetyLevel.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SafetyLevel.graphql.swift; sourceTree = ""; }; - 074321B42A83E3C900F8518D /* NftMarketplace.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftMarketplace.graphql.swift; sourceTree = ""; }; - 074321B52A83E3C900F8518D /* TransactionStatus.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionStatus.graphql.swift; sourceTree = ""; }; - 074321B72A83E3C900F8518D /* Image.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Image.graphql.swift; sourceTree = ""; }; - 074321B82A83E3C900F8518D /* NftOrderEdge.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftOrderEdge.graphql.swift; sourceTree = ""; }; - 074321B92A83E3C900F8518D /* NftApproveForAll.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftApproveForAll.graphql.swift; sourceTree = ""; }; - 074321BA2A83E3C900F8518D /* NftAssetTrait.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetTrait.graphql.swift; sourceTree = ""; }; - 074321BB2A83E3C900F8518D /* NftBalanceConnection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftBalanceConnection.graphql.swift; sourceTree = ""; }; - 074321BC2A83E3C900F8518D /* NftActivityEdge.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityEdge.graphql.swift; sourceTree = ""; }; - 074321BD2A83E3C900F8518D /* NftCollection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollection.graphql.swift; sourceTree = ""; }; - 074321BE2A83E3C900F8518D /* TimestampedAmount.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimestampedAmount.graphql.swift; sourceTree = ""; }; - 074321BF2A83E3C900F8518D /* NftAssetConnection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetConnection.graphql.swift; sourceTree = ""; }; - 074321C02A83E3C900F8518D /* TokenProject.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenProject.graphql.swift; sourceTree = ""; }; - 074321C12A83E3C900F8518D /* NftTransfer.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftTransfer.graphql.swift; sourceTree = ""; }; - 074321C22A83E3C900F8518D /* TokenApproval.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenApproval.graphql.swift; sourceTree = ""; }; - 074321C32A83E3C900F8518D /* NftOrderConnection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftOrderConnection.graphql.swift; sourceTree = ""; }; - 074321C42A83E3C900F8518D /* TokenMarket.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenMarket.graphql.swift; sourceTree = ""; }; - 074321C52A83E3C900F8518D /* NftCollectionMarket.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionMarket.graphql.swift; sourceTree = ""; }; - 074321C62A83E3C900F8518D /* TokenBalance.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenBalance.graphql.swift; sourceTree = ""; }; - 074321C72A83E3C900F8518D /* NftOrder.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftOrder.graphql.swift; sourceTree = ""; }; - 074321C82A83E3C900F8518D /* Portfolio.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Portfolio.graphql.swift; sourceTree = ""; }; - 074321C92A83E3C900F8518D /* PageInfo.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageInfo.graphql.swift; sourceTree = ""; }; - 074321CA2A83E3C900F8518D /* NftAssetEdge.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetEdge.graphql.swift; sourceTree = ""; }; - 074321CB2A83E3C900F8518D /* NftCollectionConnection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionConnection.graphql.swift; sourceTree = ""; }; - 074321CC2A83E3C900F8518D /* NftContract.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftContract.graphql.swift; sourceTree = ""; }; - 074321CD2A83E3C900F8518D /* NftActivityConnection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityConnection.graphql.swift; sourceTree = ""; }; - 074321CE2A83E3C900F8518D /* Amount.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Amount.graphql.swift; sourceTree = ""; }; - 074321CF2A83E3C900F8518D /* Query.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Query.graphql.swift; sourceTree = ""; }; - 074321D02A83E3C900F8518D /* NftAsset.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAsset.graphql.swift; sourceTree = ""; }; - 074321D12A83E3C900F8518D /* Dimensions.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dimensions.graphql.swift; sourceTree = ""; }; - 074321D22A83E3C900F8518D /* TokenProjectMarket.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenProjectMarket.graphql.swift; sourceTree = ""; }; - 074321D32A83E3C900F8518D /* AmountChange.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmountChange.graphql.swift; sourceTree = ""; }; - 074321D42A83E3C900F8518D /* NftBalanceEdge.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftBalanceEdge.graphql.swift; sourceTree = ""; }; - 074321D52A83E3C900F8518D /* NftActivity.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivity.graphql.swift; sourceTree = ""; }; - 074321D62A83E3C900F8518D /* AssetActivity.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetActivity.graphql.swift; sourceTree = ""; }; - 074321D72A83E3C900F8518D /* NftProfile.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftProfile.graphql.swift; sourceTree = ""; }; - 074321D82A83E3C900F8518D /* NftCollectionEdge.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionEdge.graphql.swift; sourceTree = ""; }; - 074321DA2A83E3C900F8518D /* TokenTransfer.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenTransfer.graphql.swift; sourceTree = ""; }; - 074321DB2A83E3C900F8518D /* NftApproval.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftApproval.graphql.swift; sourceTree = ""; }; - 074321DC2A83E3C900F8518D /* NftBalance.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftBalance.graphql.swift; sourceTree = ""; }; - 074321DD2A83E3C900F8518D /* Token.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Token.graphql.swift; sourceTree = ""; }; 074321DE2A83E3C900F8518D /* SchemaConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaConfiguration.swift; sourceTree = ""; }; - 074321E02A83E3C900F8518D /* NftBalancesFilterInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftBalancesFilterInput.graphql.swift; sourceTree = ""; }; - 074321E12A83E3C900F8518D /* NftCollectionsFilterInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionsFilterInput.graphql.swift; sourceTree = ""; }; - 074321E22A83E3C900F8518D /* ContractInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContractInput.graphql.swift; sourceTree = ""; }; - 074321E32A83E3C900F8518D /* NftActivityFilterInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityFilterInput.graphql.swift; sourceTree = ""; }; - 074321E42A83E3C900F8518D /* NftAssetTraitInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetTraitInput.graphql.swift; sourceTree = ""; }; - 074321E62A83E3C900F8518D /* NftAssetsFilterInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetsFilterInput.graphql.swift; sourceTree = ""; }; - 074321E72A83E3C900F8518D /* SchemaMetadata.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaMetadata.graphql.swift; sourceTree = ""; }; - 074321E92A83E3C900F8518D /* IAmount.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IAmount.graphql.swift; sourceTree = ""; }; - 074321EA2A83E3C900F8518D /* IContract.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IContract.graphql.swift; sourceTree = ""; }; 0743223F2A841BBD00F8518D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 0767E0372A65C8330042ADA2 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; 0767E03A2A65D2550042ADA2 /* Styling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Styling.swift; sourceTree = ""; }; - 077E60382A85587800ABC4B9 /* TokensQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensQuery.graphql.swift; sourceTree = ""; }; - 077E603A2A86D06100ABC4B9 /* MultiplePortfolioBalancesQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiplePortfolioBalancesQuery.graphql.swift; sourceTree = ""; }; 0783F7B32A619E7C009ED617 /* UIComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponents.swift; sourceTree = ""; }; 078E79452A55EB3300F59CF2 /* WidgetIntentExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetIntentExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 078E79462A55EB3300F59CF2 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; @@ -563,102 +349,88 @@ 08C60D53AB82A6D0D31D0F78 /* Pods-WidgetIntentExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.release.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.release.xcconfig"; sourceTree = ""; }; 08EBF075A4482F701892270B /* Pods-Widgets.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Widgets.dev.xcconfig"; path = "Target Support Files/Pods-Widgets/Pods-Widgets.dev.xcconfig"; sourceTree = ""; }; 0929C0B4AE1570B8C0B45D4D /* Pods_Uniswap_UniswapTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Uniswap_UniswapTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 0B132543F80E3C9191219E6D /* NftBalanceEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalanceEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftBalanceEdge.graphql.swift; sourceTree = ""; }; + 0A266B733FD2FA9C1C1A7731 /* TokenParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenParts.graphql.swift; sourceTree = ""; }; + 0A7D41CB7AA0496123E2153D /* SwapOrderType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwapOrderType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/SwapOrderType.graphql.swift; sourceTree = ""; }; 0B7E5D62E11408EB5F0F5A80 /* Pods-WidgetsCore.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.beta.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.beta.xcconfig"; sourceTree = ""; }; - 0BB4A40E30E1D985519DB3CF /* TokenProtectionInfoParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProtectionInfoParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenProtectionInfoParts.graphql.swift; sourceTree = ""; }; 0C19DE44A750FB17647FF2B6 /* Pods-Widgets.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Widgets.beta.xcconfig"; path = "Target Support Files/Pods-Widgets/Pods-Widgets.beta.xcconfig"; sourceTree = ""; }; + 0D25B69F4A69D733A5DAEC42 /* SchemaConfiguration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SchemaConfiguration.swift; path = WidgetsCore/MobileSchema/Schema/SchemaConfiguration.swift; sourceTree = ""; }; 0DB282242CDADB260014CF77 /* EmbeddedWallet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EmbeddedWallet.m; sourceTree = ""; }; 0DB282252CDADB260014CF77 /* EmbeddedWallet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmbeddedWallet.swift; sourceTree = ""; }; - 0DC6ADEF2B1E2C0F0092909C /* PortfolioValueModifier.graphql.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortfolioValueModifier.graphql.swift; sourceTree = ""; }; - 0DE251422C13B674005F47F9 /* OnRampTransactionsAuth.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnRampTransactionsAuth.graphql.swift; sourceTree = ""; }; - 0DE251442C13B69D005F47F9 /* OnRampTransfer.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnRampTransfer.graphql.swift; sourceTree = ""; }; - 0DE251452C13B69D005F47F9 /* OnRampServiceProvider.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnRampServiceProvider.graphql.swift; sourceTree = ""; }; - 0DE251462C13B69D005F47F9 /* OnRampTransactionDetails.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OnRampTransactionDetails.graphql.swift; sourceTree = ""; }; + 0E5478334318BD5D6F8366D6 /* OnRampServiceProvider.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampServiceProvider.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OnRampServiceProvider.graphql.swift; sourceTree = ""; }; + 0EA8FE9029A1E7C4EAD7F547 /* NftsTabQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftsTabQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NftsTabQuery.graphql.swift; sourceTree = ""; }; 1064E23E366D0C2C2B20C30E /* Pods_WidgetIntentExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WidgetIntentExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1193B3A845BC3BE8CAA00D01 /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.release.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.release.xcconfig"; sourceTree = ""; }; - 12D3AB6C2FC9513E9F224B7C /* OnRampTransactionDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampTransactionDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OnRampTransactionDetails.graphql.swift; sourceTree = ""; }; - 137696908D703632CEE3AB28 /* TokenBalanceMainParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalanceMainParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBalanceMainParts.graphql.swift; sourceTree = ""; }; + 132FC1B05AE3D510C3B71EBA /* SwapOrderStatus.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwapOrderStatus.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/SwapOrderStatus.graphql.swift; sourceTree = ""; }; 13B07F961A680F5B00A75B9A /* Uniswap.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Uniswap.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Uniswap/AppDelegate.h; sourceTree = ""; }; 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Uniswap/Images.xcassets; sourceTree = ""; }; 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Uniswap/Info.plist; sourceTree = ""; }; - 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Uniswap/main.m; sourceTree = ""; }; - 148617FD73F2CD7117960DBA /* SwapOrderType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwapOrderType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/SwapOrderType.graphql.swift; sourceTree = ""; }; 15092E550A1C78508ABA3280 /* Pods_OneSignalNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_OneSignalNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 17819A49DE69723EC60D9D72 /* FeeData.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeeData.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/FeeData.graphql.swift; sourceTree = ""; }; + 16F706375DFAFB58F8440CF9 /* TokenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenQuery.graphql.swift; sourceTree = ""; }; 178644A78AB62609EFDB66B3 /* Pods-Uniswap.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.release.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.release.xcconfig"; sourceTree = ""; }; 1834199AFFB04D91B05FFB64 /* InputMono-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "InputMono-Regular.ttf"; path = "../src/assets/fonts/InputMono-Regular.ttf"; sourceTree = ""; }; - 1849039FD4A210DA990363C8 /* Portfolio.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Portfolio.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Portfolio.graphql.swift; sourceTree = ""; }; - 18B9EB43501127BE1B14F1A5 /* NftContract.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftContract.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftContract.graphql.swift; sourceTree = ""; }; 1CC6ADAADCA38FDAEB181E86 /* Pods-WidgetIntentExtension.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.dev.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.dev.xcconfig"; sourceTree = ""; }; - 1FC7B80EC0E0D2134B67575B /* NftBalanceConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalanceConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftBalanceConnection.graphql.swift; sourceTree = ""; }; - 207F4DF7664454C31DE069F8 /* NFTItemScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NFTItemScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NFTItemScreenQuery.graphql.swift; sourceTree = ""; }; - 2139ABF56EA067ED792A48C9 /* OnRampTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OnRampTransfer.graphql.swift; sourceTree = ""; }; - 218B8AB05C80D919671D3970 /* NftAssetConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftAssetConnection.graphql.swift; sourceTree = ""; }; + 1D88F29D12AB0D8B4C00D065 /* IContract.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = IContract.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Interfaces/IContract.graphql.swift; sourceTree = ""; }; + 201FF7E30EC98ABC121BC8BF /* BlockaidFees.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BlockaidFees.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/BlockaidFees.graphql.swift; sourceTree = ""; }; 2226DF79BEAFECEE11A51347 /* Pods_Uniswap.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Uniswap.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 224CA82F1871016F4173F69E /* TransactionListQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionListQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TransactionListQuery.graphql.swift; sourceTree = ""; }; - 236126979349098B98C70F27 /* TokenProjectUrlsParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectUrlsParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenProjectUrlsParts.graphql.swift; sourceTree = ""; }; - 2370BAD675DC8064C00AC037 /* TokenBalanceQuantityParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalanceQuantityParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBalanceQuantityParts.graphql.swift; sourceTree = ""; }; - 25B4C7C7FEF32EAF10D030F9 /* Currency.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Currency.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/Currency.graphql.swift; sourceTree = ""; }; - 27F8B915042BD8D67D9627EA /* HomeScreenTokenParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HomeScreenTokenParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/HomeScreenTokenParts.graphql.swift; sourceTree = ""; }; - 2ABA47847A73130D356EB6BF /* TransactionStatus.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionStatus.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TransactionStatus.graphql.swift; sourceTree = ""; }; - 2BA0ACE5B9B2C41187CD41F3 /* FeedTransactionListQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeedTransactionListQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/FeedTransactionListQuery.graphql.swift; sourceTree = ""; }; - 2C2F4BE3080D4129D4478528 /* TokenBasicProjectParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBasicProjectParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBasicProjectParts.graphql.swift; sourceTree = ""; }; - 2D95FE36D2667E070345CDD0 /* NftActivityType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivityType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/NftActivityType.graphql.swift; sourceTree = ""; }; - 2E49E724432C70A2551FB2C7 /* SchemaConfiguration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SchemaConfiguration.swift; path = WidgetsCore/MobileSchema/Schema/SchemaConfiguration.swift; sourceTree = ""; }; + 2240891823DAE7BF8BB88FF4 /* HomeScreenTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HomeScreenTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/HomeScreenTokensQuery.graphql.swift; sourceTree = ""; }; + 24343423F6B8A16CB14F1E4C /* Pods-WidgetsCore.debugoptimized.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.debugoptimized.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.debugoptimized.xcconfig"; sourceTree = ""; }; + 2590691EDB017AE6FB6C10AC /* Pods-WidgetIntentExtension.debugoptimized.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.debugoptimized.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.debugoptimized.xcconfig"; sourceTree = ""; }; + 25DDE9DCAFA0EE2838508994 /* Chain.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Chain.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/Chain.graphql.swift; sourceTree = ""; }; + 284C5150E9B50FB5BE531572 /* Image.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Image.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Image.graphql.swift; sourceTree = ""; }; + 290B66F38FEC486AF1709BE4 /* FeeData.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeeData.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/FeeData.graphql.swift; sourceTree = ""; }; + 2C9E8E39C13967E447FC193F /* TimestampedAmount.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TimestampedAmount.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TimestampedAmount.graphql.swift; sourceTree = ""; }; 2E8B7D36D2E14D9488F351EB /* Pods_Widgets.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Widgets.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 315B8A905FA2C60C053F138B /* OnRampTransactionsAuth.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampTransactionsAuth.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/OnRampTransactionsAuth.graphql.swift; sourceTree = ""; }; - 31ED613D63FE9C56D9270517 /* Image.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Image.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Image.graphql.swift; sourceTree = ""; }; - 32012AC135EBD991C8F02F92 /* TokenSortableField.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenSortableField.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TokenSortableField.graphql.swift; sourceTree = ""; }; - 3377F7BB38BD5A45AE31909D /* TokenBasicInfoParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBasicInfoParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBasicInfoParts.graphql.swift; sourceTree = ""; }; - 37038CCEB11E70E2027EF4EA /* NftCollectionEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollectionEdge.graphql.swift; sourceTree = ""; }; - 396B6AB3BB41898B56EB3828 /* OffRampTransactionDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OffRampTransactionDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OffRampTransactionDetails.graphql.swift; sourceTree = ""; }; - 39F60B2B54796DC978765C99 /* AssetChange.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AssetChange.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Unions/AssetChange.graphql.swift; sourceTree = ""; }; - 39FC2A13550FA6754EC1A7FC /* NftOrderConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftOrderConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftOrderConnection.graphql.swift; sourceTree = ""; }; + 2ECFE86020AB7A252CDC9BB1 /* NftCollection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollection.graphql.swift; sourceTree = ""; }; + 2FB70424D7C456B9B72536C6 /* NetworkFee.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkFee.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NetworkFee.graphql.swift; sourceTree = ""; }; + 3063061F7C203A1DA47B8FBB /* OffRampTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OffRampTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OffRampTransfer.graphql.swift; sourceTree = ""; }; + 319AAC5377C60EB6579CC5C6 /* TokenBasicInfoParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBasicInfoParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBasicInfoParts.graphql.swift; sourceTree = ""; }; + 31CB18D02DE297D6DADB87C0 /* Pods-Uniswap.debugoptimized.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.debugoptimized.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.debugoptimized.xcconfig"; sourceTree = ""; }; + 32764E27B5D9FCF0AA4217CF /* SwapOrderDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwapOrderDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/SwapOrderDetails.graphql.swift; sourceTree = ""; }; + 346B304DE3207797FD1FDE71 /* ProtectionInfo.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtectionInfo.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/ProtectionInfo.graphql.swift; sourceTree = ""; }; + 36F66895EB592B9F61AE658C /* Pods-Uniswap-UniswapTests.debugoptimized.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap-UniswapTests.debugoptimized.xcconfig"; path = "Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests.debugoptimized.xcconfig"; sourceTree = ""; }; + 37A6D5D76D31F8C5CDD29814 /* TransactionType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TransactionType.graphql.swift; sourceTree = ""; }; + 38EAF64134981DC76B6C7E90 /* OnRampTransactionsAuth.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampTransactionsAuth.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/OnRampTransactionsAuth.graphql.swift; sourceTree = ""; }; 3A2186B1FF7FB85663D96EA9 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; - 3B9A505509D9A85BE1DB7D05 /* TokenProjectMarket.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectMarket.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenProjectMarket.graphql.swift; sourceTree = ""; }; - 3BF42F88002431BFDC9FCA9F /* HistoryDuration.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HistoryDuration.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/HistoryDuration.graphql.swift; sourceTree = ""; }; + 3AE6A0CAEDE1DC1D2FA14DEA /* TransactionHistoryUpdaterQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionHistoryUpdaterQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TransactionHistoryUpdaterQuery.graphql.swift; sourceTree = ""; }; + 3BA467FB84503BB53B081C73 /* TokenProjectMarketsParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectMarketsParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenProjectMarketsParts.graphql.swift; sourceTree = ""; }; 3C606D2C81014A0A8898F38E /* Basel-Grotesk-Medium.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Basel-Grotesk-Medium.otf"; path = "../src/assets/fonts/Basel-Grotesk-Medium.otf"; sourceTree = ""; }; + 3D3112654E5722A2F862D3FC /* ConvertQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConvertQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/ConvertQuery.graphql.swift; sourceTree = ""; }; 3D8FCE4CD401350CA74DCC89 /* Pods-WidgetsCoreTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCoreTests.debug.xcconfig"; path = "Target Support Files/Pods-WidgetsCoreTests/Pods-WidgetsCoreTests.debug.xcconfig"; sourceTree = ""; }; + 3D96819BA563CB218D29C657 /* TokenSortableField.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenSortableField.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TokenSortableField.graphql.swift; sourceTree = ""; }; 3E279F675B02CBC50D3B57D5 /* Pods-WidgetsCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.release.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.release.xcconfig"; sourceTree = ""; }; - 3F0EEAC1DADE827AE38EB8A6 /* FavoriteTokenCardQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FavoriteTokenCardQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/FavoriteTokenCardQuery.graphql.swift; sourceTree = ""; }; - 40E034582FBD2C374CEF808E /* ProtectionAttackType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtectionAttackType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/ProtectionAttackType.graphql.swift; sourceTree = ""; }; - 41003491EF939043CF9D0F4E /* PortfolioValueModifier.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PortfolioValueModifier.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/PortfolioValueModifier.graphql.swift; sourceTree = ""; }; - 41BA2129FFE961C8549C8A30 /* NftBalancesFilterInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalancesFilterInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftBalancesFilterInput.graphql.swift; sourceTree = ""; }; - 43E2ECCEFBC3BE8D13BE6CAB /* NftActivityFilterInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivityFilterInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftActivityFilterInput.graphql.swift; sourceTree = ""; }; - 4429CA6DA254ECC24A65DA14 /* MultiplePortfolioBalancesQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultiplePortfolioBalancesQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/MultiplePortfolioBalancesQuery.graphql.swift; sourceTree = ""; }; + 413CEB2443D6DD1DA7CC7231 /* TokenFeeDataParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenFeeDataParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenFeeDataParts.graphql.swift; sourceTree = ""; }; + 41FFD02A227ACFB661A44813 /* SafetyLevel.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SafetyLevel.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/SafetyLevel.graphql.swift; sourceTree = ""; }; + 42013D777BAA40652AB28985 /* NftsQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftsQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NftsQuery.graphql.swift; sourceTree = ""; }; + 42158AF500DDBEEF6B7E98E3 /* NftApproval.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftApproval.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftApproval.graphql.swift; sourceTree = ""; }; + 43AB42F3ED903CF49D7D4DE7 /* AmountChange.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AmountChange.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/AmountChange.graphql.swift; sourceTree = ""; }; + 43D7F54953D864DC067FADD1 /* TopTokenParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TopTokenParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TopTokenParts.graphql.swift; sourceTree = ""; }; + 44D824154584CD39C82904E9 /* BridgedWithdrawalInfo.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BridgedWithdrawalInfo.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/BridgedWithdrawalInfo.graphql.swift; sourceTree = ""; }; + 45FFF7DE2E8C2A6400362570 /* SilentPushEventEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SilentPushEventEmitter.m; sourceTree = ""; }; + 45FFF7E02E8C2E6100362570 /* SilentPushEventEmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SilentPushEventEmitter.swift; sourceTree = ""; }; + 47148657FBA8DCC92DD69573 /* TransactionDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TransactionDetails.graphql.swift; sourceTree = ""; }; 4781CD4CDD95B5792B793F75 /* Pods-Uniswap-UniswapTests.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap-UniswapTests.beta.xcconfig"; path = "Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests.beta.xcconfig"; sourceTree = ""; }; - 4981A14906A35E8A0B7F9457 /* TokenProject.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProject.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenProject.graphql.swift; sourceTree = ""; }; - 49BD6C776029FAF4FC711DE7 /* NftOrderEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftOrderEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftOrderEdge.graphql.swift; sourceTree = ""; }; - 4A8990A17F489F284E0DB1B3 /* Chain.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Chain.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/Chain.graphql.swift; sourceTree = ""; }; - 4ACFA5B4204741038C986C5D /* SafetyLevel.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SafetyLevel.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/SafetyLevel.graphql.swift; sourceTree = ""; }; - 4B308EC3DB7C3C1DD5DDE03D /* NftStandard.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftStandard.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/NftStandard.graphql.swift; sourceTree = ""; }; + 48DE72B77CD2D89B75BB934D /* FavoriteTokenCardQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FavoriteTokenCardQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/FavoriteTokenCardQuery.graphql.swift; sourceTree = ""; }; + 4AF8EE05291534B82ACC1105 /* TokenBalance.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalance.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenBalance.graphql.swift; sourceTree = ""; }; 4C445DB9798210862C34D0E0 /* Pods-WidgetsCoreTests.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCoreTests.beta.xcconfig"; path = "Target Support Files/Pods-WidgetsCoreTests/Pods-WidgetsCoreTests.beta.xcconfig"; sourceTree = ""; }; - 4DB4379B5E222207CA7328D0 /* NftActivityEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivityEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftActivityEdge.graphql.swift; sourceTree = ""; }; 4DF5F26A06553EFDD4D99214 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Uniswap/ExpoModulesProvider.swift"; sourceTree = ""; }; - 4E32BBB65A6DD9CFF608C8E8 /* NftApproval.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftApproval.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftApproval.graphql.swift; sourceTree = ""; }; - 4E5FA43C5E31035775917C4E /* NftBalanceAssetInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalanceAssetInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftBalanceAssetInput.graphql.swift; sourceTree = ""; }; - 4F4F47D22B749A438B5BF674 /* NftCollectionMarket.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionMarket.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollectionMarket.graphql.swift; sourceTree = ""; }; - 54B1C71AED738D17898103A6 /* Dimensions.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Dimensions.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Dimensions.graphql.swift; sourceTree = ""; }; - 556339705BD103365D4ED07B /* NftAssetTraitInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetTraitInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftAssetTraitInput.graphql.swift; sourceTree = ""; }; - 5640432978873FB030467750 /* TopTokenParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TopTokenParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TopTokenParts.graphql.swift; sourceTree = ""; }; + 4EC9CABC0B4E73B25C7A2128 /* TransactionDirection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionDirection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TransactionDirection.graphql.swift; sourceTree = ""; }; + 5442480B3C91EDC515728606 /* TokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokensQuery.graphql.swift; sourceTree = ""; }; + 56093E216B82288E1A4018F0 /* NftBalanceConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalanceConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftBalanceConnection.graphql.swift; sourceTree = ""; }; 56FE9C9AF785221B7E3F4C04 /* Pods-Uniswap.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.dev.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.dev.xcconfig"; sourceTree = ""; }; + 58641E30674C4C4241702902 /* Query.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Query.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Query.graphql.swift; sourceTree = ""; }; 5B4398E82DD3B22C00F6BE08 /* PrivateKeyDisplayManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PrivateKeyDisplayManager.m; sourceTree = ""; }; 5B4398E92DD3B22C00F6BE08 /* PrivateKeyDisplayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateKeyDisplayManager.swift; sourceTree = ""; }; 5B4398EA2DD3B22C00F6BE08 /* PrivateKeyDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateKeyDisplayView.swift; sourceTree = ""; }; 5B4CEC5E2DD65DD4009F082B /* CopyIconOutline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyIconOutline.swift; sourceTree = ""; }; - 5E0D68CC8D1F13D864F069BB /* IContract.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = IContract.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Interfaces/IContract.graphql.swift; sourceTree = ""; }; 5E5E0A622D380F5700E166AA /* Env.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Env.swift; sourceTree = ""; }; - 5EFB78352B1E585000E77EAC /* ConvertQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertQuery.graphql.swift; sourceTree = ""; }; - 600250B00B6C02AAB063818B /* NftMarketplace.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftMarketplace.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/NftMarketplace.graphql.swift; sourceTree = ""; }; - 608B07304ED1387B5AEAA3BB /* ApplicationContract.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ApplicationContract.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/ApplicationContract.graphql.swift; sourceTree = ""; }; + 5F2898E709490CC198B98228 /* PageInfo.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PageInfo.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/PageInfo.graphql.swift; sourceTree = ""; }; + 60195493B9C71EDEECA46E80 /* OnRampTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OnRampTransfer.graphql.swift; sourceTree = ""; }; + 614A491FAB9659FA08CD9AE4 /* OnRampTransactionDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampTransactionDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OnRampTransactionDetails.graphql.swift; sourceTree = ""; }; 62CEA9F2D5176D20A6402A3E /* Pods-Uniswap.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.beta.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.beta.xcconfig"; sourceTree = ""; }; - 63ACFF2A1D5AE3498731AC69 /* NftCollection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollection.graphql.swift; sourceTree = ""; }; - 647933A5DC35BDFEEF3620A1 /* Token.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Token.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Token.graphql.swift; sourceTree = ""; }; 649A7A762D9AE70B00B53589 /* KeychainConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainConstants.swift; sourceTree = ""; }; 649A7A772D9AE70B00B53589 /* KeychainUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainUtils.swift; sourceTree = ""; }; - 69F2FFD11FB5E462CC454A15 /* TransactionType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TransactionType.graphql.swift; sourceTree = ""; }; - 6A84A192F750A0F1BC8D7A69 /* OnRampServiceProvider.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OnRampServiceProvider.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OnRampServiceProvider.graphql.swift; sourceTree = ""; }; + 66697C913CF365C9460F42E6 /* NftCollectionMarket.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionMarket.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollectionMarket.graphql.swift; sourceTree = ""; }; + 6AFC48B9A573915BFA6FC6E2 /* OffRampTransactionDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OffRampTransactionDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OffRampTransactionDetails.graphql.swift; sourceTree = ""; }; 6BC7D07B2B5FF02400617C95 /* ScantasticEncryption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ScantasticEncryption.m; sourceTree = ""; }; 6BC7D07C2B5FF02400617C95 /* ScantasticEncryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScantasticEncryption.swift; sourceTree = ""; }; 6BC7D07D2B5FF02400617C95 /* EncryptionUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptionUtils.swift; sourceTree = ""; }; @@ -669,26 +441,27 @@ 6CA91BDE2A95226200C4063E /* RNCloudStorageBackupsManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCloudStorageBackupsManager.m; sourceTree = ""; }; 6CA91BDF2A95226200C4063E /* EncryptionHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptionHelper.swift; sourceTree = ""; }; 6CA91BE02A95226200C4063E /* RNCloudStorageBackupsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RNCloudStorageBackupsManager.swift; sourceTree = ""; }; - 6DF7C8888BE6AFA2F22889FE /* NftAssetEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftAssetEdge.graphql.swift; sourceTree = ""; }; - 6E3A78386B3B779B688021FF /* NftOrder.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftOrder.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftOrder.graphql.swift; sourceTree = ""; }; + 6DCC7E203AB163C9C2D3D789 /* Dimensions.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Dimensions.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Dimensions.graphql.swift; sourceTree = ""; }; + 6F02DBC2C51EFC6B1AA9C1CD /* DescriptionTranslations.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DescriptionTranslations.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/DescriptionTranslations.graphql.swift; sourceTree = ""; }; 6F33E8069B7B40AFB313B8B0 /* Basel-Grotesk-Book.otf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "Basel-Grotesk-Book.otf"; path = "../src/assets/fonts/Basel-Grotesk-Book.otf"; sourceTree = ""; }; 6F3DC921A65D749C0852B10C /* Pods-Uniswap-UniswapTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap-UniswapTests.debug.xcconfig"; path = "Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests.debug.xcconfig"; sourceTree = ""; }; 6F7814C6D40D9C348EA1F1C7 /* Pods-OneSignalNotificationServiceExtension.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.dev.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.dev.xcconfig"; sourceTree = ""; }; - 6FF8B660E9C2C0D8DDF7588A /* TokenProjectDescriptionQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectDescriptionQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenProjectDescriptionQuery.graphql.swift; sourceTree = ""; }; - 70851E3D1A4B296A1CE22A20 /* NftAssetTrait.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetTrait.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftAssetTrait.graphql.swift; sourceTree = ""; }; - 730CA356D6E1D498CC0C1472 /* NftsTabQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftsTabQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NftsTabQuery.graphql.swift; sourceTree = ""; }; - 74885865ED8CFEA875E40916 /* TokenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenQuery.graphql.swift; sourceTree = ""; }; + 73D981A2D31B6F8822C19324 /* SchemaMetadata.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SchemaMetadata.graphql.swift; path = WidgetsCore/MobileSchema/Schema/SchemaMetadata.graphql.swift; sourceTree = ""; }; + 74673620CE906C9AF34C6750 /* Amount.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Amount.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Amount.graphql.swift; sourceTree = ""; }; + 792EF7A7820987EA9E99D792 /* NftTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftTransfer.graphql.swift; sourceTree = ""; }; 7A7637BBC9B3A68E0338D96E /* Pods-WidgetIntentExtension.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.beta.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.beta.xcconfig"; sourceTree = ""; }; - 7BD3F0794908635CDFA7DAB6 /* ConvertQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ConvertQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/ConvertQuery.graphql.swift; sourceTree = ""; }; - 7E1F288B7EDDE906C4C00C46 /* DescriptionTranslations.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DescriptionTranslations.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/DescriptionTranslations.graphql.swift; sourceTree = ""; }; + 7F0C115796D33739746778E3 /* TokenBalanceMainParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalanceMainParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBalanceMainParts.graphql.swift; sourceTree = ""; }; 82C9871585F60F92D079FB95 /* Pods-Widgets.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Widgets.release.xcconfig"; path = "Target Support Files/Pods-Widgets/Pods-Widgets.release.xcconfig"; sourceTree = ""; }; - 84997376A0B436ECC0A996C5 /* ExploreSearchQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExploreSearchQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/ExploreSearchQuery.graphql.swift; sourceTree = ""; }; + 8604CAA8F7E44D704A6F4AAF /* PortfolioBalancesQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PortfolioBalancesQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/PortfolioBalancesQuery.graphql.swift; sourceTree = ""; }; 8719E5872CC41AB64503E903 /* Pods-WidgetIntentExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.debug.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.debug.xcconfig"; sourceTree = ""; }; - 8A4EB7BAD2C90E61FD6C30AF /* ActivityDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ActivityDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Unions/ActivityDetails.graphql.swift; sourceTree = ""; }; - 8A5342DEF1410E0E81E97F40 /* SearchTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SearchTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/SearchTokensQuery.graphql.swift; sourceTree = ""; }; - 8B477EBEA1EDA3914C77E8A6 /* NftAssetsFilterInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAssetsFilterInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftAssetsFilterInput.graphql.swift; sourceTree = ""; }; - 8BEADEB4AE05B860CF2232D1 /* NetworkFee.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NetworkFee.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NetworkFee.graphql.swift; sourceTree = ""; }; - 8DA4E7C052A6C4AC3A9DCDA6 /* TokenTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenTransfer.graphql.swift; sourceTree = ""; }; + 88B481513A2E780E6489DC4F /* Token.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Token.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Token.graphql.swift; sourceTree = ""; }; + 8B2A92162EB3E78E00990413 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = Uniswap/AppDelegate.swift; sourceTree = ""; }; + 8B2A92182EB3E79500990413 /* Uniswap-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "Uniswap-Bridging-Header.h"; path = "Uniswap/Uniswap-Bridging-Header.h"; sourceTree = ""; }; + 8BB8B6395E3C86412FA793CB /* AssetActivity.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AssetActivity.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/AssetActivity.graphql.swift; sourceTree = ""; }; + 8C66DD1D03A824EB6A231DCE /* TokenProjectDescriptionQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectDescriptionQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenProjectDescriptionQuery.graphql.swift; sourceTree = ""; }; + 8CBD584CCF002F58176DA863 /* Pods-OneSignalNotificationServiceExtension.debugoptimized.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.debugoptimized.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.debugoptimized.xcconfig"; sourceTree = ""; }; + 8D5FF4316461BD4FB5847D62 /* TokenBalanceQuantityParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalanceQuantityParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBalanceQuantityParts.graphql.swift; sourceTree = ""; }; + 8D973A66C23859D34E6FE175 /* NftStandard.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftStandard.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/NftStandard.graphql.swift; sourceTree = ""; }; 8E89C3A62AB8AAA400C84DE5 /* MnemonicConfirmationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicConfirmationView.swift; sourceTree = ""; }; 8E89C3A72AB8AAA400C84DE5 /* MnemonicDisplayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicDisplayView.swift; sourceTree = ""; }; 8E89C3A92AB8AAA400C84DE5 /* MnemonicConfirmationWordBankView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicConfirmationWordBankView.swift; sourceTree = ""; }; @@ -703,105 +476,77 @@ 8EA8AB402AB7ED76004E7EF3 /* AlertTriangleIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertTriangleIcon.swift; sourceTree = ""; }; 8EBFB1542ABA6AA6006B32A8 /* PasteIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteIcon.swift; sourceTree = ""; }; 8ED0562B2AA78E2C009BD5A2 /* ScrollFadeExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollFadeExtensions.swift; sourceTree = ""; }; - 8EE7C0572AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DescriptionTranslations.graphql.swift; sourceTree = ""; }; - 9127D1342CC2D3D00096F134 /* TokenBalanceMainParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenBalanceMainParts.graphql.swift; sourceTree = ""; }; - 9127D1352CC2D3D00096F134 /* TokenBalanceQuantityParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenBalanceQuantityParts.graphql.swift; sourceTree = ""; }; - 9173CEBB2D03C6F30036DA28 /* TokenBalanceParts.graphql.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenBalanceParts.graphql.swift; sourceTree = ""; }; - 91D501652CDBEAE700B09B7F /* TokenMarketParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenMarketParts.graphql.swift; sourceTree = ""; }; - 91D501662CDBEAE700B09B7F /* TokenBalanceMainParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenBalanceMainParts.graphql.swift; sourceTree = ""; }; - 91D501672CDBEAE700B09B7F /* TokenProjectMarketsParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenProjectMarketsParts.graphql.swift; sourceTree = ""; }; - 91D501682CDBEAE700B09B7F /* TokenBasicInfoParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenBasicInfoParts.graphql.swift; sourceTree = ""; }; - 91D501692CDBEAE700B09B7F /* TokenBasicProjectParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenBasicProjectParts.graphql.swift; sourceTree = ""; }; - 91D5016A2CDBEAE700B09B7F /* TokenBalanceQuantityParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenBalanceQuantityParts.graphql.swift; sourceTree = ""; }; - 91D5016B2CDBEAE700B09B7F /* TokenProtectionInfoParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenProtectionInfoParts.graphql.swift; sourceTree = ""; }; - 91D5016C2CDBEAE700B09B7F /* TokenParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenParts.graphql.swift; sourceTree = ""; }; - 91D5016D2CDBEAE700B09B7F /* TokenProjectUrlsParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenProjectUrlsParts.graphql.swift; sourceTree = ""; }; - 91D5016E2CDBEAE700B09B7F /* TopTokenParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopTokenParts.graphql.swift; sourceTree = ""; }; - 91D5016F2CDBEAE700B09B7F /* TokenFeeDataParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenFeeDataParts.graphql.swift; sourceTree = ""; }; - 91D5017C2CDBEAF600B09B7F /* HomeScreenTokenParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeScreenTokenParts.graphql.swift; sourceTree = ""; }; - 92DE390311705FF1EE65C0E6 /* NftCollectionConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftCollectionConnection.graphql.swift; sourceTree = ""; }; - 985DDC8324B872DD44E87781 /* PortfolioBalancesQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PortfolioBalancesQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/PortfolioBalancesQuery.graphql.swift; sourceTree = ""; }; - 98F596250F55A3D6E9E1F499 /* TokenParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenParts.graphql.swift; sourceTree = ""; }; - 9DFBD29C0BDFDB4464B8189C /* TokenPriceHistoryQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenPriceHistoryQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenPriceHistoryQuery.graphql.swift; sourceTree = ""; }; - 9E975BEDBED5FE51D0DC6E96 /* TokenApproval.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenApproval.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenApproval.graphql.swift; sourceTree = ""; }; - 9F00A4392B33894C0088A0D0 /* ApplicationContract.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationContract.graphql.swift; sourceTree = ""; }; - 9F29D4EC2B47126D004D003A /* NftBalanceAssetInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftBalanceAssetInput.graphql.swift; sourceTree = ""; }; + 93783F96FA74CB9A1BEB32EE /* TokenTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenTransfer.graphql.swift; sourceTree = ""; }; + 93D1BA7F635F61FC7E62C6C1 /* TokenProtectionInfoParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProtectionInfoParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenProtectionInfoParts.graphql.swift; sourceTree = ""; }; + 957594D21A42B9D77F5AD685 /* ContractInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ContractInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/ContractInput.graphql.swift; sourceTree = ""; }; + 986A89D253EEB9BEBE5F08FF /* Pods-Widgets.debugoptimized.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Widgets.debugoptimized.xcconfig"; path = "Target Support Files/Pods-Widgets/Pods-Widgets.debugoptimized.xcconfig"; sourceTree = ""; }; + 9A2AA83B4F9F4290A2BC7CCB /* NftContract.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftContract.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftContract.graphql.swift; sourceTree = ""; }; + 9BF540E1110D0FD1C219C45C /* ApplicationContract.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ApplicationContract.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/ApplicationContract.graphql.swift; sourceTree = ""; }; + 9F1055EFA8467996754739A7 /* TokenProjectUrlsParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectUrlsParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenProjectUrlsParts.graphql.swift; sourceTree = ""; }; 9F3500812A8AA5890077BFC5 /* EXSplashScreen.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = EXSplashScreen.xcframework; path = "../../../node_modules/expo-splash-screen/ios/EXSplashScreen.xcframework"; sourceTree = ""; }; - 9F8123822D2F33C4009C5D88 /* BlockaidFees.graphql.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockaidFees.graphql.swift; sourceTree = ""; }; - 9F813E9D2AA8FB5700438D89 /* ActivityDetails.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityDetails.graphql.swift; sourceTree = ""; }; - 9F813E9F2AA8FB7500438D89 /* TransactionDetails.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDetails.graphql.swift; sourceTree = ""; }; - 9F813EA12AA8FB8C00438D89 /* SwapOrderDetails.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapOrderDetails.graphql.swift; sourceTree = ""; }; - 9F813EA32AA8FBCF00438D89 /* TransactionType.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionType.graphql.swift; sourceTree = ""; }; 9FCEBEFD2A95A8E00079EDDB /* RNWalletConnect.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNWalletConnect.h; path = Uniswap/WalletConnect/RNWalletConnect.h; sourceTree = ""; }; 9FCEBEFE2A95A8E00079EDDB /* RNWalletConnect.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNWalletConnect.m; path = Uniswap/WalletConnect/RNWalletConnect.m; sourceTree = ""; }; 9FCEBEFF2A95A8E00079EDDB /* RNWalletConnect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RNWalletConnect.swift; path = Uniswap/WalletConnect/RNWalletConnect.swift; sourceTree = ""; }; 9FCEBF022A95A99B0079EDDB /* RCTThemeModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTThemeModule.h; path = Appearance/RCTThemeModule.h; sourceTree = ""; }; 9FCEBF032A95A99B0079EDDB /* RCTThemeModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTThemeModule.m; path = Appearance/RCTThemeModule.m; sourceTree = ""; }; - 9FEC9B8A2A858CF1003CD019 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Uniswap/AppDelegate.m; sourceTree = ""; }; + A02D64B698D203A7F3643C18 /* TokenMarket.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenMarket.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenMarket.graphql.swift; sourceTree = ""; }; + A182D5FF8C0919083F2E333C /* TransactionStatus.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionStatus.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TransactionStatus.graphql.swift; sourceTree = ""; }; + A24882D069B34B7DC9E8B4CE /* MobileSchema.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MobileSchema.graphql.swift; path = WidgetsCore/MobileSchema/MobileSchema.graphql.swift; sourceTree = ""; }; A32F9FBC272343C8002CFCDB /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + A34E3A6E13A4066767AA6757 /* Portfolio.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Portfolio.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Portfolio.graphql.swift; sourceTree = ""; }; + A372929BEDB706B6144334F0 /* IAmount.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = IAmount.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Interfaces/IAmount.graphql.swift; sourceTree = ""; }; + A3E638BE80543E905D072D7F /* TokenProjectMarket.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectMarket.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenProjectMarket.graphql.swift; sourceTree = ""; }; A3F0A5B0272B1DFA00895B25 /* KeychainSwiftDistrib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainSwiftDistrib.swift; sourceTree = ""; }; - A4E0C448C5D45BF1071FA458 /* MobileSchema.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MobileSchema.graphql.swift; path = WidgetsCore/MobileSchema/MobileSchema.graphql.swift; sourceTree = ""; }; - A70E4DD32C25DA0A002D6D86 /* NetworkFee.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NetworkFee.graphql.swift; path = MobileSchema/Schema/Objects/NetworkFee.graphql.swift; sourceTree = ""; }; - A70E4DD52C260416002D6D86 /* SwapOrderType.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwapOrderType.graphql.swift; path = MobileSchema/Schema/Enums/SwapOrderType.graphql.swift; sourceTree = ""; }; - A70E4DD62C260416002D6D86 /* SwapOrderStatus.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SwapOrderStatus.graphql.swift; path = MobileSchema/Schema/Enums/SwapOrderStatus.graphql.swift; sourceTree = ""; }; - A7A33933670CCAD9E62A1A80 /* TokenDetailsScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenDetailsScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenDetailsScreenQuery.graphql.swift; sourceTree = ""; }; - A7B8EFCA2BF68F0D00CA4A1C /* FeeData.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FeeData.graphql.swift; path = MobileSchema/Schema/Objects/FeeData.graphql.swift; sourceTree = ""; }; + A424FA93DB720DD7684C7674 /* TokenProject.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProject.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenProject.graphql.swift; sourceTree = ""; }; A7C9F415D0E128A43003E071 /* Pods-Uniswap.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.debug.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.debug.xcconfig"; sourceTree = ""; }; - A9030AC59702756C39396FFE /* NftsQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftsQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NftsQuery.graphql.swift; sourceTree = ""; }; - ABE51731EB853137302A7B0D /* TransactionDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TransactionDetails.graphql.swift; sourceTree = ""; }; + AB3EEA677984132330D8D279 /* HomeScreenTokenParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HomeScreenTokenParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/HomeScreenTokenParts.graphql.swift; sourceTree = ""; }; AC0EE0972BD826E700BCCF07 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; name = PrivacyInfo.xcprivacy; path = Uniswap/PrivacyInfo.xcprivacy; sourceTree = ""; }; AC2794442C51541E00F9AF68 /* sourcemaps-datadog.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "sourcemaps-datadog.sh"; sourceTree = ""; }; AC2EF4022C914B1600EEEFDB /* fonts */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fonts; path = ../src/assets/fonts; sourceTree = ""; }; - AF3D7A923E5E1FB504E1F64D /* TransactionHistoryUpdaterQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionHistoryUpdaterQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TransactionHistoryUpdaterQuery.graphql.swift; sourceTree = ""; }; - B033206DC974BCB6AF8CC613 /* SelectWalletScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SelectWalletScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/SelectWalletScreenQuery.graphql.swift; sourceTree = ""; }; B0DA4D39B1A6D74A1D05B99F /* Pods-WidgetsCore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.debug.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.debug.xcconfig"; sourceTree = ""; }; - B0DB67D6C438F623976727E9 /* TimestampedAmount.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TimestampedAmount.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TimestampedAmount.graphql.swift; sourceTree = ""; }; - B3C3F0403FAEFDD632AC6CCE /* TokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokensQuery.graphql.swift; sourceTree = ""; }; - B7E03AA02B39A7A90C96AA76 /* HomeScreenTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HomeScreenTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/HomeScreenTokensQuery.graphql.swift; sourceTree = ""; }; - BA869E362D56B0B600D7A718 /* WidgetTokensQuery.graphql.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetTokensQuery.graphql.swift; sourceTree = ""; }; + B68E391195D4587F61A33380 /* ActivityDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ActivityDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Unions/ActivityDetails.graphql.swift; sourceTree = ""; }; + B97D41BC2760803FD1C7CB3C /* TokenBalanceParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalanceParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBalanceParts.graphql.swift; sourceTree = ""; }; + BB3CBF221D5D2E3C036A1BA0 /* SelectWalletScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SelectWalletScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/SelectWalletScreenQuery.graphql.swift; sourceTree = ""; }; + BB9FB14C8B68CE1D05643256 /* TokenBasicProjectParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBasicProjectParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBasicProjectParts.graphql.swift; sourceTree = ""; }; BCB2A43E5FB0D7B69CA02312 /* Pods-WidgetsCore.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.dev.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.dev.xcconfig"; sourceTree = ""; }; - BD0FC2D534CB82B151C7F9B7 /* ProtectionResult.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtectionResult.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/ProtectionResult.graphql.swift; sourceTree = ""; }; - BD35300F81746B2D4A23065A /* BlockaidFees.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BlockaidFees.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/BlockaidFees.graphql.swift; sourceTree = ""; }; + BE4AB3FCC91BE6C73557E0E3 /* ProtectionAttackType.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtectionAttackType.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/ProtectionAttackType.graphql.swift; sourceTree = ""; }; BF9176E944C84910B1C0B057 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Uniswap/SplashScreen.storyboard; sourceTree = ""; }; - BFC36D963A2A52D1E7CA77E4 /* TokenMarketParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenMarketParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenMarketParts.graphql.swift; sourceTree = ""; }; + C050FFEC067334CF05C17EA0 /* TopTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TopTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TopTokensQuery.graphql.swift; sourceTree = ""; }; + C0C956C520AAB54BCE8C5CCC /* WidgetTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = WidgetTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/WidgetTokensQuery.graphql.swift; sourceTree = ""; }; C26D739993D5C939C6FBB58A /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Uniswap-UniswapTests/ExpoModulesProvider.swift"; sourceTree = ""; }; - C4790C2A43570CAC086B936E /* NftCollectionsFilterInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionsFilterInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftCollectionsFilterInput.graphql.swift; sourceTree = ""; }; - C65195A1CC9894C6341385C3 /* SchemaMetadata.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SchemaMetadata.graphql.swift; path = WidgetsCore/MobileSchema/Schema/SchemaMetadata.graphql.swift; sourceTree = ""; }; + C27225607FACF638581E25B5 /* TransactionListQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionListQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TransactionListQuery.graphql.swift; sourceTree = ""; }; + C394AC5DB58BDF1C33CC0927 /* NftBalanceAssetInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalanceAssetInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftBalanceAssetInput.graphql.swift; sourceTree = ""; }; + C6754D4BD9CE76AAC915F814 /* TokenMarketParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenMarketParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenMarketParts.graphql.swift; sourceTree = ""; }; C89238E3ED9F3AC98876B573 /* Pods-WidgetsCoreTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCoreTests.release.xcconfig"; path = "Target Support Files/Pods-WidgetsCoreTests/Pods-WidgetsCoreTests.release.xcconfig"; sourceTree = ""; }; - C955CE2592785712A11892DE /* SwapOrderDetails.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwapOrderDetails.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/SwapOrderDetails.graphql.swift; sourceTree = ""; }; - C9D5E6A07F404974045BD525 /* OffRampTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OffRampTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/OffRampTransfer.graphql.swift; sourceTree = ""; }; + C8BAFEAABF78617095D44E32 /* NftBalance.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalance.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftBalance.graphql.swift; sourceTree = ""; }; + CA586C522ABE5DA5363D27C7 /* ProtectionResult.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ProtectionResult.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/ProtectionResult.graphql.swift; sourceTree = ""; }; CB29AC0C0907A833F23D2C30 /* Pods_WidgetsCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WidgetsCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - CC055C3EDFDE2966FCA83C9E /* TokenProjectMarketsParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectMarketsParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenProjectMarketsParts.graphql.swift; sourceTree = ""; }; - CC2FBE9F353D3753FB94B8BF /* TransactionDirection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TransactionDirection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TransactionDirection.graphql.swift; sourceTree = ""; }; - D20E9461A6C9C1CD6A24BEED /* Amount.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Amount.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Amount.graphql.swift; sourceTree = ""; }; - D574AB5194929038A54B4D4B /* SwapOrderStatus.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwapOrderStatus.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/SwapOrderStatus.graphql.swift; sourceTree = ""; }; + CB72291CE6EFD3842A8E2A1D /* Pods-WidgetsCoreTests.debugoptimized.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCoreTests.debugoptimized.xcconfig"; path = "Target Support Files/Pods-WidgetsCoreTests/Pods-WidgetsCoreTests.debugoptimized.xcconfig"; sourceTree = ""; }; + CC2EE4B5A1180F03030E80A9 /* TokenProjectTokensTvlParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectTokensTvlParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenProjectTokensTvlParts.graphql.swift; sourceTree = ""; }; + CD3573B4E6FD61F5DDBD9C75 /* NftAsset.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAsset.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftAsset.graphql.swift; sourceTree = ""; }; + CE5E0F71E23968DEFCEB7A4B /* TokenStandard.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenStandard.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TokenStandard.graphql.swift; sourceTree = ""; }; D79B717BEAEA7857469D770A /* Pods-Widgets.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Widgets.debug.xcconfig"; path = "Target Support Files/Pods-Widgets/Pods-Widgets.debug.xcconfig"; sourceTree = ""; }; - DB35E518AD7FBFEBEA9CAB95 /* NftProfile.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftProfile.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftProfile.graphql.swift; sourceTree = ""; }; - DBA33683D2BA6BBE3251D67C /* WidgetTokensQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = WidgetTokensQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/WidgetTokensQuery.graphql.swift; sourceTree = ""; }; - E4624A41EC1468712BD77653 /* AssetActivity.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AssetActivity.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/AssetActivity.graphql.swift; sourceTree = ""; }; - E475EBABF7EA310435E2F19C /* PageInfo.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PageInfo.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/PageInfo.graphql.swift; sourceTree = ""; }; - E63EA0C75D8011BCC182492C /* AmountChange.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AmountChange.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/AmountChange.graphql.swift; sourceTree = ""; }; - E8A49D011C60137D4034CAC4 /* TokenProjectsQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectsQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenProjectsQuery.graphql.swift; sourceTree = ""; }; - EBD833F6580F9B82A68D4A98 /* TokenMarket.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenMarket.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenMarket.graphql.swift; sourceTree = ""; }; - ED157D3DB73302C8A1B9522E /* Query.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Query.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/Query.graphql.swift; sourceTree = ""; }; + DC6517FF8972DA448CDD2DFC /* HistoryDuration.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HistoryDuration.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/HistoryDuration.graphql.swift; sourceTree = ""; }; + E13150074BB66C20E7EA271A /* PortfolioValueModifier.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PortfolioValueModifier.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/PortfolioValueModifier.graphql.swift; sourceTree = ""; }; + E1B3A0C152B683215291E3BD /* TokenProjectsQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenProjectsQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenProjectsQuery.graphql.swift; sourceTree = ""; }; + E21B5EF6BC4582DD2BB0DA13 /* FeedTransactionListQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeedTransactionListQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/FeedTransactionListQuery.graphql.swift; sourceTree = ""; }; + E21EB4FB53B7B6B5848946D9 /* NftProfile.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftProfile.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftProfile.graphql.swift; sourceTree = ""; }; + E3AF9944E853669C2BC9B5AB /* MultiplePortfolioBalancesQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MultiplePortfolioBalancesQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/MultiplePortfolioBalancesQuery.graphql.swift; sourceTree = ""; }; + E5DDE9BCC6A539F6E9AE1664 /* Currency.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Currency.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/Currency.graphql.swift; sourceTree = ""; }; + E7ED83FFCB410FCCA2C6B97B /* TokenDetailsScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenDetailsScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenDetailsScreenQuery.graphql.swift; sourceTree = ""; }; ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; - EE0973BB445AAD8FAA25757F /* NftCollectionScreenQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftCollectionScreenQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/NftCollectionScreenQuery.graphql.swift; sourceTree = ""; }; - F0C34E94906CB9F0A016D566 /* TokenBalanceParts.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalanceParts.graphql.swift; path = WidgetsCore/MobileSchema/Fragments/TokenBalanceParts.graphql.swift; sourceTree = ""; }; - F1A24D364C0D262A14BB7182 /* NftActivity.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivity.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftActivity.graphql.swift; sourceTree = ""; }; + EDA02DA46906E4CDF827104A /* TokenPriceHistoryQuery.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenPriceHistoryQuery.graphql.swift; path = WidgetsCore/MobileSchema/Operations/Queries/TokenPriceHistoryQuery.graphql.swift; sourceTree = ""; }; + F182567B705FC0E15DA8F34A /* NftBalanceEdge.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalanceEdge.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftBalanceEdge.graphql.swift; sourceTree = ""; }; F1A3F4DDD7E40DA9E4BBAAD1 /* Pods-Uniswap-UniswapTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap-UniswapTests.release.xcconfig"; path = "Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests.release.xcconfig"; sourceTree = ""; }; - F2932B486DD7070DF226A27B /* NftAsset.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftAsset.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftAsset.graphql.swift; sourceTree = ""; }; - F3317EBD4E3B11E2F2A1CE58 /* NftTransfer.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftTransfer.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftTransfer.graphql.swift; sourceTree = ""; }; F35AFD3627EE49230011A725 /* Uniswap.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Uniswap.entitlements; path = Uniswap/Uniswap.entitlements; sourceTree = ""; }; F35AFD3B27EE49990011A725 /* OneSignalNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OneSignalNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F35AFD3D27EE49990011A725 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; F35AFD3F27EE49990011A725 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; F35AFD4727EE4B400011A725 /* OneSignalNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OneSignalNotificationServiceExtension.entitlements; sourceTree = ""; }; - F4B149117182C6CC58DD570C /* TokenBalance.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenBalance.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/TokenBalance.graphql.swift; sourceTree = ""; }; + F38DFE921F0467FF51A3D717 /* AssetChange.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AssetChange.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Unions/AssetChange.graphql.swift; sourceTree = ""; }; F56CC08FBB20FAC0DF6B93DA /* Pods-OneSignalNotificationServiceExtension.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.beta.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.beta.xcconfig"; sourceTree = ""; }; - F5BCBB5D46312050E4A5BD64 /* NftApproveForAll.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftApproveForAll.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftApproveForAll.graphql.swift; sourceTree = ""; }; - F7B3C6DF5DED22E892851B10 /* ContractInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ContractInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/ContractInput.graphql.swift; sourceTree = ""; }; - F912AB9AB67808B740246798 /* NftActivityConnection.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftActivityConnection.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftActivityConnection.graphql.swift; sourceTree = ""; }; - FC419570F5A025E80AB10A72 /* TokenStandard.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TokenStandard.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Enums/TokenStandard.graphql.swift; sourceTree = ""; }; + F70F4D05A45BC18EA88868A0 /* NftBalancesFilterInput.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftBalancesFilterInput.graphql.swift; path = WidgetsCore/MobileSchema/Schema/InputObjects/NftBalancesFilterInput.graphql.swift; sourceTree = ""; }; + F99987D29B574C20584CE11A /* NftApproveForAll.graphql.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NftApproveForAll.graphql.swift; path = WidgetsCore/MobileSchema/Schema/Objects/NftApproveForAll.graphql.swift; sourceTree = ""; }; FD54D51C296C79A4007A37E9 /* GoogleServiceInfo */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = GoogleServiceInfo; sourceTree = SOURCE_ROOT; }; FD7304CD28A364FC0085BDEA /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Colors.xcassets; path = Uniswap/Colors.xcassets; sourceTree = ""; }; FD7304CF28A3650A0085BDEA /* Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Colors.swift; path = Uniswap/Colors.swift; sourceTree = ""; }; @@ -928,13 +673,6 @@ 072E23872A44D5BD006AD6C9 /* WidgetsCore */ = { isa = PBXGroup; children = ( - 00265F782C933CE300A5DA57 /* ProtectionAttackType.graphql.swift */, - 00265F772C933CE300A5DA57 /* ProtectionResult.graphql.swift */, - 0013F5F62C93399400D6EF09 /* ProtectionInfo.graphql.swift */, - A70E4DD32C25DA0A002D6D86 /* NetworkFee.graphql.swift */, - A70E4DD62C260416002D6D86 /* SwapOrderStatus.graphql.swift */, - A70E4DD52C260416002D6D86 /* SwapOrderType.graphql.swift */, - A7B8EFCA2BF68F0D00CA4A1C /* FeeData.graphql.swift */, 07F0C28E2A5F3E2E00D5353E /* Env.swift */, 073C67F42A5C8FBE00F6DAD8 /* MobileSchema */, 072E23882A44D5BD006AD6C9 /* WidgetsCore.h */, @@ -969,7 +707,6 @@ isa = PBXGroup; children = ( 074321A32A83E3C900F8518D /* Fragments */, - 074321A52A83E3C900F8518D /* MobileSchema.graphql.swift */, 0743218E2A83E3C900F8518D /* Operations */, 074321A62A83E3C900F8518D /* Schema */, ); @@ -987,26 +724,6 @@ 0743218F2A83E3C900F8518D /* Queries */ = { isa = PBXGroup; children = ( - BA869E362D56B0B600D7A718 /* WidgetTokensQuery.graphql.swift */, - 5EFB78352B1E585000E77EAC /* ConvertQuery.graphql.swift */, - 077E60382A85587800ABC4B9 /* TokensQuery.graphql.swift */, - 074321902A83E3C900F8518D /* TokenDetailsScreenQuery.graphql.swift */, - 077E603A2A86D06100ABC4B9 /* MultiplePortfolioBalancesQuery.graphql.swift */, - 074321932A83E3C900F8518D /* NftsTabQuery.graphql.swift */, - 074321942A83E3C900F8518D /* TokenQuery.graphql.swift */, - 074321962A83E3C900F8518D /* TopTokensQuery.graphql.swift */, - 074321972A83E3C900F8518D /* TokenPriceHistoryQuery.graphql.swift */, - 074321982A83E3C900F8518D /* FavoriteTokenCardQuery.graphql.swift */, - 074321992A83E3C900F8518D /* NFTItemScreenQuery.graphql.swift */, - 0743219A2A83E3C900F8518D /* NftCollectionScreenQuery.graphql.swift */, - 0743219B2A83E3C900F8518D /* SearchTokensQuery.graphql.swift */, - 0743219C2A83E3C900F8518D /* ExploreSearchQuery.graphql.swift */, - 0743219D2A83E3C900F8518D /* TokenProjectsQuery.graphql.swift */, - 0743219E2A83E3C900F8518D /* SelectWalletScreenQuery.graphql.swift */, - 0743219F2A83E3C900F8518D /* PortfolioBalancesQuery.graphql.swift */, - 074321A02A83E3C900F8518D /* NftsQuery.graphql.swift */, - 074321A12A83E3C900F8518D /* TransactionListQuery.graphql.swift */, - 074321A22A83E3C900F8518D /* TransactionHistoryUpdaterQuery.graphql.swift */, ); path = Queries; sourceTree = ""; @@ -1014,22 +731,6 @@ 074321A32A83E3C900F8518D /* Fragments */ = { isa = PBXGroup; children = ( - 91D5017C2CDBEAF600B09B7F /* HomeScreenTokenParts.graphql.swift */, - 91D501662CDBEAE700B09B7F /* TokenBalanceMainParts.graphql.swift */, - 91D5016A2CDBEAE700B09B7F /* TokenBalanceQuantityParts.graphql.swift */, - 91D501682CDBEAE700B09B7F /* TokenBasicInfoParts.graphql.swift */, - 91D501692CDBEAE700B09B7F /* TokenBasicProjectParts.graphql.swift */, - 9173CEBB2D03C6F30036DA28 /* TokenBalanceParts.graphql.swift */, - 91D5016F2CDBEAE700B09B7F /* TokenFeeDataParts.graphql.swift */, - 91D501652CDBEAE700B09B7F /* TokenMarketParts.graphql.swift */, - 91D5016C2CDBEAE700B09B7F /* TokenParts.graphql.swift */, - 91D501672CDBEAE700B09B7F /* TokenProjectMarketsParts.graphql.swift */, - 91D5016D2CDBEAE700B09B7F /* TokenProjectUrlsParts.graphql.swift */, - 91D5016B2CDBEAE700B09B7F /* TokenProtectionInfoParts.graphql.swift */, - 91D5016E2CDBEAE700B09B7F /* TopTokenParts.graphql.swift */, - 9127D1342CC2D3D00096F134 /* TokenBalanceMainParts.graphql.swift */, - 9127D1352CC2D3D00096F134 /* TokenBalanceQuantityParts.graphql.swift */, - 074321A42A83E3C900F8518D /* TopTokenParts.graphql.swift */, ); path = Fragments; sourceTree = ""; @@ -1042,7 +743,6 @@ 074321B62A83E3C900F8518D /* Objects */, 074321DE2A83E3C900F8518D /* SchemaConfiguration.swift */, 074321DF2A83E3C900F8518D /* InputObjects */, - 074321E72A83E3C900F8518D /* SchemaMetadata.graphql.swift */, 074321E82A83E3C900F8518D /* Interfaces */, ); path = Schema; @@ -1051,8 +751,6 @@ 074321A72A83E3C900F8518D /* Unions */ = { isa = PBXGroup; children = ( - 9F813E9D2AA8FB5700438D89 /* ActivityDetails.graphql.swift */, - 074321A82A83E3C900F8518D /* AssetChange.graphql.swift */, ); path = Unions; sourceTree = ""; @@ -1060,18 +758,6 @@ 074321A92A83E3C900F8518D /* Enums */ = { isa = PBXGroup; children = ( - 9F813EA32AA8FBCF00438D89 /* TransactionType.graphql.swift */, - 074321AA2A83E3C900F8518D /* HistoryDuration.graphql.swift */, - 074321AB2A83E3C900F8518D /* TokenStandard.graphql.swift */, - 074321AC2A83E3C900F8518D /* Currency.graphql.swift */, - 074321AE2A83E3C900F8518D /* Chain.graphql.swift */, - 074321AF2A83E3C900F8518D /* TokenSortableField.graphql.swift */, - 074321B02A83E3C900F8518D /* TransactionDirection.graphql.swift */, - 074321B12A83E3C900F8518D /* NftStandard.graphql.swift */, - 074321B22A83E3C900F8518D /* NftActivityType.graphql.swift */, - 074321B32A83E3C900F8518D /* SafetyLevel.graphql.swift */, - 074321B42A83E3C900F8518D /* NftMarketplace.graphql.swift */, - 074321B52A83E3C900F8518D /* TransactionStatus.graphql.swift */, ); path = Enums; sourceTree = ""; @@ -1079,52 +765,6 @@ 074321B62A83E3C900F8518D /* Objects */ = { isa = PBXGroup; children = ( - 9F8123822D2F33C4009C5D88 /* BlockaidFees.graphql.swift */, - 9F00A4392B33894C0088A0D0 /* ApplicationContract.graphql.swift */, - 8EE7C0572AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift */, - 9F813EA12AA8FB8C00438D89 /* SwapOrderDetails.graphql.swift */, - 9F813E9F2AA8FB7500438D89 /* TransactionDetails.graphql.swift */, - 074321B72A83E3C900F8518D /* Image.graphql.swift */, - 074321B82A83E3C900F8518D /* NftOrderEdge.graphql.swift */, - 074321B92A83E3C900F8518D /* NftApproveForAll.graphql.swift */, - 074321BA2A83E3C900F8518D /* NftAssetTrait.graphql.swift */, - 074321BB2A83E3C900F8518D /* NftBalanceConnection.graphql.swift */, - 074321BC2A83E3C900F8518D /* NftActivityEdge.graphql.swift */, - 074321BD2A83E3C900F8518D /* NftCollection.graphql.swift */, - 074321BE2A83E3C900F8518D /* TimestampedAmount.graphql.swift */, - 074321BF2A83E3C900F8518D /* NftAssetConnection.graphql.swift */, - 074321C02A83E3C900F8518D /* TokenProject.graphql.swift */, - 074321C12A83E3C900F8518D /* NftTransfer.graphql.swift */, - 074321C22A83E3C900F8518D /* TokenApproval.graphql.swift */, - 074321C32A83E3C900F8518D /* NftOrderConnection.graphql.swift */, - 074321C42A83E3C900F8518D /* TokenMarket.graphql.swift */, - 074321C52A83E3C900F8518D /* NftCollectionMarket.graphql.swift */, - 074321C62A83E3C900F8518D /* TokenBalance.graphql.swift */, - 074321C72A83E3C900F8518D /* NftOrder.graphql.swift */, - 074321C82A83E3C900F8518D /* Portfolio.graphql.swift */, - 074321C92A83E3C900F8518D /* PageInfo.graphql.swift */, - 074321CA2A83E3C900F8518D /* NftAssetEdge.graphql.swift */, - 074321CB2A83E3C900F8518D /* NftCollectionConnection.graphql.swift */, - 074321CC2A83E3C900F8518D /* NftContract.graphql.swift */, - 074321CD2A83E3C900F8518D /* NftActivityConnection.graphql.swift */, - 074321CE2A83E3C900F8518D /* Amount.graphql.swift */, - 074321CF2A83E3C900F8518D /* Query.graphql.swift */, - 074321D02A83E3C900F8518D /* NftAsset.graphql.swift */, - 074321D12A83E3C900F8518D /* Dimensions.graphql.swift */, - 074321D22A83E3C900F8518D /* TokenProjectMarket.graphql.swift */, - 074321D32A83E3C900F8518D /* AmountChange.graphql.swift */, - 074321D42A83E3C900F8518D /* NftBalanceEdge.graphql.swift */, - 074321D52A83E3C900F8518D /* NftActivity.graphql.swift */, - 074321D62A83E3C900F8518D /* AssetActivity.graphql.swift */, - 074321D72A83E3C900F8518D /* NftProfile.graphql.swift */, - 074321D82A83E3C900F8518D /* NftCollectionEdge.graphql.swift */, - 074321DA2A83E3C900F8518D /* TokenTransfer.graphql.swift */, - 074321DB2A83E3C900F8518D /* NftApproval.graphql.swift */, - 074321DC2A83E3C900F8518D /* NftBalance.graphql.swift */, - 074321DD2A83E3C900F8518D /* Token.graphql.swift */, - 0DE251452C13B69D005F47F9 /* OnRampServiceProvider.graphql.swift */, - 0DE251462C13B69D005F47F9 /* OnRampTransactionDetails.graphql.swift */, - 0DE251442C13B69D005F47F9 /* OnRampTransfer.graphql.swift */, ); path = Objects; sourceTree = ""; @@ -1132,15 +772,6 @@ 074321DF2A83E3C900F8518D /* InputObjects */ = { isa = PBXGroup; children = ( - 0DC6ADEF2B1E2C0F0092909C /* PortfolioValueModifier.graphql.swift */, - 9F29D4EC2B47126D004D003A /* NftBalanceAssetInput.graphql.swift */, - 074321E02A83E3C900F8518D /* NftBalancesFilterInput.graphql.swift */, - 074321E12A83E3C900F8518D /* NftCollectionsFilterInput.graphql.swift */, - 074321E22A83E3C900F8518D /* ContractInput.graphql.swift */, - 074321E32A83E3C900F8518D /* NftActivityFilterInput.graphql.swift */, - 074321E42A83E3C900F8518D /* NftAssetTraitInput.graphql.swift */, - 074321E62A83E3C900F8518D /* NftAssetsFilterInput.graphql.swift */, - 0DE251422C13B674005F47F9 /* OnRampTransactionsAuth.graphql.swift */, ); path = InputObjects; sourceTree = ""; @@ -1148,8 +779,6 @@ 074321E82A83E3C900F8518D /* Interfaces */ = { isa = PBXGroup; children = ( - 074321E92A83E3C900F8518D /* IAmount.graphql.swift */, - 074321EA2A83E3C900F8518D /* IContract.graphql.swift */, ); path = Interfaces; sourceTree = ""; @@ -1195,6 +824,27 @@ path = Uniswap/Onboarding/EmbeddedWallet; sourceTree = ""; }; + 0E2552A1FE1E968DB6E45EDB /* Enums */ = { + isa = PBXGroup; + children = ( + 25DDE9DCAFA0EE2838508994 /* Chain.graphql.swift */, + E5DDE9BCC6A539F6E9AE1664 /* Currency.graphql.swift */, + DC6517FF8972DA448CDD2DFC /* HistoryDuration.graphql.swift */, + 8D973A66C23859D34E6FE175 /* NftStandard.graphql.swift */, + BE4AB3FCC91BE6C73557E0E3 /* ProtectionAttackType.graphql.swift */, + CA586C522ABE5DA5363D27C7 /* ProtectionResult.graphql.swift */, + 41FFD02A227ACFB661A44813 /* SafetyLevel.graphql.swift */, + 132FC1B05AE3D510C3B71EBA /* SwapOrderStatus.graphql.swift */, + 0A7D41CB7AA0496123E2153D /* SwapOrderType.graphql.swift */, + 3D96819BA563CB218D29C657 /* TokenSortableField.graphql.swift */, + CE5E0F71E23968DEFCEB7A4B /* TokenStandard.graphql.swift */, + 4EC9CABC0B4E73B25C7A2128 /* TransactionDirection.graphql.swift */, + A182D5FF8C0919083F2E333C /* TransactionStatus.graphql.swift */, + 37A6D5D76D31F8C5CDD29814 /* TransactionType.graphql.swift */, + ); + name = Enums; + sourceTree = ""; + }; 13B07FAE1A68108700A75B9A /* Uniswap */ = { isa = PBXGroup; children = ( @@ -1204,8 +854,7 @@ 07B067692A7D6EC8001DD9B9 /* Widget */, 03DD298C2A4CE34B00E3E0F5 /* Appearance */, F35AFD3627EE49230011A725 /* Uniswap.entitlements */, - 13B07FAF1A68108700A75B9A /* AppDelegate.h */, - 9FEC9B8A2A858CF1003CD019 /* AppDelegate.m */, + 45FFF7DD2E8C2A3A00362570 /* Notifications */, 6C84F055283D83CF0071FA2E /* Onboarding */, 6CE631B928186D4500716D29 /* WalletConnect */, A32F9FBC272343C8002CFCDB /* GoogleService-Info.plist */, @@ -1213,14 +862,47 @@ 6CA91BDD2A95226200C4063E /* RNCloudBackupsManager */, 13B07FB51A68108700A75B9A /* Images.xcassets */, 13B07FB61A68108700A75B9A /* Info.plist */, - 13B07FB71A68108700A75B9A /* main.m */, BF9176E944C84910B1C0B057 /* SplashScreen.storyboard */, FD7304CD28A364FC0085BDEA /* Colors.xcassets */, FD7304CF28A3650A0085BDEA /* Colors.swift */, + 8B2A92162EB3E78E00990413 /* AppDelegate.swift */, + 8B2A92182EB3E79500990413 /* Uniswap-Bridging-Header.h */, ); name = Uniswap; sourceTree = ""; }; + 14138E8BFDEA86F0825B9C6D /* MobileSchema */ = { + isa = PBXGroup; + children = ( + A24882D069B34B7DC9E8B4CE /* MobileSchema.graphql.swift */, + 14D11F5F6A001B2F9CC05DC9 /* Fragments */, + 7A9EBBAEE00CEC84AFF4B49E /* Operations */, + 73DBB3ED6E3D25FBAA098F91 /* Schema */, + ); + name = MobileSchema; + sourceTree = ""; + }; + 14D11F5F6A001B2F9CC05DC9 /* Fragments */ = { + isa = PBXGroup; + children = ( + AB3EEA677984132330D8D279 /* HomeScreenTokenParts.graphql.swift */, + 7F0C115796D33739746778E3 /* TokenBalanceMainParts.graphql.swift */, + B97D41BC2760803FD1C7CB3C /* TokenBalanceParts.graphql.swift */, + 8D5FF4316461BD4FB5847D62 /* TokenBalanceQuantityParts.graphql.swift */, + 319AAC5377C60EB6579CC5C6 /* TokenBasicInfoParts.graphql.swift */, + BB9FB14C8B68CE1D05643256 /* TokenBasicProjectParts.graphql.swift */, + 413CEB2443D6DD1DA7CC7231 /* TokenFeeDataParts.graphql.swift */, + C6754D4BD9CE76AAC915F814 /* TokenMarketParts.graphql.swift */, + 0A266B733FD2FA9C1C1A7731 /* TokenParts.graphql.swift */, + 3BA467FB84503BB53B081C73 /* TokenProjectMarketsParts.graphql.swift */, + CC2EE4B5A1180F03030E80A9 /* TokenProjectTokensTvlParts.graphql.swift */, + 9F1055EFA8467996754739A7 /* TokenProjectUrlsParts.graphql.swift */, + 93D1BA7F635F61FC7E62C6C1 /* TokenProtectionInfoParts.graphql.swift */, + 43D7F54953D864DC067FADD1 /* TopTokenParts.graphql.swift */, + ); + name = Fragments; + sourceTree = ""; + }; 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1240,6 +922,64 @@ name = Frameworks; sourceTree = ""; }; + 2F1F2ABA5F5FE75352ED9B7C /* Objects */ = { + isa = PBXGroup; + children = ( + 74673620CE906C9AF34C6750 /* Amount.graphql.swift */, + 43AB42F3ED903CF49D7D4DE7 /* AmountChange.graphql.swift */, + 9BF540E1110D0FD1C219C45C /* ApplicationContract.graphql.swift */, + 8BB8B6395E3C86412FA793CB /* AssetActivity.graphql.swift */, + 201FF7E30EC98ABC121BC8BF /* BlockaidFees.graphql.swift */, + 44D824154584CD39C82904E9 /* BridgedWithdrawalInfo.graphql.swift */, + 6F02DBC2C51EFC6B1AA9C1CD /* DescriptionTranslations.graphql.swift */, + 6DCC7E203AB163C9C2D3D789 /* Dimensions.graphql.swift */, + 290B66F38FEC486AF1709BE4 /* FeeData.graphql.swift */, + 284C5150E9B50FB5BE531572 /* Image.graphql.swift */, + 2FB70424D7C456B9B72536C6 /* NetworkFee.graphql.swift */, + 42158AF500DDBEEF6B7E98E3 /* NftApproval.graphql.swift */, + F99987D29B574C20584CE11A /* NftApproveForAll.graphql.swift */, + CD3573B4E6FD61F5DDBD9C75 /* NftAsset.graphql.swift */, + C8BAFEAABF78617095D44E32 /* NftBalance.graphql.swift */, + 56093E216B82288E1A4018F0 /* NftBalanceConnection.graphql.swift */, + F182567B705FC0E15DA8F34A /* NftBalanceEdge.graphql.swift */, + 2ECFE86020AB7A252CDC9BB1 /* NftCollection.graphql.swift */, + 66697C913CF365C9460F42E6 /* NftCollectionMarket.graphql.swift */, + 9A2AA83B4F9F4290A2BC7CCB /* NftContract.graphql.swift */, + E21EB4FB53B7B6B5848946D9 /* NftProfile.graphql.swift */, + 792EF7A7820987EA9E99D792 /* NftTransfer.graphql.swift */, + 6AFC48B9A573915BFA6FC6E2 /* OffRampTransactionDetails.graphql.swift */, + 3063061F7C203A1DA47B8FBB /* OffRampTransfer.graphql.swift */, + 0E5478334318BD5D6F8366D6 /* OnRampServiceProvider.graphql.swift */, + 614A491FAB9659FA08CD9AE4 /* OnRampTransactionDetails.graphql.swift */, + 60195493B9C71EDEECA46E80 /* OnRampTransfer.graphql.swift */, + 5F2898E709490CC198B98228 /* PageInfo.graphql.swift */, + A34E3A6E13A4066767AA6757 /* Portfolio.graphql.swift */, + 346B304DE3207797FD1FDE71 /* ProtectionInfo.graphql.swift */, + 58641E30674C4C4241702902 /* Query.graphql.swift */, + 32764E27B5D9FCF0AA4217CF /* SwapOrderDetails.graphql.swift */, + 2C9E8E39C13967E447FC193F /* TimestampedAmount.graphql.swift */, + 88B481513A2E780E6489DC4F /* Token.graphql.swift */, + 009F4258CE111989223E99A4 /* TokenApproval.graphql.swift */, + 4AF8EE05291534B82ACC1105 /* TokenBalance.graphql.swift */, + A02D64B698D203A7F3643C18 /* TokenMarket.graphql.swift */, + A424FA93DB720DD7684C7674 /* TokenProject.graphql.swift */, + A3E638BE80543E905D072D7F /* TokenProjectMarket.graphql.swift */, + 93783F96FA74CB9A1BEB32EE /* TokenTransfer.graphql.swift */, + 47148657FBA8DCC92DD69573 /* TransactionDetails.graphql.swift */, + ); + name = Objects; + sourceTree = ""; + }; + 45FFF7DD2E8C2A3A00362570 /* Notifications */ = { + isa = PBXGroup; + children = ( + 45FFF7E02E8C2E6100362570 /* SilentPushEventEmitter.swift */, + 45FFF7DE2E8C2A6400362570 /* SilentPushEventEmitter.m */, + ); + name = Notifications; + path = Uniswap/Notifications; + sourceTree = ""; + }; 47914D9EE3A4DE926EFC5089 /* UniswapTests */ = { isa = PBXGroup; children = ( @@ -1248,27 +988,13 @@ name = UniswapTests; sourceTree = ""; }; - 4EEA950E66FC2881D331F823 /* Enums */ = { + 49A0455CC5D07F432B367D3F /* Interfaces */ = { isa = PBXGroup; children = ( - 4A8990A17F489F284E0DB1B3 /* Chain.graphql.swift */, - 25B4C7C7FEF32EAF10D030F9 /* Currency.graphql.swift */, - 3BF42F88002431BFDC9FCA9F /* HistoryDuration.graphql.swift */, - 2D95FE36D2667E070345CDD0 /* NftActivityType.graphql.swift */, - 600250B00B6C02AAB063818B /* NftMarketplace.graphql.swift */, - 4B308EC3DB7C3C1DD5DDE03D /* NftStandard.graphql.swift */, - 40E034582FBD2C374CEF808E /* ProtectionAttackType.graphql.swift */, - BD0FC2D534CB82B151C7F9B7 /* ProtectionResult.graphql.swift */, - 4ACFA5B4204741038C986C5D /* SafetyLevel.graphql.swift */, - D574AB5194929038A54B4D4B /* SwapOrderStatus.graphql.swift */, - 148617FD73F2CD7117960DBA /* SwapOrderType.graphql.swift */, - 32012AC135EBD991C8F02F92 /* TokenSortableField.graphql.swift */, - FC419570F5A025E80AB10A72 /* TokenStandard.graphql.swift */, - CC2FBE9F353D3753FB94B8BF /* TransactionDirection.graphql.swift */, - 2ABA47847A73130D356EB6BF /* TransactionStatus.graphql.swift */, - 69F2FFD11FB5E462CC454A15 /* TransactionType.graphql.swift */, + A372929BEDB706B6144334F0 /* IAmount.graphql.swift */, + 1D88F29D12AB0D8B4C00D065 /* IContract.graphql.swift */, ); - name = Enums; + name = Interfaces; sourceTree = ""; }; 5754C6A1B51170788A63F6F3 /* ExpoModulesProviders */ = { @@ -1283,7 +1009,7 @@ 5841D897B122046172ACD989 /* WidgetsCore */ = { isa = PBXGroup; children = ( - 8D57A3CBC005A567F7303961 /* MobileSchema */, + 14138E8BFDEA86F0825B9C6D /* MobileSchema */, ); name = WidgetsCore; sourceTree = ""; @@ -1298,6 +1024,18 @@ path = PrivateKeyDisplay; sourceTree = ""; }; + 6583DF0353495046646DE918 /* InputObjects */ = { + isa = PBXGroup; + children = ( + 957594D21A42B9D77F5AD685 /* ContractInput.graphql.swift */, + C394AC5DB58BDF1C33CC0927 /* NftBalanceAssetInput.graphql.swift */, + F70F4D05A45BC18EA88868A0 /* NftBalancesFilterInput.graphql.swift */, + 38EAF64134981DC76B6C7E90 /* OnRampTransactionsAuth.graphql.swift */, + E13150074BB66C20E7EA271A /* PortfolioValueModifier.graphql.swift */, + ); + name = InputObjects; + sourceTree = ""; + }; 6BC7D07A2B5FF02400617C95 /* Scantastic */ = { isa = PBXGroup; children = ( @@ -1356,6 +1094,28 @@ name = WalletConnect; sourceTree = ""; }; + 73DBB3ED6E3D25FBAA098F91 /* Schema */ = { + isa = PBXGroup; + children = ( + 0D25B69F4A69D733A5DAEC42 /* SchemaConfiguration.swift */, + 73D981A2D31B6F8822C19324 /* SchemaMetadata.graphql.swift */, + 0E2552A1FE1E968DB6E45EDB /* Enums */, + 6583DF0353495046646DE918 /* InputObjects */, + 49A0455CC5D07F432B367D3F /* Interfaces */, + 2F1F2ABA5F5FE75352ED9B7C /* Objects */, + C371BE586C38E8B49D49C32A /* Unions */, + ); + name = Schema; + sourceTree = ""; + }; + 7A9EBBAEE00CEC84AFF4B49E /* Operations */ = { + isa = PBXGroup; + children = ( + FAB5CD57887CDC6FE536B147 /* Queries */, + ); + name = Operations; + sourceTree = ""; + }; 832341AE1AAA6A7D00B99B32 /* Libraries */ = { isa = PBXGroup; children = ( @@ -1403,17 +1163,6 @@ name = Products; sourceTree = ""; }; - 8D57A3CBC005A567F7303961 /* MobileSchema */ = { - isa = PBXGroup; - children = ( - A4E0C448C5D45BF1071FA458 /* MobileSchema.graphql.swift */, - CA8FF4CDF9B448F7BE77CA07 /* Fragments */, - F79593090BE522E6D2A3330F /* Operations */, - 9723B33A3716C9068F5FD2B1 /* Schema */, - ); - name = MobileSchema; - sourceTree = ""; - }; 8E566D9F2AA1095000D4AA76 /* Components */ = { isa = PBXGroup; children = ( @@ -1462,20 +1211,6 @@ path = Uniswap/Icons; sourceTree = ""; }; - 9723B33A3716C9068F5FD2B1 /* Schema */ = { - isa = PBXGroup; - children = ( - 2E49E724432C70A2551FB2C7 /* SchemaConfiguration.swift */, - C65195A1CC9894C6341385C3 /* SchemaMetadata.graphql.swift */, - 4EEA950E66FC2881D331F823 /* Enums */, - B741FF3580D33B5968CB9841 /* InputObjects */, - A60630E00A276B08BE85275D /* Interfaces */, - AB0BCF231B03A3F4E547DFF5 /* Objects */, - F23767E14A7943D5969AFDC1 /* Unions */, - ); - name = Schema; - sourceTree = ""; - }; 9759A762F61D6B2F01C79DBF /* Uniswap */ = { isa = PBXGroup; children = ( @@ -1484,89 +1219,6 @@ name = Uniswap; sourceTree = ""; }; - A60630E00A276B08BE85275D /* Interfaces */ = { - isa = PBXGroup; - children = ( - 03AD8C2B1226EBE1524F2573 /* IAmount.graphql.swift */, - 5E0D68CC8D1F13D864F069BB /* IContract.graphql.swift */, - ); - name = Interfaces; - sourceTree = ""; - }; - AB0BCF231B03A3F4E547DFF5 /* Objects */ = { - isa = PBXGroup; - children = ( - D20E9461A6C9C1CD6A24BEED /* Amount.graphql.swift */, - E63EA0C75D8011BCC182492C /* AmountChange.graphql.swift */, - 608B07304ED1387B5AEAA3BB /* ApplicationContract.graphql.swift */, - E4624A41EC1468712BD77653 /* AssetActivity.graphql.swift */, - BD35300F81746B2D4A23065A /* BlockaidFees.graphql.swift */, - 7E1F288B7EDDE906C4C00C46 /* DescriptionTranslations.graphql.swift */, - 54B1C71AED738D17898103A6 /* Dimensions.graphql.swift */, - 17819A49DE69723EC60D9D72 /* FeeData.graphql.swift */, - 31ED613D63FE9C56D9270517 /* Image.graphql.swift */, - 8BEADEB4AE05B860CF2232D1 /* NetworkFee.graphql.swift */, - F1A24D364C0D262A14BB7182 /* NftActivity.graphql.swift */, - F912AB9AB67808B740246798 /* NftActivityConnection.graphql.swift */, - 4DB4379B5E222207CA7328D0 /* NftActivityEdge.graphql.swift */, - 4E32BBB65A6DD9CFF608C8E8 /* NftApproval.graphql.swift */, - F5BCBB5D46312050E4A5BD64 /* NftApproveForAll.graphql.swift */, - F2932B486DD7070DF226A27B /* NftAsset.graphql.swift */, - 218B8AB05C80D919671D3970 /* NftAssetConnection.graphql.swift */, - 6DF7C8888BE6AFA2F22889FE /* NftAssetEdge.graphql.swift */, - 70851E3D1A4B296A1CE22A20 /* NftAssetTrait.graphql.swift */, - 05BE2F49DD1FB9A692E21C13 /* NftBalance.graphql.swift */, - 1FC7B80EC0E0D2134B67575B /* NftBalanceConnection.graphql.swift */, - 0B132543F80E3C9191219E6D /* NftBalanceEdge.graphql.swift */, - 63ACFF2A1D5AE3498731AC69 /* NftCollection.graphql.swift */, - 92DE390311705FF1EE65C0E6 /* NftCollectionConnection.graphql.swift */, - 37038CCEB11E70E2027EF4EA /* NftCollectionEdge.graphql.swift */, - 4F4F47D22B749A438B5BF674 /* NftCollectionMarket.graphql.swift */, - 18B9EB43501127BE1B14F1A5 /* NftContract.graphql.swift */, - 6E3A78386B3B779B688021FF /* NftOrder.graphql.swift */, - 39FC2A13550FA6754EC1A7FC /* NftOrderConnection.graphql.swift */, - 49BD6C776029FAF4FC711DE7 /* NftOrderEdge.graphql.swift */, - DB35E518AD7FBFEBEA9CAB95 /* NftProfile.graphql.swift */, - F3317EBD4E3B11E2F2A1CE58 /* NftTransfer.graphql.swift */, - 396B6AB3BB41898B56EB3828 /* OffRampTransactionDetails.graphql.swift */, - C9D5E6A07F404974045BD525 /* OffRampTransfer.graphql.swift */, - 6A84A192F750A0F1BC8D7A69 /* OnRampServiceProvider.graphql.swift */, - 12D3AB6C2FC9513E9F224B7C /* OnRampTransactionDetails.graphql.swift */, - 2139ABF56EA067ED792A48C9 /* OnRampTransfer.graphql.swift */, - E475EBABF7EA310435E2F19C /* PageInfo.graphql.swift */, - 1849039FD4A210DA990363C8 /* Portfolio.graphql.swift */, - 0702203B17BDD858CED28F8B /* ProtectionInfo.graphql.swift */, - ED157D3DB73302C8A1B9522E /* Query.graphql.swift */, - C955CE2592785712A11892DE /* SwapOrderDetails.graphql.swift */, - B0DB67D6C438F623976727E9 /* TimestampedAmount.graphql.swift */, - 647933A5DC35BDFEEF3620A1 /* Token.graphql.swift */, - 9E975BEDBED5FE51D0DC6E96 /* TokenApproval.graphql.swift */, - F4B149117182C6CC58DD570C /* TokenBalance.graphql.swift */, - EBD833F6580F9B82A68D4A98 /* TokenMarket.graphql.swift */, - 4981A14906A35E8A0B7F9457 /* TokenProject.graphql.swift */, - 3B9A505509D9A85BE1DB7D05 /* TokenProjectMarket.graphql.swift */, - 8DA4E7C052A6C4AC3A9DCDA6 /* TokenTransfer.graphql.swift */, - ABE51731EB853137302A7B0D /* TransactionDetails.graphql.swift */, - ); - name = Objects; - sourceTree = ""; - }; - B741FF3580D33B5968CB9841 /* InputObjects */ = { - isa = PBXGroup; - children = ( - F7B3C6DF5DED22E892851B10 /* ContractInput.graphql.swift */, - 43E2ECCEFBC3BE8D13BE6CAB /* NftActivityFilterInput.graphql.swift */, - 556339705BD103365D4ED07B /* NftAssetTraitInput.graphql.swift */, - 8B477EBEA1EDA3914C77E8A6 /* NftAssetsFilterInput.graphql.swift */, - 4E5FA43C5E31035775917C4E /* NftBalanceAssetInput.graphql.swift */, - 41BA2129FFE961C8549C8A30 /* NftBalancesFilterInput.graphql.swift */, - C4790C2A43570CAC086B936E /* NftCollectionsFilterInput.graphql.swift */, - 315B8A905FA2C60C053F138B /* OnRampTransactionsAuth.graphql.swift */, - 41003491EF939043CF9D0F4E /* PortfolioValueModifier.graphql.swift */, - ); - name = InputObjects; - sourceTree = ""; - }; C2C18ECBEF5A4489BF3A314C /* Resources */ = { isa = PBXGroup; children = ( @@ -1577,54 +1229,13 @@ name = Resources; sourceTree = ""; }; - CA8FF4CDF9B448F7BE77CA07 /* Fragments */ = { - isa = PBXGroup; - children = ( - 27F8B915042BD8D67D9627EA /* HomeScreenTokenParts.graphql.swift */, - 137696908D703632CEE3AB28 /* TokenBalanceMainParts.graphql.swift */, - F0C34E94906CB9F0A016D566 /* TokenBalanceParts.graphql.swift */, - 2370BAD675DC8064C00AC037 /* TokenBalanceQuantityParts.graphql.swift */, - 3377F7BB38BD5A45AE31909D /* TokenBasicInfoParts.graphql.swift */, - 2C2F4BE3080D4129D4478528 /* TokenBasicProjectParts.graphql.swift */, - 018603D0FA4D05DBF16F1441 /* TokenFeeDataParts.graphql.swift */, - BFC36D963A2A52D1E7CA77E4 /* TokenMarketParts.graphql.swift */, - 98F596250F55A3D6E9E1F499 /* TokenParts.graphql.swift */, - CC055C3EDFDE2966FCA83C9E /* TokenProjectMarketsParts.graphql.swift */, - 236126979349098B98C70F27 /* TokenProjectUrlsParts.graphql.swift */, - 0BB4A40E30E1D985519DB3CF /* TokenProtectionInfoParts.graphql.swift */, - 5640432978873FB030467750 /* TopTokenParts.graphql.swift */, - ); - name = Fragments; - sourceTree = ""; - }; - D9DDAE0E99B4703AF96A81AB /* Queries */ = { + C371BE586C38E8B49D49C32A /* Unions */ = { isa = PBXGroup; children = ( - 7BD3F0794908635CDFA7DAB6 /* ConvertQuery.graphql.swift */, - 84997376A0B436ECC0A996C5 /* ExploreSearchQuery.graphql.swift */, - 3F0EEAC1DADE827AE38EB8A6 /* FavoriteTokenCardQuery.graphql.swift */, - 2BA0ACE5B9B2C41187CD41F3 /* FeedTransactionListQuery.graphql.swift */, - B7E03AA02B39A7A90C96AA76 /* HomeScreenTokensQuery.graphql.swift */, - 4429CA6DA254ECC24A65DA14 /* MultiplePortfolioBalancesQuery.graphql.swift */, - 207F4DF7664454C31DE069F8 /* NFTItemScreenQuery.graphql.swift */, - EE0973BB445AAD8FAA25757F /* NftCollectionScreenQuery.graphql.swift */, - A9030AC59702756C39396FFE /* NftsQuery.graphql.swift */, - 730CA356D6E1D498CC0C1472 /* NftsTabQuery.graphql.swift */, - 985DDC8324B872DD44E87781 /* PortfolioBalancesQuery.graphql.swift */, - 8A5342DEF1410E0E81E97F40 /* SearchTokensQuery.graphql.swift */, - B033206DC974BCB6AF8CC613 /* SelectWalletScreenQuery.graphql.swift */, - A7A33933670CCAD9E62A1A80 /* TokenDetailsScreenQuery.graphql.swift */, - 9DFBD29C0BDFDB4464B8189C /* TokenPriceHistoryQuery.graphql.swift */, - 6FF8B660E9C2C0D8DDF7588A /* TokenProjectDescriptionQuery.graphql.swift */, - E8A49D011C60137D4034CAC4 /* TokenProjectsQuery.graphql.swift */, - 74885865ED8CFEA875E40916 /* TokenQuery.graphql.swift */, - B3C3F0403FAEFDD632AC6CCE /* TokensQuery.graphql.swift */, - 04E5C2900254E7BA7F10CE51 /* TopTokensQuery.graphql.swift */, - AF3D7A923E5E1FB504E1F64D /* TransactionHistoryUpdaterQuery.graphql.swift */, - 224CA82F1871016F4173F69E /* TransactionListQuery.graphql.swift */, - DBA33683D2BA6BBE3251D67C /* WidgetTokensQuery.graphql.swift */, + B68E391195D4587F61A33380 /* ActivityDetails.graphql.swift */, + F38DFE921F0467FF51A3D717 /* AssetChange.graphql.swift */, ); - name = Queries; + name = Unions; sourceTree = ""; }; E233CBF5F47BEE60B243DCF8 /* Pods */ = { @@ -1658,19 +1269,17 @@ C89238E3ED9F3AC98876B573 /* Pods-WidgetsCoreTests.release.xcconfig */, 065A981F892F7A06A900FCD5 /* Pods-WidgetsCoreTests.dev.xcconfig */, 4C445DB9798210862C34D0E0 /* Pods-WidgetsCoreTests.beta.xcconfig */, + 8CBD584CCF002F58176DA863 /* Pods-OneSignalNotificationServiceExtension.debugoptimized.xcconfig */, + 31CB18D02DE297D6DADB87C0 /* Pods-Uniswap.debugoptimized.xcconfig */, + 36F66895EB592B9F61AE658C /* Pods-Uniswap-UniswapTests.debugoptimized.xcconfig */, + 2590691EDB017AE6FB6C10AC /* Pods-WidgetIntentExtension.debugoptimized.xcconfig */, + 986A89D253EEB9BEBE5F08FF /* Pods-Widgets.debugoptimized.xcconfig */, + 24343423F6B8A16CB14F1E4C /* Pods-WidgetsCore.debugoptimized.xcconfig */, + CB72291CE6EFD3842A8E2A1D /* Pods-WidgetsCoreTests.debugoptimized.xcconfig */, ); path = Pods; sourceTree = ""; }; - F23767E14A7943D5969AFDC1 /* Unions */ = { - isa = PBXGroup; - children = ( - 8A4EB7BAD2C90E61FD6C30AF /* ActivityDetails.graphql.swift */, - 39F60B2B54796DC978765C99 /* AssetChange.graphql.swift */, - ); - name = Unions; - sourceTree = ""; - }; F35AFD3C27EE49990011A725 /* OneSignalNotificationServiceExtension */ = { isa = PBXGroup; children = ( @@ -1682,12 +1291,30 @@ path = OneSignalNotificationServiceExtension; sourceTree = ""; }; - F79593090BE522E6D2A3330F /* Operations */ = { + FAB5CD57887CDC6FE536B147 /* Queries */ = { isa = PBXGroup; children = ( - D9DDAE0E99B4703AF96A81AB /* Queries */, + 3D3112654E5722A2F862D3FC /* ConvertQuery.graphql.swift */, + 48DE72B77CD2D89B75BB934D /* FavoriteTokenCardQuery.graphql.swift */, + E21B5EF6BC4582DD2BB0DA13 /* FeedTransactionListQuery.graphql.swift */, + 2240891823DAE7BF8BB88FF4 /* HomeScreenTokensQuery.graphql.swift */, + E3AF9944E853669C2BC9B5AB /* MultiplePortfolioBalancesQuery.graphql.swift */, + 42013D777BAA40652AB28985 /* NftsQuery.graphql.swift */, + 0EA8FE9029A1E7C4EAD7F547 /* NftsTabQuery.graphql.swift */, + 8604CAA8F7E44D704A6F4AAF /* PortfolioBalancesQuery.graphql.swift */, + BB3CBF221D5D2E3C036A1BA0 /* SelectWalletScreenQuery.graphql.swift */, + E7ED83FFCB410FCCA2C6B97B /* TokenDetailsScreenQuery.graphql.swift */, + EDA02DA46906E4CDF827104A /* TokenPriceHistoryQuery.graphql.swift */, + 8C66DD1D03A824EB6A231DCE /* TokenProjectDescriptionQuery.graphql.swift */, + E1B3A0C152B683215291E3BD /* TokenProjectsQuery.graphql.swift */, + 16F706375DFAFB58F8440CF9 /* TokenQuery.graphql.swift */, + 5442480B3C91EDC515728606 /* TokensQuery.graphql.swift */, + C050FFEC067334CF05C17EA0 /* TopTokensQuery.graphql.swift */, + 3AE6A0CAEDE1DC1D2FA14DEA /* TransactionHistoryUpdaterQuery.graphql.swift */, + C27225607FACF638581E25B5 /* TransactionListQuery.graphql.swift */, + C0C956C520AAB54BCE8C5CCC /* WidgetTokensQuery.graphql.swift */, ); - name = Operations; + name = Queries; sourceTree = ""; }; /* End PBXGroup section */ @@ -1822,7 +1449,7 @@ 9F7898182A819D62004D5A98 /* Embed Frameworks */, 163678CCBB906C7B12421609 /* [CP] Embed Pods Frameworks */, 0487071ABBC71F28EF79F4AA /* [CP] Copy Pods Resources */, - A98B71284330BF95E7CAB037 /* [CP-User] [RNFB] Core Configuration */, + 868B6279F959D6931E7A870E /* [CP-User] [RNFB] Core Configuration */, ); buildRules = ( ); @@ -1991,7 +1618,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export PROJECT_ROOT=$PWD/..\nexport EXTRA_PACKAGER_ARGS=\"--sourcemap-output $PROJECT_ROOT/main.jsbundle.map\"\n\nset -e\nif [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\nsource \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\nsource \"$PODS_ROOT/../.xcode.env.local\"\nfi\nexport CONFIG_CMD=\"dummy-workaround-value\"\nexport CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@rnef/cli/package.json')) + '/dist/src/bin.js'\")\"\n\nWITH_ENVIRONMENT=\"../../../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../../../node_modules/react-native/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; + shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\n# The project root by default is one level up from the ios directory\nexport PROJECT_ROOT=\"$PROJECT_DIR\"/..\n\nif [[ \"$CONFIGURATION\" = *Debug* ]]; then\n export SKIP_BUNDLING=1\nfi\nif [[ -z \"$ENTRY_FILE\" ]]; then\n # Set the entry JS file using the bundler's entry resolution.\n export ENTRY_FILE=\"$(\"$NODE_BINARY\" -e \"require('expo/scripts/resolveAppEntry')\" \"$PROJECT_ROOT\" ios absolute | tail -n 1)\"\nfi\n\nif [[ -z \"$CLI_PATH\" ]]; then\n # Use Expo CLI\n export CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require.resolve('@expo/cli')\")\"\nfi\nif [[ -z \"$BUNDLE_COMMAND\" ]]; then\n # Default Expo CLI command for bundling\n export BUNDLE_COMMAND=\"export:embed\"\nfi\n\n`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/react-native-xcode.sh'\"`\n"; }; 0487071ABBC71F28EF79F4AA /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; @@ -2092,6 +1719,7 @@ "$(SRCROOT)/WidgetsCore/MobileSchema/Fragments/TokenMarketParts.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Fragments/TokenParts.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Fragments/TokenProjectMarketsParts.graphql.swift", + "$(SRCROOT)/WidgetsCore/MobileSchema/Fragments/TokenProjectTokensTvlParts.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Fragments/TokenProjectUrlsParts.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Fragments/TokenProtectionInfoParts.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Fragments/TopTokenParts.graphql.swift", @@ -2149,6 +1777,7 @@ "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/ApplicationContract.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/AssetActivity.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/BlockaidFees.graphql.swift", + "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/BridgedWithdrawalInfo.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/DescriptionTranslations.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/Dimensions.graphql.swift", "$(SRCROOT)/WidgetsCore/MobileSchema/Schema/Objects/FeeData.graphql.swift", @@ -2387,7 +2016,7 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - A98B71284330BF95E7CAB037 /* [CP-User] [RNFB] Core Configuration */ = { + 868B6279F959D6931E7A870E /* [CP-User] [RNFB] Core Configuration */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -2416,7 +2045,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "set -e\nexport SOURCEMAP_FILE=$DERIVED_FILE_DIR/main.jsbundle.map\n\nset -e\nif [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\nsource \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\nsource \"$PODS_ROOT/../.xcode.env.local\"\nfi\nexport CONFIG_CMD=\"dummy-workaround-value\"\nexport CLI_PATH=\"$(\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@rnef/cli/package.json')) + '/dist/src/bin.js'\")\"\n\nWITH_ENVIRONMENT=\"../../../node_modules/react-native/scripts/xcode/with-environment.sh\"\nDATADOG_XCODE=\"./sourcemaps-datadog.sh\"\n\nif [[ -n \"$DATADOG_API_KEY\" ]]; then\n # JS source maps\n /bin/sh -c \"$WITH_ENVIRONMENT $DATADOG_XCODE\"\n # iOS dSYM\n ../../../node_modules/.bin/datadog-ci dsyms upload $DWARF_DSYM_FOLDER_PATH\nelse\n echo \"Ignoring upload step for local, API key is missing.\"\nfi\n"; + shellScript = "set -e\n\n# Skip in debug configs\nif [ \"$CONFIGURATION\" = \"Debug\" ] || [ \"$CONFIGURATION\" = \"DebugOptimized\" ]; then\n echo \"warning: Skipping Datadog upload in $CONFIGURATION\"\n exit 0\nfi\n\nWITH_ENVIRONMENT=\"../../../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"./sourcemaps-datadog.sh\"\n\nexport SOURCEMAP_FILE=$DERIVED_FILE_DIR/main.jsbundle.map\n\nif [[ -n \"$DATADOG_API_KEY\" && \"$SKIP_DATADOG_UPLOAD\" != \"true\" ]]; then\n echo \"warning: Starting Datadog Uploads\"\n echo \"warning: Build Configuration: $CONFIGURATION\"\n echo \"warning: Product Name: $PRODUCT_NAME\"\n echo \"warning: Source Map File: $SOURCEMAP_FILE\"\n echo \"warning: dSYM Path: $DWARF_DSYM_FOLDER_PATH\"\n echo \"\"\n\n # JS source maps\n echo \"warning: Uploading JS source maps...\"\n \n /bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n\n echo \"warning: \"\n\n # iOS dSYM\n echo \"warning: Uploading iOS dSYMs...\"\n echo \"warning: dSYM command: datadog-ci dsyms upload $DWARF_DSYM_FOLDER_PATH\"\n\n DSYM_LOG=$(mktemp)\n if ../../../node_modules/.bin/datadog-ci dsyms upload $DWARF_DSYM_FOLDER_PATH 2>&1 | tee \"$DSYM_LOG\"; then\n echo \"warning: dSYM upload completed successfully\"\n rm -f \"$DSYM_LOG\"\n else\n DSYM_EXIT_CODE=$?\n echo \"warning: \"\n echo \"warning: dSYM Upload Failed\"\n echo \"warning: Exit Code: $DSYM_EXIT_CODE\"\n echo \"warning: \"\n echo \"warning:Full Error Output:\"\n echo \"warning: ---\"\n echo \"warning: $(cat \"$DSYM_LOG\")\"\n echo \"warning: ---\"\n echo \"warning: \"\n echo \"warning: Debug Information:\"\n echo \"warning: - datadog-ci version: $(../../../node_modules/.bin/datadog-ci version 2>&1 || echo 'Failed to get version')\"\n echo \"warning: - dSYM folder exists: $([ -d \\\"$DWARF_DSYM_FOLDER_PATH\\\" ] && echo 'Yes' || echo 'No')\"\n echo \"warning: - dSYM contents: $(ls -la \\\"$DWARF_DSYM_FOLDER_PATH\\\" 2>&1 || echo 'Failed to list')\"\n echo \"warning: - DATADOG_API_KEY set: $([ -n \\\"$DATADOG_API_KEY\\\" ] && echo 'Yes (${#DATADOG_API_KEY} chars)' || echo 'No')\"\n echo \"warning: - Working directory: $(pwd)\"\n echo \"warning: \"\n echo \"warning: This is non-critical. Build will continue.\"\n rm -f \"$DSYM_LOG\"\n fi\n\n echo \"warning: \"\n echo \"warning: Datadog upload phase completed (build continues regardless of upload status)\"\nelse\n echo \"warning: Skipping Datadog upload (DATADOG_API_KEY not set or SKIP_DATADOG_UPLOAD=true)\"\nfi\n\n# Always exit 0 to not fail the build\nexit 0\n"; }; F5C7F44CBF58F052A43EB4AA /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -2473,7 +2102,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n"; + shellScript = "if [[ -f \"$PODS_ROOT/../.xcode.env\" ]]; then\n source \"$PODS_ROOT/../.xcode.env\"\nfi\nif [[ -f \"$PODS_ROOT/../.xcode.env.local\" ]]; then\n source \"$PODS_ROOT/../.xcode.env.local\"\nfi\n\nexport RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > `$NODE_BINARY --print \"require('path').dirname(require.resolve('react-native/package.json')) + '/scripts/.packager.env'\"`\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open `$NODE_BINARY --print \"require('path').dirname(require.resolve('expo/package.json')) + '/scripts/launchPackager.command'\"` || echo \"Can't start packager automatically\"\n fi\nfi\n"; showEnvVarsInLog = 0; }; FD54D51B296C780A007A37E9 /* Copy configuration-specific GoogleServices-Info.plist */ = { @@ -2495,7 +2124,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\nif [ $CONFIGURATION == \"Release\" ]\nthen\n cp \"${SRCROOT}/GoogleServiceInfo/GoogleService-Info-Prod.plist\" \"${SRCROOT}/GoogleService-Info.plist\"\nelif [ $CONFIGURATION == 'Debug' ]\nthen\n cp \"${SRCROOT}/GoogleServiceInfo/GoogleService-Info-Dev.plist\" \"${SRCROOT}/GoogleService-Info.plist\"\nelse\n cp \"${SRCROOT}/GoogleServiceInfo/GoogleService-Info-$CONFIGURATION.plist\" \"${SRCROOT}/GoogleService-Info.plist\"\nfi\n"; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\nif [ $CONFIGURATION == \"Release\" ]\nthen\n cp \"${SRCROOT}/GoogleServiceInfo/GoogleService-Info-Prod.plist\" \"${SRCROOT}/GoogleService-Info.plist\"\nelif [ $CONFIGURATION == 'Debug' ] || [ $CONFIGURATION == 'DebugOptimized' ]\nthen\n cp \"${SRCROOT}/GoogleServiceInfo/GoogleService-Info-Dev.plist\" \"${SRCROOT}/GoogleService-Info.plist\"\nelse\n cp \"${SRCROOT}/GoogleServiceInfo/GoogleService-Info-$CONFIGURATION.plist\" \"${SRCROOT}/GoogleService-Info.plist\"\nfi\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -2514,248 +2143,119 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 074322392A83E3CA00F8518D /* NftAssetTraitInput.graphql.swift in Sources */, - 9F813EA42AA8FBCF00438D89 /* TransactionType.graphql.swift in Sources */, 074322342A83E3CA00F8518D /* SchemaConfiguration.swift in Sources */, - 0743222A2A83E3CA00F8518D /* NftBalanceEdge.graphql.swift in Sources */, 0703EE032A5734A600AED1DA /* UserDefaults.swift in Sources */, - 91D501702CDBEAE700B09B7F /* TokenMarketParts.graphql.swift in Sources */, - 074322212A83E3CA00F8518D /* NftCollectionConnection.graphql.swift in Sources */, - 0743220A2A83E3CA00F8518D /* SafetyLevel.graphql.swift in Sources */, 074322402A841BBD00F8518D /* Constants.swift in Sources */, - 91D501762CDBEAE700B09B7F /* TokenProtectionInfoParts.graphql.swift in Sources */, 0703EE052A57351800AED1DA /* Logging.swift in Sources */, - 074321EB2A83E3CA00F8518D /* TokenDetailsScreenQuery.graphql.swift in Sources */, 0767E03B2A65D2550042ADA2 /* Styling.swift in Sources */, - 0DE251492C13B69D005F47F9 /* OnRampTransactionDetails.graphql.swift in Sources */, - 074322092A83E3CA00F8518D /* NftActivityType.graphql.swift in Sources */, - 074322222A83E3CA00F8518D /* NftContract.graphql.swift in Sources */, - 0743220E2A83E3CA00F8518D /* NftOrderEdge.graphql.swift in Sources */, - 91D501782CDBEAE700B09B7F /* TokenProjectUrlsParts.graphql.swift in Sources */, - 074321FB2A83E3CA00F8518D /* NftsQuery.graphql.swift in Sources */, - 074322022A83E3CA00F8518D /* TokenStandard.graphql.swift in Sources */, 07F0C28F2A5F3E2E00D5353E /* Env.swift in Sources */, - 074321F82A83E3CA00F8518D /* TokenProjectsQuery.graphql.swift in Sources */, - 074322142A83E3CA00F8518D /* TimestampedAmount.graphql.swift in Sources */, - 91D501732CDBEAE700B09B7F /* TokenBasicInfoParts.graphql.swift in Sources */, - 0743223B2A83E3CA00F8518D /* NftAssetsFilterInput.graphql.swift in Sources */, - 0DC6ADF02B1E2C100092909C /* PortfolioValueModifier.graphql.swift in Sources */, - 0013F5F72C93399400D6EF09 /* ProtectionInfo.graphql.swift in Sources */, - 91D501712CDBEAE700B09B7F /* TokenBalanceMainParts.graphql.swift in Sources */, - 91D5017E2CDBEAF600B09B7F /* HomeScreenTokenParts.graphql.swift in Sources */, - 9127D1362CC2D3D00096F134 /* TokenBalanceMainParts.graphql.swift in Sources */, - 074322282A83E3CA00F8518D /* TokenProjectMarket.graphql.swift in Sources */, - A7B8EFCB2BF68F0D00CA4A1C /* FeeData.graphql.swift in Sources */, - 0743220F2A83E3CA00F8518D /* NftApproveForAll.graphql.swift in Sources */, - 0743223C2A83E3CA00F8518D /* SchemaMetadata.graphql.swift in Sources */, - 074321FA2A83E3CA00F8518D /* PortfolioBalancesQuery.graphql.swift in Sources */, - 074321FC2A83E3CA00F8518D /* TransactionListQuery.graphql.swift in Sources */, - 0743221E2A83E3CA00F8518D /* Portfolio.graphql.swift in Sources */, - 074322242A83E3CA00F8518D /* Amount.graphql.swift in Sources */, 07F136422A5763480067004F /* Network.swift in Sources */, - 074322352A83E3CA00F8518D /* NftBalancesFilterInput.graphql.swift in Sources */, - 074322132A83E3CA00F8518D /* NftCollection.graphql.swift in Sources */, - 074322052A83E3CA00F8518D /* Chain.graphql.swift in Sources */, - 91D5017A2CDBEAE700B09B7F /* TokenFeeDataParts.graphql.swift in Sources */, - 074322072A83E3CA00F8518D /* TransactionDirection.graphql.swift in Sources */, - 0743221D2A83E3CA00F8518D /* NftOrder.graphql.swift in Sources */, - 074322292A83E3CA00F8518D /* AmountChange.graphql.swift in Sources */, - 074322302A83E3CA00F8518D /* TokenTransfer.graphql.swift in Sources */, - 074322192A83E3CA00F8518D /* NftOrderConnection.graphql.swift in Sources */, - 074322372A83E3CA00F8518D /* ContractInput.graphql.swift in Sources */, - 0743221F2A83E3CA00F8518D /* PageInfo.graphql.swift in Sources */, - 074322252A83E3CA00F8518D /* Query.graphql.swift in Sources */, - 9F813EA22AA8FB8C00438D89 /* SwapOrderDetails.graphql.swift in Sources */, - 074321F92A83E3CA00F8518D /* SelectWalletScreenQuery.graphql.swift in Sources */, - 9F813E9E2AA8FB5700438D89 /* ActivityDetails.graphql.swift in Sources */, 0767E0382A65C8330042ADA2 /* Colors.swift in Sources */, - 0743220C2A83E3CA00F8518D /* TransactionStatus.graphql.swift in Sources */, - 074321EF2A83E3CA00F8518D /* TokenQuery.graphql.swift in Sources */, - 91D501752CDBEAE700B09B7F /* TokenBalanceQuantityParts.graphql.swift in Sources */, - 074322332A83E3CA00F8518D /* Token.graphql.swift in Sources */, - 074322312A83E3CA00F8518D /* NftApproval.graphql.swift in Sources */, - 074322062A83E3CA00F8518D /* TokenSortableField.graphql.swift in Sources */, - 91D501792CDBEAE700B09B7F /* TopTokenParts.graphql.swift in Sources */, - 074322272A83E3CA00F8518D /* Dimensions.graphql.swift in Sources */, - 0DE251482C13B69D005F47F9 /* OnRampServiceProvider.graphql.swift in Sources */, - 074322002A83E3CA00F8518D /* AssetChange.graphql.swift in Sources */, - 074322232A83E3CA00F8518D /* NftActivityConnection.graphql.swift in Sources */, - 9173CEBC2D03C6F30036DA28 /* TokenBalanceParts.graphql.swift in Sources */, - 9127D1372CC2D3D00096F134 /* TokenBalanceQuantityParts.graphql.swift in Sources */, - 0DE251432C13B674005F47F9 /* OnRampTransactionsAuth.graphql.swift in Sources */, - BA869E372D56B0B600D7A718 /* WidgetTokensQuery.graphql.swift in Sources */, - 0743221B2A83E3CA00F8518D /* NftCollectionMarket.graphql.swift in Sources */, - 074322162A83E3CA00F8518D /* TokenProject.graphql.swift in Sources */, - 074322032A83E3CA00F8518D /* Currency.graphql.swift in Sources */, - 077E603B2A86D06100ABC4B9 /* MultiplePortfolioBalancesQuery.graphql.swift in Sources */, - 9F29D4ED2B47126D004D003A /* NftBalanceAssetInput.graphql.swift in Sources */, - 074322102A83E3CA00F8518D /* NftAssetTrait.graphql.swift in Sources */, - 074322382A83E3CA00F8518D /* NftActivityFilterInput.graphql.swift in Sources */, - A70E4DD82C260416002D6D86 /* SwapOrderStatus.graphql.swift in Sources */, - 0743220D2A83E3CA00F8518D /* Image.graphql.swift in Sources */, 07F5CF752A7020FD00C648A5 /* Format.swift in Sources */, - 91D501722CDBEAE700B09B7F /* TokenProjectMarketsParts.graphql.swift in Sources */, 0783F7B42A619E7C009ED617 /* UIComponents.swift in Sources */, - 0743220B2A83E3CA00F8518D /* NftMarketplace.graphql.swift in Sources */, - A70E4DD72C260416002D6D86 /* SwapOrderType.graphql.swift in Sources */, - 0DE251472C13B69D005F47F9 /* OnRampTransfer.graphql.swift in Sources */, - 5EFB78362B1E585000E77EAC /* ConvertQuery.graphql.swift in Sources */, - 0743223E2A83E3CA00F8518D /* IContract.graphql.swift in Sources */, - 074321EE2A83E3CA00F8518D /* NftsTabQuery.graphql.swift in Sources */, - 0743222B2A83E3CA00F8518D /* NftActivity.graphql.swift in Sources */, - 074321F52A83E3CA00F8518D /* NftCollectionScreenQuery.graphql.swift in Sources */, - 8EE7C0582AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift in Sources */, - 074322172A83E3CA00F8518D /* NftTransfer.graphql.swift in Sources */, - 074322202A83E3CA00F8518D /* NftAssetEdge.graphql.swift in Sources */, - 00265F792C933CE300A5DA57 /* ProtectionResult.graphql.swift in Sources */, - 077E60392A85587800ABC4B9 /* TokensQuery.graphql.swift in Sources */, - 0743223D2A83E3CA00F8518D /* IAmount.graphql.swift in Sources */, - 074321F32A83E3CA00F8518D /* FavoriteTokenCardQuery.graphql.swift in Sources */, - 074322262A83E3CA00F8518D /* NftAsset.graphql.swift in Sources */, - 074322012A83E3CA00F8518D /* HistoryDuration.graphql.swift in Sources */, - 074322182A83E3CA00F8518D /* TokenApproval.graphql.swift in Sources */, - 00265F7A2C933CE300A5DA57 /* ProtectionAttackType.graphql.swift in Sources */, - 91D501742CDBEAE700B09B7F /* TokenBasicProjectParts.graphql.swift in Sources */, 07F5CF712A6AD97D00C648A5 /* Chart.swift in Sources */, - 074321F42A83E3CA00F8518D /* NFTItemScreenQuery.graphql.swift in Sources */, - 91D501772CDBEAE700B09B7F /* TokenParts.graphql.swift in Sources */, - 0743222D2A83E3CA00F8518D /* NftProfile.graphql.swift in Sources */, - 074322122A83E3CA00F8518D /* NftActivityEdge.graphql.swift in Sources */, 074143402A588F5800A157D3 /* Structs.swift in Sources */, - 074321FE2A83E3CA00F8518D /* TopTokenParts.graphql.swift in Sources */, - 0743222E2A83E3CA00F8518D /* NftCollectionEdge.graphql.swift in Sources */, - 074322152A83E3CA00F8518D /* NftAssetConnection.graphql.swift in Sources */, - 074321F12A83E3CA00F8518D /* TopTokensQuery.graphql.swift in Sources */, - 074321FF2A83E3CA00F8518D /* MobileSchema.graphql.swift in Sources */, - 074321F22A83E3CA00F8518D /* TokenPriceHistoryQuery.graphql.swift in Sources */, - 074322082A83E3CA00F8518D /* NftStandard.graphql.swift in Sources */, - 0743222C2A83E3CA00F8518D /* AssetActivity.graphql.swift in Sources */, - 0743221C2A83E3CA00F8518D /* TokenBalance.graphql.swift in Sources */, - 9F00A43A2B33894C0088A0D0 /* ApplicationContract.graphql.swift in Sources */, - 074321FD2A83E3CA00F8518D /* TransactionHistoryUpdaterQuery.graphql.swift in Sources */, - A70E4DD42C25DA0A002D6D86 /* NetworkFee.graphql.swift in Sources */, 07F136402A575EC00067004F /* DataQueries.swift in Sources */, - 074322112A83E3CA00F8518D /* NftBalanceConnection.graphql.swift in Sources */, - 9F8123832D2F33C4009C5D88 /* BlockaidFees.graphql.swift in Sources */, - 0743221A2A83E3CA00F8518D /* TokenMarket.graphql.swift in Sources */, - 074322322A83E3CA00F8518D /* NftBalance.graphql.swift in Sources */, - 9F813EA02AA8FB7500438D89 /* TransactionDetails.graphql.swift in Sources */, - 30872BFB39EEC66944E3EFD7 /* MobileSchema.graphql.swift in Sources */, - 9822D243ED2F5C350404A375 /* HomeScreenTokenParts.graphql.swift in Sources */, - 61988A922656857278F7CBF9 /* TokenBalanceMainParts.graphql.swift in Sources */, - C8402101FF510BAA358DCA46 /* TokenBalanceParts.graphql.swift in Sources */, - FA8EB6A63AB3F56194C6CFB8 /* TokenBalanceQuantityParts.graphql.swift in Sources */, - A974633048E27D5D23420F34 /* TokenBasicInfoParts.graphql.swift in Sources */, - C8338C5BE951EF9111C48217 /* TokenBasicProjectParts.graphql.swift in Sources */, - 09F9DEB33392F3051BEA8D52 /* TokenFeeDataParts.graphql.swift in Sources */, - D7E3642851C618D369D26B82 /* TokenMarketParts.graphql.swift in Sources */, - 302E24504C4FCE674CF95984 /* TokenParts.graphql.swift in Sources */, - F53121FB1B070DB9A81347DF /* TokenProjectMarketsParts.graphql.swift in Sources */, - E0F63BA3A99DB10FA1CCAB5E /* TokenProjectUrlsParts.graphql.swift in Sources */, - 2B12DFE797E2CBF314565D81 /* TokenProtectionInfoParts.graphql.swift in Sources */, - 71CF37F19F1C138B57CC135C /* TopTokenParts.graphql.swift in Sources */, - 93566FBDE94E1A2D8CC5AB62 /* ConvertQuery.graphql.swift in Sources */, - 63BDDE4499AC6B2375C9F795 /* FavoriteTokenCardQuery.graphql.swift in Sources */, - FD4E55146E046C7F5983A379 /* FeedTransactionListQuery.graphql.swift in Sources */, - F0D6A92BB4FCE19CDFA5BBE1 /* HomeScreenTokensQuery.graphql.swift in Sources */, - D660CA59775C3634324037ED /* MultiplePortfolioBalancesQuery.graphql.swift in Sources */, - 2B422D705C68BB51FBDA9895 /* NFTItemScreenQuery.graphql.swift in Sources */, - EF3D92CA76DEE66090F18D0C /* NftCollectionScreenQuery.graphql.swift in Sources */, - AC70FF8207ED26561B634E30 /* NftsQuery.graphql.swift in Sources */, - 1C3559D557BC09C709104802 /* NftsTabQuery.graphql.swift in Sources */, - A104024A1861354EC1DD53C1 /* PortfolioBalancesQuery.graphql.swift in Sources */, - F772B09295DABC5E8895C1C5 /* SelectWalletScreenQuery.graphql.swift in Sources */, - 9381B5EA5839DA17D07A45F0 /* TokenDetailsScreenQuery.graphql.swift in Sources */, - DC0641C5870F91D26C914F1D /* TokenPriceHistoryQuery.graphql.swift in Sources */, - 93AEECDBDB160B5E0C9E3149 /* TokenProjectDescriptionQuery.graphql.swift in Sources */, - 8CEA6459A5B739C3A0382000 /* TokenProjectsQuery.graphql.swift in Sources */, - 677FC15A05D9BD23930FAC95 /* TokenQuery.graphql.swift in Sources */, - 62B32E9623DA0E939F062033 /* TokensQuery.graphql.swift in Sources */, - 171DD1C966C7FBF9DD68CFAC /* TopTokensQuery.graphql.swift in Sources */, - 257C7A9F2C4AC3C99C6B7348 /* TransactionHistoryUpdaterQuery.graphql.swift in Sources */, - 3361FE5837059AEF4AA877BE /* TransactionListQuery.graphql.swift in Sources */, - 8410B98A8D7974A941AAF299 /* WidgetTokensQuery.graphql.swift in Sources */, - 9AF0D1FDF2BFB17FB732C5FD /* SchemaConfiguration.swift in Sources */, - 8950F8354810AA673E1E35DA /* SchemaMetadata.graphql.swift in Sources */, - 4917B04DB81579CF4243A1DA /* Chain.graphql.swift in Sources */, - 614FCC3D8E9AA8175E950726 /* Currency.graphql.swift in Sources */, - 326CFFDBB89D95FA4CB95EBB /* HistoryDuration.graphql.swift in Sources */, - 143C638C2CCAC689371BFC93 /* NftActivityType.graphql.swift in Sources */, - 648B919F00D069DE1F0040F6 /* NftMarketplace.graphql.swift in Sources */, - 7BE97D20155AE69FF36C4CB4 /* NftStandard.graphql.swift in Sources */, - 1BC3A49161EB906542F8E23B /* ProtectionAttackType.graphql.swift in Sources */, - 8FE0F30936E893297558F467 /* ProtectionResult.graphql.swift in Sources */, - 66D2765A00D8CE3136D28F1F /* SafetyLevel.graphql.swift in Sources */, - 85ADD03923353DB3D6CD7301 /* SwapOrderStatus.graphql.swift in Sources */, - C791C5505DBB3036B9D5066D /* SwapOrderType.graphql.swift in Sources */, - 0C6FC49E0FC9BCB2DF5B627E /* TokenSortableField.graphql.swift in Sources */, - FD84AA379C4562598807843D /* TokenStandard.graphql.swift in Sources */, - 2E05037B51AF526A47701B09 /* TransactionDirection.graphql.swift in Sources */, - CCBC45FD4310D8153D859361 /* TransactionStatus.graphql.swift in Sources */, - 5D06BAB366D14A5AFE2355E8 /* TransactionType.graphql.swift in Sources */, - FBE634E2AEC7A62B89863366 /* ContractInput.graphql.swift in Sources */, - 3E338E14E33C721DAB708664 /* NftActivityFilterInput.graphql.swift in Sources */, - 50C89DFAF2DBC80D6343B164 /* NftAssetTraitInput.graphql.swift in Sources */, - D680C4844F2C5DACF5D0892E /* NftAssetsFilterInput.graphql.swift in Sources */, - AA3AE4E5C2AAC3F9460A3637 /* NftBalanceAssetInput.graphql.swift in Sources */, - C7ABAADA504107D152A52FD4 /* NftBalancesFilterInput.graphql.swift in Sources */, - 09E8C497051C37FE604FD40E /* OnRampTransactionsAuth.graphql.swift in Sources */, - C1FC1F8B4A2725A195F66D60 /* PortfolioValueModifier.graphql.swift in Sources */, - E091F379106E92D3181103CB /* IAmount.graphql.swift in Sources */, - EF05F69E61EA8A16C6A66D53 /* IContract.graphql.swift in Sources */, - CE1DF567D58943ACCBB8360C /* Amount.graphql.swift in Sources */, - 1CE49305114BF4792290DDC6 /* AmountChange.graphql.swift in Sources */, - F5B577CA1201CB6FF04A9A58 /* ApplicationContract.graphql.swift in Sources */, - 9A3E861F5D7B0F2CB0EFA7A6 /* AssetActivity.graphql.swift in Sources */, - 8ADDD2E2AB1FD9D1D9AF5627 /* BlockaidFees.graphql.swift in Sources */, - 03291E0DA448AF438F97EA5D /* DescriptionTranslations.graphql.swift in Sources */, - 4C187202229BDC4D8898E067 /* Dimensions.graphql.swift in Sources */, - 5C3958DA046B5A84FE90C1BD /* FeeData.graphql.swift in Sources */, - 0CEBEB8BE3AB95C3F584F6CE /* Image.graphql.swift in Sources */, - B377DB0418EA15695F208D9F /* NetworkFee.graphql.swift in Sources */, - BD59A9C8414D6A4BFE9C9A2E /* NftActivity.graphql.swift in Sources */, - B746C09DA19B4C7F9C700989 /* NftActivityConnection.graphql.swift in Sources */, - FE9CE5C3B5A456B249FC34C5 /* NftActivityEdge.graphql.swift in Sources */, - 1DC34AE3E11264475E8B9F62 /* NftApproval.graphql.swift in Sources */, - FC7C117CEA5EA63460E6A2C6 /* NftApproveForAll.graphql.swift in Sources */, - BD1FF76A8E50EFEA0720CC04 /* NftAsset.graphql.swift in Sources */, - 8CC06A8205186D0640F0BC55 /* NftAssetConnection.graphql.swift in Sources */, - 5804A1B1BB144D9EFBBE177D /* NftAssetEdge.graphql.swift in Sources */, - 126CC4BC99F3F15CC1E2F1C3 /* NftAssetTrait.graphql.swift in Sources */, - CF36F741187285B430EFE557 /* NftBalance.graphql.swift in Sources */, - 33928A7366AFE7F892CDC89F /* NftBalanceConnection.graphql.swift in Sources */, - CA0EDABCA1B6C2B0936BF5CF /* NftBalanceEdge.graphql.swift in Sources */, - 252BD40CB9B46FE47B2440B1 /* NftCollection.graphql.swift in Sources */, - D81BAFFCC105668B88B607FC /* NftCollectionConnection.graphql.swift in Sources */, - A3318C676D7FBF78A1583B16 /* NftCollectionEdge.graphql.swift in Sources */, - 85D0E81B798D0F2D841386E9 /* NftCollectionMarket.graphql.swift in Sources */, - 3FE25F715DFFD1D03DCDA57D /* NftContract.graphql.swift in Sources */, - 1B59968CF49C45B127E9C768 /* NftOrder.graphql.swift in Sources */, - 9B6C88F7D8D542AAC353A3EA /* NftOrderConnection.graphql.swift in Sources */, - 4BB9E218D89B8AF5E4E27CCB /* NftOrderEdge.graphql.swift in Sources */, - 6A60BDC9D46A710D871DEC6E /* NftProfile.graphql.swift in Sources */, - E54093DC857B8C634804D42B /* NftTransfer.graphql.swift in Sources */, - 0F282C26344F1AE3622232B0 /* OffRampTransactionDetails.graphql.swift in Sources */, - 1E2AF2C38C8FBEB2A95B644E /* OffRampTransfer.graphql.swift in Sources */, - CF2BAECCF9A2EC43AC3EC583 /* OnRampServiceProvider.graphql.swift in Sources */, - B61E182CF4937B5169885C95 /* OnRampTransactionDetails.graphql.swift in Sources */, - 553E6B467BEE49C9984691C5 /* OnRampTransfer.graphql.swift in Sources */, - 35B8176433A98BA798BBEE79 /* PageInfo.graphql.swift in Sources */, - 4FED6AAF896BA371C56D88B1 /* Portfolio.graphql.swift in Sources */, - 818179F5724AA35E1B1D5A9C /* ProtectionInfo.graphql.swift in Sources */, - ADE104A101B3DFFBDB308189 /* Query.graphql.swift in Sources */, - 03E3515E38E247F459218CAA /* SwapOrderDetails.graphql.swift in Sources */, - 31661D70B58410EA030E2C53 /* TimestampedAmount.graphql.swift in Sources */, - F2EB62621E64B57B07DE8B13 /* Token.graphql.swift in Sources */, - 9301D18644F3DFA9ABB8F0BE /* TokenApproval.graphql.swift in Sources */, - E24ED8688E9B18BBD543F8F0 /* TokenBalance.graphql.swift in Sources */, - AF83E7713BB625D787DD1A1D /* TokenMarket.graphql.swift in Sources */, - 0094F4FBBC1C0A2FFABF7157 /* TokenProject.graphql.swift in Sources */, - 869F3639FBC6D8156FFE3BD3 /* TokenProjectMarket.graphql.swift in Sources */, - 4EF8D293BB1EBCFBFC65A330 /* TokenTransfer.graphql.swift in Sources */, - 97CA219E1FFD832D8FA02C20 /* TransactionDetails.graphql.swift in Sources */, - BA83003638263D702D03C3C1 /* ActivityDetails.graphql.swift in Sources */, - 36E601F269D40A67FC353947 /* AssetChange.graphql.swift in Sources */, + CFA408C9A92DB1F808BA57CD /* MobileSchema.graphql.swift in Sources */, + 1E01B5593B54A53D822972C1 /* HomeScreenTokenParts.graphql.swift in Sources */, + 333C6D9C267BB7783F9CA56E /* TokenBalanceMainParts.graphql.swift in Sources */, + 4AA5D71F770AA5E8A5AB8D6A /* TokenBalanceParts.graphql.swift in Sources */, + 4ECB63464100262E5A163345 /* TokenBalanceQuantityParts.graphql.swift in Sources */, + 90B93D7970186C8FC83F9292 /* TokenBasicInfoParts.graphql.swift in Sources */, + 74D0262298BD1BE5363444B5 /* TokenBasicProjectParts.graphql.swift in Sources */, + 6FAEE2539C8AFB99445F29BE /* TokenFeeDataParts.graphql.swift in Sources */, + 3B27BBB18E152F19B68D6FAB /* TokenMarketParts.graphql.swift in Sources */, + 7154698A2773F05CAD639211 /* TokenParts.graphql.swift in Sources */, + 23FC1CDFE7B5E25CEDF338F2 /* TokenProjectMarketsParts.graphql.swift in Sources */, + 7BDC2CBE10DC34F9BCFF86EA /* TokenProjectTokensTvlParts.graphql.swift in Sources */, + D0AC45D21734567F720877AD /* TokenProjectUrlsParts.graphql.swift in Sources */, + 722E7F2ECE654320C2452CC8 /* TokenProtectionInfoParts.graphql.swift in Sources */, + CA4994AA5EF5F36F42117ECA /* TopTokenParts.graphql.swift in Sources */, + D6B69EFB74ACEF485C289266 /* ConvertQuery.graphql.swift in Sources */, + B83B63F900E565F5C3F11589 /* FavoriteTokenCardQuery.graphql.swift in Sources */, + 0781032B76A7F58B5776207B /* FeedTransactionListQuery.graphql.swift in Sources */, + 890E98060D98F2F5617D1914 /* HomeScreenTokensQuery.graphql.swift in Sources */, + 462433873C56042BDAA3D8E1 /* MultiplePortfolioBalancesQuery.graphql.swift in Sources */, + B2692B7521F4E7767DECE974 /* NftsQuery.graphql.swift in Sources */, + 12A7E560842C954098B3A558 /* NftsTabQuery.graphql.swift in Sources */, + 29BC0E8E68D0365EB77456AF /* PortfolioBalancesQuery.graphql.swift in Sources */, + 63E84504F6D89EFBF97F4ACF /* SelectWalletScreenQuery.graphql.swift in Sources */, + 554AB3FB0D09B2DBC870E044 /* TokenDetailsScreenQuery.graphql.swift in Sources */, + F36073C9BDFF611771D04683 /* TokenPriceHistoryQuery.graphql.swift in Sources */, + 72567EB83861EBC04FAA0941 /* TokenProjectDescriptionQuery.graphql.swift in Sources */, + 19AB4E8D176A5656BBB2D797 /* TokenProjectsQuery.graphql.swift in Sources */, + 038FC04C9385A04F3EA5EBF4 /* TokenQuery.graphql.swift in Sources */, + F3E5BB84916B1F496A0C740D /* TokensQuery.graphql.swift in Sources */, + 6B8E2BD6B9A12FF05F826BDB /* TopTokensQuery.graphql.swift in Sources */, + 50F87796A463D0224875F0F4 /* TransactionHistoryUpdaterQuery.graphql.swift in Sources */, + 5FED383DD705AEFE8B7DA8DC /* TransactionListQuery.graphql.swift in Sources */, + 99ED3ACCFE04709CFA7FD490 /* WidgetTokensQuery.graphql.swift in Sources */, + CBFF07DDFB9C638D6E383290 /* SchemaConfiguration.swift in Sources */, + 5A9BA87096096C8BDC5FF210 /* SchemaMetadata.graphql.swift in Sources */, + F6EA6445BF4E0DEEDD076C7A /* Chain.graphql.swift in Sources */, + 57D4FACDC13E20AD220D7AEC /* Currency.graphql.swift in Sources */, + 6602483D8F6C6481FB8128E9 /* HistoryDuration.graphql.swift in Sources */, + B8ADB9BF8BB2D4E456D80B23 /* NftStandard.graphql.swift in Sources */, + 914DFEC13BB1853D25D23E34 /* ProtectionAttackType.graphql.swift in Sources */, + E774B10E7FB502BE97D3895B /* ProtectionResult.graphql.swift in Sources */, + E6CC238CB9AF565DE89087B7 /* SafetyLevel.graphql.swift in Sources */, + F7B8B2FAEB30B3343CFF6F29 /* SwapOrderStatus.graphql.swift in Sources */, + 340A73A44EC57C9376FF24E9 /* SwapOrderType.graphql.swift in Sources */, + 5F14564B8F6814A85EF53C46 /* TokenSortableField.graphql.swift in Sources */, + 4D846E92BAEED7A4F5B72919 /* TokenStandard.graphql.swift in Sources */, + F5FD688A33BCADBF601A2D7F /* TransactionDirection.graphql.swift in Sources */, + B009D229E81544EA0F47C6DD /* TransactionStatus.graphql.swift in Sources */, + D86DB22D27B00DA71F968324 /* TransactionType.graphql.swift in Sources */, + B4F84A94618C5A8D4F12007E /* ContractInput.graphql.swift in Sources */, + 121CD8F92A5E382AD1A84AA9 /* NftBalanceAssetInput.graphql.swift in Sources */, + 2FF904EA65F2A7B960547609 /* NftBalancesFilterInput.graphql.swift in Sources */, + F8E54B79B2742008CF1C0AA9 /* OnRampTransactionsAuth.graphql.swift in Sources */, + A3EEE7EE3CC93DDBA3CE6A5C /* PortfolioValueModifier.graphql.swift in Sources */, + D1642682124F702EE2454A64 /* IAmount.graphql.swift in Sources */, + 6E2E4593B2C40DBBA7C861BF /* IContract.graphql.swift in Sources */, + FAB057109F187E5375D137FD /* Amount.graphql.swift in Sources */, + 530FBF9EB2190828460BD496 /* AmountChange.graphql.swift in Sources */, + D420E10F9BA8C789530E70F4 /* ApplicationContract.graphql.swift in Sources */, + 95AA27A2056B51265EE643F1 /* AssetActivity.graphql.swift in Sources */, + DEBB37600A7C5C9A273DA38E /* BlockaidFees.graphql.swift in Sources */, + C403639465231038D196D7B5 /* BridgedWithdrawalInfo.graphql.swift in Sources */, + D179B805D7A77EA127F41F13 /* DescriptionTranslations.graphql.swift in Sources */, + 939256080FE6141E53B49E90 /* Dimensions.graphql.swift in Sources */, + 2DE0A41ECCCE673BAE68F715 /* FeeData.graphql.swift in Sources */, + 213BE982C7E2CCE304B88A6E /* Image.graphql.swift in Sources */, + 75FA3F2F18047B759472AEA8 /* NetworkFee.graphql.swift in Sources */, + 8989D182DEC9682661D588F3 /* NftApproval.graphql.swift in Sources */, + ECB6546D9AA307163172BEA3 /* NftApproveForAll.graphql.swift in Sources */, + 1A4145B8CC5F5F5B9297B9D6 /* NftAsset.graphql.swift in Sources */, + 6682BC9B8E38430BE82FADDA /* NftBalance.graphql.swift in Sources */, + 1F9BD005762B8BFA302E1B0C /* NftBalanceConnection.graphql.swift in Sources */, + 2C7ED9DF09914F82D85DE7C9 /* NftBalanceEdge.graphql.swift in Sources */, + 72272D14F0DB24D05F1162FE /* NftCollection.graphql.swift in Sources */, + ADD898CA4B87F6E7C990E268 /* NftCollectionMarket.graphql.swift in Sources */, + 5CC5D4B276CC1E3BA1906120 /* NftContract.graphql.swift in Sources */, + F784F1CA9FCEB5FFE4C1DD62 /* NftProfile.graphql.swift in Sources */, + 1B9BE681A85AE55F18054F37 /* NftTransfer.graphql.swift in Sources */, + A0ACC9C3ABF174616E0CBCA4 /* OffRampTransactionDetails.graphql.swift in Sources */, + 1FB2F652907A72BD28CF6DD4 /* OffRampTransfer.graphql.swift in Sources */, + DFBC904E6C0B818152912819 /* OnRampServiceProvider.graphql.swift in Sources */, + 8D90B4306573344A1FFC4832 /* OnRampTransactionDetails.graphql.swift in Sources */, + 100A336AFEC40D106F257664 /* OnRampTransfer.graphql.swift in Sources */, + 77206ACF669BD9DAAC096227 /* PageInfo.graphql.swift in Sources */, + BD07375602C71B48961CD5A0 /* Portfolio.graphql.swift in Sources */, + 9F1AE0C43E80AE592CA4AD7E /* ProtectionInfo.graphql.swift in Sources */, + 44B612C45F986F8ACB66D366 /* Query.graphql.swift in Sources */, + 0DF7B81A4727A8CA063CD5B5 /* SwapOrderDetails.graphql.swift in Sources */, + 7B49698C356931577828B41E /* TimestampedAmount.graphql.swift in Sources */, + AEA0B1AC57BB6F11F5861BCE /* Token.graphql.swift in Sources */, + 5F581541EFD85236EF98D3F3 /* TokenApproval.graphql.swift in Sources */, + AF467A9F1C200706537B24E5 /* TokenBalance.graphql.swift in Sources */, + 37A47FF4EEEB8E9D839D5DD6 /* TokenMarket.graphql.swift in Sources */, + EEEE88236C7EBC4B67BBE858 /* TokenProject.graphql.swift in Sources */, + B5EF58A67FC42D684B96C0F0 /* TokenProjectMarket.graphql.swift in Sources */, + 498F01286C660E8241774216 /* TokenTransfer.graphql.swift in Sources */, + 0901AAD2DCDD615889B6680A /* TransactionDetails.graphql.swift in Sources */, + 8C19BAD465EA9DFEB20EFB24 /* ActivityDetails.graphql.swift in Sources */, + F7CA47B62C91F3B0DA4106C0 /* AssetChange.graphql.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2796,11 +2296,12 @@ 8E89C3B22AB8AAA400C84DE5 /* MnemonicTextField.swift in Sources */, FD7304D028A3650A0085BDEA /* Colors.swift in Sources */, 8E89C3AF2AB8AAA400C84DE5 /* MnemonicDisplayView.swift in Sources */, - 9FEC9B8B2A858CF1003CD019 /* AppDelegate.m in Sources */, + 45FFF7E12E8C2E6900362570 /* SilentPushEventEmitter.swift in Sources */, 6BC7D0802B5FF02400617C95 /* EncryptionUtils.swift in Sources */, 03C788232C10E7390011E5DC /* ActionButtons.swift in Sources */, 8EA8AB3B2AB7ED3C004E7EF3 /* SeedPhraseInputManager.m in Sources */, 03D2F3182C218D390030D987 /* RelativeOffsetView.swift in Sources */, + 45FFF7DF2E8C2A8100362570 /* SilentPushEventEmitter.m in Sources */, 6CA91BDB2A95223C00C4063E /* RNEthersRS.swift in Sources */, 8EA8AB3C2AB7ED3C004E7EF3 /* SeedPhraseInputViewModel.swift in Sources */, 072F6C2E2A44A32F00DA720A /* TokenPriceWidget.intentdefinition in Sources */, @@ -2810,6 +2311,7 @@ 6BC7D07F2B5FF02400617C95 /* ScantasticEncryption.swift in Sources */, 07B0676C2A7D6EC8001DD9B9 /* RNWidgets.swift in Sources */, 8E89C3AE2AB8AAA400C84DE5 /* MnemonicConfirmationView.swift in Sources */, + 8B2A92172EB3E78E00990413 /* AppDelegate.swift in Sources */, 5B4398EC2DD3B22C00F6BE08 /* PrivateKeyDisplayManager.m in Sources */, 5B4398ED2DD3B22C00F6BE08 /* PrivateKeyDisplayManager.swift in Sources */, 5B4398EE2DD3B22C00F6BE08 /* PrivateKeyDisplayView.swift in Sources */, @@ -2819,7 +2321,6 @@ 649A7A782D9AE70B00B53589 /* KeychainUtils.swift in Sources */, 649A7A792D9AE70B00B53589 /* KeychainConstants.swift in Sources */, 9FCEBF002A95A8E00079EDDB /* RNWalletConnect.m in Sources */, - 13B07FC11A68108700A75B9A /* main.m in Sources */, 6CA91BE32A95226200C4063E /* RNCloudStorageBackupsManager.swift in Sources */, 9FCEBF042A95A99C0079EDDB /* RCTThemeModule.m in Sources */, 9FCEBF012A95A8E00079EDDB /* RNWalletConnect.swift in Sources */, @@ -2985,7 +2486,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3038,7 +2539,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; @@ -3091,7 +2592,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; @@ -3144,7 +2645,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; @@ -3182,7 +2683,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3218,7 +2719,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; @@ -3253,7 +2754,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; @@ -3288,7 +2789,7 @@ GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.1; - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; @@ -3335,7 +2836,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3381,7 +2882,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets; @@ -3427,7 +2928,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; @@ -3473,7 +2974,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets; @@ -3515,7 +3016,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3558,7 +3059,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension; @@ -3601,7 +3102,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; @@ -3644,7 +3145,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension; @@ -3664,7 +3165,7 @@ baseConfigurationReference = A7C9F415D0E128A43003E071 /* Pods-Uniswap.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon${BUNDLE_ID_SUFFIX}"; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(BUNDLE_ID_SUFFIX)"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = ""; BUNDLE_ID_SUFFIX = .dev; CLANG_ENABLE_MODULES = YES; @@ -3680,14 +3181,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; - PRODUCT_BUNDLE_IDENTIFIER = "com.uniswap.mobile${BUNDLE_ID_SUFFIX}"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev; PRODUCT_NAME = Uniswap; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h"; @@ -3702,7 +3203,7 @@ baseConfigurationReference = 178644A78AB62609EFDB66B3 /* Pods-Uniswap.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon${BUNDLE_ID_SUFFIX}"; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(BUNDLE_ID_SUFFIX)"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = ""; BUNDLE_ID_SUFFIX = ""; CLANG_ENABLE_MODULES = YES; @@ -3718,14 +3219,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = "com.uniswap.mobile${BUNDLE_ID_SUFFIX}"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile; PRODUCT_NAME = Uniswap; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile"; SWIFT_OBJC_BRIDGING_HEADER = "Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h"; @@ -3734,6 +3235,333 @@ }; name = Release; }; + 31A0707BE53EF0E39440A5EA /* DebugOptimized */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 31CB18D02DE297D6DADB87C0 /* Pods-Uniswap.debugoptimized.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(BUNDLE_ID_SUFFIX)"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = ""; + BUNDLE_ID_SUFFIX = .dev; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Uniswap/Uniswap.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JH3UHGZD75; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Uniswap/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.70; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev; + PRODUCT_NAME = Uniswap; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = DebugOptimized; + }; + 32CACF657BA5BEC28E2F1B29 /* DebugOptimized */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CB72291CE6EFD3842A8E2A1D /* Pods-WidgetsCoreTests.debugoptimized.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + MARKETING_VERSION = 1.70; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Uniswap.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Uniswap"; + }; + name = DebugOptimized; + }; + 699CC042160867DCE68402B2 /* DebugOptimized */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COMPILER_INDEX_STORE_ENABLE = NO; + COPY_PHASE_STRIP = NO; + DEVELOPMENT_TEAM = CGCYLJG7GA; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = NO; + GCC_WARN_UNUSED_FUNCTION = NO; + GCC_WARN_UNUSED_VARIABLE = NO; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers/react/nativemodule/core", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", + ); + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "$(SDKROOT)/usr/lib/swift", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = NO; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "$(inherited)"; + OTHER_CPLUSPLUSFLAGS = "$(inherited)"; + OTHER_LDFLAGS = "$(inherited)"; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; + SWIFT_ENFORCE_EXCLUSIVE_ACCESS = "compile-time"; + SWIFT_VERSION = 5.0; + USE_HERMES = true; + }; + name = DebugOptimized; + }; + 6DC48512A1ADEC1FF10D690A /* DebugOptimized */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 36F66895EB592B9F61AE658C /* Pods-Uniswap-UniswapTests.debugoptimized.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; + BUNDLE_LOADER = "$(TEST_HOST)"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = UniswapTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + "$(inherited)", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Uniswap.app/Uniswap"; + }; + name = DebugOptimized; + }; + 7385AD0991A48D80AD74DA33 /* DebugOptimized */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 24343423F6B8A16CB14F1E4C /* Pods-WidgetsCore.debugoptimized.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = JH3UHGZD75; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.70; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = DebugOptimized; + }; + 7932E66A3404F08533393AEE /* DebugOptimized */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2590691EDB017AE6FB6C10AC /* Pods-WidgetIntentExtension.debugoptimized.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = WidgetIntentExtension/WidgetIntentExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WidgetIntentExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = WidgetIntentExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.70; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = DebugOptimized; + }; + 7ACBAE66C5ABCA063B6627AC /* DebugOptimized */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8CBD584CCF002F58176DA863 /* Pods-OneSignalNotificationServiceExtension.debugoptimized.xcconfig */; + buildSettings = { + BUNDLE_ID_SUFFIX = .dev; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = OneSignalNotificationServiceExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.70; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = DebugOptimized; + }; 83CBBA201A601CBA00E9B192 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -3813,6 +3641,7 @@ REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; + SWIFT_VERSION = 5.0; USE_HERMES = true; }; name = Debug; @@ -3887,11 +3716,57 @@ OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; USE_HERMES = true; VALIDATE_PRODUCT = YES; }; name = Release; }; + DFB0E04B2D800AD0DA05A6FA /* DebugOptimized */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 986A89D253EEB9BEBE5F08FF /* Pods-Widgets.debugoptimized.xcconfig */; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = Widgets/Widgets.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Widgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Widgets; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.70; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = DebugOptimized; + }; F35AFD4427EE49990011A725 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 3A2186B1FF7FB85663D96EA9 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */; @@ -3920,7 +3795,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; @@ -3965,7 +3840,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension; @@ -4050,6 +3925,7 @@ OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; USE_HERMES = true; VALIDATE_PRODUCT = YES; }; @@ -4060,7 +3936,7 @@ baseConfigurationReference = 62CEA9F2D5176D20A6402A3E /* Pods-Uniswap.beta.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon${BUNDLE_ID_SUFFIX}"; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(BUNDLE_ID_SUFFIX)"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = ""; BUNDLE_ID_SUFFIX = .beta; CLANG_ENABLE_MODULES = YES; @@ -4076,14 +3952,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = "com.uniswap.mobile${BUNDLE_ID_SUFFIX}"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta; PRODUCT_NAME = Uniswap; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.beta"; SWIFT_OBJC_BRIDGING_HEADER = "Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h"; @@ -4148,7 +4024,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension; @@ -4233,6 +4109,7 @@ OTHER_LDFLAGS = "$(inherited)"; REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; USE_HERMES = true; VALIDATE_PRODUCT = YES; }; @@ -4243,7 +4120,7 @@ baseConfigurationReference = 56FE9C9AF785221B7E3F4C04 /* Pods-Uniswap.dev.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = "$(inherited)"; - ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon${BUNDLE_ID_SUFFIX}"; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon$(BUNDLE_ID_SUFFIX)"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = ""; BUNDLE_ID_SUFFIX = .dev; CLANG_ENABLE_MODULES = YES; @@ -4259,14 +4136,14 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; OTHER_LDFLAGS = ( "$(inherited)", "-ObjC", "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = "com.uniswap.mobile${BUNDLE_ID_SUFFIX}"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev; PRODUCT_NAME = Uniswap; PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.dev"; SWIFT_OBJC_BRIDGING_HEADER = "Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h"; @@ -4331,7 +4208,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.61; + MARKETING_VERSION = 1.70; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; @@ -4356,6 +4233,7 @@ 00E356F71AD99517003FC87E /* Release */, FDB6FD42294D3A8200C7B822 /* Dev */, FDB6FD3E294D3A6E00C7B822 /* Beta */, + 6DC48512A1ADEC1FF10D690A /* DebugOptimized */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -4367,6 +4245,7 @@ 072E239D2A44D5BD006AD6C9 /* Release */, 072E239E2A44D5BD006AD6C9 /* Dev */, 072E239F2A44D5BD006AD6C9 /* Beta */, + 7385AD0991A48D80AD74DA33 /* DebugOptimized */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -4378,6 +4257,7 @@ 072E23A12A44D5BD006AD6C9 /* Release */, 072E23A22A44D5BD006AD6C9 /* Dev */, 072E23A32A44D5BD006AD6C9 /* Beta */, + 32CACF657BA5BEC28E2F1B29 /* DebugOptimized */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -4389,6 +4269,7 @@ 072F6C332A44A32F00DA720A /* Release */, 072F6C342A44A32F00DA720A /* Dev */, 072F6C352A44A32F00DA720A /* Beta */, + DFB0E04B2D800AD0DA05A6FA /* DebugOptimized */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -4400,6 +4281,7 @@ 078E79512A55EB3400F59CF2 /* Release */, 078E79522A55EB3400F59CF2 /* Dev */, 078E79532A55EB3400F59CF2 /* Beta */, + 7932E66A3404F08533393AEE /* DebugOptimized */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -4411,6 +4293,7 @@ 13B07F951A680F5B00A75B9A /* Release */, FDB6FD41294D3A8200C7B822 /* Dev */, FDB6FD3D294D3A6E00C7B822 /* Beta */, + 31A0707BE53EF0E39440A5EA /* DebugOptimized */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -4422,6 +4305,7 @@ 83CBBA211A601CBA00E9B192 /* Release */, FDB6FD40294D3A8200C7B822 /* Dev */, FDB6FD3C294D3A6E00C7B822 /* Beta */, + 699CC042160867DCE68402B2 /* DebugOptimized */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -4433,6 +4317,7 @@ F35AFD4527EE49990011A725 /* Release */, FDB6FD43294D3A8200C7B822 /* Dev */, FDB6FD3F294D3A6E00C7B822 /* Beta */, + 7ACBAE66C5ABCA063B6627AC /* DebugOptimized */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/apps/mobile/ios/Uniswap.xcodeproj/xcshareddata/xcschemes/Uniswap.xcscheme b/apps/mobile/ios/Uniswap.xcodeproj/xcshareddata/xcschemes/Uniswap.xcscheme index c5e27e3a5eb..eb14b39fe0e 100644 --- a/apps/mobile/ios/Uniswap.xcodeproj/xcshareddata/xcschemes/Uniswap.xcscheme +++ b/apps/mobile/ios/Uniswap.xcodeproj/xcshareddata/xcschemes/Uniswap.xcscheme @@ -52,14 +52,19 @@ @@ -71,6 +76,13 @@ ReferencedContainer = "container:Uniswap.xcodeproj"> + + + + -#import -#import - -@interface AppDelegate : RCTAppDelegate - -@end diff --git a/apps/mobile/ios/Uniswap/AppDelegate.m b/apps/mobile/ios/Uniswap/AppDelegate.m deleted file mode 100644 index 28b0f7e1310..00000000000 --- a/apps/mobile/ios/Uniswap/AppDelegate.m +++ /dev/null @@ -1,131 +0,0 @@ -#import "AppDelegate.h" - -#import - -#import "Uniswap-Swift.h" - -#import -#import -#import -#import -#import - -@implementation AppDelegate - -static NSString *const hasLaunchedOnceKey = @"HasLaunchedOnce"; - -/** - * Handles keychain cleanup on first run of the app. - * A migration flag is persisted in the keychain to avoid clearing the keychain for existing users, while the first run flag is saved in NSUserDefaults, which is cleared every install. - */ -- (void)handleKeychainCleanup { - NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; - BOOL isFirstRun = ![defaults boolForKey:hasLaunchedOnceKey]; - BOOL canClearKeychainOnReinstall = [KeychainUtils getCanClearKeychainOnReinstall]; - - if (canClearKeychainOnReinstall && isFirstRun) { - [KeychainUtils clearKeychain]; - } - - if (!canClearKeychainOnReinstall || isFirstRun) { - [defaults setBool:YES forKey:hasLaunchedOnceKey]; - [KeychainUtils setCanClearKeychainOnReinstall]; - } -} - -- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions -{ - // Must be first line in startup routine - [ReactNativePerformance onAppStarted]; - - [self handleKeychainCleanup]; - - [FIRApp configure]; - - // This is needed so universal links opened from OneSignal notifications navigate to the proper page. - // More details here: - // https://documentation.onesignal.com/v7.0/docs/react-native-sdk in the deep linking warning section. - NSMutableDictionary *newLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; - if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { - NSDictionary *remoteNotif = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; - if (remoteNotif[@"custom"] && remoteNotif[@"custom"][@"u"]) { - NSString *initialURL = remoteNotif[@"custom"][@"u"]; - if (!launchOptions[UIApplicationLaunchOptionsURLKey]) { - newLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL]; - } - } - } - - self.moduleName = @"Uniswap"; - self.dependencyProvider = [RCTAppDependencyProvider new]; - self.initialProps = @{}; - - [self.window makeKeyAndVisible]; - - if (@available(iOS 13.0, *)) { - self.window.rootViewController.view.backgroundColor = [UIColor systemBackgroundColor]; - } else { - self.window.rootViewController.view.backgroundColor = [UIColor whiteColor]; - } - - [super application:application didFinishLaunchingWithOptions:newLaunchOptions]; - - [[RCTI18nUtil sharedInstance] allowRTL:NO]; - [RNBootSplash initWithStoryboard:@"SplashScreen" rootView:self.window.rootViewController.view]; - - return YES; -} - -- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge -{ - return [self bundleURL]; -} - -- (NSURL *)bundleURL -{ -#if DEBUG - return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; -#else - return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; -#endif -} - -/// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. -/// -/// @see: https://reactjs.org/blog/2022/03/29/react-v18.html -/// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). -/// @return: `true` if the `concurrentRoot` feature is enabled. Otherwise, it returns `false`. -- (BOOL)concurrentRootEnabled -{ - return true; -} - -// Enable deep linking -- (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - options:(NSDictionary *)options -{ - return [RCTLinkingManager application:application openURL:url options:options]; -} - -// Enable universal links -- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity - restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler -{ - return [RCTLinkingManager application:application - continueUserActivity:userActivity - restorationHandler:restorationHandler]; -} - -// Disable 3rd party keyboard --(BOOL)application:(UIApplication *)application shouldAllowExtensionPointIdentifier:(NSString *)extensionPointIdentifier -{ - if (extensionPointIdentifier == UIApplicationKeyboardExtensionPointIdentifier) - { - return NO; - } - - return YES; -} - -@end diff --git a/apps/mobile/ios/Uniswap/AppDelegate.swift b/apps/mobile/ios/Uniswap/AppDelegate.swift new file mode 100644 index 00000000000..ca105e69da3 --- /dev/null +++ b/apps/mobile/ios/Uniswap/AppDelegate.swift @@ -0,0 +1,163 @@ +import UIKit +import Expo +import ExpoModulesCore +import React +import ReactAppDependencyProvider +import Firebase +import ReactNativePerformance +import RNBootSplash +import UserNotifications + +@main +class AppDelegate: ExpoAppDelegate { + + static let hasLaunchedOnceKey = "HasLaunchedOnce" + + var window: UIWindow? + var reactNativeDelegate: ExpoReactNativeFactoryDelegate? + var reactNativeFactory: ExpoReactNativeFactory? + + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + print("🚀 AppDelegate: Starting initialization") + + // Must be first line in startup routine + ReactNativePerformance.onAppStarted() + print("📊 ReactNativePerformance started") + + // Handle keychain cleanup on first launch + handleKeychainCleanup() + print("🔐 Keychain cleanup completed") + + // Configure Firebase + FirebaseApp.configure() + print("🔥 Firebase configured") + + // Handle OneSignal deep linking + var newLaunchOptions = launchOptions ?? [:] + if let remoteNotif = launchOptions?[UIApplication.LaunchOptionsKey.remoteNotification] as? [String: Any], + let custom = remoteNotif["custom"] as? [String: Any], + let initialURL = custom["u"] as? String, + launchOptions?[UIApplication.LaunchOptionsKey.url] == nil { + newLaunchOptions[UIApplication.LaunchOptionsKey.url] = URL(string: initialURL) + print("🔗 OneSignal deep link processed") + } + + // Set up Expo React Native factory + let delegate = ReactNativeDelegate() + let factory = ExpoReactNativeFactory(delegate: delegate) + delegate.dependencyProvider = RCTAppDependencyProvider() + + reactNativeDelegate = delegate + reactNativeFactory = factory + bindReactNativeFactory(factory) + + window = UIWindow(frame: UIScreen.main.bounds) + factory.startReactNative( + withModuleName: "Uniswap", + in: window, + launchOptions: newLaunchOptions + ) + + let result = super.application(application, didFinishLaunchingWithOptions: newLaunchOptions) + + print("🏁 AppDelegate initialization complete") + return result + } + + // MARK: - Keychain Cleanup + private func handleKeychainCleanup() { + let defaults = UserDefaults.standard + let isFirstRun = !defaults.bool(forKey: AppDelegate.hasLaunchedOnceKey) + let canClearKeychainOnReinstall = KeychainUtils.getCanClearKeychainOnReinstall() + + if canClearKeychainOnReinstall && isFirstRun { + KeychainUtils.clearKeychain() + } + + if !canClearKeychainOnReinstall || isFirstRun { + defaults.set(true, forKey: AppDelegate.hasLaunchedOnceKey) + KeychainUtils.setCanClearKeychainOnReinstall() + } + } + + // MARK: - Deep Linking + override func application( + _ app: UIApplication, + open url: URL, + options: [UIApplication.OpenURLOptionsKey: Any] = [:] + ) -> Bool { + return super.application(app, open: url, options: options) || RCTLinkingManager.application(app, open: url, options: options) + } + + // Universal Links + override func application( + _ application: UIApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void + ) -> Bool { + let result = RCTLinkingManager.application(application, continue: userActivity, restorationHandler: restorationHandler) + return super.application(application, continue: userActivity, restorationHandler: restorationHandler) || result + } + + // MARK: - Push Notifications + override func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + // Handle device token registration + // OneSignal and other services will handle this via swizzling + } + + override func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + // Handle registration failure + print("Failed to register for remote notifications: \(error)") + } + + override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + if let aps = userInfo["aps"] as? [String: Any] { + let contentAvailable = aps["content-available"] ?? aps["content_available"] + + if let contentNumber = contentAvailable as? NSNumber, contentNumber.intValue == 1 { + // Convert obj-c payload to SilentPushEventEmitter + let payload = userInfo.reduce(into: [String: Any]()) { result, entry in + if let key = entry.key as? String { + result[key] = entry.value + } + } + + SilentPushEventEmitter.emitEvent(with: payload) + } + } + completionHandler(.noData) + } + + // MARK: - Security + @objc(application:shouldAllowExtensionPointIdentifier:) + func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier) -> Bool { + // Disable 3rd party keyboards + if extensionPointIdentifier == .keyboard { + return false + } + return true + } +} + +class ReactNativeDelegate: ExpoReactNativeFactoryDelegate { + override func sourceURL(for bridge: RCTBridge) -> URL? { + bridge.bundleURL ?? bundleURL() + } + + override func bundleURL() -> URL? { + #if DEBUG + return RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: ".expo/.virtual-metro-entry") + #else + return Bundle.main.url(forResource: "main", withExtension: "jsbundle") + #endif + } + + // Override customize to initialize RNBootSplash BEFORE the window becomes visible + public override func customize(_ rootView: UIView) { + super.customize(rootView) + RNBootSplash.initWithStoryboard("SplashScreen", rootView: rootView) + } +} diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/Contents.json index 73c00596a7f..74d6a722cf3 100644 --- a/apps/mobile/ios/Uniswap/Colors.xcassets/Contents.json +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/Contents.json @@ -1,6 +1,6 @@ { - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/accent1.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/accent1.colorset/Contents.json index 72e5d6d4833..40b40a549cc 100644 --- a/apps/mobile/ios/Uniswap/Colors.xcassets/accent1.colorset/Contents.json +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/accent1.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "255", - "green" : "114", - "red" : "252" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "255", + "green": "114", + "red": "252" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "255", - "green" : "114", - "red" : "252" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "255", + "green": "114", + "red": "252" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/neutral1.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/neutral1.colorset/Contents.json index f2ef8800583..ba4d681c482 100644 --- a/apps/mobile/ios/Uniswap/Colors.xcassets/neutral1.colorset/Contents.json +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/neutral1.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "34", - "green" : "34", - "red" : "34" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "34", + "green": "34", + "red": "34" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "255", - "green" : "255", - "red" : "255" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "255", + "green": "255", + "red": "255" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/neutral2.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/neutral2.colorset/Contents.json index 36e475a94f7..56eb96f47cc 100644 --- a/apps/mobile/ios/Uniswap/Colors.xcassets/neutral2.colorset/Contents.json +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/neutral2.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x7D", - "green" : "0x7D", - "red" : "0x7D" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0x7D", + "green": "0x7D", + "red": "0x7D" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x9B", - "green" : "0x9B", - "red" : "0x9B" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0x9B", + "green": "0x9B", + "red": "0x9B" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/neutral3.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/neutral3.colorset/Contents.json index 7529f063454..977f2f8db01 100644 --- a/apps/mobile/ios/Uniswap/Colors.xcassets/neutral3.colorset/Contents.json +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/neutral3.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "206", - "green" : "206", - "red" : "206" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "206", + "green": "206", + "red": "206" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "94", - "green" : "94", - "red" : "94" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "94", + "green": "94", + "red": "94" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/onboardingBlue.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/onboardingBlue.colorset/Contents.json index c282d29cd29..595b4cf1ab3 100644 --- a/apps/mobile/ios/Uniswap/Colors.xcassets/onboardingBlue.colorset/Contents.json +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/onboardingBlue.colorset/Contents.json @@ -1,20 +1,20 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "251", - "green" : "130", - "red" : "76" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "251", + "green": "130", + "red": "76" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/statusCritical.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/statusCritical.colorset/Contents.json index d226ce2798b..4d296fb5976 100644 --- a/apps/mobile/ios/Uniswap/Colors.xcassets/statusCritical.colorset/Contents.json +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/statusCritical.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "82", - "green" : "95", - "red" : "255" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "82", + "green": "95", + "red": "255" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "82", - "green" : "95", - "red" : "255" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "82", + "green": "95", + "red": "255" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/statusSuccess.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/statusSuccess.colorset/Contents.json index a2260c63480..4d9026dbee6 100644 --- a/apps/mobile/ios/Uniswap/Colors.xcassets/statusSuccess.colorset/Contents.json +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/statusSuccess.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x6B", - "green" : "0xB6", - "red" : "0x40" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0x6B", + "green": "0xB6", + "red": "0x40" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x6B", - "green" : "0xB6", - "red" : "0x40" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0x6B", + "green": "0xB6", + "red": "0x40" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/surface1.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/surface1.colorset/Contents.json index a72cd2503c8..67a72f350ca 100644 --- a/apps/mobile/ios/Uniswap/Colors.xcassets/surface1.colorset/Contents.json +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/surface1.colorset/Contents.json @@ -1,56 +1,56 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0xFF", + "green": "0xFF", + "red": "0xFF" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "light" + "appearance": "luminosity", + "value": "light" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0xFF", + "green": "0xFF", + "red": "0xFF" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x13", - "green" : "0x13", - "red" : "0x13" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0x13", + "green": "0x13", + "red": "0x13" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/surface2.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/surface2.colorset/Contents.json index 6c88e846703..3835aa7323d 100644 --- a/apps/mobile/ios/Uniswap/Colors.xcassets/surface2.colorset/Contents.json +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/surface2.colorset/Contents.json @@ -1,56 +1,56 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF9", - "green" : "0xF9", - "red" : "0xF9" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0xF9", + "green": "0xF9", + "red": "0xF9" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "light" + "appearance": "luminosity", + "value": "light" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF9", - "green" : "0xF9", - "red" : "0xF9" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0xF9", + "green": "0xF9", + "red": "0xF9" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x1B", - "green" : "0x1B", - "red" : "0x1B" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0x1B", + "green": "0x1B", + "red": "0x1B" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/surface3.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/surface3.colorset/Contents.json index 34aff131d39..728ffc1d094 100644 --- a/apps/mobile/ios/Uniswap/Colors.xcassets/surface3.colorset/Contents.json +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/surface3.colorset/Contents.json @@ -1,56 +1,56 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.050", - "blue" : "0x22", - "green" : "0x22", - "red" : "0x22" + "color": { + "color-space": "srgb", + "components": { + "alpha": "0.050", + "blue": "0x22", + "green": "0x22", + "red": "0x22" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "light" + "appearance": "luminosity", + "value": "light" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.050", - "blue" : "0x22", - "green" : "0x22", - "red" : "0x22" + "color": { + "color-space": "srgb", + "components": { + "alpha": "0.050", + "blue": "0x22", + "green": "0x22", + "red": "0x22" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.120", - "blue" : "0xFF", - "green" : "0xFF", - "red" : "0xFF" + "color": { + "color-space": "srgb", + "components": { + "alpha": "0.120", + "blue": "0xFF", + "green": "0xFF", + "red": "0xFF" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/Contents.json b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/Contents.json index 4fdf88263a7..03f35aa2fcf 100644 --- a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/Contents.json @@ -1,158 +1,158 @@ { - "images" : [ + "images": [ { - "filename" : "40.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" + "filename": "40.png", + "idiom": "iphone", + "scale": "2x", + "size": "20x20" }, { - "filename" : "60.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" + "filename": "60.png", + "idiom": "iphone", + "scale": "3x", + "size": "20x20" }, { - "filename" : "29.png", - "idiom" : "iphone", - "scale" : "1x", - "size" : "29x29" + "filename": "29.png", + "idiom": "iphone", + "scale": "1x", + "size": "29x29" }, { - "filename" : "58.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" + "filename": "58.png", + "idiom": "iphone", + "scale": "2x", + "size": "29x29" }, { - "filename" : "87.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" + "filename": "87.png", + "idiom": "iphone", + "scale": "3x", + "size": "29x29" }, { - "filename" : "80.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" + "filename": "80.png", + "idiom": "iphone", + "scale": "2x", + "size": "40x40" }, { - "filename" : "120.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" + "filename": "120.png", + "idiom": "iphone", + "scale": "3x", + "size": "40x40" }, { - "filename" : "57.png", - "idiom" : "iphone", - "scale" : "1x", - "size" : "57x57" + "filename": "57.png", + "idiom": "iphone", + "scale": "1x", + "size": "57x57" }, { - "filename" : "114.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "57x57" + "filename": "114.png", + "idiom": "iphone", + "scale": "2x", + "size": "57x57" }, { - "filename" : "120.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" + "filename": "120.png", + "idiom": "iphone", + "scale": "2x", + "size": "60x60" }, { - "filename" : "180.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" + "filename": "180.png", + "idiom": "iphone", + "scale": "3x", + "size": "60x60" }, { - "filename" : "20.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" + "filename": "20.png", + "idiom": "ipad", + "scale": "1x", + "size": "20x20" }, { - "filename" : "40.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" + "filename": "40.png", + "idiom": "ipad", + "scale": "2x", + "size": "20x20" }, { - "filename" : "29.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" + "filename": "29.png", + "idiom": "ipad", + "scale": "1x", + "size": "29x29" }, { - "filename" : "58.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" + "filename": "58.png", + "idiom": "ipad", + "scale": "2x", + "size": "29x29" }, { - "filename" : "40.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" + "filename": "40.png", + "idiom": "ipad", + "scale": "1x", + "size": "40x40" }, { - "filename" : "80.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" + "filename": "80.png", + "idiom": "ipad", + "scale": "2x", + "size": "40x40" }, { - "filename" : "50.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "50x50" + "filename": "50.png", + "idiom": "ipad", + "scale": "1x", + "size": "50x50" }, { - "filename" : "100.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "50x50" + "filename": "100.png", + "idiom": "ipad", + "scale": "2x", + "size": "50x50" }, { - "filename" : "72.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "72x72" + "filename": "72.png", + "idiom": "ipad", + "scale": "1x", + "size": "72x72" }, { - "filename" : "144.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "72x72" + "filename": "144.png", + "idiom": "ipad", + "scale": "2x", + "size": "72x72" }, { - "filename" : "76.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" + "filename": "76.png", + "idiom": "ipad", + "scale": "1x", + "size": "76x76" }, { - "filename" : "152.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" + "filename": "152.png", + "idiom": "ipad", + "scale": "2x", + "size": "76x76" }, { - "filename" : "167.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" + "filename": "167.png", + "idiom": "ipad", + "scale": "2x", + "size": "83.5x83.5" }, { - "filename" : "1024.png", - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" + "filename": "1024.png", + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/Contents.json b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/Contents.json index 8e70699a7f5..c03a6b92acf 100644 --- a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/Contents.json +++ b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/Contents.json @@ -1,346 +1,346 @@ { - "images" : [ + "images": [ { - "filename" : "40.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" + "filename": "40.png", + "idiom": "iphone", + "scale": "2x", + "size": "20x20" }, { - "filename" : "60.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" + "filename": "60.png", + "idiom": "iphone", + "scale": "3x", + "size": "20x20" }, { - "filename" : "29.png", - "idiom" : "iphone", - "scale" : "1x", - "size" : "29x29" + "filename": "29.png", + "idiom": "iphone", + "scale": "1x", + "size": "29x29" }, { - "filename" : "58.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" + "filename": "58.png", + "idiom": "iphone", + "scale": "2x", + "size": "29x29" }, { - "filename" : "87.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" + "filename": "87.png", + "idiom": "iphone", + "scale": "3x", + "size": "29x29" }, { - "filename" : "80.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" + "filename": "80.png", + "idiom": "iphone", + "scale": "2x", + "size": "40x40" }, { - "filename" : "120.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" + "filename": "120.png", + "idiom": "iphone", + "scale": "3x", + "size": "40x40" }, { - "filename" : "57.png", - "idiom" : "iphone", - "scale" : "1x", - "size" : "57x57" + "filename": "57.png", + "idiom": "iphone", + "scale": "1x", + "size": "57x57" }, { - "filename" : "114.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "57x57" + "filename": "114.png", + "idiom": "iphone", + "scale": "2x", + "size": "57x57" }, { - "filename" : "120.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" + "filename": "120.png", + "idiom": "iphone", + "scale": "2x", + "size": "60x60" }, { - "filename" : "180.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" + "filename": "180.png", + "idiom": "iphone", + "scale": "3x", + "size": "60x60" }, { - "filename" : "20.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" + "filename": "20.png", + "idiom": "ipad", + "scale": "1x", + "size": "20x20" }, { - "filename" : "40.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" + "filename": "40.png", + "idiom": "ipad", + "scale": "2x", + "size": "20x20" }, { - "filename" : "29.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" + "filename": "29.png", + "idiom": "ipad", + "scale": "1x", + "size": "29x29" }, { - "filename" : "58.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" + "filename": "58.png", + "idiom": "ipad", + "scale": "2x", + "size": "29x29" }, { - "filename" : "40.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" + "filename": "40.png", + "idiom": "ipad", + "scale": "1x", + "size": "40x40" }, { - "filename" : "80.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" + "filename": "80.png", + "idiom": "ipad", + "scale": "2x", + "size": "40x40" }, { - "filename" : "50.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "50x50" + "filename": "50.png", + "idiom": "ipad", + "scale": "1x", + "size": "50x50" }, { - "filename" : "100.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "50x50" + "filename": "100.png", + "idiom": "ipad", + "scale": "2x", + "size": "50x50" }, { - "filename" : "72.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "72x72" + "filename": "72.png", + "idiom": "ipad", + "scale": "1x", + "size": "72x72" }, { - "filename" : "144.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "72x72" + "filename": "144.png", + "idiom": "ipad", + "scale": "2x", + "size": "72x72" }, { - "filename" : "76.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" + "filename": "76.png", + "idiom": "ipad", + "scale": "1x", + "size": "76x76" }, { - "filename" : "152.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" + "filename": "152.png", + "idiom": "ipad", + "scale": "2x", + "size": "76x76" }, { - "filename" : "167.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" + "filename": "167.png", + "idiom": "ipad", + "scale": "2x", + "size": "83.5x83.5" }, { - "filename" : "1024.png", - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" + "filename": "1024.png", + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" }, { - "filename" : "16.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" + "filename": "16.png", + "idiom": "mac", + "scale": "1x", + "size": "16x16" }, { - "filename" : "32.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" + "filename": "32.png", + "idiom": "mac", + "scale": "2x", + "size": "16x16" }, { - "filename" : "32.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" + "filename": "32.png", + "idiom": "mac", + "scale": "1x", + "size": "32x32" }, { - "filename" : "64.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" + "filename": "64.png", + "idiom": "mac", + "scale": "2x", + "size": "32x32" }, { - "filename" : "128.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" + "filename": "128.png", + "idiom": "mac", + "scale": "1x", + "size": "128x128" }, { - "filename" : "256.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" + "filename": "256.png", + "idiom": "mac", + "scale": "2x", + "size": "128x128" }, { - "filename" : "256.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" + "filename": "256.png", + "idiom": "mac", + "scale": "1x", + "size": "256x256" }, { - "filename" : "512.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" + "filename": "512.png", + "idiom": "mac", + "scale": "2x", + "size": "256x256" }, { - "filename" : "512.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" + "filename": "512.png", + "idiom": "mac", + "scale": "1x", + "size": "512x512" }, { - "filename" : "1024.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" + "filename": "1024.png", + "idiom": "mac", + "scale": "2x", + "size": "512x512" }, { - "filename" : "48.png", - "idiom" : "watch", - "role" : "notificationCenter", - "scale" : "2x", - "size" : "24x24", - "subtype" : "38mm" + "filename": "48.png", + "idiom": "watch", + "role": "notificationCenter", + "scale": "2x", + "size": "24x24", + "subtype": "38mm" }, { - "filename" : "55.png", - "idiom" : "watch", - "role" : "notificationCenter", - "scale" : "2x", - "size" : "27.5x27.5", - "subtype" : "42mm" + "filename": "55.png", + "idiom": "watch", + "role": "notificationCenter", + "scale": "2x", + "size": "27.5x27.5", + "subtype": "42mm" }, { - "filename" : "58.png", - "idiom" : "watch", - "role" : "companionSettings", - "scale" : "2x", - "size" : "29x29" + "filename": "58.png", + "idiom": "watch", + "role": "companionSettings", + "scale": "2x", + "size": "29x29" }, { - "filename" : "87.png", - "idiom" : "watch", - "role" : "companionSettings", - "scale" : "3x", - "size" : "29x29" + "filename": "87.png", + "idiom": "watch", + "role": "companionSettings", + "scale": "3x", + "size": "29x29" }, { - "filename" : "66.png", - "idiom" : "watch", - "role" : "notificationCenter", - "scale" : "2x", - "size" : "33x33", - "subtype" : "45mm" + "filename": "66.png", + "idiom": "watch", + "role": "notificationCenter", + "scale": "2x", + "size": "33x33", + "subtype": "45mm" }, { - "filename" : "80.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "40x40", - "subtype" : "38mm" + "filename": "80.png", + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "40x40", + "subtype": "38mm" }, { - "filename" : "88.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "44x44", - "subtype" : "40mm" + "filename": "88.png", + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "44x44", + "subtype": "40mm" }, { - "filename" : "92.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "46x46", - "subtype" : "41mm" + "filename": "92.png", + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "46x46", + "subtype": "41mm" }, { - "filename" : "100.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "50x50", - "subtype" : "44mm" + "filename": "100.png", + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "50x50", + "subtype": "44mm" }, { - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "51x51", - "subtype" : "45mm" + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "51x51", + "subtype": "45mm" }, { - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "54x54", - "subtype" : "49mm" + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "54x54", + "subtype": "49mm" }, { - "filename" : "172.png", - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "86x86", - "subtype" : "38mm" + "filename": "172.png", + "idiom": "watch", + "role": "quickLook", + "scale": "2x", + "size": "86x86", + "subtype": "38mm" }, { - "filename" : "196.png", - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "98x98", - "subtype" : "42mm" + "filename": "196.png", + "idiom": "watch", + "role": "quickLook", + "scale": "2x", + "size": "98x98", + "subtype": "42mm" }, { - "filename" : "216.png", - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "108x108", - "subtype" : "44mm" + "filename": "216.png", + "idiom": "watch", + "role": "quickLook", + "scale": "2x", + "size": "108x108", + "subtype": "44mm" }, { - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "117x117", - "subtype" : "45mm" + "idiom": "watch", + "role": "quickLook", + "scale": "2x", + "size": "117x117", + "subtype": "45mm" }, { - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "129x129", - "subtype" : "49mm" + "idiom": "watch", + "role": "quickLook", + "scale": "2x", + "size": "129x129", + "subtype": "49mm" }, { - "filename" : "1024.png", - "idiom" : "watch-marketing", - "scale" : "1x", - "size" : "1024x1024" + "filename": "1024.png", + "idiom": "watch-marketing", + "scale": "1x", + "size": "1024x1024" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/Contents.json b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/Contents.json index 8e70699a7f5..c03a6b92acf 100644 --- a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/Contents.json +++ b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/Contents.json @@ -1,346 +1,346 @@ { - "images" : [ + "images": [ { - "filename" : "40.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" + "filename": "40.png", + "idiom": "iphone", + "scale": "2x", + "size": "20x20" }, { - "filename" : "60.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" + "filename": "60.png", + "idiom": "iphone", + "scale": "3x", + "size": "20x20" }, { - "filename" : "29.png", - "idiom" : "iphone", - "scale" : "1x", - "size" : "29x29" + "filename": "29.png", + "idiom": "iphone", + "scale": "1x", + "size": "29x29" }, { - "filename" : "58.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" + "filename": "58.png", + "idiom": "iphone", + "scale": "2x", + "size": "29x29" }, { - "filename" : "87.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" + "filename": "87.png", + "idiom": "iphone", + "scale": "3x", + "size": "29x29" }, { - "filename" : "80.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" + "filename": "80.png", + "idiom": "iphone", + "scale": "2x", + "size": "40x40" }, { - "filename" : "120.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" + "filename": "120.png", + "idiom": "iphone", + "scale": "3x", + "size": "40x40" }, { - "filename" : "57.png", - "idiom" : "iphone", - "scale" : "1x", - "size" : "57x57" + "filename": "57.png", + "idiom": "iphone", + "scale": "1x", + "size": "57x57" }, { - "filename" : "114.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "57x57" + "filename": "114.png", + "idiom": "iphone", + "scale": "2x", + "size": "57x57" }, { - "filename" : "120.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" + "filename": "120.png", + "idiom": "iphone", + "scale": "2x", + "size": "60x60" }, { - "filename" : "180.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" + "filename": "180.png", + "idiom": "iphone", + "scale": "3x", + "size": "60x60" }, { - "filename" : "20.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" + "filename": "20.png", + "idiom": "ipad", + "scale": "1x", + "size": "20x20" }, { - "filename" : "40.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" + "filename": "40.png", + "idiom": "ipad", + "scale": "2x", + "size": "20x20" }, { - "filename" : "29.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" + "filename": "29.png", + "idiom": "ipad", + "scale": "1x", + "size": "29x29" }, { - "filename" : "58.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" + "filename": "58.png", + "idiom": "ipad", + "scale": "2x", + "size": "29x29" }, { - "filename" : "40.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" + "filename": "40.png", + "idiom": "ipad", + "scale": "1x", + "size": "40x40" }, { - "filename" : "80.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" + "filename": "80.png", + "idiom": "ipad", + "scale": "2x", + "size": "40x40" }, { - "filename" : "50.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "50x50" + "filename": "50.png", + "idiom": "ipad", + "scale": "1x", + "size": "50x50" }, { - "filename" : "100.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "50x50" + "filename": "100.png", + "idiom": "ipad", + "scale": "2x", + "size": "50x50" }, { - "filename" : "72.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "72x72" + "filename": "72.png", + "idiom": "ipad", + "scale": "1x", + "size": "72x72" }, { - "filename" : "144.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "72x72" + "filename": "144.png", + "idiom": "ipad", + "scale": "2x", + "size": "72x72" }, { - "filename" : "76.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" + "filename": "76.png", + "idiom": "ipad", + "scale": "1x", + "size": "76x76" }, { - "filename" : "152.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" + "filename": "152.png", + "idiom": "ipad", + "scale": "2x", + "size": "76x76" }, { - "filename" : "167.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" + "filename": "167.png", + "idiom": "ipad", + "scale": "2x", + "size": "83.5x83.5" }, { - "filename" : "1024.png", - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" + "filename": "1024.png", + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" }, { - "filename" : "16.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" + "filename": "16.png", + "idiom": "mac", + "scale": "1x", + "size": "16x16" }, { - "filename" : "32.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" + "filename": "32.png", + "idiom": "mac", + "scale": "2x", + "size": "16x16" }, { - "filename" : "32.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" + "filename": "32.png", + "idiom": "mac", + "scale": "1x", + "size": "32x32" }, { - "filename" : "64.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" + "filename": "64.png", + "idiom": "mac", + "scale": "2x", + "size": "32x32" }, { - "filename" : "128.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" + "filename": "128.png", + "idiom": "mac", + "scale": "1x", + "size": "128x128" }, { - "filename" : "256.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" + "filename": "256.png", + "idiom": "mac", + "scale": "2x", + "size": "128x128" }, { - "filename" : "256.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" + "filename": "256.png", + "idiom": "mac", + "scale": "1x", + "size": "256x256" }, { - "filename" : "512.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" + "filename": "512.png", + "idiom": "mac", + "scale": "2x", + "size": "256x256" }, { - "filename" : "512.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" + "filename": "512.png", + "idiom": "mac", + "scale": "1x", + "size": "512x512" }, { - "filename" : "1024.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" + "filename": "1024.png", + "idiom": "mac", + "scale": "2x", + "size": "512x512" }, { - "filename" : "48.png", - "idiom" : "watch", - "role" : "notificationCenter", - "scale" : "2x", - "size" : "24x24", - "subtype" : "38mm" + "filename": "48.png", + "idiom": "watch", + "role": "notificationCenter", + "scale": "2x", + "size": "24x24", + "subtype": "38mm" }, { - "filename" : "55.png", - "idiom" : "watch", - "role" : "notificationCenter", - "scale" : "2x", - "size" : "27.5x27.5", - "subtype" : "42mm" + "filename": "55.png", + "idiom": "watch", + "role": "notificationCenter", + "scale": "2x", + "size": "27.5x27.5", + "subtype": "42mm" }, { - "filename" : "58.png", - "idiom" : "watch", - "role" : "companionSettings", - "scale" : "2x", - "size" : "29x29" + "filename": "58.png", + "idiom": "watch", + "role": "companionSettings", + "scale": "2x", + "size": "29x29" }, { - "filename" : "87.png", - "idiom" : "watch", - "role" : "companionSettings", - "scale" : "3x", - "size" : "29x29" + "filename": "87.png", + "idiom": "watch", + "role": "companionSettings", + "scale": "3x", + "size": "29x29" }, { - "filename" : "66.png", - "idiom" : "watch", - "role" : "notificationCenter", - "scale" : "2x", - "size" : "33x33", - "subtype" : "45mm" + "filename": "66.png", + "idiom": "watch", + "role": "notificationCenter", + "scale": "2x", + "size": "33x33", + "subtype": "45mm" }, { - "filename" : "80.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "40x40", - "subtype" : "38mm" + "filename": "80.png", + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "40x40", + "subtype": "38mm" }, { - "filename" : "88.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "44x44", - "subtype" : "40mm" + "filename": "88.png", + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "44x44", + "subtype": "40mm" }, { - "filename" : "92.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "46x46", - "subtype" : "41mm" + "filename": "92.png", + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "46x46", + "subtype": "41mm" }, { - "filename" : "100.png", - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "50x50", - "subtype" : "44mm" + "filename": "100.png", + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "50x50", + "subtype": "44mm" }, { - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "51x51", - "subtype" : "45mm" + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "51x51", + "subtype": "45mm" }, { - "idiom" : "watch", - "role" : "appLauncher", - "scale" : "2x", - "size" : "54x54", - "subtype" : "49mm" + "idiom": "watch", + "role": "appLauncher", + "scale": "2x", + "size": "54x54", + "subtype": "49mm" }, { - "filename" : "172.png", - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "86x86", - "subtype" : "38mm" + "filename": "172.png", + "idiom": "watch", + "role": "quickLook", + "scale": "2x", + "size": "86x86", + "subtype": "38mm" }, { - "filename" : "196.png", - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "98x98", - "subtype" : "42mm" + "filename": "196.png", + "idiom": "watch", + "role": "quickLook", + "scale": "2x", + "size": "98x98", + "subtype": "42mm" }, { - "filename" : "216.png", - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "108x108", - "subtype" : "44mm" + "filename": "216.png", + "idiom": "watch", + "role": "quickLook", + "scale": "2x", + "size": "108x108", + "subtype": "44mm" }, { - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "117x117", - "subtype" : "45mm" + "idiom": "watch", + "role": "quickLook", + "scale": "2x", + "size": "117x117", + "subtype": "45mm" }, { - "idiom" : "watch", - "role" : "quickLook", - "scale" : "2x", - "size" : "129x129", - "subtype" : "49mm" + "idiom": "watch", + "role": "quickLook", + "scale": "2x", + "size": "129x129", + "subtype": "49mm" }, { - "filename" : "1024.png", - "idiom" : "watch-marketing", - "scale" : "1x", - "size" : "1024x1024" + "filename": "1024.png", + "idiom": "watch-marketing", + "scale": "1x", + "size": "1024x1024" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/Contents.json b/apps/mobile/ios/Uniswap/Images.xcassets/Contents.json index 73c00596a7f..74d6a722cf3 100644 --- a/apps/mobile/ios/Uniswap/Images.xcassets/Contents.json +++ b/apps/mobile/ios/Uniswap/Images.xcassets/Contents.json @@ -1,6 +1,6 @@ { - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashLogo.imageset/Contents.json b/apps/mobile/ios/Uniswap/Images.xcassets/SplashLogo.imageset/Contents.json index 5c4b9312239..342525220dd 100644 --- a/apps/mobile/ios/Uniswap/Images.xcassets/SplashLogo.imageset/Contents.json +++ b/apps/mobile/ios/Uniswap/Images.xcassets/SplashLogo.imageset/Contents.json @@ -1,23 +1,23 @@ { - "images" : [ + "images": [ { - "filename" : "SplashLogo 1.png", - "idiom" : "universal", - "scale" : "1x" + "filename": "SplashLogo 1.png", + "idiom": "universal", + "scale": "1x" }, { - "filename" : "SplashLogo@2x 1.png", - "idiom" : "universal", - "scale" : "2x" + "filename": "SplashLogo@2x 1.png", + "idiom": "universal", + "scale": "2x" }, { - "filename" : "SplashLogo@3x 1.png", - "idiom" : "universal", - "scale" : "3x" + "filename": "SplashLogo@3x 1.png", + "idiom": "universal", + "scale": "3x" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/Contents.json b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/Contents.json index d1531a38b24..1110e730dbf 100644 --- a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/Contents.json +++ b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/Contents.json @@ -1,89 +1,89 @@ { - "images" : [ + "images": [ { - "filename" : "background.png", - "idiom" : "universal", - "scale" : "1x" + "filename": "background.png", + "idiom": "universal", + "scale": "1x" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "light" + "appearance": "luminosity", + "value": "light" } ], - "filename" : "background-3.png", - "idiom" : "universal", - "scale" : "1x" + "filename": "background-3.png", + "idiom": "universal", + "scale": "1x" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "filename" : "background-1-dark.png", - "idiom" : "universal", - "scale" : "1x" + "filename": "background-1-dark.png", + "idiom": "universal", + "scale": "1x" }, { - "filename" : "background-1.png", - "idiom" : "universal", - "scale" : "2x" + "filename": "background-1.png", + "idiom": "universal", + "scale": "2x" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "light" + "appearance": "luminosity", + "value": "light" } ], - "filename" : "background-4.png", - "idiom" : "universal", - "scale" : "2x" + "filename": "background-4.png", + "idiom": "universal", + "scale": "2x" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "filename" : "background-1-dark-1.png", - "idiom" : "universal", - "scale" : "2x" + "filename": "background-1-dark-1.png", + "idiom": "universal", + "scale": "2x" }, { - "filename" : "background-2.png", - "idiom" : "universal", - "scale" : "3x" + "filename": "background-2.png", + "idiom": "universal", + "scale": "3x" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "light" + "appearance": "luminosity", + "value": "light" } ], - "filename" : "background-5.png", - "idiom" : "universal", - "scale" : "3x" + "filename": "background-5.png", + "idiom": "universal", + "scale": "3x" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "filename" : "background-1-dark-2.png", - "idiom" : "universal", - "scale" : "3x" + "filename": "background-1-dark-2.png", + "idiom": "universal", + "scale": "3x" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.m b/apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.m new file mode 100644 index 00000000000..343b5119cf9 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.m @@ -0,0 +1,18 @@ +// +// SilentPushEventEmitter.m +// Uniswap +// +// Created by John Short on 9/29/25. +// + +#import +#import +#import + +@interface RCT_EXTERN_MODULE(SilentPushEventEmitter, RCTEventEmitter) + +RCT_EXTERN_METHOD(supportedEvents) +RCT_EXTERN_METHOD(addListener:(NSString *)eventName) +RCT_EXTERN_METHOD(removeListeners:(nonnull NSNumber *)count) + +@end diff --git a/apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.swift b/apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.swift new file mode 100644 index 00000000000..c79a3fb051a --- /dev/null +++ b/apps/mobile/ios/Uniswap/Notifications/SilentPushEventEmitter.swift @@ -0,0 +1,31 @@ +// +// SilentPushEventEmitter.swift +// Uniswap +// +// Created by John Short on 9/29/25. +// + +import React + +@objc(SilentPushEventEmitter) +open class SilentPushEventEmitter: RCTEventEmitter { + + public static weak var emitter: RCTEventEmitter? + + override init() { + super.init() + SilentPushEventEmitter.emitter = self + } + + open override func supportedEvents() -> [String] { + ["SilentPushReceived"] + } + + @objc(emitEventWithPayload:) + public static func emitEvent(with payload: [String: Any]) { + guard let emitter = emitter else { + return + } + emitter.sendEvent(withName: "SilentPushReceived", body: payload) + } +} diff --git a/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputViewModel.swift b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputViewModel.swift index 8e0d6f307ea..b7d3166876e 100644 --- a/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputViewModel.swift +++ b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputViewModel.swift @@ -15,7 +15,7 @@ class SeedPhraseInputViewModel: ObservableObject { case error } - enum MnemonicError { + enum MnemonicError: Equatable { case invalidPhrase case invalidWord(String) case notEnoughWords @@ -190,18 +190,22 @@ class SeedPhraseInputViewModel: ObservableObject { let firstInvalidWord = rnEthersRS.findInvalidWord(mnemonic: mnemonic) let isAddress = mnemonic.starts(with: "0x") && mnemonic.count == 42 + let isFirstWordInvalid = firstInvalidWord == words.last && skipInvalidWord + let isInvalidLengthError = (error == .notEnoughWords || error == .tooManyWords) && !isValidLength - if (firstInvalidWord == words.last && skipInvalidWord) { - status = .none - } else if (firstInvalidWord == "" && isValidLength) { - status = .valid + if (isFirstWordInvalid) { + return } else if (isAddress) { status = .error error = .wordIsAddress } else if (firstInvalidWord != "") { status = .error error = .invalidWord(firstInvalidWord) - } else { + } else if (isInvalidLengthError) { + return + } else if (firstInvalidWord == "" && isValidLength) { + status = .valid + } else{ status = .none } @@ -209,7 +213,7 @@ class SeedPhraseInputViewModel: ObservableObject { error = nil } - let canSubmit = error == nil && mnemonic != "" && firstInvalidWord == "" && isValidLength + let canSubmit = error == nil && mnemonic != "" onInputValidated(["canSubmit": canSubmit]) } } diff --git a/apps/mobile/ios/Uniswap/Uniswap-Bridging-Header.h b/apps/mobile/ios/Uniswap/Uniswap-Bridging-Header.h new file mode 100644 index 00000000000..8a24474f8ac --- /dev/null +++ b/apps/mobile/ios/Uniswap/Uniswap-Bridging-Header.h @@ -0,0 +1,23 @@ +// +// Uniswap-Bridging-Header.h +// Uniswap +// +// Bridging header for Swift/Objective-C interoperability +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import "libethers_ffi.h" + +// Import any other Objective-C headers that need to be accessible from Swift \ No newline at end of file diff --git a/apps/mobile/ios/Uniswap/main.m b/apps/mobile/ios/Uniswap/main.m deleted file mode 100644 index b1df44b953e..00000000000 --- a/apps/mobile/ios/Uniswap/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import - -#import "AppDelegate.h" - -int main(int argc, char * argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/apps/mobile/ios/Widgets/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/mobile/ios/Widgets/Assets.xcassets/AccentColor.colorset/Contents.json index eb878970081..0afb3cf0eec 100644 --- a/apps/mobile/ios/Widgets/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/apps/mobile/ios/Widgets/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,11 +1,11 @@ { - "colors" : [ + "colors": [ { - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/mobile/ios/Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json index 13613e3ee1a..b121e3bce8f 100644 --- a/apps/mobile/ios/Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/apps/mobile/ios/Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,13 +1,13 @@ { - "images" : [ + "images": [ { - "idiom" : "universal", - "platform" : "ios", - "size" : "1024x1024" + "idiom": "universal", + "platform": "ios", + "size": "1024x1024" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Widgets/Assets.xcassets/Contents.json b/apps/mobile/ios/Widgets/Assets.xcassets/Contents.json index 73c00596a7f..74d6a722cf3 100644 --- a/apps/mobile/ios/Widgets/Assets.xcassets/Contents.json +++ b/apps/mobile/ios/Widgets/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json b/apps/mobile/ios/Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json index eb878970081..0afb3cf0eec 100644 --- a/apps/mobile/ios/Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json +++ b/apps/mobile/ios/Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -1,11 +1,11 @@ { - "colors" : [ + "colors": [ { - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/Widgets/Assets.xcassets/caret-up.imageset/Contents.json b/apps/mobile/ios/Widgets/Assets.xcassets/caret-up.imageset/Contents.json index c7fea1d89d8..4062f18db02 100644 --- a/apps/mobile/ios/Widgets/Assets.xcassets/caret-up.imageset/Contents.json +++ b/apps/mobile/ios/Widgets/Assets.xcassets/caret-up.imageset/Contents.json @@ -1,21 +1,21 @@ { - "images" : [ + "images": [ { - "filename" : "caret-up.svg", - "idiom" : "universal", - "scale" : "1x" + "filename": "caret-up.svg", + "idiom": "universal", + "scale": "1x" }, { - "idiom" : "universal", - "scale" : "2x" + "idiom": "universal", + "scale": "2x" }, { - "idiom" : "universal", - "scale" : "3x" + "idiom": "universal", + "scale": "3x" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/apps/mobile/ios/WidgetsCore/.mobileschema_fingerprint b/apps/mobile/ios/WidgetsCore/.mobileschema_fingerprint index dcdbb6388ae..716115572f2 100644 --- a/apps/mobile/ios/WidgetsCore/.mobileschema_fingerprint +++ b/apps/mobile/ios/WidgetsCore/.mobileschema_fingerprint @@ -1 +1 @@ -7a03144e3423ea77bd883f7e79a0e3d6a3c54ea3e98424ac288e5f817f88931c \ No newline at end of file +025ee5453cc59f194838e233659e0c7da667eff8a4440c563dcdbb9efd3e880a \ No newline at end of file diff --git a/apps/mobile/ios/apollo-codegen-config.json b/apps/mobile/ios/apollo-codegen-config.json index a3bffe80cbb..30598d576a0 100644 --- a/apps/mobile/ios/apollo-codegen-config.json +++ b/apps/mobile/ios/apollo-codegen-config.json @@ -9,9 +9,7 @@ "../../../apps/mobile/src/components/explore/search/SearchPopularTokens.graphql", "../../../packages/api/src/clients/graphql/queries.graphql" ], - "schemaSearchPaths": [ - "../../../packages/api/src/clients/graphql/schema.graphql" - ] + "schemaSearchPaths": ["../../../packages/api/src/clients/graphql/schema.graphql"] }, "output": { "testMocks": { diff --git a/apps/mobile/ios/sourcemaps-datadog.sh b/apps/mobile/ios/sourcemaps-datadog.sh index b182975a380..0bf96a9cb4a 100755 --- a/apps/mobile/ios/sourcemaps-datadog.sh +++ b/apps/mobile/ios/sourcemaps-datadog.sh @@ -1,6 +1,64 @@ #!/bin/sh +# Note: Not using 'set -e' because we want to handle errors gracefully -REACT_NATIVE_XCODE="../../../node_modules/react-native/scripts/react-native-xcode.sh" -DATADOG_XCODE="../../../node_modules/.bin/datadog-ci react-native xcode" +# Fix invalid paths for --entry-file and --assets-dest params, +# needed for react-native/scripts/bundle.js script. +export ENTRY_FILE="apps/mobile/index.js" +export DEST="ios/Uniswap.app" -/bin/sh -c "$DATADOG_XCODE $REACT_NATIVE_XCODE" +# Store the starting directory, and if we're in an `ios` dir, move up to parent +START_DIR=$(pwd) +BASENAME=$(basename "$START_DIR") +if [ "$BASENAME" = "ios" ]; then + cd .. +fi + +DATADOG_XCODE="../../node_modules/.bin/datadog-ci react-native xcode" +REACT_NATIVE_XCODE="../../node_modules/react-native/scripts/react-native-xcode.sh" + +# Create a temporary file for capturing output. +TEMP_LOG=$(mktemp) + +# As Xcode doesn't show echo messages by default, we enforce printing logs with the warning label. +echo "warning: Starting Datadog source map generation and upload..." +echo "warning: Command: $DATADOG_XCODE $REACT_NATIVE_XCODE" +echo "warning: SOURCEMAP_FILE: $SOURCEMAP_FILE" +echo "warning: Configuration: $CONFIGURATION" +echo "" + +# Run the datadog-ci command and capture both stdout and stderr +# Use pipefail to catch the exit code of the datadog command, not tee +set -o pipefail +if /bin/sh -c "$DATADOG_XCODE $REACT_NATIVE_XCODE" 2>&1 | tee "$TEMP_LOG"; then + set +o pipefail + echo "warning: Datadog source map upload completed successfully" + rm -f "$TEMP_LOG" + exit 0 +else + set +o pipefail + EXIT_CODE=$? + echo "error: " + echo "error: Datadog Source Map Upload Failed" + echo "error: Exit Code: $EXIT_CODE" + echo "error: " + echo "error: Full Error Output:" + echo "error: ---" + echo "error: $(cat "$TEMP_LOG")" + echo "error: ---" + echo "error: " + echo "error: Debug Information:" + echo "error: - datadog-ci version: $(../../../node_modules/.bin/datadog-ci version 2>&1 || echo 'Failed to get version')" + echo "error: - Node version: $(node --version 2>&1 || echo 'Node not found')" + echo "error: - React Native CLI: $(../../../node_modules/.bin/react-native --version 2>&1 || echo 'RN CLI not found')" + echo "error: - Working directory: $(pwd)" + echo "error: - DATADOG_API_KEY set: $([ -n "$DATADOG_API_KEY" ] && echo 'Yes' || echo 'No')" + echo "error: - Bundle file exists: $([ -f "$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/main.jsbundle" ] && echo 'Yes' || echo 'No')" + echo "error: - Source map exists: $([ -f "$SOURCEMAP_FILE" ] && echo "Yes ($SOURCEMAP_FILE)" || echo "No ($SOURCEMAP_FILE)")" + echo "error: " + echo "error: This is non-critical. Build will continue." + echo "error: Please report this error for investigation." + + rm -f "$TEMP_LOG" + # Exit with 0 to not fail the build + exit 0 +fi diff --git a/apps/mobile/jest-setup.js b/apps/mobile/jest-setup.js index 979c7cc8709..9562c059011 100644 --- a/apps/mobile/jest-setup.js +++ b/apps/mobile/jest-setup.js @@ -1,15 +1,19 @@ -// Setups and mocks can go here -// For example: https://reactnavigation.org/docs/testing/ - +// From https://reactnavigation.org/docs/testing/#setting-up-jest +import 'react-native-gesture-handler/jestSetup' +// Other import 'core-js' // necessary so setImmediate works in tests import 'utilities/jest-package-mocks' import 'uniswap/jest-package-mocks' import 'wallet/jest-package-mocks' import 'config/jest-presets/ui/ui-package-mocks' - import 'uniswap/src/i18n' // Uses real translations for tests - import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock.js' +import { setUpTests } from 'react-native-reanimated' + +setUpTests() + +// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing +jest.mock('react-native/Libraries/Animated/NativeAnimatedModule') jest.mock('@uniswap/client-explore/dist/uniswap/explore/v1/service-ExploreStatsService_connectquery', () => {}) @@ -33,9 +37,9 @@ jest.mock('react-native-onesignal', () => { getOnesignalId: jest.fn(() => 'dummyUserId'), pushSubscription: { getTokenAsync: jest.fn(() => 'dummyPushToken'), - } + }, }, - } + }, } }) @@ -65,6 +69,22 @@ jest.mock('@react-native-community/netinfo', () => ({ ...mockRNCNetInfo, NetInfo jest.mock('react-native', () => { const RN = jest.requireActual('react-native') // use original implementation, which comes with mocks out of the box + // Mock Linking module within React Native + RN.Linking = { + openURL: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + canOpenURL: jest.fn(), + getInitialURL: jest.fn(), + } + + // Mock Share module within React Native + RN.Share = { + share: jest.fn(), + sharedAction: 'sharedAction', + dismissedAction: 'dismissedAction', + } + return RN }) @@ -74,23 +94,11 @@ jest.mock('@react-navigation/elements', () => ({ require('react-native-reanimated').setUpTests() -jest.mock('react-native/Libraries/Share/Share', () => ({ - share: jest.fn(), -})) - jest.mock('@react-native-firebase/auth', () => () => ({ signInAnonymously: jest.fn(), })) -jest.mock('react-native/Libraries/Linking/Linking', () => ({ - openURL: jest.fn(), - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - canOpenURL: jest.fn(), - getInitialURL: jest.fn(), -})) - -jest.mock("react-native-bootsplash", () => { +jest.mock('react-native-bootsplash', () => { return { hide: jest.fn().mockResolvedValue(), isVisible: jest.fn().mockResolvedValue(false), @@ -99,9 +107,20 @@ jest.mock("react-native-bootsplash", () => { logo: { source: 0 }, brand: { source: 0 }, }), - }; -}); + } +}) -jest.mock("react-native-keyboard-controller", () => - require("react-native-keyboard-controller/jest"), -); +jest.mock('react-native-keyboard-controller', () => require('react-native-keyboard-controller/jest')) + +// Mock @gorhom/bottom-sheet with plain View components +jest.mock('@gorhom/bottom-sheet', () => { + const reactNative = jest.requireActual('react-native') + const { View } = reactNative + return { + __esModule: true, + default: View, + BottomSheetModal: View, + BottomSheetModalProvider: View, + BottomSheetView: View, + } +}) diff --git a/apps/mobile/jest.config.js b/apps/mobile/jest.config.js index 6b32f9ca40d..32246c3ef2f 100644 --- a/apps/mobile/jest.config.js +++ b/apps/mobile/jest.config.js @@ -20,9 +20,8 @@ module.exports = { lines: 0, }, }, - setupFiles: [ - '../../config/jest-presets/jest/setup.js', - './jest-setup.js', - '../../node_modules/react-native-gesture-handler/jestSetup.js', - ], + // Override moduleFileExtensions to NOT prioritize .web.ts for native tests + // This ensures mobile tests use moti animations from index.ts, not CSS from index.web.ts + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + setupFiles: ['../../config/jest-presets/jest/setup.js', './jest-setup.js'], } diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js index 26513961f50..2b25f7705ff 100644 --- a/apps/mobile/metro.config.js +++ b/apps/mobile/metro.config.js @@ -1,61 +1,34 @@ -/** - * Metro configuration for React Native with support for SVG files - * https://github.com/react-native-svg/react-native-svg#use-with-svg-files - * - * @format - */ -const { getMetroAndroidAssetsResolutionFix } = require('react-native-monorepo-tools') -const androidAssetsResolutionFix = getMetroAndroidAssetsResolutionFix() - const withStorybook = require('@storybook/react-native/metro/withStorybook') +const { mergeConfig } = require('@react-native/metro-config') +const { getDefaultConfig: getExpoDefaultConfig } = require('expo/metro-config') -const path = require('path') -const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config') - -const mobileRoot = path.resolve(__dirname) -const workspaceRoot = path.resolve(mobileRoot, '../..') - -const watchFolders = [mobileRoot, `${workspaceRoot}/node_modules`, `${workspaceRoot}/packages`] - - -const defaultConfig = getDefaultConfig(__dirname) +const defaultConfig = getExpoDefaultConfig(__dirname) const { resolver: { sourceExts, assetExts }, } = defaultConfig -const config = { +// Only customize necessary fields for SVG and Storybook support +const customConfig = { resolver: { - nodeModulesPaths: [`${workspaceRoot}/node_modules`], assetExts: assetExts.filter((ext) => ext !== 'svg'), sourceExts: [...sourceExts, 'svg', 'cjs'], }, transformer: { + babelTransformerPath: require.resolve('react-native-svg-transformer'), getTransformOptions: async () => ({ transform: { experimentalImportSupport: false, inlineRequires: true, }, }), - babelTransformerPath: require.resolve('react-native-svg-transformer'), - publicPath: androidAssetsResolutionFix.publicPath, - }, - server: { - enhanceMiddleware: (middleware) => { - return androidAssetsResolutionFix.applyMiddleware(middleware) - }, }, - watchFolders, } const IS_STORYBOOK_ENABLED = process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test' -// Checkout more useful options in the docs: https://github.com/storybookjs/react-native?tab=readme-ov-file#options -module.exports = withStorybook(mergeConfig(defaultConfig, config), { - // Set to false to remove storybook specific options - // you can also use a env variable to set this +module.exports = withStorybook(mergeConfig(getExpoDefaultConfig(__dirname), defaultConfig, customConfig), { enabled: IS_STORYBOOK_ENABLED, onDisabledRemoveStorybook: true, - // Path to your storybook config - configPath: path.resolve(__dirname, './.storybook'), + configPath: require('path').resolve(__dirname, './.storybook'), }) diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 853a8cb2918..7a2ad588dbd 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -3,6 +3,7 @@ "version": "0.0.1", "private": true, "license": "GPL-3.0-or-later", + "main": "./index.js", "scripts": { "android": "nx android mobile", "android:release": "nx android:release mobile", @@ -10,6 +11,8 @@ "android:beta:release": "nx android:beta:release mobile", "android:prod": "nx android:prod mobile", "android:prod:release": "nx android:prod:release mobile", + "check": "nx check mobile", + "check:fast": "nx check:fast mobile", "check:deps:usage": "nx check:deps:usage mobile", "check:bundlesize": "nx check:bundlesize mobile", "clean": "nx clean mobile", @@ -43,8 +46,8 @@ "ios:beta": "nx ios:beta mobile", "ios:bundle": "nx ios:bundle mobile", "ios:release": "nx ios:release mobile", - "lint:biome": "nx lint:biome mobile", - "lint:biome:fix": "nx lint:biome:fix mobile", + "format": "nx format mobile", + "format:check": "nx format:check mobile", "lint": "nx lint mobile", "lint:fix": "nx lint:fix mobile", "start": "nx start mobile", @@ -66,11 +69,12 @@ }, "dependencies": { "@amplitude/analytics-react-native": "1.4.11", - "@apollo/client": "3.10.4", - "@datadog/mobile-react-native": "2.8.2", - "@datadog/mobile-react-navigation": "2.8.2", + "@apollo/client": "3.11.10", + "@datadog/mobile-react-native": "2.12.2", + "@datadog/mobile-react-navigation": "2.12.2", "@ethersproject/bignumber": "5.7.0", "@ethersproject/shims": "5.6.0", + "@expo/fingerprint": "0.15.3", "@formatjs/intl-datetimeformat": "4.5.1", "@formatjs/intl-getcanonicallocales": "1.9.0", "@formatjs/intl-locale": "2.4.44", @@ -79,42 +83,44 @@ "@formatjs/intl-relativetimeformat": "11.1.2", "@gorhom/bottom-sheet": "4.6.4", "@legendapp/list": "1.1.4", - "@react-native-async-storage/async-storage": "1.23.1", + "@react-native-async-storage/async-storage": "2.1.2", "@react-native-community/netinfo": "11.4.1", "@react-native-firebase/app": "21.0.0", "@react-native-firebase/auth": "21.0.0", "@react-native-firebase/firestore": "21.0.0", "@react-native-masked-view/masked-view": "0.3.2", - "@react-native/metro-config": "0.77.2", + "@react-native/metro-config": "0.79.5", "@react-navigation/bottom-tabs": "6.6.1", "@react-navigation/core": "7.9.2", "@react-navigation/native": "7.1.9", "@react-navigation/native-stack": "7.3.13", "@react-navigation/stack": "7.3.2", "@reduxjs/toolkit": "1.9.3", - "@reown/walletkit": "1.2.8", - "@rnef/cli": "0.7.18", - "@rnef/platform-android": "0.7.18", - "@rnef/platform-ios": "0.7.18", - "@rnef/plugin-metro": "0.7.18", - "@rnef/provider-github": "0.7.18", - "@shopify/flash-list": "1.7.3", + "@reown/walletkit": "1.4.1", + "@scure/base": "2.0.0", + "@shopify/flash-list": "1.7.6", "@shopify/react-native-performance": "4.1.2", "@shopify/react-native-performance-navigation": "3.0.0", - "@shopify/react-native-skia": "1.12.4", + "@shopify/react-native-skia": "2.2.20", "@sparkfabrik/react-native-idfa-aaid": "1.2.0", - "@tanstack/react-query": "5.77.2", - "@testing-library/react": "16.1.0", + "@tanstack/react-query": "5.90.20", + "@testing-library/react": "16.3.0", "@uniswap/analytics": "1.7.2", "@uniswap/analytics-events": "2.43.0", + "@uniswap/client-data-api": "0.0.65", "@uniswap/client-explore": "0.0.17", + "@uniswap/client-notification-service": "0.0.11", "@uniswap/ethers-rs-mobile": "0.0.5", - "@uniswap/sdk-core": "7.7.2", + "@uniswap/sdk-core": "7.12.1", "@universe/api": "workspace:^", - "@walletconnect/core": "2.21.4", - "@walletconnect/react-native-compat": "2.21.4", - "@walletconnect/types": "2.21.4", - "@walletconnect/utils": "2.21.4", + "@universe/gating": "workspace:^", + "@universe/hashcash-native": "workspace:^", + "@universe/notifications": "workspace:^", + "@universe/sessions": "workspace:^", + "@walletconnect/core": "2.23.0", + "@walletconnect/react-native-compat": "2.23.0", + "@walletconnect/types": "2.23.0", + "@walletconnect/utils": "2.23.0", "apollo3-cache-persist": "0.14.1", "babel-plugin-transform-inline-environment-variables": "0.4.4", "babel-plugin-transform-remove-console": "6.9.4", @@ -122,51 +128,53 @@ "d3-shape": "3.2.0", "dayjs": "1.11.7", "dotenv": "16.0.3", - "eslint-plugin-rulesdir": "0.2.2", + "eas-build-cache-provider": "16.4.2", "ethers": "5.7.2", - "expo": "52.0.46", - "expo-blur": "14.0.3", - "expo-camera": "16.0.18", - "expo-clipboard": "7.0.1", - "expo-linear-gradient": "14.0.2", - "expo-linking": "7.0.5", - "expo-local-authentication": "15.0.2", - "expo-localization": "16.0.1", - "expo-screen-capture": "7.0.1", + "expo": "53.0.22", + "expo-blur": "14.1.5", + "expo-camera": "16.1.11", + "expo-clipboard": "7.1.5", + "expo-dev-client": "5.2.4", + "expo-image": "2.4.1", + "expo-linear-gradient": "14.1.5", + "expo-linking": "7.1.7", + "expo-local-authentication": "16.0.5", + "expo-localization": "16.1.6", + "expo-screen-capture": "7.2.0", "expo-secure-store": "14.0.1", - "expo-store-review": "8.0.1", - "expo-web-browser": "14.0.2", + "expo-store-review": "8.1.5", + "expo-web-browser": "14.2.0", "fuse.js": "6.5.3", "i18next": "23.10.0", - "lodash": "4.17.21", - "react": "18.3.1", + "lodash": "4.17.23", + "react": "19.0.3", "react-freeze": "1.0.3", "react-i18next": "14.1.0", - "react-native": "0.77.2", + "react-native": "0.79.5", "react-native-appsflyer": "6.13.1", - "react-native-bootsplash": "6.3.1", + "react-native-bootsplash": "6.3.10", "react-native-context-menu-view": "1.15.0", "react-native-device-info": "10.11.0", "react-native-dotenv": "3.2.0", - "react-native-fast-image": "8.6.3", - "react-native-gesture-handler": "2.22.1", + "react-native-gesture-handler": "2.24.0", "react-native-get-random-values": "1.11.0", "react-native-image-colors": "1.5.2", "react-native-image-picker": "7.1.0", "react-native-keyboard-controller": "1.17.5", "react-native-localize": "2.2.6", "react-native-markdown-display": "7.0.0-alpha.2", - "react-native-mmkv": "2.10.1", + "react-native-mmkv": "2.12.0", + "react-native-nitro-modules": "0.31.10", "react-native-onesignal": "5.2.9", - "react-native-pager-view": "6.5.1", + "react-native-pager-view": "6.7.1", "react-native-passkey": "3.1.0", "react-native-permissions": "4.1.5", - "react-native-reanimated": "3.16.7", + "react-native-reanimated": "3.19.3", "react-native-restart": "0.0.27", - "react-native-safe-area-context": "5.1.0", - "react-native-screens": "4.11.0", + "react-native-safe-area-context": "5.4.0", + "react-native-screens": "4.11.1", "react-native-sortables": "1.7.1", - "react-native-svg": "15.11.2", + "react-native-svg": "15.13.0", "react-native-tab-view": "3.5.2", "react-native-url-polyfill": "1.3.0", "react-native-video": "6.13.0", @@ -186,7 +194,9 @@ "uniswap": "workspace:^", "utilities": "workspace:^", "uuid": "9.0.0", - "wallet": "workspace:^" + "wallet": "workspace:^", + "zod": "4.3.6", + "zustand": "5.0.6" }, "devDependencies": { "@babel/core": "7.26.0", @@ -195,61 +205,47 @@ "@babel/plugin-proposal-numeric-separator": "7.16.7", "@babel/runtime": "7.26.0", "@datadog/datadog-ci": "2.48.0", - "@react-native-community/datetimepicker": "8.2.0", - "@react-native-community/slider": "4.5.5", + "@react-native-community/cli": "18.0.1", + "@react-native-community/cli-platform-android": "18.0.0", + "@react-native-community/cli-platform-ios": "18.0.0", + "@react-native-community/datetimepicker": "8.4.1", + "@react-native-community/slider": "4.5.6", "@storybook/addon-ondevice-controls": "8.5.2", "@storybook/react": "8.5.2", "@storybook/react-native": "8.5.2", - "@tamagui/babel-plugin": "1.125.17", - "@testing-library/react-native": "13.0.0", + "@tamagui/babel-plugin": "1.136.1", + "@testing-library/react-native": "13.3.3", "@types/inquirer": "9.0.8", "@types/jest": "29.5.14", "@types/node": "22.13.1", - "@types/react": "18.3.18", + "@types/react": "19.0.10", "@types/redux-mock-store": "1.0.6", - "@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", "babel-loader": "8.2.3", "babel-plugin-module-resolver": "5.0.0", "babel-plugin-react-native-web": "0.17.5", - "babel-preset-expo": "12.0.6", + "babel-preset-expo": "13.0.0", "core-js": "2.6.12", "esbuild": "0.25.9", - "eslint": "8.44.0", - "expo-modules-core": "2.2.3", + "expo-modules-core": "2.5.0", "inquirer": "8.2.6", "jest": "29.7.0", - "jest-expo": "52.0.3", + "jest-expo": "53.0.10", "jest-extended": "4.0.2", "jest-transformer-svg": "2.0.0", "madge": "6.1.0", "mockdate": "3.0.5", "postinstall-postinstall": "2.1.0", - "react-dom": "18.3.1", + "react-dom": "19.0.3", "react-native-asset": "2.1.1", "react-native-clean-project": "4.0.1", - "react-native-monorepo-tools": "1.2.1", "react-native-svg-transformer": "1.3.0", - "react-test-renderer": "18.3.1", - "reactotron-react-native": "5.1.10", - "reactotron-react-native-mmkv": "0.2.7", - "reactotron-redux": "3.1.10", + "react-test-renderer": "19.0.3", + "reactotron-react-native": "5.1.15", + "reactotron-react-native-mmkv": "0.2.8", + "reactotron-redux": "3.2.0", "redux-saga-test-plan": "4.0.4", - "typescript": "5.3.3" - }, - "expo": { - "install": { - "exclude": [ - "react-native@~0.76.9", - "react-native-reanimated@~3.16.7", - "react-native-gesture-handler@~2.20.0", - "react-native-screens@~4.4.0", - "react-native-safe-area-context@~4.12.0", - "react-native-webview@~13.12.5" - ] - }, - "autolinking": { - "exclude": ["expo-constants"] - } + "typescript": "5.8.3" } } diff --git a/apps/mobile/project.json b/apps/mobile/project.json index 64583483260..9a8deae42b2 100644 --- a/apps/mobile/project.json +++ b/apps/mobile/project.json @@ -1,40 +1,41 @@ { + "tags": ["scope:mobile", "type:app"], "targets": { "build": { "executor": "nx:noop" }, "android": { - "command": "rnef run:android --variant=devDebug --app-id-suffix=dev && bun run start", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile.dev/com.uniswap.MainActivity' bunx expo run:android --variant=devDebug --app-id=com.uniswap.mobile.dev", "options": { "cwd": "{projectRoot}" } }, "android:release": { - "command": "rnef run:android --variant=devRelease --app-id-suffix=dev", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile.dev/com.uniswap.MainActivity' bunx expo run:android --variant=devRelease --app-id=com.uniswap.mobile.dev", "options": { "cwd": "{projectRoot}" } }, "android:beta": { - "command": "rnef run:android --variant=betaDebug --app-id-suffix=beta", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile.beta/com.uniswap.MainActivity' bunx expo run:android --variant=betaDebug --app-id=com.uniswap.mobile.beta", "options": { "cwd": "{projectRoot}" } }, "android:beta:release": { - "command": "rnef run:android --variant=betaRelease --app-id-suffix=beta", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile.beta/com.uniswap.MainActivity' bunx expo run:android --variant=betaRelease --app-id=com.uniswap.mobile.beta", "options": { "cwd": "{projectRoot}" } }, "android:prod": { - "command": "rnef run:android --variant=prodDebug", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile/com.uniswap.MainActivity' bunx expo run:android --variant=prodDebug", "options": { "cwd": "{projectRoot}" } }, "android:prod:release": { - "command": "rnef run:android --variant=prodRelease", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile/com.uniswap.MainActivity' bunx expo run:android --variant=prodRelease", "options": { "cwd": "{projectRoot}" } @@ -112,40 +113,41 @@ } }, "e2e:prepare": { - "command": "bun run e2e:generate-ids && bun run e2e:build-js", - "options": { - "cwd": "{projectRoot}" - } + "executor": "nx:noop", + "dependsOn": ["e2e:generate-ids", "e2e:build-js"] }, "e2e:generate-ids": { - "command": "mkdir -p .maestro/scripts/dist && ts-node .maestro/scripts/tooling/generateTestIds.ts > .maestro/scripts/dist/testIds.js", + "command": "mkdir -p .maestro/scripts/dist && bun run .maestro/scripts/tooling/generateTestIds.ts > .maestro/scripts/dist/testIds.js", "options": { "cwd": "{projectRoot}" } }, "e2e:build-js": { - "command": "ts-node .maestro/scripts/tooling/buildPerformanceScripts.ts", + "command": "bun run .maestro/scripts/tooling/buildPerformanceScripts.ts", "options": { "cwd": "{projectRoot}" } }, "e2e": { - "command": "bun run e2e:prepare && maestro test .maestro/flows", + "command": "maestro test .maestro", "options": { "cwd": "{projectRoot}" - } + }, + "dependsOn": ["e2e:prepare"] }, "e2e:onboarding": { - "command": "bun run e2e:prepare && maestro test .maestro/flows/onboarding", + "command": "maestro test .maestro/flows/onboarding", "options": { "cwd": "{projectRoot}" - } + }, + "dependsOn": ["e2e:prepare"] }, "e2e:swap": { - "command": "bun run e2e:prepare && maestro test .maestro/flows/swap", + "command": "maestro test .maestro/flows/swap", "options": { "cwd": "{projectRoot}" - } + }, + "dependsOn": ["e2e:prepare"] }, "e2e:clear-logs": { "command": "rm -rf $HOME/.maestro/tests", @@ -154,16 +156,17 @@ } }, "e2e:local:submit-metrics": { - "command": "cd .maestro/scripts/performance && ./submit-local.sh && cd ../../..", + "command": ".maestro/scripts/performance/submit-local.sh metrics.jsonl", "options": { "cwd": "{projectRoot}" } }, "e2e:local:extract-metrics": { - "command": "cd .maestro/scripts/performance && ./extract-metrics.sh && cd ../../..", + "command": "bun .maestro/scripts/performance/dist/utils/extract-metrics.js $HOME/.maestro/tests metrics.jsonl", "options": { "cwd": "{projectRoot}" - } + }, + "dependsOn": ["e2e:build-js"] }, "e2e:local:process-metrics": { "command": "bun run e2e:local:extract-metrics && bun run e2e:local:submit-metrics", @@ -172,10 +175,11 @@ } }, "e2e:interactive": { - "command": "bun run e2e:prepare && ts-node .maestro/scripts/yarn/e2e-interactive.ts", + "command": "bun run .maestro/scripts/e2e-interactive.ts", "options": { "cwd": "{projectRoot}" - } + }, + "dependsOn": ["e2e:prepare"] }, "firestore:deploy:rules": { "command": "firebase deploy --only firestore:rules", @@ -190,82 +194,104 @@ } }, "check:circular": { - "command": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 0", + "command": "bunx madge --circular ./src/app/App.tsx", "options": { "cwd": "{projectRoot}" } }, "ios": { - "command": "rnef run:ios --scheme Uniswap --configuration Debug && bun run start", + "command": "bunx expo run:ios --scheme Uniswap --configuration DebugOptimized", + "options": { + "cwd": "{projectRoot}" + } + }, + "ios:debug": { + "command": "bunx expo run:ios --scheme Uniswap --configuration Debug", "options": { "cwd": "{projectRoot}" } }, "ios:interactive": { - "command": "ts-node scripts/ios-build-interactive/main.ts", + "command": "bun run scripts/ios-build-interactive/main.ts", "options": { "cwd": "{projectRoot}" } }, "ios:smol": { - "command": "rnef run:ios --device=\"iPhone SE (3rd generation)\"", + "command": "bunx expo run:ios --device=\"iPhone SE (3rd generation)\"", "options": { "cwd": "{projectRoot}" } }, "ios:dev:release": { - "command": "rnef run:ios --configuration Dev", + "command": "bunx expo run:ios --configuration Dev", "options": { "cwd": "{projectRoot}" } }, "ios:beta": { - "command": "rnef run:ios --configuration Beta", + "command": "bunx expo run:ios --configuration Beta", "options": { "cwd": "{projectRoot}" } }, "ios:bundle": { - "command": "rnef bundle --entry-file='index.js' --dev false --bundle-output='./ios/main.jsbundle' --sourcemap-output ./ios/main.jsbundle.map --dev=false --platform='ios' --assets-dest='./ios'", + "command": "bunx react-native bundle --entry-file apps/mobile/index.js --platform ios --dev false --bundle-output ./ios/main.jsbundle --assets-dest ./ios --sourcemap-output ./ios/main.jsbundle.map", "options": { "cwd": "{projectRoot}" } }, "ios:release": { - "command": "rnef run:ios --configuration Release", + "command": "bunx expo run:ios --configuration Release", "options": { "cwd": "{projectRoot}" } }, - "lint:biome": {}, - "lint:biome:fix": {}, + "format": {}, + "format:check": {}, + "lint:oxlint": {}, + "lint:oxlint:fix": {}, + "lint:oxlint:fast": {}, + "lint:typeaware-custom": { + "command": "bun config/oxlint-plugins/typeaware-custom.ts apps/mobile --native" + }, "lint": {}, "lint:fix": {}, - "lint:eslint": {}, - "lint:eslint:fix": {}, + "check": {}, + "check:fast": {}, "start": { - "command": "NODE_ENV=development rnef start --client-logs", + "command": "EXPO_ANDROID_LAUNCH_ACTIVITY='com.uniswap.mobile.dev/com.uniswap.MainActivity' EXPO_BUILD_CONFIGURATION=Debug NODE_ENV=development bunx expo start --scheme uniswap", "options": { "cwd": "{projectRoot}" - } + }, + "dependsOn": ["pod:ensure", "android:ensure"] }, "start:e2e": { - "command": "NODE_ENV=development IS_E2E_TEST=true rnef start --client-logs", + "command": "NODE_ENV=development IS_E2E_TEST=true bunx expo start", "options": { "cwd": "{projectRoot}" - } + }, + "dependsOn": ["pod:ensure", "android:ensure"] }, "start:production": { - "command": "NODE_ENV=production rnef start --reset-cache", + "command": "NODE_ENV=production bunx expo start --reset-cache", "options": { "cwd": "{projectRoot}" - } + }, + "dependsOn": ["pod:ensure", "android:ensure"] }, "test": { "command": "node --max-old-space-size=8912 ../../node_modules/.bin/jest", "options": { "cwd": "{projectRoot}" - } + }, + "inputs": [ + "sourceFiles", + "mobileGlobals", + "^production", + "{projectRoot}/**/*.test.*", + "{projectRoot}/**/*.spec.*" + ] }, "snapshots": { "command": "jest -u", @@ -274,6 +300,8 @@ } }, "typecheck": {}, + "typecheck:tsc": {}, + "typecheck:tsgo": {}, "unicons": { "command": "cd scripts && python3 populate_svgs.py && cd .. && bun run lint --fix", "options": { @@ -286,6 +314,27 @@ "cwd": "{projectRoot}" } }, + "pod:ensure": { + "command": "./scripts/check-podfile.sh", + "options": { + "cwd": "{projectRoot}" + }, + "inputs": ["{projectRoot}/ios/Podfile", "{projectRoot}/ios/Podfile.lock"], + "cache": true + }, + "android:ensure": { + "command": "./scripts/check-android-gradle.sh", + "options": { + "cwd": "{projectRoot}" + }, + "inputs": [ + "{projectRoot}/android/build.gradle", + "{projectRoot}/android/app/build.gradle", + "{projectRoot}/android/settings.gradle", + "{projectRoot}/android/gradle.properties" + ], + "cache": true + }, "pod:update": { "command": "./scripts/podinstall.sh -u", "options": { diff --git a/apps/mobile/rnef.config.mjs b/apps/mobile/rnef.config.mjs deleted file mode 100644 index b513fc0efc3..00000000000 --- a/apps/mobile/rnef.config.mjs +++ /dev/null @@ -1,36 +0,0 @@ -import { platformAndroid } from '@rnef/platform-android' -import { platformIOS } from '@rnef/platform-ios' -import { pluginMetro } from '@rnef/plugin-metro' -import { providerGitHub } from '@rnef/provider-github' -import { config } from 'dotenv' -config({ path: '../../.env.defaults.local' }) - -const isGitHubAction = process.env.GITHUB_ACTIONS === 'true' - -export default { - plugins: [pluginMetro()], - platforms: { - ios: platformIOS(), - android: platformAndroid(), - }, - remoteCacheProvider: isGitHubAction - ? 'github-actions' - : providerGitHub({ - owner: 'uniswap', - repository: 'universe', - token: process.env.GH_TOKEN_RN_CLI, - }), - fingerprint: { - ignorePaths: [ - // Files generated by [GraphQL] Apollo Generate Swift script phase in Xcode, making fingerprint unstable when installing pods vs not - 'ios/OneSignalNotificationServiceExtension/Env.swift', - 'ios/WidgetsCore/Env.swift', - 'ios/WidgetsCore/MobileSchema/MobileSchema.graphql.swift', - 'ios/WidgetsCore/MobileSchema/Fragments/**/*', - 'ios/WidgetsCore/MobileSchema/Operations/**/*', - 'ios/WidgetsCore/MobileSchema/Schema/**/*', - // There's a setup script in Podfile that changes the podspec in node_modules, making fingerprint unstable when installing pods vs not - '../../node_modules/react-native-permissions/RNPermissions.podspec', - ], - }, -} diff --git a/apps/mobile/scripts/check-android-gradle.sh b/apps/mobile/scripts/check-android-gradle.sh new file mode 100755 index 00000000000..775576a1897 --- /dev/null +++ b/apps/mobile/scripts/check-android-gradle.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# This script warns users if they need to sync Gradle +# It's designed to be used with NX caching - NX will only run this +# when Android Gradle files change + +# Detect if we're in a workspace (monorepo) by checking for workspace root +# Script runs from apps/mobile, so check if ../../nx.json exists (workspace root) +if [ -f "../../nx.json" ]; then + # We're in a workspace, user likely runs commands from root + ANDROID_CMD="bun mobile android" +else + # We're not in a workspace, user runs commands from mobile dir + ANDROID_CMD="bun android" +fi + +echo "⚠️ Warning: Android Gradle files have changed since last build" +echo "" +echo "You may encounter issues when running the Android app." +echo "To fix this, run one of the following:" +echo " • $ANDROID_CMD (build Android app, which will sync Gradle automatically)" +echo "" +echo "Metro bundler will continue starting, but you should build the Android app before" +echo "attempting to run it to ensure Gradle dependencies are synced." + diff --git a/apps/mobile/scripts/check-podfile.sh b/apps/mobile/scripts/check-podfile.sh new file mode 100755 index 00000000000..4aec05ffce3 --- /dev/null +++ b/apps/mobile/scripts/check-podfile.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +# This script warns users if they need to run pod install +# It's designed to be used with NX caching - NX will only run this +# when Podfile or Podfile.lock changes + +# Detect if we're in a workspace (monorepo) by checking for workspace root +# Script runs from apps/mobile, so check if ../../nx.json exists (workspace root) +if [ -f "../../nx.json" ]; then + # We're in a workspace, user likely runs commands from root + IOS_CMD="bun mobile ios" +else + # We're not in a workspace, user runs commands from mobile dir + IOS_CMD="bun ios" +fi + +echo "⚠️ Warning: Podfile or Podfile.lock has changed since last pod install" +echo "" +echo "You may encounter issues when running the iOS app." +echo "To fix this, run:" +echo " • $IOS_CMD (build iOS app, which will install pods automatically)" +echo "" +echo "Metro bundler will continue starting, but you should build the iOS app before" +echo "attempting to run it to ensure pods are installed." + diff --git a/apps/mobile/scripts/checkBundleSize.sh b/apps/mobile/scripts/checkBundleSize.sh index b9a4e5a57da..c4a40f7bd1f 100755 --- a/apps/mobile/scripts/checkBundleSize.sh +++ b/apps/mobile/scripts/checkBundleSize.sh @@ -1,5 +1,5 @@ #!/bin/bash -MAX_SIZE=24.60 +MAX_SIZE=26.00 MAX_BUFFER=0.5 # Check OS type and use appropriate stat command diff --git a/apps/mobile/scripts/getFingerprintForRadonIDE.ts b/apps/mobile/scripts/getFingerprintForRadonIDE.ts new file mode 100644 index 00000000000..61d1e332131 --- /dev/null +++ b/apps/mobile/scripts/getFingerprintForRadonIDE.ts @@ -0,0 +1,22 @@ +#!/usr/bin/env bun +import { createProjectHashAsync } from '@expo/fingerprint' + +async function main(): Promise { + try { + const projectRoot = process.cwd() + const hash = await createProjectHashAsync(projectRoot, { + silent: true, + }) + console.log(hash) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + console.error(`Failed to generate fingerprint: ${errorMessage}`) + process.exit(1) + } +} + +main().catch((error) => { + const errorMessage = error instanceof Error ? error.message : String(error) + console.error(`Fatal error: ${errorMessage}`) + process.exit(1) +}) diff --git a/apps/mobile/scripts/ios-build-interactive/main.ts b/apps/mobile/scripts/ios-build-interactive/main.ts index db54dae187a..540e4bc3ef1 100755 --- a/apps/mobile/scripts/ios-build-interactive/main.ts +++ b/apps/mobile/scripts/ios-build-interactive/main.ts @@ -1,11 +1,11 @@ #!/usr/bin/env ts-node -// biome-ignore lint/suspicious/noConsole: CLI tool needs console for user interaction +// oxlint-disable-next-line no-console -- CLI tool needs console for user interaction import { spawn } from 'child_process' import { existsSync } from 'fs' -import inquirer from 'inquirer' import { homedir } from 'os' import { join } from 'path' -// eslint-disable-next-line no-relative-import-paths/no-relative-import-paths +import inquirer from 'inquirer' +// oxlint-disable-next-line universe-custom/no-relative-import-paths import { type BuildConfig, type BuildType, @@ -219,11 +219,11 @@ const resetMetroCache = async (): Promise => { const buildForSimulator = async (config: BuildConfig): Promise => { printBuildInfo(config, 'iOS Simulator') - const args = ['rnef', 'run:ios', '--scheme', 'Uniswap', '--configuration', config.configuration] + const args = ['expo', 'run:ios', '--scheme', 'Uniswap', '--configuration', config.configuration] if (config.simulator) { const simulatorName = config.simulator.split('(')[0]?.trim() - args.push(`--device="${simulatorName}"`) + args.push(`--device=${simulatorName}`) } log.info(`Command: bun run ${args.join(' ')}\n`) @@ -241,13 +241,13 @@ const buildForDevice = async (config: BuildConfig): Promise => { printBuildInfo(config, 'iOS Device') const args = [ - 'rnef', + 'expo', 'run:ios', '--scheme', config.scheme, '--configuration', config.configuration, - '--destination', + '--device', 'device', ] diff --git a/apps/mobile/scripts/ios-build-interactive/utils.ts b/apps/mobile/scripts/ios-build-interactive/utils.ts index 0608c2409d2..888dbf5511a 100644 --- a/apps/mobile/scripts/ios-build-interactive/utils.ts +++ b/apps/mobile/scripts/ios-build-interactive/utils.ts @@ -1,4 +1,4 @@ -// biome-ignore-all lint/suspicious/noConsole: CLI tool needs console for user interaction +/* oxlint-disable no-console -- CLI tool needs console for user interaction */ import { exec, spawn } from 'child_process' import { promisify } from 'util' @@ -109,7 +109,7 @@ export const runCommand = async (command: string): Promise<{ stdout: string; std export const spawnProcess = (command: string, args: string[]): Promise => { return new Promise((resolve, reject) => { - // biome-ignore lint/suspicious/noExplicitAny: Node spawn options type requires any for stdio config + // oxlint-disable-next-line typescript/no-explicit-any -- Node spawn options type requires any for stdio config const process = spawn(command, args, { stdio: 'inherit' } as any) process.on('close', (code) => { if (code === 0) { diff --git a/apps/mobile/scripts/podinstall.sh b/apps/mobile/scripts/podinstall.sh index b61574d94fb..42de223c528 100755 --- a/apps/mobile/scripts/podinstall.sh +++ b/apps/mobile/scripts/podinstall.sh @@ -2,7 +2,7 @@ set -e -REQUIRED_XCODE_VERSION="16.4" +REQUIRED_XCODE_VERSION="$(cat "$(dirname "$0")/../../../.xcode-version" | tr -d '\n')" UPDATE_REPOS=false while [[ $# -gt 0 ]]; do diff --git a/apps/mobile/src/app/App.tsx b/apps/mobile/src/app/App.tsx index 7554650723a..d20bc83a61d 100644 --- a/apps/mobile/src/app/App.tsx +++ b/apps/mobile/src/app/App.tsx @@ -2,11 +2,42 @@ import { ApolloProvider } from '@apollo/client' import { loadDevMessages, loadErrorMessages } from '@apollo/client/dev' import { DdRum, RumActionType } from '@datadog/mobile-react-native' import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' -import { PerformanceProfiler, RenderPassReport } from '@shopify/react-native-performance' +import { PerformanceProfiler, type RenderPassReport } from '@shopify/react-native-performance' +import { ApiInit, getEntryGatewayUrl, provideSessionService } from '@universe/api' +import { + DatadogSessionSampleRateKey, + DynamicConfigs, + Experiments, + FeatureFlags, + getDynamicConfigValue, + getIsHashcashSolverEnabled, + getIsSessionServiceEnabled, + getIsSessionsPerformanceTrackingEnabled, + getIsSessionUpgradeAutoEnabled, + getIsTurnstileSolverEnabled, + getStatsigClient, + StatsigCustomAppValue, + type StatsigUser, + Storage, + useFeatureFlag, + useIsSessionServiceEnabled, + WALLET_FEATURE_FLAG_NAMES, +} from '@universe/gating' +import { + type ChallengeSolver, + ChallengeType, + createChallengeSolverService, + createHashcashMockSolver, + createHashcashSolver, + createPerformanceTracker, + createSessionInitializationService, + createTurnstileMockSolver, + type SessionInitializationService, +} from '@universe/sessions' import { MMKVWrapper } from 'apollo3-cache-persist' import { default as React, StrictMode, useCallback, useEffect, useMemo, useRef } from 'react' import { I18nextProvider } from 'react-i18next' -import { LogBox, NativeModules, StatusBar } from 'react-native' +import { NativeModules, StatusBar } from 'react-native' import appsFlyer from 'react-native-appsflyer' import DeviceInfo, { getUniqueIdSync } from 'react-native-device-info' import { GestureHandlerRootView } from 'react-native-gesture-handler' @@ -21,13 +52,14 @@ import { PersistGate } from 'redux-persist/integration/react' import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider' import { AppModals } from 'src/app/modals/AppModals' import { useIsPartOfNavigationTree } from 'src/app/navigation/hooks' -import { NavigationContainer } from 'src/app/navigation/NavigationContainer' import { AppStackNavigator } from 'src/app/navigation/navigation' +import { NavigationContainer } from 'src/app/navigation/NavigationContainer' import { store } from 'src/app/store' -import { OfflineBanner } from 'src/components/banners/OfflineBanner' import { TraceUserProperties } from 'src/components/Trace/TraceUserProperties' import { initAppsFlyer } from 'src/features/analytics/appsflyer' import { useLogMissingMnemonic } from 'src/features/analytics/useLogMissingMnemonic' +import { useLogUnexpectedOnboardingReset } from 'src/features/analytics/useLogUnexpectedOnboardingReset' +import { useAppStateResetter } from 'src/features/appState/appStateResetter' import { DatadogProviderWrapper, MOBILE_DEFAULT_DATADOG_SESSION_SAMPLE_RATE, @@ -36,6 +68,7 @@ import { setDatadogUserWithUniqueId } from 'src/features/datadog/user' import { OneSignalUserTagField } from 'src/features/notifications/constants' import { NotificationToastWrapper } from 'src/features/notifications/NotificationToastWrapper' import { initOneSignal } from 'src/features/notifications/Onesignal' +import { createHashcashWorkerChannel } from 'src/features/sessions/createHashcashWorkerChannel' import { statsigMMKVStorageProvider } from 'src/features/statsig/statsigMMKVStorageProvider' import { shouldLogScreen } from 'src/features/telemetry/directLogScreens' import { selectCustomEndpoint } from 'src/features/tweaks/selectors' @@ -45,51 +78,49 @@ import { setFavoritesUserDefaults, setI18NUserDefaults, } from 'src/features/widgets/widgets' +import { SystemBannerPortalProvider } from 'src/notification-service/notification-renderer/SystemBannerPortal' import { initDynamicIntlPolyfills } from 'src/polyfills/intl-delayed' import { useDatadogUserAttributesTracking } from 'src/screens/HomeScreen/useDatadogUserAttributesTracking' import { useAppStateTrigger } from 'src/utils/useAppStateTrigger' -import { flexStyles, useIsDarkMode } from 'ui/src' +import { flexStyles, ImageSettingsProvider, useIsDarkMode } from 'ui/src' import { TestnetModeBanner } from 'uniswap/src/components/banners/TestnetModeBanner' -import { config } from 'uniswap/src/config' import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext' import { initializePortfolioQueryOverrides } from 'uniswap/src/data/rest/portfolioBalanceOverrides' +import { useCurrentAppearanceSetting } from 'uniswap/src/features/appearance/hooks' import { selectFavoriteTokens } from 'uniswap/src/features/favorites/selectors' import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' -import { DatadogSessionSampleRateKey, DynamicConfigs } from 'uniswap/src/features/gating/configs' -import { StatsigCustomAppValue } from 'uniswap/src/features/gating/constants' -import { Experiments } from 'uniswap/src/features/gating/experiments' -import { WALLET_FEATURE_FLAG_NAMES } from 'uniswap/src/features/gating/flags' -import { getDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { StatsigProviderWrapper } from 'uniswap/src/features/gating/StatsigProviderWrapper' -import { getStatsigClient, StatsigUser, Storage } from 'uniswap/src/features/gating/sdk/statsig' +import { mapLanguageToLocale } from 'uniswap/src/features/language/constants' import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks' import { LocalizationContextProvider } from 'uniswap/src/features/language/LocalizationContext' import { clearNotificationQueue } from 'uniswap/src/features/notifications/slice/slice' +import { TokenPriceProvider } from 'uniswap/src/features/prices/TokenPriceContext' +import { selectCurrentLanguage } from 'uniswap/src/features/settings/selectors' import { MobileEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import Trace from 'uniswap/src/features/telemetry/Trace' -import i18n from 'uniswap/src/i18n' -import { CurrencyId } from 'uniswap/src/types/currency' +import i18n, { changeLanguage } from 'uniswap/src/i18n' +import { type CurrencyId } from 'uniswap/src/types/currency' import { datadogEnabledBuild } from 'utilities/src/environment/constants' import { isTestEnv } from 'utilities/src/environment/env' import { registerConsoleOverrides } from 'utilities/src/logger/console' import { attachUnhandledRejectionHandler, setAttributesToDatadog } from 'utilities/src/logger/datadog/Datadog' import { DDRumAction, DDRumTiming } from 'utilities/src/logger/datadog/datadogEvents' -import { logger } from 'utilities/src/logger/logger' +import { getLogger, logger } from 'utilities/src/logger/logger' import { isIOS } from 'utilities/src/platform' import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext' import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' -// biome-ignore lint/style/noRestrictedImports: Required for Apollo client initialization at app root +// oxlint-disable-next-line no-restricted-imports -- Required for Apollo client initialization at app root import { usePersistedApolloClient } from 'wallet/src/data/apollo/usePersistedApolloClient' import { AccountsStoreContextProvider } from 'wallet/src/features/accounts/store/provider' -import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks' +import { StatsigUserIdentifiersUpdater } from 'wallet/src/features/gating/StatsigUserIdentifiersUpdater' import { useHeartbeatReporter } from 'wallet/src/features/telemetry/hooks/useHeartbeatReporter' import { useLastBalancesReporter } from 'wallet/src/features/telemetry/hooks/useLastBalancesReporter' import { selectAllowAnalytics } from 'wallet/src/features/telemetry/selectors' import { useTestnetModeForLoggingAndAnalytics } from 'wallet/src/features/testnetMode/hooks/useTestnetModeForLoggingAndAnalytics' import { WalletUniswapProvider } from 'wallet/src/features/transactions/contexts/WalletUniswapContext' import { TransactionHistoryUpdater } from 'wallet/src/features/transactions/TransactionHistoryUpdater' -import { Account } from 'wallet/src/features/wallet/accounts/types' +import { type Account } from 'wallet/src/features/wallet/accounts/types' import { WalletContextProvider } from 'wallet/src/features/wallet/context' import { useAccounts } from 'wallet/src/features/wallet/hooks' import { NativeWalletProvider } from 'wallet/src/features/wallet/providers/NativeWalletProvider' @@ -110,17 +141,71 @@ if (__DEV__ && !isTestEnv()) { loadErrorMessages() } -// Log boxes on simulators can block e2e tap event when they cover buttons placed at -// the bottom of the screen and cause tests to fail. -if (config.isE2ETest) { - LogBox.ignoreAllLogs() -} - initDynamicIntlPolyfills() initOneSignal() initAppsFlyer() +initializePortfolioQueryOverrides({ store }) + +/** + * Wrapper component that provides the app state resetter to ErrorBoundary. + * Necessary to access the redux and query providers + */ +function ErrorBoundaryWrapper({ children }: { children: React.ReactNode }): JSX.Element { + const appStateResetter = useAppStateResetter() + return {children} +} + +const provideSessionInitializationService = (): SessionInitializationService => { + // Create performance tracker with feature flag control + // Platform-specific: uses React Native's performance.now() API + const performanceTracker = createPerformanceTracker({ + getIsPerformanceTrackingEnabled: getIsSessionsPerformanceTrackingEnabled, + getNow: () => performance.now(), + }) + + // Build solvers map based on feature flags + const solvers = new Map() + + if (getIsTurnstileSolverEnabled()) { + // Turnstile not supported on mobile - use mock + solvers.set(ChallengeType.TURNSTILE, createTurnstileMockSolver()) + } else { + solvers.set(ChallengeType.TURNSTILE, createTurnstileMockSolver()) + } + if (getIsHashcashSolverEnabled()) { + // Use real hashcash solver with native Nitro module + // The native implementation runs on background threads via platform-native APIs + solvers.set( + ChallengeType.HASHCASH, + createHashcashSolver({ + performanceTracker, + getWorkerChannel: () => createHashcashWorkerChannel(), + getLogger, + }), + ) + } else { + solvers.set(ChallengeType.HASHCASH, createHashcashMockSolver()) + } + + return createSessionInitializationService({ + getSessionService: () => + provideSessionService({ + getBaseUrl: getEntryGatewayUrl, + getIsSessionServiceEnabled, + getLogger, + }), + challengeSolverService: createChallengeSolverService({ + solvers, + getLogger, + }), + performanceTracker, + getIsSessionUpgradeAutoEnabled, + getLogger, + }) +} + function App(): JSX.Element | null { useEffect(() => { if (!__DEV__) { @@ -183,6 +268,20 @@ function App(): JSX.Element | null { const MAX_CACHE_SIZE_IN_BYTES = 1024 * 1024 * 25 // 25 MB +/** + * Applies the persisted language from Redux to i18n on app launch. + * Renders inside PersistGate so Redux is already rehydrated when this mounts. + */ +function ApplyPersistedLanguage(): null { + const currentLanguage = useSelector(selectCurrentLanguage) + + useEffect(() => { + changeLanguage(mapLanguageToLocale[currentLanguage]).catch(() => undefined) + }, [currentLanguage]) + + return null +} + // Ensures redux state is available inside usePersistedApolloClient for the custom endpoint function AppOuter(): JSX.Element | null { const customEndpoint = useSelector(selectCustomEndpoint) @@ -214,6 +313,8 @@ function AppOuter(): JSX.Element | null { sendAnalyticsEvent(MobileEventName.PerformanceReport, report) }, []) + const enableExpoImage = useFeatureFlag(FeatureFlags.ExpoImage) + useEffect(() => { for (const [_, flagKey] of WALLET_FEATURE_FLAG_NAMES.entries()) { DdRum.addFeatureFlagEvaluation( @@ -240,12 +341,6 @@ function AppOuter(): JSX.Element | null { } }, []) - useEffect(() => { - if (client) { - initializePortfolioQueryOverrides({ store, apolloClient: client }) - } - }, [client]) - if (!client) { return null } @@ -253,34 +348,39 @@ function AppOuter(): JSX.Element | null { return ( - + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - + ) @@ -315,18 +415,18 @@ function AppInner(): JSX.Element { useEffect(() => { // TODO: This is a temporary solution (it should be replaced with Appearance.setColorScheme // after updating RN to 0.72.0 or higher) - NativeModules.ThemeModule.setColorScheme(themeSetting) + NativeModules['ThemeModule'].setColorScheme(themeSetting) }, [themeSetting]) useLogMissingMnemonic() + useLogUnexpectedOnboardingReset() return ( - <> - + - + ) } @@ -341,6 +441,7 @@ function DataUpdaters(): JSX.Element { const { locale } = useCurrentLanguageInfo() const { code } = useAppFiatCurrencyInfo() const finishedOnboarding = useSelector(selectFinishedOnboarding) + const isSessionServiceEnabled = useIsSessionServiceEnabled() useDatadogUserAttributesTracking({ isOnboarded: !!finishedOnboarding }) useHeartbeatReporter({ isOnboarded: !!finishedOnboarding }) @@ -365,6 +466,11 @@ function DataUpdaters(): JSX.Element { return ( <> + + ) diff --git a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx index 9e692d3fe02..5fc03f4d24b 100644 --- a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx +++ b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx @@ -1,4 +1,5 @@ import { StackActions } from '@react-navigation/native' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { PropsWithChildren, useCallback } from 'react' import { Share } from 'react-native' import { useDispatch } from 'react-redux' @@ -6,17 +7,16 @@ import { exploreNavigationRef, navigationRef } from 'src/app/navigation/navigati import { useAppStackNavigation } from 'src/app/navigation/types' import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal' import { closeAllModals, closeModal, openModal } from 'src/features/modals/modalSlice' +import { useAdvancedSettingsMenuState } from 'src/features/settings/hooks/useAdvancedSettingsMenuState' import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex' import { ScannerModalState } from 'uniswap/src/components/ReceiveQRCode/constants' -import { NavigateToNftItemArgs } from 'uniswap/src/contexts/UniswapContext' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useFiatOnRampAggregatorCountryListQuery, useFiatOnRampAggregatorGetCountryQuery, -} from 'uniswap/src/features/fiatOnRamp/api' +} from 'uniswap/src/features/fiatOnRamp/hooks/useFiatOnRampQueries' import { RampDirection } from 'uniswap/src/features/fiatOnRamp/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { useNavigateToNftExplorerLink } from 'uniswap/src/features/nfts/hooks/useNavigateToNftExplorerLink' import { ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' import { TransactionState } from 'uniswap/src/features/transactions/types/transactionState' @@ -33,7 +33,6 @@ import { isNavigateToSwapFlowArgsPartialState, NavigateToExternalProfileArgs, NavigateToFiatOnRampArgs, - NavigateToNftCollectionArgs, NavigateToSendFlowArgs, NavigateToSwapFlowArgs, ShareTokenArgs, @@ -45,14 +44,14 @@ export function MobileWalletNavigationProvider({ children }: PropsWithChildren): const navigateToAccountActivityList = useNavigateToActivity() const navigateToAccountTokenList = useNavigateToHomepageTab(HomeScreenTabIndex.Tokens) const navigateToBuyOrReceiveWithEmptyWallet = useNavigateToBuyOrReceiveWithEmptyWallet() - const navigateToNftCollection = useNavigateToNftCollection() - const navigateToNftDetails = useNavigateToNftDetails() + const navigateToNftDetails = useNavigateToNftExplorerLink() const navigateToReceive = useNavigateToReceive() const navigateToSend = useNavigateToSend() const navigateToSwapFlow = useNavigateToSwapFlow() const navigateToTokenDetails = useNavigateToTokenDetails() const navigateToFiatOnRamp = useNavigateToFiatOnRamp() const navigateToExternalProfile = useNavigateToExternalProfile() + const navigateToAdvancedSettings = useNavigateToAdvancedSettings() return ( {children} @@ -298,52 +297,12 @@ function useNavigateToTokenDetails(): (currencyId: string) => void { ) } -function useNavigateToNftDetails(): (args: NavigateToNftItemArgs) => void { - const navigation = useAppStackNavigation() - - return useCallback( - ({ owner, contractAddress: address, tokenId, isSpam, fallbackData }: NavigateToNftItemArgs): void => { - closeKeyboardBeforeCallback(() => { - navigation.navigate(MobileScreens.NFTItem, { - owner, - address, - tokenId, - isSpam, - fallbackData, - }) - }) - }, - [navigation], - ) -} - -function useNavigateToNftCollection(): (args: NavigateToNftCollectionArgs) => void { - const appNavigation = useAppStackNavigation() - - return useCallback( - ({ collectionAddress }: NavigateToNftCollectionArgs): void => { - closeKeyboardBeforeCallback(() => { - if (exploreNavigationRef.current && exploreNavigationRef.isFocused()) { - exploreNavigationRef.navigate(MobileScreens.NFTCollection, { - collectionAddress, - }) - } else { - appNavigation.navigate(MobileScreens.NFTCollection, { - collectionAddress, - }) - } - }) - }, - [appNavigation], - ) -} - function useNavigateToBuyOrReceiveWithEmptyWallet(): () => void { const dispatch = useDispatch() const { data: countryResult } = useFiatOnRampAggregatorGetCountryQuery() const { data: countryOptionsResult } = useFiatOnRampAggregatorCountryListQuery({ - rampDirection: RampDirection.ONRAMP, + rampDirection: RampDirection.ON_RAMP, }) const forAggregatorEnabled = countryOptionsResult?.supportedCountries.some( (c) => c.countryCode === countryResult?.countryCode, @@ -396,3 +355,14 @@ function useNavigateToExternalProfile(): (args: NavigateToExternalProfileArgs) = [appNavigation], ) } + +function useNavigateToAdvancedSettings(): () => void { + const navigation = useAppStackNavigation() + const advancedSettingsState = useAdvancedSettingsMenuState() + + return useCallback((): void => { + closeKeyboardBeforeCallback(() => { + navigation.navigate(ModalName.SmartWalletAdvancedSettingsModal, advancedSettingsState) + }) + }, [navigation, advancedSettingsState]) +} diff --git a/apps/mobile/src/app/globalActions.ts b/apps/mobile/src/app/globalActions.ts index 010ba457fb5..7cb6be57ca6 100644 --- a/apps/mobile/src/app/globalActions.ts +++ b/apps/mobile/src/app/globalActions.ts @@ -4,5 +4,5 @@ import { createAction } from '@reduxjs/toolkit' // fired once when the app reloads but before the app renders // allows any updates to be applied to store data loaded from localStorage -// eslint-disable-next-line import/no-unused-modules +// oxlint-disable-next-line import/no-unused-modules export const updateVersion = createAction('global/updateVersion') diff --git a/apps/mobile/src/app/migrations.test.ts b/apps/mobile/src/app/migrations.test.ts index 55b7a04a610..df3337f280f 100644 --- a/apps/mobile/src/app/migrations.test.ts +++ b/apps/mobile/src/app/migrations.test.ts @@ -1,7 +1,76 @@ -import { BigNumber } from '@ethersproject/bignumber' +/* oxlint-disable jest/expect-expect */ import { toIncludeSameMembers } from 'jest-extended' -import mockdate from 'mockdate' -import { migrations, OLD_DEMO_ACCOUNT_ADDRESS } from 'src/app/migrations' +import { migrations } from 'src/app/migrations' +import { + testAddAllowAnalyticsSwitch, + testAddAppearanceSetting, + testAddBehaviorHistory, + testAddBiometricSettings, + testAddCloudBackup, + testAddCompletedUnitagsIntroBoolean, + testAddEnsState, + testAddExperimentsSlice, + testAddExtensionOnboardingState, + testAddFiatCurrencySettings, + testAddHiddenNfts, + testAddLanguageSettings, + testAddLastBalancesReport, + testAddLastBalancesReportValue, + testAddModalsState, + testAddPasswordLockout, + testAddPushNotifications, + testAddPushNotificationsEnabledToAccounts, + testAddReplaceAccountOptions, + testAddSearchHistory, + testAddSkippedUnitagBoolean, + testAddSwapProtectionSetting, + testAddTimeImportedAndDerivationIndex, + testAddTokensVisibility, + testAddTweaksStartingState, + testAddUniconV2IntroModalBoolean, + testAddWalletConnectPendingSessionAndSettings, + testAddWalletIsFunded, + testChangeNativeTypeToSignerMnemonic, + testConvertHiddenNftsToNftsData, + testCorrectFailedFiatOnRampTxIds, + testDeleteChainsSlice, + testDeleteOldOnRampTxData, + testDeleteRTKQuerySlices, + testFilterToSupportedChains, + testFlattenTokenVisibility, + testMigrateAndRemoveCloudBackupSlice, + testMigrateBiometricSettings, + testMigrateDappRequestInfoTypes, + testMigrateFiatPurchaseTransactionInfo, + testMoveSettingStateToGlobal, + testRemoveCoingeckoApiAndTokenLists, + testRemoveDataApi, + testRemoveDemoAccount, + testRemoveEnsState, + testRemoveExperimentsSlice, + testRemoveFlashbotsEnabledFromWalletSlice, + testRemoveLocalTypeAccounts, + testRemoveNonZeroDerivationIndexAccounts, + testRemovePersistedWalletConnectSlice, + testRemoveProviders, + testRemoveReplaceAccountOptions, + testRemoveShowSmallBalances, + testRemoveTokenListsAndCustomTokens, + testRemoveTokensMetadataDisplayType, + testRemoveWalletConnectModalState, + testRenameFollowedAddressesToWatchedAddresses, + testResetActiveChains, + testResetEnsApi, + testResetLastTxNotificationUpdate, + testResetOnboardingStateForGA, + testResetPushNotificationsEnabled, + testResetTokensOrderBy, + testResetTokensOrderByAndMetadataDisplayType, + testRestructureTransactionsAndNotifications, + testSetWalletDeviceLanguage, + testTransformNotificationCountToStatus, + testUpdateLanguageSettings, +} from 'src/app/mobileMigrationTests' import { getSchema, initialSchema, @@ -96,6 +165,9 @@ import { v90Schema, v91Schema, v92Schema, + v93Schema, + v95Schema, + v96Schema, } from 'src/app/schema' import { persistConfig } from 'src/app/store' import { initialBiometricsSettingsState } from 'src/features/biometricsSettings/slice' @@ -105,7 +177,9 @@ import { initialPushNotificationsState } from 'src/features/notifications/slice' import { initialTweaksState } from 'src/features/tweaks/slice' import { initialWalletConnectState } from 'src/features/walletConnect/walletConnectSlice' import { ScannerModalState } from 'uniswap/src/components/ReceiveQRCode/constants' +import { USDC } from 'uniswap/src/constants/tokens' import { AccountType } from 'uniswap/src/features/accounts/types' +import { initialAppearanceSettingsState } from 'uniswap/src/features/appearance/slice' import { initialUniswapBehaviorHistoryState } from 'uniswap/src/features/behaviorHistory/slice' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { initialFavoritesState } from 'uniswap/src/features/favorites/slice' @@ -114,15 +188,20 @@ import { initialNotificationsState } from 'uniswap/src/features/notifications/sl import { initialSearchHistoryState } from 'uniswap/src/features/search/searchHistorySlice' import { initialUserSettingsState } from 'uniswap/src/features/settings/slice' import { ModalName } from 'uniswap/src/features/telemetry/constants' -import { initialTokensState } from 'uniswap/src/features/tokens/slice/slice' +import { initialTokensState } from 'uniswap/src/features/tokens/warnings/slice/slice' import { initialTransactionsState } from 'uniswap/src/features/transactions/slice' import { TransactionStatus, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' import { initialVisibilityState } from 'uniswap/src/features/visibility/slice' -import { testMigrateSearchHistory, testRemoveTHBFromCurrency } from 'uniswap/src/state/uniswapMigrationTests' +import { getWalletDeviceLanguage } from 'uniswap/src/i18n/utils' +import { + testAddActivityVisibility, + testMigrateDismissedTokenWarnings, + testMigrateSearchHistory, + testRemoveTHBFromCurrency, +} from 'uniswap/src/state/uniswapMigrationTests' import { transactionDetails } from 'uniswap/src/test/fixtures' import { DappRequestType } from 'uniswap/src/types/walletConnect' import { getAllKeysOfNestedObject } from 'utilities/src/primitives/objects' -import { initialAppearanceSettingsState } from 'wallet/src/features/appearance/slice' import { initialBatchedTransactionsState } from 'wallet/src/features/batchedTransactions/slice' import { initialBehaviorHistoryState } from 'wallet/src/features/behaviorHistory/slice' import { initialTelemetryState } from 'wallet/src/features/telemetry/slice' @@ -134,7 +213,12 @@ import { testActivatePendingAccounts, testAddBatchedTransactions, testAddCreatedOnboardingRedesignAccount, + testAddExploreAndWelcomeBehaviorHistory, testAddedHapticSetting, + testAddRoutingFieldToTransactions, + testDeleteBetaOnboardingState, + testDeleteDefaultFavoritesFromFavoritesState, + testDeleteExtensionOnboardingState, testDeleteWelcomeWalletCard, testMigrateLiquidityTransactionInfoRename, testMovedCurrencySetting, @@ -146,11 +230,23 @@ import { testRemoveCreatedOnboardingRedesignAccount, testRemoveHoldToSwap, testRemovePriceAlertsEnabledFromPushNotifications, + testRemoveUniconV2BehaviorState, + testRemoveWalletIsUnlockedState, testUnchecksumDismissedTokenWarningKeys, testUpdateExploreOrderByType, } from 'wallet/src/state/walletMigrationsTests' import { signerMnemonicAccount } from 'wallet/src/test/fixtures' +jest.mock('uniswap/src/i18n/utils', () => { + const actual = jest.requireActual('uniswap/src/i18n/utils') + const { Language } = + require('uniswap/src/features/language/constants') as typeof import('uniswap/src/features/language/constants') + return { + ...actual, + getWalletDeviceLanguage: jest.fn(() => Language.English), + } +}) + expect.extend({ toIncludeSameMembers }) const account = signerMnemonicAccount() @@ -267,1216 +363,263 @@ describe('Redux state migrations', () => { }) it('migrates from initialSchema to v0Schema', () => { - const txDetails0 = { - chainId: UniverseChainId.Mainnet, - id: '0', - from: '0xShadowySuperCoder', - options: { - request: { - from: '0x123', - to: '0x456', - value: '0x0', - data: '0x789', - nonce: 10, - gasPrice: BigNumber.from('10000'), - }, - }, - typeInfo: { - type: TransactionType.Approve, - tokenAddress: '0xtokenAddress', - spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', - }, - status: TransactionStatus.Pending, - addedTime: 1487076708000, - hash: '0x123', - } - - const txDetails1 = { - chainId: UniverseChainId.Optimism, - id: '1', - from: '0xKingHodler', - options: { - request: { - from: '0x123', - to: '0x456', - value: '0x0', - data: '0x789', - nonce: 10, - gasPrice: BigNumber.from('10000'), - }, - }, - typeInfo: { - type: TransactionType.Approve, - tokenAddress: '0xtokenAddress', - spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', - }, - status: TransactionStatus.Success, - addedTime: 1487076708000, - hash: '0x123', - } - - const initialSchemaStub = { - ...initialSchema, - transactions: { - byChainId: { - [UniverseChainId.Mainnet]: { - '0': txDetails0, - }, - [UniverseChainId.Optimism]: { - '1': txDetails1, - }, - }, - lastTxHistoryUpdate: { - '0xShadowySuperCoder': 12345678912345, - '0xKingHodler': 9876543210987, - }, - }, - } - - const newSchema = migrations[0](initialSchemaStub) - expect(newSchema.transactions[UniverseChainId.Mainnet]).toBeUndefined() - expect(newSchema.transactions.lastTxHistoryUpdate).toBeUndefined() - - expect(newSchema.transactions['0xShadowySuperCoder'][UniverseChainId.Mainnet]['0'].status).toEqual( - TransactionStatus.Pending, - ) - expect(newSchema.transactions['0xKingHodler'][UniverseChainId.Mainnet]).toBeUndefined() - expect(newSchema.transactions['0xKingHodler'][UniverseChainId.Optimism]['0']).toBeUndefined() - expect(newSchema.transactions['0xKingHodler'][UniverseChainId.Optimism]['1'].from).toEqual('0xKingHodler') - - expect(newSchema.notifications.lastTxNotificationUpdate).toBeDefined() - expect(newSchema.notifications.lastTxNotificationUpdate['0xShadowySuperCoder'][UniverseChainId.Mainnet]).toEqual( - 12345678912345, - ) + testRestructureTransactionsAndNotifications(migrations[0], initialSchema) }) it('migrates from v0 to v1', () => { - const initialSchemaStub = { - ...initialSchema, - walletConnect: { - ...initialSchema.wallet, - modalState: ScannerModalState.ScanQr, - }, - } - - const v0 = migrations[0](initialSchemaStub) - const v1 = migrations[1](v0) - expect(v1.walletConnect.modalState).toEqual(undefined) + testRemoveWalletConnectModalState(migrations[1], migrations[0](initialSchema)) }) it('migrates from v1 to v2', () => { - const TEST_ADDRESSES = ['0xTest'] - - const v1SchemaStub = { - ...v1Schema, - favorites: { - ...v1Schema.favorites, - followedAddresses: TEST_ADDRESSES, - }, - } - - const v2 = migrations[2](v1SchemaStub) - - expect(v2.favorites.watchedAddresses).toEqual(TEST_ADDRESSES) - expect(v2.favorites.followedAddresses).toBeUndefined() + testRenameFollowedAddressesToWatchedAddresses(migrations[2], v1Schema) }) it('migrates from v2 to v3', () => { - const v3 = migrations[3](v2Schema) - expect(v3.searchHistory.results).toEqual([]) + testAddSearchHistory(migrations[3], v2Schema) }) it('migrates from v3 to v4', () => { - const TEST_ADDRESSES = ['0xTest', '0xTest2', '0xTest3', '0xTest4'] - const TEST_IMPORT_TIME_MS = 12345678912345 - - const v3SchemaStub = { - ...v3Schema, - wallet: { - ...v3Schema.wallet, - accounts: [ - { - type: AccountType.Readonly, - address: TEST_ADDRESSES[0], - name: 'Test Account 1', - pending: false, - }, - { - type: AccountType.Readonly, - address: TEST_ADDRESSES[1], - name: 'Test Account 2', - pending: false, - }, - { - type: 'native', - address: TEST_ADDRESSES[2], - name: 'Test Account 3', - pending: false, - }, - { - type: 'native', - address: TEST_ADDRESSES[3], - name: 'Test Account 4', - pending: false, - }, - ], - }, - } - - mockdate.set(TEST_IMPORT_TIME_MS) - - const v4 = migrations[4](v3SchemaStub) - expect(v4.wallet.accounts[0].timeImportedMs).toEqual(TEST_IMPORT_TIME_MS) - expect(v4.wallet.accounts[2].derivationIndex).toBeDefined() + testAddTimeImportedAndDerivationIndex(migrations[4], v3Schema) }) it('migrates from v4 to v5', () => { - const v5 = migrations[5](v4Schema) - - expect(v4Schema.balances).toBeDefined() - expect(v5.balances).toBeUndefined() - - expect(v5.modals[ModalName.Swap].isOpen).toEqual(false) - expect(v5.modals[ModalName.Send].isOpen).toEqual(false) + testAddModalsState(migrations[5], v4Schema) }) it('migrates from v5 to v6', () => { - const v6 = migrations[6](v5Schema) - - expect(v6.walletConnect.pendingSession).toBe(null) - - expect(typeof v6.wallet.settings).toBe('object') - - expect(v5Schema.wallet.bluetooth).toBeDefined() - expect(v6.wallet.bluetooth).toBeUndefined() + testAddWalletConnectPendingSessionAndSettings(migrations[6], v5Schema) }) it('migrates from v6 to v7', () => { - const TEST_ADDRESSES: [string, string, string, string] = ['0xTest', '0xTest2', '0xTest3', '0xTest4'] - const TEST_IMPORT_TIME_MS = 12345678912345 - - const v6SchemaStub = { - ...v6Schema, - wallet: { - ...v6Schema.wallet, - accounts: { - [TEST_ADDRESSES[0]]: { - type: 'native', - address: TEST_ADDRESSES[0], - name: 'Test Account 1', - pending: false, - derivationIndex: 0, - timeImportedMs: TEST_IMPORT_TIME_MS, - }, - [TEST_ADDRESSES[1]]: { - type: 'native', - address: TEST_ADDRESSES[1], - name: 'Test Account 2', - pending: false, - derivationIndex: 1, - timeImportedMs: TEST_IMPORT_TIME_MS, - }, - [TEST_ADDRESSES[2]]: { - type: 'native', - address: TEST_ADDRESSES[2], - name: 'Test Account 3', - pending: false, - derivationIndex: 2, - timeImportedMs: TEST_IMPORT_TIME_MS, - }, - [TEST_ADDRESSES[3]]: { - type: 'native', - address: TEST_ADDRESSES[3], - name: 'Test Account 4', - pending: false, - derivationIndex: 3, - timeImportedMs: TEST_IMPORT_TIME_MS, - }, - }, - }, - } - - expect(Object.values(v6SchemaStub.wallet.accounts)).toHaveLength(4) - const v7 = migrations[7](v6SchemaStub) - - const accounts = Object.values(v7.wallet.accounts) as SignerMnemonicAccount[] - expect(accounts).toHaveLength(1) - expect(accounts[0]?.mnemonicId).toEqual(TEST_ADDRESSES[0]) + testRemoveNonZeroDerivationIndexAccounts(migrations[7], v6Schema) }) it('migrates from v7 to v8', () => { - const v8 = migrations[8](v7Schema) - expect(v8.cloudBackup.backupsFound).toEqual([]) + testAddCloudBackup(migrations[8], v7Schema) }) it('migrates from v8 to v9', () => { - const TEST_ADDRESSES: [string, string, string, string] = ['0xTest', '0xTest2', '0xTest3', '0xTest4'] - const TEST_IMPORT_TIME_MS = 12345678912345 - - const v8SchemaStub = { - ...v8Schema, - wallet: { - ...v6Schema.wallet, - accounts: { - [TEST_ADDRESSES[0]]: { - type: 'native', - address: TEST_ADDRESSES[0], - name: 'Test Account 1', - pending: false, - derivationIndex: 0, - timeImportedMs: TEST_IMPORT_TIME_MS, - }, - [TEST_ADDRESSES[1]]: { - type: 'local', - address: TEST_ADDRESSES[1], - name: 'Test Account 2', - pending: false, - timeImportedMs: TEST_IMPORT_TIME_MS, - }, - }, - }, - } - - expect(Object.values(v8SchemaStub.wallet.accounts)).toHaveLength(2) - const v9 = migrations[9](v8SchemaStub) - expect(Object.values(v9.wallet.accounts)).toHaveLength(1) + testRemoveLocalTypeAccounts(migrations[9], v8Schema) }) it('migrates from v9 to v10', () => { - const TEST_ADDRESSES = ['0xTest', OLD_DEMO_ACCOUNT_ADDRESS, '0xTest2', '0xTest3'] - const TEST_IMPORT_TIME_MS = 12345678912345 - - const accounts = TEST_ADDRESSES.reduce( - (acc, address) => { - acc[address] = { - address, - timeImportedMs: TEST_IMPORT_TIME_MS, - type: 'native', - } as unknown as Account - - return acc - }, - {} as { [address: string]: Account }, - ) - - const v9SchemaStub = { - ...v9Schema, - wallet: { - ...v9Schema.wallet, - accounts, - }, - } - - expect(Object.values(v9SchemaStub.wallet.accounts)).toHaveLength(4) - expect(Object.keys(v9SchemaStub.wallet.accounts)).toContain(OLD_DEMO_ACCOUNT_ADDRESS) - - const migratedSchema = migrations[10](v9SchemaStub) - expect(Object.values(migratedSchema.wallet.accounts)).toHaveLength(3) - expect(Object.keys(migratedSchema.wallet.accounts)).not.toContain(OLD_DEMO_ACCOUNT_ADDRESS) + testRemoveDemoAccount(migrations[10], v9Schema) }) it('migrates from v10 to v11', () => { - const v11 = migrations[11](v10Schema) - - expect(v11.biometricSettings).toBeDefined() - expect(v11.biometricSettings.requiredForAppAccess).toBeDefined() - expect(v11.biometricSettings.requiredForTransactions).toBeDefined() + testAddBiometricSettings(migrations[11], v10Schema) }) it('migrates from v11 to v12', () => { - const TEST_ADDRESS = '0xTestAddress' - const ACCOUNT_NAME = 'Test Account' - const v11Stub = { - ...v11Schema, - wallet: { - ...v11Schema.wallet, - accounts: { - [TEST_ADDRESS]: { - type: 'native', - address: TEST_ADDRESS, - name: ACCOUNT_NAME, - pending: false, - derivationIndex: 0, - timeImportedMs: 123, - }, - }, - }, - } - - const v12 = migrations[12](v11Stub) - - expect(v12.wallet.accounts[TEST_ADDRESS].pushNotificationsEnabled).toEqual(false) - expect(v12.wallet.accounts[TEST_ADDRESS].type).toEqual('native') - expect(v12.wallet.accounts[TEST_ADDRESS].address).toEqual(TEST_ADDRESS) - expect(v12.wallet.accounts[TEST_ADDRESS].name).toEqual(ACCOUNT_NAME) + testAddPushNotificationsEnabledToAccounts(migrations[12], v11Schema) }) it('migrates from v12 to v13', () => { - const v13 = migrations[13](v12Schema) - expect(v13.ens.ensForAddress).toEqual({}) + testAddEnsState(migrations[13], v12Schema) }) it('migrates from v13 to v14', () => { - const v13Stub = { - ...v13Schema, - wallet: { - ...v13Schema.wallet, - isBiometricAuthEnabled: true, - }, - biometricSettings: { - requiredForAppAccess: false, - requiredForTransactions: false, - }, - } - - const v14 = migrations[14](v13Stub) - expect(v14.biometricSettings.requiredForAppAccess).toEqual(true) - expect(v14.biometricSettings.requiredForTransactions).toEqual(true) + testMigrateBiometricSettings(migrations[14], v13Schema) }) it('migrates from v14 to v15', () => { - const TEST_ADDRESS = '0xTestAddress' - const ACCOUNT_NAME = 'Test Account' - const v14Stub = { - ...v14Schema, - wallet: { - ...v14Schema.wallet, - accounts: { - [TEST_ADDRESS]: { - type: 'native', - address: TEST_ADDRESS, - name: ACCOUNT_NAME, - pending: false, - derivationIndex: 0, - timeImportedMs: 123, - }, - }, - }, - } - - const v15 = migrations[15](v14Stub) - const accounts = Object.values(v15.wallet.accounts) - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - expect((accounts[0] as Account)?.type).toEqual(AccountType.SignerMnemonic) + testChangeNativeTypeToSignerMnemonic(migrations[15], v14Schema) }) it('migrates from v15 to v16', () => { - const v15Stub = { - ...v15Schema, - dataApi: {}, - } - - const v16 = migrations[16](v15Stub) - - expect(v16.dataApi).toBeUndefined() + testRemoveDataApi(migrations[16], v15Schema) }) it('migrates from v16 to v17', () => { - const TEST_ADDRESS = '0xTestAddress' - const ACCOUNT_NAME = 'Test Account' - const v16Stub = { - ...v16Schema, - wallet: { - ...v16Schema.wallet, - accounts: { - [TEST_ADDRESS]: { - type: 'native', - address: TEST_ADDRESS, - name: ACCOUNT_NAME, - pending: false, - derivationIndex: 0, - timeImportedMs: 123, - pushNotificationsEnabled: true, - }, - }, - }, - } - - const v17 = migrations[17](v16Stub) - - expect(v17.wallet.accounts[TEST_ADDRESS].pushNotificationsEnabled).toEqual(false) - expect(v17.wallet.accounts[TEST_ADDRESS].type).toEqual('native') - expect(v17.wallet.accounts[TEST_ADDRESS].address).toEqual(TEST_ADDRESS) - expect(v17.wallet.accounts[TEST_ADDRESS].name).toEqual(ACCOUNT_NAME) + testResetPushNotificationsEnabled(migrations[17], v16Schema) }) it('migrates from v17 to v18', () => { - const v17Stub = { - ...v17Schema, - ens: {}, - } - const v18 = migrations[18](v17Stub) - expect(v18.ens).toBeUndefined() + testRemoveEnsState(migrations[18], v17Schema) }) it('migrates from v18 to v19', () => { - const ROPSTEN = 3 as UniverseChainId - const RINKEBY = 4 as UniverseChainId - const GOERLI = 5 as UniverseChainId - const KOVAN = 42 as UniverseChainId - - const TEST_ADDRESS = '0xShadowySuperCoder' - const txDetails0 = { - chainId: UniverseChainId.Mainnet, - id: '0', - from: TEST_ADDRESS, - options: { - request: { - from: '0x123', - to: '0x456', - value: '0x0', - data: '0x789', - nonce: 10, - gasPrice: BigNumber.from('10000'), - }, - }, - typeInfo: { - type: TransactionType.Approve, - tokenAddress: '0xtokenAddress', - spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', - }, - status: TransactionStatus.Pending, - addedTime: 1487076708000, - hash: '0x123', - } - - const TEST_ADDRESS_2 = '0xKingHodler' - const txDetails1 = { - chainId: GOERLI, - id: '1', - from: TEST_ADDRESS_2, - options: { - request: { - from: '0x123', - to: '0x456', - value: '0x0', - data: '0x789', - nonce: 10, - gasPrice: BigNumber.from('10000'), - }, - }, - typeInfo: { - type: TransactionType.Approve, - tokenAddress: '0xtokenAddress', - spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', - }, - status: TransactionStatus.Success, - addedTime: 1487076708000, - hash: '0x123', - } - - const transactions = { - [TEST_ADDRESS]: { - [UniverseChainId.Mainnet]: { - '0': txDetails0, - }, - [UniverseChainId.Base]: { - '0': txDetails0, - '1': txDetails1, - }, - [GOERLI]: { - '0': txDetails0, - '1': txDetails1, - }, - [ROPSTEN]: { - '0': txDetails0, - '1': txDetails1, - }, - [RINKEBY]: { - '0': txDetails1, - }, - [KOVAN]: { - '1': txDetails1, - }, - }, - [TEST_ADDRESS_2]: { - [UniverseChainId.ArbitrumOne]: { - '0': txDetails0, - }, - [UniverseChainId.Optimism]: { - '0': txDetails0, - '1': txDetails1, - }, - [ROPSTEN]: { - '0': txDetails0, - '1': txDetails1, - }, - [RINKEBY]: { - '0': txDetails1, - }, - [KOVAN]: { - '1': txDetails1, - }, - }, - } - - const blocks = { - byChainId: { - [UniverseChainId.Mainnet]: { latestBlockNumber: 123456789 }, - [UniverseChainId.Optimism]: { latestBlockNumber: 123456789 }, - [UniverseChainId.ArbitrumOne]: { latestBlockNumber: 123456789 }, - [UniverseChainId.Base]: { latestBlockNumber: 123456789 }, - [GOERLI]: { latestBlockNumber: 123456789 }, - [ROPSTEN]: { latestBlockNumber: 123456789 }, - [RINKEBY]: { latestBlockNumber: 123456789 }, - [KOVAN]: { latestBlockNumber: 123456789 }, - }, - } - - const chains = { - byChainId: { - [UniverseChainId.Mainnet]: { isActive: true }, - [UniverseChainId.Optimism]: { isActive: true }, - [UniverseChainId.ArbitrumOne]: { isActive: true }, - [UniverseChainId.Base]: { isActive: true }, - [GOERLI]: { isActive: true }, - [ROPSTEN]: { isActive: true }, - [RINKEBY]: { isActive: true }, - [KOVAN]: { isActive: true }, - }, - } - - const v18Stub = { - ...v18Schema, - transactions, - blocks, - chains, - } - - const v19 = migrations[19](v18Stub) - - expect(v19.transactions[TEST_ADDRESS][UniverseChainId.Mainnet]).toBeDefined() - expect(v19.transactions[TEST_ADDRESS][UniverseChainId.Base]).toBeDefined() - expect(v19.transactions[TEST_ADDRESS][GOERLI]).toBeUndefined() - expect(v19.transactions[TEST_ADDRESS][ROPSTEN]).toBeUndefined() - expect(v19.transactions[TEST_ADDRESS][RINKEBY]).toBeUndefined() - expect(v19.transactions[TEST_ADDRESS][KOVAN]).toBeUndefined() - - expect(v19.transactions[TEST_ADDRESS_2][UniverseChainId.ArbitrumOne]).toBeDefined() - expect(v19.transactions[TEST_ADDRESS_2][UniverseChainId.Optimism]).toBeDefined() - expect(v19.transactions[TEST_ADDRESS_2][ROPSTEN]).toBeUndefined() - expect(v19.transactions[TEST_ADDRESS_2][RINKEBY]).toBeUndefined() - expect(v19.transactions[TEST_ADDRESS_2][KOVAN]).toBeUndefined() - - expect(v19.blocks.byChainId[UniverseChainId.Mainnet]).toBeDefined() - expect(v19.blocks.byChainId[UniverseChainId.Optimism]).toBeDefined() - expect(v19.blocks.byChainId[UniverseChainId.ArbitrumOne]).toBeDefined() - expect(v19.blocks.byChainId[UniverseChainId.Base]).toBeDefined() - expect(v19.blocks.byChainId[GOERLI]).toBeUndefined() - expect(v19.blocks.byChainId[ROPSTEN]).toBeUndefined() - expect(v19.blocks.byChainId[RINKEBY]).toBeUndefined() - expect(v19.blocks.byChainId[KOVAN]).toBeUndefined() - - expect(v19.chains.byChainId[UniverseChainId.Mainnet]).toBeDefined() - expect(v19.chains.byChainId[UniverseChainId.Optimism]).toBeDefined() - expect(v19.chains.byChainId[UniverseChainId.ArbitrumOne]).toBeDefined() - expect(v19.chains.byChainId[UniverseChainId.Base]).toBeDefined() - expect(v19.chains.byChainId[GOERLI]).toBeUndefined() - expect(v19.chains.byChainId[ROPSTEN]).toBeUndefined() - expect(v19.chains.byChainId[RINKEBY]).toBeUndefined() - expect(v19.chains.byChainId[KOVAN]).toBeUndefined() + testFilterToSupportedChains(migrations[19], v18Schema) }) it('migrates from v19 to v20', () => { - const v19Stub = { - ...v19Schema, - notifications: { - ...v19Schema.notifications, - lastTxNotificationUpdate: { 1: 122342134 }, - }, - } - - const v20 = migrations[20](v19Stub) - expect(v20.notifications.lastTxNotificationUpdate).toEqual({}) + testResetLastTxNotificationUpdate(migrations[20], v19Schema) }) it('migrates from v20 to v21', () => { - const v20Stub = { - ...v20Schema, - } - - const v21 = migrations[21](v20Stub) - expect(v21.experiments).toBeDefined() + testAddExperimentsSlice(migrations[21], v20Schema) }) it('migrates from v21 to v22', () => { - const v21Stub = { - ...v21Schema, - coingeckoApi: {}, - } - const v22 = migrations[22](v21Stub) - expect(v22.coingeckoApi).toBeUndefined() - expect(v22.tokens.watchedTokens).toBeUndefined() - expect(v22.tokens.tokenPairs).toBeUndefined() + testRemoveCoingeckoApiAndTokenLists(migrations[22], v21Schema) }) it('migrates from v22 to v23', () => { - const v22Stub = { - ...v22Schema, - } - const v23 = migrations[23](v22Stub) - expect(v23.wallet.settings.tokensOrderBy).toBeUndefined() - expect(v23.wallet.settings.tokensMetadataDisplayType).toBeUndefined() + testResetTokensOrderByAndMetadataDisplayType(migrations[23], v22Schema) }) it('migrates from v23 to v24', () => { - const dummyAddress1 = '0xDumDum1' - const dummyAddress2 = '0xDumDum2' - const dummyAddress3 = '0xDumDum3' - const v23Stub = { - ...v23Schema, - notifications: { - ...v23Schema.notifications, - notificationCount: { [dummyAddress1]: 5, [dummyAddress2]: 0, [dummyAddress3]: undefined }, - }, - } - const v24 = migrations[24](v23Stub) - expect(v24.notifications.notificationCount).toBeUndefined() - expect(v24.notifications.notificationStatus[dummyAddress1]).toBe(true) - expect(v24.notifications.notificationStatus[dummyAddress2]).toBe(false) - expect(v24.notifications.notificationStatus[dummyAddress2]).toBe(false) + testTransformNotificationCountToStatus(migrations[24], v23Schema) }) it('migrates from v24 to v25', () => { - const v24Stub = { - ...v24Schema, - } - const v25 = migrations[25](v24Stub) - expect(v25.passwordLockout.passwordAttempts).toBe(0) + testAddPasswordLockout(migrations[25], v24Schema) }) it('migrates from v25 to v26', () => { - const v25Stub = { - ...v25Schema, - } - const v26 = migrations[26](v25Stub) - expect(v26.wallet.settings.showSmallBalances).toBeUndefined() + testRemoveShowSmallBalances(migrations[26], v25Schema) }) it('migrates from v26 to v27', () => { - const v26Stub = { - ...v26Schema, - } - const v27 = migrations[27](v26Stub) - expect(v27.wallet.settings.tokensOrderBy).toBeUndefined() + testResetTokensOrderBy(migrations[27], v26Schema) }) it('migrates from v27 to v28', () => { - const v27Stub = { - ...v27Schema, - } - const v28 = migrations[28](v27Stub) - expect(v28.wallet.settings.tokensMetadataDisplayType).toBeUndefined() + testRemoveTokensMetadataDisplayType(migrations[28], v27Schema) }) it('migrates from v28 to v29', () => { - const v28Stub = { - ...v28Schema, - } - const v29 = migrations[29](v28Stub) - expect(v29.tokenLists).toBeUndefined() - expect(v29.tokens.customTokens).toBeUndefined() + testRemoveTokenListsAndCustomTokens(migrations[29], v28Schema) }) it('migrates from v29 to v30', () => { - const oldFiatOnRampTxDetails = { - chainId: UniverseChainId.Mainnet, - id: '0', - from: account.address, - options: { - request: {}, - }, - // expect this payload to change - typeInfo: { - type: TransactionType.FiatPurchaseDeprecated, - explorerUrl: 'explorer', - outputTokenAddress: '0xtokenAddress', - outputCurrencyAmountFormatted: 50, - outputCurrencyAmountPrice: 2, - syncedWithBackend: true, - }, - status: TransactionStatus.Pending, - addedTime: 1487076708000, - hash: '0x123', - } - const expectedTypeInfo = { - type: TransactionType.FiatPurchaseDeprecated, - explorerUrl: 'explorer', - inputCurrency: undefined, - inputCurrencyAmount: 25, - outputCurrency: { - type: 'crypto', - metadata: { - chainId: undefined, - contractAddress: '0xtokenAddress', - }, - }, - outputCurrencyAmount: undefined, - syncedWithBackend: true, - } - const transactions = { - [account.address]: { - [UniverseChainId.Mainnet]: { - '0': oldFiatOnRampTxDetails, - '1': txDetailsConfirmed, - }, - [UniverseChainId.Base]: { - '0': { ...oldFiatOnRampTxDetails, status: TransactionStatus.Failed }, - '1': txDetailsConfirmed, - }, - [UniverseChainId.ArbitrumOne]: { - '0': { ...oldFiatOnRampTxDetails, status: TransactionStatus.Failed }, - }, - }, - '0xshadowySuperCoder': { - [UniverseChainId.ArbitrumOne]: { - '0': oldFiatOnRampTxDetails, - '1': txDetailsConfirmed, - }, - [UniverseChainId.Optimism]: { - '0': oldFiatOnRampTxDetails, - '1': oldFiatOnRampTxDetails, - '2': txDetailsConfirmed, - }, - }, - '0xdeleteMe': { - [UniverseChainId.Mainnet]: { - '0': { ...oldFiatOnRampTxDetails, status: TransactionStatus.Failed }, - }, - }, - } - const v29Stub = { ...v29Schema, transactions } - - const v30 = migrations[30](v29Stub) - - // expect fiat onramp txdetails to change - expect(v30.transactions[account.address][UniverseChainId.Mainnet]['0'].typeInfo).toEqual(expectedTypeInfo) - expect(v30.transactions[account.address][UniverseChainId.Base]['0']).toBeUndefined() - expect(v30.transactions[account.address][UniverseChainId.ArbitrumOne]).toBeUndefined() // does not create an object for chain - expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.ArbitrumOne]['0'].typeInfo).toEqual(expectedTypeInfo) - expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.Optimism]['0'].typeInfo).toEqual(expectedTypeInfo) - expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.Optimism]['1'].typeInfo).toEqual(expectedTypeInfo) - expect(v30.transactions['0xdeleteMe']).toBe(undefined) - // expect non-for txDetails to not change - expect(v30.transactions[account.address][UniverseChainId.Mainnet]['1']).toEqual(txDetailsConfirmed) - expect(v30.transactions[account.address][UniverseChainId.Base]['1']).toEqual(txDetailsConfirmed) - expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.ArbitrumOne]['1']).toEqual(txDetailsConfirmed) - expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.Optimism]['2']).toEqual(txDetailsConfirmed) + testMigrateFiatPurchaseTransactionInfo(migrations[30], v29Schema, account, txDetailsConfirmed) }) it('migrates from v31 to 32', () => { - const v31Stub = { ...v31Schema, ENS: 'defined' } - - const v32 = migrations[32](v31Stub) - - expect(v32.ENS).toBe(undefined) + testResetEnsApi(migrations[32], v31Schema) }) it('migrates from v32 to 33', () => { - const v32Stub = { ...v32Schema } - - const v33 = migrations[33](v32Stub) - - expect(v33.wallet.replaceAccountOptions.isReplacingAccount).toBe(false) - expect(v33.wallet.replaceAccountOptions.skipToSeedPhrase).toBe(false) + testAddReplaceAccountOptions(migrations[33], v32Schema) }) it('migrates from v33 to 34', () => { - const v33Stub = { ...v33Schema } - - const v34 = migrations[34](v33Stub) - - expect(v34.telemetry.lastBalancesReport).toBe(0) + testAddLastBalancesReport(migrations[34], v33Schema) }) it('migrates from v34 to 35', () => { - const v34Stub = { ...v34Schema } - - const v35 = migrations[35](v34Stub) - - expect(v35.appearanceSettings.selectedAppearanceSettings).toBe('system') + testAddAppearanceSetting(migrations[35], v34Schema) }) it('migrates from v35 to 36', () => { - const v35Stub = { ...v35Schema } - - const v36 = migrations[36](v35Stub) - - expect(v36.favorites.hiddenNfts).toEqual({}) + testAddHiddenNfts(migrations[36], v35Schema) }) it('migrates from v36 to 37', () => { - const id1 = '123' - const id2 = '456' - const id3 = '789' - const transactions = { - [account.address]: { - [UniverseChainId.Mainnet]: { - [id1]: { - ...fiatOnRampTxDetailsFailed, - typeInfo: { - ...fiatOnRampTxDetailsFailed.typeInfo, - id: undefined, - }, - }, - [id2]: { - ...fiatOnRampTxDetailsFailed, - typeInfo: { - ...fiatOnRampTxDetailsFailed.typeInfo, - id: undefined, - explorerUrl: undefined, - }, - }, - [id3]: txDetailsConfirmed, - }, - }, - } - - const v36Stub = { ...v36Schema, transactions } - - expect(v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id1].typeInfo.id).toBeUndefined() - expect(v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id2].typeInfo.id).toBeUndefined() - - const v37 = migrations[37](v36Stub) - - expect(v37.transactions[account.address]?.[UniverseChainId.Mainnet][id1].typeInfo.id).toEqual( - fiatOnRampTxDetailsFailed.typeInfo.id, - ) - expect(v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id2].typeInfo.id).toBeUndefined() - expect(v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id3]).toEqual(txDetailsConfirmed) + testCorrectFailedFiatOnRampTxIds(migrations[37], v36Schema, account, fiatOnRampTxDetailsFailed, txDetailsConfirmed) }) it('migrates from v37 to 38', () => { - const v37Stub = { ...v37Schema } - const v38 = migrations[38](v37Stub) - expect(v38.wallet.replaceAccountOptions).toBeUndefined() + testRemoveReplaceAccountOptions(migrations[38], v37Schema) }) it('migrates from v38 to 39', () => { - const v38Stub = { ...v38Schema } - expect(v38Stub.experiments).toBeDefined() - const v39 = migrations[39](v38Stub) - expect(v39.experiments).toBeUndefined() + testRemoveExperimentsSlice(migrations[39], v38Schema) }) it('migrates from v39 to 40', () => { - const v39Stub = { ...v39Schema } - - const v40 = migrations[40](v39Stub) - - // walletConnect slice still exists but should not be persisted - expect(v40.walletConnect).toBeUndefined() + testRemovePersistedWalletConnectSlice(migrations[40], v39Schema) }) it('migrates from v40 to 41', () => { - const v40Stub = { ...v40Schema } - - const v41 = migrations[41](v40Stub) - - expect(v41.telemetry.lastBalancesReportValue).toBe(0) + testAddLastBalancesReportValue(migrations[41], v40Schema) }) it('migrates from v41 to 42', () => { - const v41Stub = { ...v41Schema } - - const v42 = migrations[42](v41Stub) - - expect(v42.wallet.flashbotsenabled).toBeUndefined() + testRemoveFlashbotsEnabledFromWalletSlice(migrations[42], v41Schema) }) it('migrates from v42 to 43', () => { - const v42Stub = { ...v42Schema } - - v42Stub.favorites.hiddenNfts = { - '0xAFa9bAb987E3D7bcD40EB510838aEC663C8b7264': { - 'nftItem.0xb96e881BD4Cd7BCCc8CB47d3aa0e254a72d2F074.3971': true, // checksummed 1 - 'nftItem.0xb96e881bd4cd7bccc8cb47d3aa0e254a72d2f074.3971': true, // not checksummed 1 - 'nftItem.0x25E503331e69EFCBbc50d2a4D661900B23D47662.2': true, // checksummed 2 - 'nftItem.0xe94abea3932576ff957a0b92190d0191aeb1a782.2': true, // not checksummed 3 - }, - } - - const v43 = migrations[43](v42Stub) - - // expect(v43.favorites.hiddenNfts).toEqual(undefined) - // all checksummed keys should be converted to not checksummed ones and duplicates should be removed - expect(v43.favorites.nftsData).toEqual({ - '0xAFa9bAb987E3D7bcD40EB510838aEC663C8b7264': { - 'nftItem.0xb96e881bd4cd7bccc8cb47d3aa0e254a72d2f074.3971': { isHidden: true }, // not checksummed 1 - 'nftItem.0x25e503331e69efcbbc50d2a4d661900b23d47662.2': { isHidden: true }, // not checksummed 2 - 'nftItem.0xe94abea3932576ff957a0b92190d0191aeb1a782.2': { isHidden: true }, // not checksummed 3 - }, - }) + testConvertHiddenNftsToNftsData(migrations[43], v42Schema) }) it('migrates from v43 to v44', () => { - const v43Stub = { ...v43Schema } - - v43Stub.providers = { isInitialized: true } - - const v44 = migrations[44](v43Stub) - - expect(v44.providers).toBeUndefined() + testRemoveProviders(migrations[44], v43Schema) }) it('migrates from v44 to 45', () => { - const v44Stub = { ...v44Schema } - - const v45 = migrations[45](v44Stub) - - expect(v45.favorites.tokensVisibility).toEqual({}) + testAddTokensVisibility(migrations[45], v44Schema) }) it('migrates from v45 to 46', () => { - const v45Stub = { ...v45Schema } - const v46 = migrations[46](v45Stub) - - expect(v46.ENS).toBeUndefined() - expect(v46.ens).toBeUndefined() - expect(v46.gasApi).toBeUndefined() - expect(v46.onChainBalanceApi).toBeUndefined() - expect(v46.routingApi).toBeUndefined() - expect(v46.trmApi).toBeUndefined() + testDeleteRTKQuerySlices(migrations[46], v45Schema) }) it('migrates from v46 to 47', () => { - const v46Stub = { ...v46Schema } - const v47 = migrations[47](v46Stub) - - expect(v47.chains.byChainId).toStrictEqual({ - '1': { isActive: true }, - '10': { isActive: true }, - '56': { isActive: true }, - '137': { isActive: true }, - '8453': { isActive: true }, - '42161': { isActive: true }, - }) + testResetActiveChains(migrations[47], v46Schema) }) it('migrates from v47 to 48', () => { - const v47Stub = { ...v47Schema } - const v48 = migrations[48](v47Stub) - - expect(v48.tweaks).toEqual({}) + testAddTweaksStartingState(migrations[48], v47Schema) }) it('migrates from v48 to 49', () => { - const v48Stub = { ...v48Schema } - const v49 = migrations[49](v48Stub) - - expect(v49.wallet.settings.swapProtection).toEqual(SwapProtectionSetting.On) + testAddSwapProtectionSetting(migrations[49], v48Schema) }) it('migrates from v49 to 50', () => { - const v449Stub = { ...v49Schema } - const v50 = migrations[50](v449Stub) - - expect(v50.chains).toBeUndefined() + testDeleteChainsSlice(migrations[50], v49Schema) }) it('migrates from v50 to 51', () => { - const v50Stub = { ...v50Schema } - const v51 = migrations[51](v50Stub) - - expect(v51.languageSettings).not.toBeUndefined() + testAddLanguageSettings(migrations[51], v50Schema) }) it('migrates from v51 to 52', () => { - const v51Stub = { ...v51Schema } - const v52 = migrations[52](v51Stub) - - expect(v52.fiatCurrencySettings).not.toBeUndefined() + testAddFiatCurrencySettings(migrations[52], v51Schema) }) it('migrates from v52 to 53', () => { - const v52Stub = { ...v52Schema } - const v53 = migrations[53](v52Stub) - - expect(v53.languageSettings).not.toBeUndefined() + testUpdateLanguageSettings(migrations[53], v52Schema) }) it('migrates from v53 to 54', () => { - const v53Stub = { ...v53Schema } - const v54 = migrations[54](v53Stub) - - expect(v54.telemetry.walletIsFunded).toBe(false) + testAddWalletIsFunded(migrations[54], v53Schema) }) it('migrates from v54 to 55', () => { - const v54Stub = { ...v54Schema } - const v55 = migrations[55](v54Stub) - - expect(v55.behaviorHistory.hasViewedReviewScreen).toBe(false) + testAddBehaviorHistory(migrations[55], v54Schema) }) it('migrates from v55 to 56', () => { - const v55Stub = { ...v55Schema } - const v56 = migrations[56](v55Stub) - - expect(v56.telemetry.allowAnalytics).toBe(true) - expect(v56.telemetry.lastHeartbeat).toBe(0) + testAddAllowAnalyticsSwitch(migrations[56], v55Schema) }) it('migrates from v56 to 57', () => { - const v56Stub = { - ...v56Schema, - wallet: { - ...v56Schema.wallet, - accounts: [ - { - type: AccountType.Readonly, - address: '0x', - name: 'Test Account 1', - pending: false, - hideSpamTokens: true, - }, - ], - }, - } - const v57 = migrations[57](v56Stub) - expect(v57.wallet.settings.hideSmallBalances).toBe(true) - expect(v57.wallet.settings.hideSpamTokens).toBe(true) - expect(v57.wallet.accounts[0].showSpamTokens).toBeUndefined() - expect(v57.wallet.accounts[0].showSmallBalances).toBeUndefined() + testMoveSettingStateToGlobal(migrations[57], v56Schema) }) it('migrates from v57 to 58', () => { - const v57Stub = { ...v57Schema } - const v58 = migrations[58](v57Stub) - - expect(v58.behaviorHistory.hasSkippedUnitagPrompt).toBe(false) + testAddSkippedUnitagBoolean(migrations[58], v57Schema) }) it('migrates from v58 to 59', () => { - const v58Stub = { ...v58Schema } - const v59 = migrations[59](v58Stub) - - expect(v59.behaviorHistory.hasCompletedUnitagsIntroModal).toBe(false) + testAddCompletedUnitagsIntroBoolean(migrations[59], v58Schema) }) it('migrates from v59 to 60', () => { - const v59Stub = { ...v59Schema } - const v60 = migrations[60](v59Stub) - - expect(v60.behaviorHistory.hasViewedUniconV2IntroModal).toBe(false) + testAddUniconV2IntroModalBoolean(migrations[60], v59Schema) }) it('migrates from v60 to 61', () => { - const v60Stub = { ...v60Schema } - const address1 = '0x123' - const address2 = '0x456' - const nftKey1 = '0xNFTKey1' - const nftKey2 = '0xNFTKey2' - const nftKey3 = '0xNFTKey3' - const nftKey4 = '0xNFTKey4' - - const currency1ToVisibility = { '0xCurrency1': { isVisible: true } } - const currency2ToVisibility = { '0xCurrency2': { isVisible: false } } - const currency3ToVisibility = { '0xCurrency3': { isVisible: false } } - const nft1ToVisibility = { [nftKey1]: { isSpamIgnored: true } } - const nft2ToVisibility = { [nftKey2]: { isHidden: true } } - const nft3ToVisibility = { [nftKey3]: { isSpamIgnored: false, isHidden: false } } - const nft4ToVisibility = { [nftKey4]: { isSpamIgnored: false, isHidden: true } } - - v60Stub.favorites = { - ...v60Stub.favorites, - tokensVisibility: { - [address1]: { ...currency1ToVisibility, ...currency2ToVisibility }, - [address2]: { ...currency2ToVisibility, ...currency3ToVisibility }, - }, - nftsData: { - [address1]: { ...nft1ToVisibility, ...nft2ToVisibility, ...nft3ToVisibility }, - [address2]: { ...nft3ToVisibility, ...nft4ToVisibility }, - }, - } - - const v61 = migrations[61](v60Stub) - - expect(v61.favorites.nftsData).toBeUndefined() - expect(v61.favorites.tokensVisibility).toMatchObject({ - ...currency1ToVisibility, - ...currency2ToVisibility, - ...currency3ToVisibility, - }) - expect(v61.favorites.nftsVisibility).toMatchObject({ - [nftKey1]: { isVisible: true }, - [nftKey2]: { isVisible: false }, - [nftKey3]: { isVisible: true }, - [nftKey4]: { isVisible: false }, - }) + testFlattenTokenVisibility(migrations[61], v60Schema) }) it('migrates from v61 to 62', () => { - const v61Stub = { ...v61Schema } - const v62 = migrations[62](v61Stub) - - // Removed in schema 69 - expect(v62.behaviorHistory.extensionOnboardingState).toBe('Undefined') + testAddExtensionOnboardingState(migrations[62], v61Schema) }) it('migrates from v62 to 63', () => { - const v62Stub = { ...v62Schema } - const v63 = migrations[63](v62Stub) - - expect(v63.wallet.isUnlocked).toBe(undefined) + testRemoveWalletIsUnlockedState(migrations[63], v62Schema) }) it('migrates from v63 to 64', () => { - const v63Stub = { ...v63Schema } - const v64 = migrations[64](v63Stub) - - expect(v64.behaviorHistory.hasViewedUniconV2IntroModal).toBe(undefined) + testRemoveUniconV2BehaviorState(migrations[64], v63Schema) }) it('migrates from v64 to 65', () => { - const TEST_ADDRESS = '0xTestAddress' - const txDetails0 = { - chainId: UniverseChainId.Mainnet, - id: '0', - from: '0xTestAddress', - options: { - request: { - from: '0x123', - to: '0x456', - value: '0x0', - data: '0x789', - nonce: 10, - gasPrice: BigNumber.from('10000'), - }, - }, - typeInfo: { - type: TransactionType.Approve, - tokenAddress: '0xtokenAddress', - spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', - }, - status: TransactionStatus.Pending, - addedTime: 1487076708000, - hash: '0x123', - } - - const txDetails1 = { - ...txDetails0, - chainId: UniverseChainId.Optimism, - id: '1', - } - - const transactions = { - [TEST_ADDRESS]: { - [UniverseChainId.Mainnet]: { - '0': txDetails0, - }, - [UniverseChainId.Optimism]: { - '1': txDetails1, - }, - }, - } - - const v64Stub = { ...v64Schema, transactions } - - const v65 = migrations[65](v64Stub) - - expect(v65.transactions[TEST_ADDRESS][UniverseChainId.Mainnet]['0'].routing).toBe('CLASSIC') - expect(v65.transactions[TEST_ADDRESS][UniverseChainId.Optimism]['1'].routing).toBe('CLASSIC') + testAddRoutingFieldToTransactions(migrations[65], v64Schema) }) it('migrates from v65 to v66', () => { const v66 = migrations[66] @@ -1484,31 +627,19 @@ describe('Redux state migrations', () => { }) it('migrates from v66 to v67', () => { - const v66Stub = { ...v66Schema } - const v67 = migrations[67](v66Stub) - - // Removed in migration 69 - expect(v67.behaviorHistory.extensionOnboardingState).toBe('Undefined') + testResetOnboardingStateForGA(migrations[67], v66Schema) }) it('migrates from v67 to v68', () => { - const v67Stub = { ...v67Schema } - const v68 = migrations[68](v67Stub) - - expect(v68.behaviorHistory.extensionBetaFeedbackState).toBe(undefined) + testDeleteBetaOnboardingState(migrations[68], v67Schema) }) it('migrates from v68 to v69', async () => { - const v68Stub = { ...v68Schema } - const v69 = await migrations[69](v68Stub) - expect(v69.behaviorHistory.extensionBetaFeedbackState).toBe(undefined) + testDeleteExtensionOnboardingState(migrations[69], v68Schema) }) it('migrates from v69 to v70', async () => { - const v69Stub = { ...v69Schema } - v69Stub.favorites.watchedAddresses = [HAYDEN_ETH_ADDRESS] as never - const v70 = await migrations[70](v69Stub) - expect(v70.favorites.watchedAddresses).toEqual([]) + testDeleteDefaultFavoritesFromFavoritesState(migrations[70], v69Schema, HAYDEN_ETH_ADDRESS) }) it('migrates from v70 to v71', async () => { @@ -1516,11 +647,7 @@ describe('Redux state migrations', () => { }) it('migrates from v71 to v72', () => { - const v71Stub = { ...v71Schema } - const v72 = migrations[72](v71Stub) - - expect(v72.behaviorHistory.hasViewedWelcomeWalletCard).toBe(false) - expect(v72.behaviorHistory.hasUsedExplore).toBe(false) + testAddExploreAndWelcomeBehaviorHistory(migrations[72], v71Schema) }) it('migrates from v72 to v73', async () => { @@ -1528,66 +655,7 @@ describe('Redux state migrations', () => { }) it('migrates from v73 to v74', () => { - const oldFiatOnRampTxDetails = { - chainId: UniverseChainId.Mainnet, - id: '0', - from: account.address, - options: { - request: {}, - }, - typeInfo: { - type: TransactionType.FiatPurchaseDeprecated, - explorerUrl: 'explorer', - inputCurrencyAmount: 25, - outputSymbol: 'USDC', - }, - status: TransactionStatus.Pending, - addedTime: 1487076708000, - hash: '0x123', - } - const transactions = { - [account.address]: { - [UniverseChainId.Mainnet]: { - '0': oldFiatOnRampTxDetails, - '1': txDetailsConfirmed, - }, - [UniverseChainId.Optimism]: { - '0': oldFiatOnRampTxDetails, - '1': { - ...oldFiatOnRampTxDetails, - typeInfo: { - ...oldFiatOnRampTxDetails.typeInfo, - type: TransactionType.Send, - }, - }, - '2': { - ...oldFiatOnRampTxDetails, - typeInfo: { - ...oldFiatOnRampTxDetails.typeInfo, - type: TransactionType.Receive, - }, - }, - '3': txDetailsConfirmed, - }, - }, - } - const v73Stub = { ...v73Schema, transactions } - - const v74 = migrations[74](v73Stub) - - expect(v74.transactions[account.address][UniverseChainId.Mainnet]['0']).toBe(undefined) - expect(v74.transactions[account.address][UniverseChainId.Mainnet]['1']).toEqual(txDetailsConfirmed) - - expect(v74.transactions[account.address][UniverseChainId.Optimism]['0']).toBe(undefined) - expect(v74.transactions[account.address][UniverseChainId.Optimism]['1'].typeInfo).toEqual({ - ...oldFiatOnRampTxDetails.typeInfo, - type: TransactionType.Send, - }) - expect(v74.transactions[account.address][UniverseChainId.Optimism]['2'].typeInfo).toEqual({ - ...oldFiatOnRampTxDetails.typeInfo, - type: TransactionType.Receive, - }) - expect(v74.transactions[account.address][UniverseChainId.Optimism]['3']).toEqual(txDetailsConfirmed) + testDeleteOldOnRampTxData(migrations[74], v73Schema, account, txDetailsConfirmed) }) it('migrates from v74 to v75', () => { @@ -1623,12 +691,7 @@ describe('Redux state migrations', () => { }) it('migrates from v82 to v83', () => { - // v82 didn't have a new schema - const v82Stub = { ...v82Schema } - const v83 = migrations[83](v82Stub) - - expect(v83.pushNotifications.generalUpdatesEnabled).toBe(false) - expect(v83.pushNotifications.priceAlertsEnabled).toBe(false) + testAddPushNotifications(migrations[83], v82Schema) }) it('migrates from v83 to v84', () => { @@ -1644,73 +707,7 @@ describe('Redux state migrations', () => { }) it('migrates from v86 to v87', () => { - /** test migration on uwulink transaction */ - const stateWithUwulinkTransaction = { - transactions: { - testAddress: { - testChainId: { - testTxnId: { - typeInfo: { - dappRequestInfo: { - name: 'testDapp', - }, - externalDappInfo: { - source: 'uwulink', - }, - }, - }, - }, - }, - }, - } - - const v86Stub = { ...v86Schema, ...stateWithUwulinkTransaction } - const v87 = migrations[87](v86Stub) - - expect(v87.transactions).toBeDefined() - expect(v87.transactions.testAddress.testChainId.testTxnId.typeInfo).toEqual({ - dappRequestInfo: { - name: 'testDapp', - }, - externalDappInfo: { - requestType: DappRequestType.UwULink, - }, - }) - - /** test migration on walletconnect transaction */ - const stateWithWalletConnectTransaction = { - transactions: { - testAddress: { - testChainId: { - testTxnId: { - typeInfo: { - type: TransactionType.WCConfirm, - dapp: { - name: 'testDapp', - }, - externalDappInfo: { - source: 'walletconnect', - }, - }, - }, - }, - }, - }, - } - - const v86StubWalletConnect = { ...v86Schema, ...stateWithWalletConnectTransaction } - const v87WalletConnect = migrations[87](v86StubWalletConnect) - - expect(v87WalletConnect.transactions).toBeDefined() - expect(v87WalletConnect.transactions.testAddress.testChainId.testTxnId.typeInfo).toEqual({ - type: TransactionType.WCConfirm, - dappRequestInfo: { - name: 'testDapp', - }, - externalDappInfo: { - requestType: DappRequestType.WalletConnectSessionRequest, - }, - }) + testMigrateDappRequestInfoTypes(migrations[87], v86Schema) }) it('migrates from v87 to v88', () => { @@ -1734,54 +731,34 @@ describe('Redux state migrations', () => { }) it('migrates from v91 to v92', () => { - const androidCloudBackupEmail = 'test@test.com' - - const { cloudBackup: _oldCloudBackup, ...v91WithoutCloudBackup } = v91Schema + testMigrateAndRemoveCloudBackupSlice(migrations[92], v91Schema) + }) - const v91WithoutCloudBackupSlice = { - ...v91WithoutCloudBackup, - wallet: { - ...v91Schema.wallet, - activeAccountAddress: '0xabc', - }, - } + it('migrates from v92 to v93', () => { + testMigrateSearchHistory(migrations[93], v92Schema) + }) - const v91WithCloudBackup = { - ...v91WithoutCloudBackupSlice, - cloudBackup: { - backupsFound: [{ mnemonicId: '0xabc', email: androidCloudBackupEmail }], - }, - wallet: { - ...v91Schema.wallet, - activeAccountAddress: '0xabc', - }, - } + it('migrates from v93 to v95', () => { + testAddActivityVisibility(migrations[95], v93Schema) + }) - const v91WithDifferentActiveAccountAddress = { - ...v91WithoutCloudBackupSlice, - cloudBackup: { - backupsFound: [{ mnemonicId: '0xdef', email: androidCloudBackupEmail }], - }, - wallet: { - ...v91Schema.wallet, - activeAccountAddress: '0xabc', + it('migrates from v95 to v96', () => { + testMigrateDismissedTokenWarnings(migrations[96], { + ...v95Schema, + tokens: { + dismissedTokenWarnings: { + [UniverseChainId.Mainnet]: { + [USDC.address]: { + chainId: UniverseChainId.Mainnet, + address: USDC.address, + }, + }, + }, }, - } - - const v92 = migrations[92](v91WithCloudBackup) - expect(v92.cloudBackup).toBeUndefined() - expect(v92.wallet.androidCloudBackupEmail).toBe(androidCloudBackupEmail) - - const v92WithoutCloudBackup = migrations[92](v91WithoutCloudBackupSlice) - expect(v92WithoutCloudBackup.cloudBackup).toBeUndefined() - expect(v92WithoutCloudBackup.wallet.androidCloudBackupEmail).toBe(undefined) - - const v92WithDifferentActiveAccountAddress = migrations[92](v91WithDifferentActiveAccountAddress) - expect(v92WithDifferentActiveAccountAddress.cloudBackup).toBeUndefined() - expect(v92WithDifferentActiveAccountAddress.wallet.androidCloudBackupEmail).toBe(androidCloudBackupEmail) + }) }) - it('migrates from v92 to v93', () => { - testMigrateSearchHistory(migrations[93], v92Schema) + it('migrates from v96 to v97', () => { + testSetWalletDeviceLanguage(migrations[97], v96Schema, jest.mocked(getWalletDeviceLanguage)) }) }) diff --git a/apps/mobile/src/app/migrations.ts b/apps/mobile/src/app/migrations.ts index 779ea388446..9d64885c695 100644 --- a/apps/mobile/src/app/migrations.ts +++ b/apps/mobile/src/app/migrations.ts @@ -1,33 +1,85 @@ -// Type information currently gets lost after a migration -// biome-ignore-all lint/suspicious/noExplicitAny: Migration logic requires flexible typing -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -/* eslint-disable max-lines */ - -import dayjs from 'dayjs' -import { AccountType } from 'uniswap/src/features/accounts/types' -import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { toSupportedChainId } from 'uniswap/src/features/chains/utils' -import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' -import { Language } from 'uniswap/src/features/language/constants' -import { getNFTAssetKey } from 'uniswap/src/features/nfts/utils' -import { ModalName } from 'uniswap/src/features/telemetry/constants' -import { TransactionsState } from 'uniswap/src/features/transactions/slice' import { - ChainIdToTxIdToDetails, - TransactionStatus, - TransactionType, -} from 'uniswap/src/features/transactions/types/transactionDetails' + addAllowAnalyticsSwitch, + addAppearanceSetting, + addBehaviorHistory, + addBiometricSettings, + addCloudBackup, + addCompletedUnitagsIntroBoolean, + addEnsState, + addExperimentsSlice, + addExtensionOnboardingState, + addFiatCurrencySettings, + addHiddenNfts, + addLanguageSettings, + addLastBalancesReport, + addLastBalancesReportValue, + addModalsState, + addPasswordLockout, + addPushNotifications, + addPushNotificationsEnabledToAccounts, + addReplaceAccountOptions, + addSearchHistory, + addSkippedUnitagBoolean, + addSwapProtectionSetting, + addTimeImportedAndDerivationIndex, + addTokensVisibility, + addTweaksStartingState, + addUniconV2IntroModalBoolean, + addWalletConnectPendingSessionAndSettings, + addWalletIsFunded, + changeNativeTypeToSignerMnemonic, + convertHiddenNftsToNftsData, + correctFailedFiatOnRampTxIds, + deleteChainsSlice, + deleteOldOnRampTxData, + deleteRTKQuerySlices, + emptyMigration, + filterToSupportedChains, + flattenTokenVisibility, + migrateAndRemoveCloudBackupSlice, + migrateBiometricSettings, + migrateDappRequestInfoTypes, + migrateFiatPurchaseTransactionInfo, + moveSettingStateToGlobal, + removeCoingeckoApiAndTokenLists, + removeDataApi, + removeDemoAccount, + removeEnsState, + removeExperimentsSlice, + removeFlashbotsEnabledFromWalletSlice, + removeLocalTypeAccounts, + removeNonZeroDerivationIndexAccounts, + removePersistedWalletConnectSlice, + removeProviders, + removeReplaceAccountOptions, + removeShowSmallBalances, + removeTokenListsAndCustomTokens, + removeTokensMetadataDisplayType, + removeWalletConnectModalState, + renameFollowedAddressesToWatchedAddresses, + resetActiveChains, + resetEnsApi, + resetLastTxNotificationUpdate, + resetOnboardingStateForGA, + resetPushNotificationsEnabled, + resetTokensOrderBy, + resetTokensOrderByAndMetadataDisplayType, + restructureTransactionsAndNotifications, + setWalletDeviceLanguage, + transformNotificationCountToStatus, + updateLanguageSettings, +} from 'src/app/mobileMigrations' import { + addActivityVisibility, addDismissedBridgedAndCompatibleWarnings, + migrateDismissedTokenWarnings, migrateSearchHistory, removeThaiBahtFromFiatCurrency, unchecksumDismissedTokenWarningKeys, } from 'uniswap/src/state/uniswapMigrations' -import { DappRequestType } from 'uniswap/src/types/walletConnect' -import { Account } from 'wallet/src/features/wallet/accounts/types' -import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' import { activatePendingAccounts, + addBatchedTransactions, addCreatedOnboardingRedesignAccountBehaviorHistory, addExploreAndWelcomeBehaviorHistory, addHapticSetting, @@ -51,1039 +103,105 @@ import { updateExploreOrderByType, } from 'wallet/src/state/walletMigrations' -export const OLD_DEMO_ACCOUNT_ADDRESS = '0xdd0E380579dF30E38524F9477808d9eE37E2dEa6' - export const migrations = { - 0: (state: any) => { - const oldTransactionState = state?.transactions - const newTransactionState: any = {} - - const chainIds = Object.keys(oldTransactionState?.byChainId ?? {}) - for (const chainId of chainIds) { - const transactions = oldTransactionState.byChainId?.[chainId] ?? [] - const txIds = Object.keys(transactions) - for (const txId of txIds) { - const txDetails = transactions[txId] - const address = txDetails.from - newTransactionState[address] ??= {} - newTransactionState[address][chainId] ??= {} - newTransactionState[address][chainId][txId] = { ...txDetails } - } - } - - const oldNotificationState = state.notifications - const newNotificationState = { ...oldNotificationState, lastTxNotificationUpdate: {} } - const addresses = Object.keys(oldTransactionState?.lastTxHistoryUpdate || []) - for (const address of addresses) { - newNotificationState.lastTxNotificationUpdate[address] = { - [UniverseChainId.Mainnet]: oldTransactionState.lastTxHistoryUpdate[address], - } - } - - return { ...state, transactions: newTransactionState, notifications: newNotificationState } - }, - - 1: (state: any) => { - const newState = { ...state } - delete newState.walletConnect?.modalState - return newState - }, - - 2: (state: any) => { - const newState = { ...state } - const oldFollowingAddresses = state?.favorites?.followedAddresses - if (oldFollowingAddresses) { - newState.favorites.watchedAddresses = oldFollowingAddresses - } - delete newState?.favorites?.followedAddresses - return newState - }, - - 3: (state: any) => { - const newState = { ...state } - newState.searchHistory = { results: [] } - return newState - }, - - 4: (state: any) => { - const newState = { ...state } - const accounts = newState?.wallet?.accounts ?? {} - let derivationIndex = 0 - for (const account of Object.keys(accounts)) { - newState.wallet.accounts[account].timeImportedMs = dayjs().valueOf() - if (newState.wallet.accounts[account].type === 'native') { - newState.wallet.accounts[account].derivationIndex = derivationIndex - derivationIndex += 1 - } - } - return newState - }, - - 5: (state: any) => { - const newState = { ...state } - newState.modals = { - [ModalName.WalletConnectScan]: { - isOpen: false, - initialState: 0, - }, - [ModalName.Swap]: { - isOpen: false, - initialState: undefined, - }, - [ModalName.Send]: { - isOpen: false, - initialState: undefined, - }, - } - - delete newState?.balances - return newState - }, - - 6: (state: any) => { - const newState = { ...state } - newState.walletConnect = { ...newState.walletConnect, pendingSession: null } - newState.wallet = { ...newState.wallet, settings: {} } - - delete newState?.wallet?.bluetooth - return newState - }, - - 7: (state: any) => { - const newState = { ...state } - const accounts = newState?.wallet?.accounts ?? {} - const originalAccountValues = Object.keys(accounts) - for (const account of originalAccountValues) { - if (accounts[account].type === 'native' && accounts[account].derivationIndex !== 0) { - delete accounts[account] - } else if (accounts[account].type === 'native' && accounts[account].derivationIndex === 0) { - accounts[account].mnemonicId = accounts[account].address - } - } - return newState - }, - - 8: (state: any) => { - const newState = { ...state } - newState.cloudBackup = { backupsFound: [] } - return newState - }, - - 9: (state: any) => { - const newState = { ...state } - const accounts = newState?.wallet?.accounts ?? {} - for (const account of Object.keys(accounts)) { - if (newState.wallet.accounts[account].type === 'local') { - delete newState.wallet.accounts[account] - } - } - return newState - }, - - 10: (state: any) => { - const newState = { ...state } - const accounts = newState?.wallet?.accounts ?? {} - - if (accounts[OLD_DEMO_ACCOUNT_ADDRESS]) { - delete accounts[OLD_DEMO_ACCOUNT_ADDRESS] - } - - return newState - }, - - 11: (state: any) => { - const newState = { ...state } - newState.biometricSettings = { - requiredForAppAccess: false, - requiredForTransactions: false, - } - - return newState - }, - - 12: (state: any) => { - const accounts: Record | undefined = state?.wallet?.accounts - const newAccounts = Object.values(accounts ?? {}).map((account: Account) => { - const newAccount = { ...account } - newAccount.pushNotificationsEnabled = false - return newAccount - }) - - const newAccountObj = newAccounts.reduce>((accountObj, account) => { - accountObj[account.address] = account - return accountObj - }, {}) - - const newState = { ...state } - newState.wallet = { ...state.wallet, accounts: newAccountObj } - return newState - }, - - 13: (state: any) => { - const newState = { ...state } - newState.ens = { ensForAddress: {} } - return newState - }, - - 14: (state: any) => { - const newState = { ...state } - newState.biometricSettings = { - requiredForAppAccess: state.wallet.isBiometricAuthEnabled, - requiredForTransactions: state.wallet.isBiometricAuthEnabled, - } - delete newState.wallet?.isBiometricAuthEnabled - return newState - }, - - 15: (state: any) => { - const newState = { ...state } - const accounts = newState?.wallet?.accounts ?? {} - for (const account of Object.keys(accounts)) { - if (newState.wallet.accounts[account].type === 'native') { - newState.wallet.accounts[account].type = AccountType.SignerMnemonic - } - } - return newState - }, - - 16: (state: any) => { - const newState = { ...state } - delete newState.dataApi - return newState - }, - - 17: (state: any) => { - const accounts: Record | undefined = state?.wallet?.accounts - if (!accounts) { - return undefined - } - - for (const account of Object.values(accounts)) { - account.pushNotificationsEnabled = false - } - - const newState = { ...state } - newState.wallet = { ...state.wallet, accounts } - return newState - }, - - 18: (state: any) => { - const newState = { ...state } - delete newState.ens - return newState - }, - - 19: (state: any) => { - const newState = { ...state } - - const chainState: - | { - byChainId: Partial> - } - | undefined = newState?.chains - const newChainState = Object.keys(chainState?.byChainId ?? {}).reduce<{ - byChainId: Partial> - }>( - (tempState, chainIdString) => { - const chainId = toSupportedChainId(chainIdString) - if (!chainId) { - return tempState - } - - const chainInfo = chainState?.byChainId[chainId] - if (!chainInfo) { - return tempState - } - - tempState.byChainId[chainId] = chainInfo - return tempState - }, - { byChainId: {} }, - ) - - const blockState: any | undefined = newState?.blocks - const newBlockState = Object.keys(blockState?.byChainId ?? {}).reduce( - (tempState, chainIdString) => { - const chainId = toSupportedChainId(chainIdString) - if (!chainId) { - return tempState - } - - const blockInfo = blockState?.byChainId[chainId] - if (!blockInfo) { - return tempState - } - - tempState.byChainId[chainId] = blockInfo - return tempState - }, - { byChainId: {} }, - ) - - const transactionState: TransactionsState | undefined = newState?.transactions - const newTransactionState = Object.keys(transactionState ?? {}).reduce((tempState, address) => { - const txs = transactionState?.[address] - if (!txs) { - return tempState - } - - const newAddressTxState = Object.keys(txs).reduce((tempAddressState, chainIdString) => { - const chainId = toSupportedChainId(chainIdString) - if (!chainId) { - return tempAddressState - } - - const txInfo = txs[chainId] - if (!txInfo) { - return tempAddressState - } - - tempAddressState[chainId] = txInfo - return tempAddressState - }, {}) - - tempState[address] = newAddressTxState - return tempState - }, {}) - - return { - ...newState, - chains: newChainState, - blocks: newBlockState, - transactions: newTransactionState, - } - }, - - 20: (state: any) => { - const newState = { ...state } - newState.notifications = { ...state?.notifications, lastTxNotificationUpdate: {} } - return newState - }, - - 21: (state: any) => { - const newState = { ...state } - // newState.experiments = { experiments: {}, featureFlags: {} } - return { - ...newState, - experiments: { experiments: {}, featureFlags: {} }, - } - }, - - 22: (state: any) => { - const newState = { ...state } - delete newState.coingeckoApi - delete newState.tokens?.watchedTokens - delete newState.tokens?.tokenPairs - return newState - }, - - 23: (state: any) => { - const newState = { ...state } - // Reset values because of changed types for these two optional variables - delete newState.wallet.settings?.tokensOrderBy - delete newState.wallet.settings?.tokensMetadataDisplayType - return newState - }, - - 24: (state: any) => { - const newState = { ...state } - const notificationCount = state.notifications?.notificationCount - const notificationStatus = Object.keys(notificationCount ?? {}).reduce((obj, address) => { - const count = notificationCount[address] - if (count) { - return { ...obj, [address]: true } - } - - return { ...obj, [address]: false } - }, {}) - - delete newState.notifications?.notificationCount - newState.notifications = { ...newState.notifications, notificationStatus } - return newState - }, - - 25: (state: any) => { - return { - ...state, - passwordLockout: { passwordAttempts: 0 }, - } - }, - - 26: (state: any) => { - const newState = { ...state } - delete newState.wallet.settings.showSmallBalances - return newState - }, - - 27: (state: any) => { - const newState = { ...state } - // Reset tokensOrder by because of updated types of TokensOrderBy - delete newState.wallet.settings.tokensOrderBy - return newState - }, - - 28: (state: any) => { - const newState = { ...state } - // Removed storing tokensMetadataDisplayType - delete newState.wallet.settings.tokensMetadataDisplayType - return newState - }, - - 29: (state: any) => { - const newState = { ...state } - delete newState.tokenLists - delete newState.tokens?.customTokens - return newState - }, - - // Fiat onramp tx typeInfo schema changed - // Updates every fiat onramp tx in store to new schema - // leaves non-for txs untouched - 30: function MigrateFiatPurchaseTransactionInfo(state: any) { - const newState = { ...state } - - const oldTransactionState = state?.transactions - const newTransactionState: any = {} - - const addresses = Object.keys(oldTransactionState ?? {}) - for (const address of addresses) { - const chainIds = Object.keys(oldTransactionState[address] ?? {}) - for (const chainId of chainIds) { - const transactions = oldTransactionState[address][chainId] - const txIds = Object.keys(transactions ?? {}) - - for (const txId of txIds) { - const txDetails = transactions[txId] - - if (!txDetails) { - // we iterative over very chain, need to no-op on some combinations - continue - } - - if (txDetails.typeInfo.type !== TransactionType.FiatPurchaseDeprecated) { - newTransactionState[address] ??= {} - newTransactionState[address][chainId] ??= {} - newTransactionState[address][chainId][txId] = txDetails - - continue - } - - if (txDetails.status === TransactionStatus.Failed) { - // delete failed moonpay transactions as we do not have enough information to migrate - continue - } - - const { - explorerUrl, - outputTokenAddress, - outputCurrencyAmountFormatted, - outputCurrencyAmountPrice, - syncedWithBackend, - } = txDetails.typeInfo - - const newTypeInfo = { - type: TransactionType.FiatPurchaseDeprecated, - explorerUrl, - inputCurrency: undefined, - inputCurrencyAmount: outputCurrencyAmountFormatted / outputCurrencyAmountPrice, - outputCurrency: { - type: 'crypto', - metadata: { chainId: undefined, contractAddress: outputTokenAddress }, - }, - outputCurrencyAmount: undefined, - syncedWithBackend, - } - - newTransactionState[address] ??= {} - newTransactionState[address][chainId] ??= {} - newTransactionState[address][chainId][txId] = { ...txDetails, typeInfo: newTypeInfo } - } - } - } - - return { ...newState, transactions: newTransactionState } - }, - - 31: function emptyMigration(state: any) { - // no persisted state removed but need to update schema - return state - }, - - 32: function resetEnsApi(state: any) { - const newState = { ...state } - - delete newState.ENS - - return newState - }, - - 33: function addReplaceAccount(state: any) { - const newState = { ...state } - - newState.wallet.replaceAccountOptions = { - isReplacingAccount: false, - skipToSeedPhrase: false, - } - return newState - }, - - 34: function addLastBalancesReport(state: any) { - const newState = { ...state } - - newState.telemetry = { - lastBalancesReport: 0, - } - return newState - }, - - 35: function addAppearanceSetting(state: any) { - const newState = { ...state } - - newState.appearanceSettings = { - selectedAppearanceSettings: 'system', - } - return newState - }, - - 36: function addNfts(state: any) { - const newState = { ...state } - - newState.favorites = { - ...state.favorites, - hiddenNfts: {}, - } - return newState - }, - 37: function correctFailedFiatOnRampTxIds(state: any) { - const newState = { ...state } - - const oldTransactionState = state?.transactions - const newTransactionState: any = {} - - const addresses = Object.keys(oldTransactionState ?? {}) - for (const address of addresses) { - const chainIds = Object.keys(oldTransactionState[address] ?? {}) - for (const chainId of chainIds) { - const transactions = oldTransactionState[address][chainId] - const txIds = Object.keys(transactions ?? {}) - - for (const txId of txIds) { - const txDetails = transactions[txId] - - if (!txDetails) { - // we iterate over every chain, need to no-op on some combinations - continue - } - - newTransactionState[address] ??= {} - newTransactionState[address][chainId] ??= {} - newTransactionState[address][chainId][txId] = - txDetails.typeInfo.type === TransactionType.FiatPurchaseDeprecated && - txDetails.status === TransactionStatus.Failed - ? { - ...txDetails, - typeInfo: { - ...txDetails.typeInfo, - id: txDetails.typeInfo?.explorerUrl?.split('=')?.[1], - }, - } - : txDetails - } - } - } - return { ...newState, transactions: newTransactionState } - }, - 38: function removeReplaceAccountOptions(state: any) { - const newState = { ...state } - delete newState.wallet.replaceAccountOptions - return newState - }, - 39: function removeExperimentsSlice(state: any) { - const newState = { ...state } - delete newState.experiments - return newState - }, - 40: function removePersistedWalletConnectSlice(state: any) { - // Remove `walletConnect` slice from persisted whitelist - const newState = { ...state } - delete newState.walletConnect - return newState - }, - - 41: function addLastBalancesReportValue(state: any) { - const newState = { ...state } - - newState.telemetry = { - ...state.telemetry, - lastBalancesReportValue: 0, - } - return newState - }, - - 42: function removeFlashbotsEnabledFromWalletSlice(state: any) { - const newState = { ...state } - - delete newState.wallet.flashbotsEnabled - - return newState - }, - - 43: function convertHiddenNftsToNftsData(state: any) { - // see its test to get a better idea of what this migration does - const newState = { ...state } - - const accountAddresses = Object.keys(state.favorites?.hiddenNfts ?? {}) - - type AccountToNftData = Record> - - const nftsData: AccountToNftData = {} - for (const accountAddress of accountAddresses) { - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - nftsData[accountAddress] ??= {} - const hiddenNftKeys = Object.keys(state.favorites.hiddenNfts[accountAddress]) - - for (const hiddenNftKey of hiddenNftKeys) { - const [, nftKey, tokenId] = hiddenNftKey.split('.') - - // we need to convert NFTs key to the new all not checksummed version - const newNftKey = nftKey && tokenId && getNFTAssetKey(nftKey, tokenId) - - const accountNftsData = nftsData[accountAddress] - if (newNftKey && accountNftsData) { - accountNftsData[newNftKey] = { isHidden: true } - } - } - } - - newState.favorites = { - ...state.favorites, - nftsData, - } - delete newState.favorites.hiddenNfts - return newState - }, - - 44: function removeProviders(state: any) { - const newState = { ...state } - - delete newState.providers - - return newState - }, - - 45: function addTokensData(state: any) { - const newState = { ...state } - - newState.favorites = { - ...state.favorites, - tokensVisibility: {}, - } - return newState - }, - - 46: function deleteRTKQuerySlices(state: any) { - const newState = { ...state } - - delete newState.ENS - delete newState.ens - delete newState.gasApi - delete newState.onChainBalanceApi - delete newState.routingApi - delete newState.trmApi - - return newState - }, - - 47: function resetActiveChains(state: any) { - const newState = { ...state } - - newState.chains.byChainId = { - '1': { isActive: true }, - '10': { isActive: true }, - '56': { isActive: true }, - '137': { isActive: true }, - '8453': { isActive: true }, - '42161': { isActive: true }, - } - - return newState - }, - - 48: function addTweakStartingState(state: any) { - const newState = { ...state } - - newState.tweaks = {} - - return newState - }, - - 49: function addSwapProtectionSetting(state: any) { - const newState = { ...state } - newState.wallet.settings = { - ...state.wallet.settings, - swapProtection: SwapProtectionSetting.On, - } - return newState - }, - - 50: function deleteChainsSlice(state: any) { - const newState = { ...state } - delete newState.chains - return newState - }, - - 51: function addLanguageSettings(state: any) { - return { - ...state, - languageSettings: { currentLanguage: Language.English }, - } - }, - - 52: function addFiatCurrencySettings(state: any) { - return { - ...state, - fiatCurrencySettings: { currentCurrency: FiatCurrency.UnitedStatesDollar }, - } - }, - - 53: function updateLanguageSettings(state: any) { - return { - ...state, - languageSettings: { currentLanguage: Language.English }, - } - }, - - 54: function addWalletIsFunded(state: any) { - const newState = { ...state } - - newState.telemetry = { - ...state.telemetry, - walletIsFunded: false, - } - - return newState - }, - - 55: function addBehaviorHistory(state: any) { - const newState = { ...state } - - newState.behaviorHistory = { - hasViewedReviewScreen: false, - hasSubmittedHoldToSwap: false, - } - - return newState - }, - - 56: function addAllowAnalyticsSwitch(state: any) { - const newState = { ...state } - - newState.telemetry = { - ...state.telemetry, - allowAnalytics: true, - lastHeartbeat: 0, - } - - return newState - }, - - 57: function moveSettingStateToGlobal(state: any) { - const newState = { ...state } - - // get old accounts - const accounts = newState?.wallet?.accounts ?? {} - const firstAccountKey = Object.keys(accounts)[0] - - // Read setting from the first wallet, or assign default value - const hideSmallBalances = firstAccountKey ? !accounts[firstAccountKey].showSmallBalances : true // default to true - const hideSpamTokens = firstAccountKey ? !accounts[firstAccountKey].showSpamTokens : true // default to true - - newState.wallet.settings.hideSmallBalances = hideSmallBalances - newState.wallet.settings.hideSpamTokens = hideSpamTokens - - // delete old account specific state - const accountKeys = Object.keys(accounts ?? {}) - for (const accountKey of accountKeys) { - delete accounts[accountKey].showSmallBalances - delete accounts[accountKey].showSpamTokens - } - - return newState - }, - - 58: function addSkippedUnitagBoolean(state: any) { - const newState = { ...state } - - newState.behaviorHistory = { - ...state.behaviorHistory, - hasSkippedUnitagPrompt: false, - } - - return newState - }, - - 59: function addCompletedUnitagsIntroBoolean(state: any) { - const newState = { ...state } - - newState.behaviorHistory = { - ...state.behaviorHistory, - hasCompletedUnitagsIntroModal: false, - } - - return newState - }, - - 60: function addUniconV2IntroModalBoolean(state: any) { - const newState = { ...state } - - newState.behaviorHistory = { - ...state.behaviorHistory, - hasViewedUniconV2IntroModal: false, - } - - return newState - }, - - 61: function flattenTokenVisibility(state: any) { - const newState = { ...state } - - type AccountToNftData = Record> - type NFTKeyToVisibility = Record - - type AccountToTokenVisibility = Record> - type CurrencyIdToVisibility = Record - - const tokenVisibilityByAccount: AccountToTokenVisibility = state.favorites.tokensVisibility - const flattenedTokenVisibility: CurrencyIdToVisibility = Object.values(tokenVisibilityByAccount).reduce( - (acc, currencyIdToVisibility) => ({ ...acc, ...currencyIdToVisibility }), - {}, - ) - - const nftDataByAccount: AccountToNftData = state.favorites.nftsData - const flattenedNFTData = Object.values(nftDataByAccount).reduce( - (acc, nftIdToVisibility) => ({ ...acc, ...nftIdToVisibility }), - {}, - ) - - const flattenedTransformedNFTData: NFTKeyToVisibility = Object.keys(flattenedNFTData).reduce( - (acc, nftKey) => { - const { isHidden, isSpamIgnored } = flattenedNFTData[nftKey] ?? {} - return { - ...acc, - [nftKey]: { isVisible: isHidden === false || isSpamIgnored === true }, - } - }, - {}, - ) - - newState.favorites = { - ...state.favorites, - tokensVisibility: flattenedTokenVisibility, - nftsVisibility: flattenedTransformedNFTData, - } - - delete newState.favorites.nftsData - - return newState - }, - - 62: function addExtensionOnboardingState(state: any) { - const newState = { ...state } - - newState.behaviorHistory = { - ...state.behaviorHistory, - // Removed in schema 69 - extensionOnboardingState: 'Undefined', - } - - return newState - }, - + 0: restructureTransactionsAndNotifications, + 1: removeWalletConnectModalState, + 2: renameFollowedAddressesToWatchedAddresses, + 3: addSearchHistory, + 4: addTimeImportedAndDerivationIndex, + 5: addModalsState, + 6: addWalletConnectPendingSessionAndSettings, + 7: removeNonZeroDerivationIndexAccounts, + 8: addCloudBackup, + 9: removeLocalTypeAccounts, + 10: removeDemoAccount, + 11: addBiometricSettings, + 12: addPushNotificationsEnabledToAccounts, + 13: addEnsState, + 14: migrateBiometricSettings, + 15: changeNativeTypeToSignerMnemonic, + 16: removeDataApi, + 17: resetPushNotificationsEnabled, + 18: removeEnsState, + 19: filterToSupportedChains, + 20: resetLastTxNotificationUpdate, + 21: addExperimentsSlice, + 22: removeCoingeckoApiAndTokenLists, + 23: resetTokensOrderByAndMetadataDisplayType, + 24: transformNotificationCountToStatus, + 25: addPasswordLockout, + 26: removeShowSmallBalances, + 27: resetTokensOrderBy, + 28: removeTokensMetadataDisplayType, + 29: removeTokenListsAndCustomTokens, + 30: migrateFiatPurchaseTransactionInfo, + 31: emptyMigration, + 32: resetEnsApi, + 33: addReplaceAccountOptions, + 34: addLastBalancesReport, + 35: addAppearanceSetting, + 36: addHiddenNfts, + 37: correctFailedFiatOnRampTxIds, + 38: removeReplaceAccountOptions, + 39: removeExperimentsSlice, + 40: removePersistedWalletConnectSlice, + 41: addLastBalancesReportValue, + 42: removeFlashbotsEnabledFromWalletSlice, + 43: convertHiddenNftsToNftsData, + 44: removeProviders, + 45: addTokensVisibility, + 46: deleteRTKQuerySlices, + 47: resetActiveChains, + 48: addTweaksStartingState, + 49: addSwapProtectionSetting, + 50: deleteChainsSlice, + 51: addLanguageSettings, + 52: addFiatCurrencySettings, + 53: updateLanguageSettings, + 54: addWalletIsFunded, + 55: addBehaviorHistory, + 56: addAllowAnalyticsSwitch, + 57: moveSettingStateToGlobal, + 58: addSkippedUnitagBoolean, + 59: addCompletedUnitagsIntroBoolean, + 60: addUniconV2IntroModalBoolean, + 61: flattenTokenVisibility, + 62: addExtensionOnboardingState, 63: removeWalletIsUnlockedState, - 64: removeUniconV2BehaviorState, - 65: addRoutingFieldToTransactions, - 66: activatePendingAccounts, - - 67: function resetOnboardingStateForGA(state: any) { - const newState = { ...state } - - // Reset state so that everyone gets the new promo banner even if theyve dismissed the beta version. - newState.behaviorHistory = { - ...state.behaviorHistory, - // Removed in schema 69 - extensionOnboardingState: 'Undefined', - } - - return newState - }, - + 67: resetOnboardingStateForGA, 68: deleteBetaOnboardingState, - 69: deleteExtensionOnboardingState, - 70: deleteDefaultFavoritesFromFavoritesState, - 71: addHapticSetting, - 72: addExploreAndWelcomeBehaviorHistory, - 73: moveUserSettings, - - 74: function deleteOldOnRampTxData(state: any) { - const newState = { ...state } - - const transactionsState = newState.transactions - - const addresses = Object.keys(transactionsState ?? {}) - for (const address of addresses) { - const chainIds = Object.keys(transactionsState[address] ?? {}) - for (const chainId of chainIds) { - const transactions = transactionsState[address][chainId] - const txIds = Object.keys(transactions ?? {}) - for (const txId of txIds) { - if (transactions[txId]?.typeInfo?.type === TransactionType.FiatPurchaseDeprecated) { - delete transactionsState[address][chainId][txId] - } - } - } - } - - return { ...newState, transactions: transactionsState } - }, - + 74: deleteOldOnRampTxData, 75: deleteHoldToSwapBehaviorHistory, - 76: addCreatedOnboardingRedesignAccountBehaviorHistory, - 77: moveDismissedTokenWarnings, - 78: moveLanguageSetting, - 79: moveCurrencySetting, - 80: updateExploreOrderByType, - 81: removeCreatedOnboardingRedesignAccountBehaviorHistory, - 82: unchecksumDismissedTokenWarningKeys, - - 83: function addPushNotifications(state: any) { - // Enabling new notifications unless they have all wallet activity notifs disabled - const hasAllWalletNotifsDisabled = Object.values(state.wallet.accounts).every( - (account) => - account && - typeof account === 'object' && - 'pushNotificationsEnabled' in account && - !account.pushNotificationsEnabled, - ) - - return { - ...state, - pushNotifications: { - generalUpdatesEnabled: !hasAllWalletNotifsDisabled, - priceAlertsEnabled: !hasAllWalletNotifsDisabled, - }, - } - }, - + 83: addPushNotifications, 84: deleteWelcomeWalletCardBehaviorHistory, - 85: moveTokenAndNFTVisibility, - - 86: function addBatchedTransactions(state: any) { - return { - ...state, - batchedTransactions: {}, - } - }, - - 87: function migrateDappRequestInfoTypes(state: any): any { - const newState = { ...state } - - if (!newState?.transactions) { - return newState - } - - const newTransactionState = {} as Record - - for (const [address, chainIdToTxIdToDetails] of Object.entries(newState.transactions as Record)) { - for (const [chainId, txIdToDetails] of Object.entries(chainIdToTxIdToDetails as Record)) { - for (const [txId, details] of Object.entries(txIdToDetails as Record)) { - let newDetails = { ...details } - - if (details.typeInfo.externalDappInfo?.source === 'uwulink') { - newDetails = { - ...details, - typeInfo: { - ...details.typeInfo, - externalDappInfo: { - ...(details.typeInfo.externalDappInfo ?? {}), - requestType: DappRequestType.UwULink, - }, - }, - } - } - - if (details.typeInfo.externalDappInfo?.source === 'walletconnect') { - newDetails = { - ...details, - typeInfo: { - ...details.typeInfo, - externalDappInfo: { - ...(details.typeInfo.externalDappInfo ?? {}), - requestType: DappRequestType.WalletConnectSessionRequest, - }, - }, - } - } - - // Change the field `dapp` to `dappRequestInfo` - if (details.typeInfo.type === TransactionType.WCConfirm && details.typeInfo.dapp) { - newDetails.typeInfo.dappRequestInfo = { - ...(details.typeInfo.dapp ?? {}), - } - } - - delete newDetails.typeInfo.dapp - delete newDetails.typeInfo.externalDappInfo?.source - - newTransactionState[address] ??= {} - newTransactionState[address][chainId] ??= {} - newTransactionState[address][chainId][txId] = newDetails - } - } - } - - return { - ...newState, - transactions: newTransactionState, - } - }, - + 86: addBatchedTransactions, + 87: migrateDappRequestInfoTypes, 88: moveHapticsToUserSettings, - 89: removeThaiBahtFromFiatCurrency, - 90: migrateLiquidityTransactionInfo, - 91: removePriceAlertsEnabledFromPushNotifications, - - 92: function migrateAndRemoveCloudBackupSlice(state: any) { - const newState = { ...state } - const backupEmail = newState.cloudBackup?.backupsFound?.find((backup: any) => backup.email)?.email - if (backupEmail) { - newState.wallet.androidCloudBackupEmail = backupEmail - } - delete newState.cloudBackup - - return newState - }, - + 92: migrateAndRemoveCloudBackupSlice, 93: migrateSearchHistory, 94: addDismissedBridgedAndCompatibleWarnings, + 95: addActivityVisibility, + 96: migrateDismissedTokenWarnings, + 97: setWalletDeviceLanguage, } -export const MOBILE_STATE_VERSION = 94 +export const MOBILE_STATE_VERSION = 97 diff --git a/apps/mobile/src/app/mobileMigrationTests.ts b/apps/mobile/src/app/mobileMigrationTests.ts new file mode 100644 index 00000000000..6fe5a9ccc33 --- /dev/null +++ b/apps/mobile/src/app/mobileMigrationTests.ts @@ -0,0 +1,1422 @@ +/** + * Test helpers for testing migrations run in sequence. + * + * Called by migrations.test.ts to verify migrations work correctly with realistic + * data that has passed through all prior migrations in the chain. + * + * For unit tests of individual migrations, see mobileMigrations.test.ts. + */ +/* oxlint-disable typescript/no-explicit-any -- Migration test functions need flexible any types */ +/* oxlint-disable max-lines */ +/* oxlint-disable max-params */ +import { BigNumber } from '@ethersproject/bignumber' +import mockdate from 'mockdate' +import { OLD_DEMO_ACCOUNT_ADDRESS } from 'src/app/mobileMigrations' +import { ScannerModalState } from 'uniswap/src/components/ReceiveQRCode/constants' +import { AccountType } from 'uniswap/src/features/accounts/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { Language } from 'uniswap/src/features/language/constants' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { TransactionStatus, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' +import { DappRequestType } from 'uniswap/src/types/walletConnect' +import { type Account, type SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' +import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' + +export function testRestructureTransactionsAndNotifications(migration: (state: any) => any, prevSchema: any): void { + const txDetails0 = { + chainId: UniverseChainId.Mainnet, + id: '0', + from: '0xShadowySuperCoder', + options: { + request: { + from: '0x123', + to: '0x456', + value: '0x0', + data: '0x789', + nonce: 10, + gasPrice: BigNumber.from('10000'), + }, + }, + typeInfo: { + type: TransactionType.Approve, + tokenAddress: '0xtokenAddress', + spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', + }, + status: TransactionStatus.Pending, + addedTime: 1487076708000, + hash: '0x123', + } + + const txDetails1 = { + chainId: UniverseChainId.Optimism, + id: '1', + from: '0xKingHodler', + options: { + request: { + from: '0x123', + to: '0x456', + value: '0x0', + data: '0x789', + nonce: 10, + gasPrice: BigNumber.from('10000'), + }, + }, + typeInfo: { + type: TransactionType.Approve, + tokenAddress: '0xtokenAddress', + spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', + }, + status: TransactionStatus.Success, + addedTime: 1487076708000, + hash: '0x123', + } + + const initialSchemaStub = { + ...prevSchema, + transactions: { + byChainId: { + [UniverseChainId.Mainnet]: { + '0': txDetails0, + }, + [UniverseChainId.Optimism]: { + '1': txDetails1, + }, + }, + lastTxHistoryUpdate: { + '0xShadowySuperCoder': 12345678912345, + '0xKingHodler': 9876543210987, + }, + }, + } + + const newSchema = migration(initialSchemaStub) + expect(newSchema.transactions[UniverseChainId.Mainnet]).toBeUndefined() + expect(newSchema.transactions.lastTxHistoryUpdate).toBeUndefined() + + expect(newSchema.transactions['0xShadowySuperCoder'][UniverseChainId.Mainnet]['0'].status).toEqual( + TransactionStatus.Pending, + ) + expect(newSchema.transactions['0xKingHodler'][UniverseChainId.Mainnet]).toBeUndefined() + expect(newSchema.transactions['0xKingHodler'][UniverseChainId.Optimism]['0']).toBeUndefined() + expect(newSchema.transactions['0xKingHodler'][UniverseChainId.Optimism]['1'].from).toEqual('0xKingHodler') + + expect(newSchema.notifications.lastTxNotificationUpdate).toBeDefined() + expect(newSchema.notifications.lastTxNotificationUpdate['0xShadowySuperCoder'][UniverseChainId.Mainnet]).toEqual( + 12345678912345, + ) +} + +export function testRemoveWalletConnectModalState(migration: (state: any) => any, prevSchema: any): void { + const v0Stub = { + ...prevSchema, + walletConnect: { + ...prevSchema.wallet, + modalState: ScannerModalState.ScanQr, + }, + } + + const v1 = migration(v0Stub) + expect(v1.walletConnect.modalState).toEqual(undefined) +} + +export function testRenameFollowedAddressesToWatchedAddresses(migration: (state: any) => any, prevSchema: any): void { + const TEST_ADDRESSES = ['0xTest'] + + const v1SchemaStub = { + ...prevSchema, + favorites: { + ...prevSchema.favorites, + followedAddresses: TEST_ADDRESSES, + }, + } + + const v2 = migration(v1SchemaStub) + + expect(v2.favorites.watchedAddresses).toEqual(TEST_ADDRESSES) + expect(v2.favorites.followedAddresses).toBeUndefined() +} + +export function testAddSearchHistory(migration: (state: any) => any, prevSchema: any): void { + const v3 = migration(prevSchema) + expect(v3.searchHistory.results).toEqual([]) +} + +export function testAddTimeImportedAndDerivationIndex(migration: (state: any) => any, prevSchema: any): void { + const TEST_ADDRESSES = ['0xTest', '0xTest2', '0xTest3', '0xTest4'] + const TEST_IMPORT_TIME_MS = 12345678912345 + + const v3SchemaStub = { + ...prevSchema, + wallet: { + ...prevSchema.wallet, + accounts: [ + { + type: AccountType.Readonly, + address: TEST_ADDRESSES[0], + name: 'Test Account 1', + pending: false, + }, + { + type: AccountType.Readonly, + address: TEST_ADDRESSES[1], + name: 'Test Account 2', + pending: false, + }, + { + type: 'native', + address: TEST_ADDRESSES[2], + name: 'Test Account 3', + pending: false, + }, + { + type: 'native', + address: TEST_ADDRESSES[3], + name: 'Test Account 4', + pending: false, + }, + ], + }, + } + + mockdate.set(TEST_IMPORT_TIME_MS) + + const v4 = migration(v3SchemaStub) + expect(v4.wallet.accounts[0].timeImportedMs).toEqual(TEST_IMPORT_TIME_MS) + expect(v4.wallet.accounts[2].derivationIndex).toBeDefined() +} + +export function testAddModalsState(migration: (state: any) => any, prevSchema: any): void { + const v5 = migration(prevSchema) + + expect(prevSchema.balances).toBeDefined() + expect(v5.balances).toBeUndefined() + + expect(v5.modals[ModalName.Swap].isOpen).toEqual(false) + expect(v5.modals[ModalName.Send].isOpen).toEqual(false) +} + +export function testAddWalletConnectPendingSessionAndSettings(migration: (state: any) => any, prevSchema: any): void { + const v6 = migration(prevSchema) + + expect(v6.walletConnect.pendingSession).toBe(null) + + expect(typeof v6.wallet.settings).toBe('object') + + expect(prevSchema.wallet.bluetooth).toBeDefined() + expect(v6.wallet.bluetooth).toBeUndefined() +} + +export function testRemoveNonZeroDerivationIndexAccounts(migration: (state: any) => any, prevSchema: any): void { + const TEST_ADDRESSES: [string, string, string, string] = ['0xTest', '0xTest2', '0xTest3', '0xTest4'] + const TEST_IMPORT_TIME_MS = 12345678912345 + + const v6SchemaStub = { + ...prevSchema, + wallet: { + ...prevSchema.wallet, + accounts: { + [TEST_ADDRESSES[0]]: { + type: 'native', + address: TEST_ADDRESSES[0], + name: 'Test Account 1', + pending: false, + derivationIndex: 0, + timeImportedMs: TEST_IMPORT_TIME_MS, + }, + [TEST_ADDRESSES[1]]: { + type: 'native', + address: TEST_ADDRESSES[1], + name: 'Test Account 2', + pending: false, + derivationIndex: 1, + timeImportedMs: TEST_IMPORT_TIME_MS, + }, + [TEST_ADDRESSES[2]]: { + type: 'native', + address: TEST_ADDRESSES[2], + name: 'Test Account 3', + pending: false, + derivationIndex: 2, + timeImportedMs: TEST_IMPORT_TIME_MS, + }, + [TEST_ADDRESSES[3]]: { + type: 'native', + address: TEST_ADDRESSES[3], + name: 'Test Account 4', + pending: false, + derivationIndex: 3, + timeImportedMs: TEST_IMPORT_TIME_MS, + }, + }, + }, + } + + expect(Object.values(v6SchemaStub.wallet.accounts)).toHaveLength(4) + const v7 = migration(v6SchemaStub) + + const accounts = Object.values(v7.wallet.accounts) as SignerMnemonicAccount[] + expect(accounts).toHaveLength(1) + expect(accounts[0]?.mnemonicId).toEqual(TEST_ADDRESSES[0]) +} + +export function testAddCloudBackup(migration: (state: any) => any, prevSchema: any): void { + const v8 = migration(prevSchema) + expect(v8.cloudBackup.backupsFound).toEqual([]) +} + +export function testRemoveLocalTypeAccounts(migration: (state: any) => any, prevSchema: any): void { + const TEST_ADDRESSES: [string, string] = ['0xTest', '0xTest2'] + const TEST_IMPORT_TIME_MS = 12345678912345 + + const v8SchemaStub = { + ...prevSchema, + wallet: { + ...prevSchema.wallet, + accounts: { + [TEST_ADDRESSES[0]]: { + type: 'native', + address: TEST_ADDRESSES[0], + name: 'Test Account 1', + pending: false, + derivationIndex: 0, + timeImportedMs: TEST_IMPORT_TIME_MS, + }, + [TEST_ADDRESSES[1]]: { + type: 'local', + address: TEST_ADDRESSES[1], + name: 'Test Account 2', + pending: false, + timeImportedMs: TEST_IMPORT_TIME_MS, + }, + }, + }, + } + + expect(Object.values(v8SchemaStub.wallet.accounts)).toHaveLength(2) + const v9 = migration(v8SchemaStub) + expect(Object.values(v9.wallet.accounts)).toHaveLength(1) +} + +export function testRemoveDemoAccount(migration: (state: any) => any, prevSchema: any): void { + const TEST_ADDRESSES = ['0xTest', OLD_DEMO_ACCOUNT_ADDRESS, '0xTest2', '0xTest3'] + const TEST_IMPORT_TIME_MS = 12345678912345 + + const accounts = TEST_ADDRESSES.reduce( + (acc, address) => { + acc[address] = { + address, + timeImportedMs: TEST_IMPORT_TIME_MS, + type: 'native', + } as unknown as Account + + return acc + }, + {} as { [address: string]: Account }, + ) + + const v9SchemaStub = { + ...prevSchema, + wallet: { + ...prevSchema.wallet, + accounts, + }, + } + + expect(Object.values(v9SchemaStub.wallet.accounts)).toHaveLength(4) + expect(Object.keys(v9SchemaStub.wallet.accounts)).toContain(OLD_DEMO_ACCOUNT_ADDRESS) + + const migratedSchema = migration(v9SchemaStub) + expect(Object.values(migratedSchema.wallet.accounts)).toHaveLength(3) + expect(Object.keys(migratedSchema.wallet.accounts)).not.toContain(OLD_DEMO_ACCOUNT_ADDRESS) +} + +export function testAddBiometricSettings(migration: (state: any) => any, prevSchema: any): void { + const v11 = migration(prevSchema) + + expect(v11.biometricSettings).toBeDefined() + expect(v11.biometricSettings.requiredForAppAccess).toBeDefined() + expect(v11.biometricSettings.requiredForTransactions).toBeDefined() +} + +export function testAddPushNotificationsEnabledToAccounts(migration: (state: any) => any, prevSchema: any): void { + const TEST_ADDRESS = '0xTestAddress' + const ACCOUNT_NAME = 'Test Account' + const v11Stub = { + ...prevSchema, + wallet: { + ...prevSchema.wallet, + accounts: { + [TEST_ADDRESS]: { + type: 'native', + address: TEST_ADDRESS, + name: ACCOUNT_NAME, + pending: false, + derivationIndex: 0, + timeImportedMs: 123, + }, + }, + }, + } + + const v12 = migration(v11Stub) + + expect(v12.wallet.accounts[TEST_ADDRESS].pushNotificationsEnabled).toEqual(false) + expect(v12.wallet.accounts[TEST_ADDRESS].type).toEqual('native') + expect(v12.wallet.accounts[TEST_ADDRESS].address).toEqual(TEST_ADDRESS) + expect(v12.wallet.accounts[TEST_ADDRESS].name).toEqual(ACCOUNT_NAME) +} + +export function testAddEnsState(migration: (state: any) => any, prevSchema: any): void { + const v13 = migration(prevSchema) + expect(v13.ens.ensForAddress).toEqual({}) +} + +export function testMigrateBiometricSettings(migration: (state: any) => any, prevSchema: any): void { + const v13Stub = { + ...prevSchema, + wallet: { + ...prevSchema.wallet, + isBiometricAuthEnabled: true, + }, + biometricSettings: { + requiredForAppAccess: false, + requiredForTransactions: false, + }, + } + + const v14 = migration(v13Stub) + expect(v14.biometricSettings.requiredForAppAccess).toEqual(true) + expect(v14.biometricSettings.requiredForTransactions).toEqual(true) +} + +export function testChangeNativeTypeToSignerMnemonic(migration: (state: any) => any, prevSchema: any): void { + const TEST_ADDRESS = '0xTestAddress' + const ACCOUNT_NAME = 'Test Account' + const v14Stub = { + ...prevSchema, + wallet: { + ...prevSchema.wallet, + accounts: { + [TEST_ADDRESS]: { + type: 'native', + address: TEST_ADDRESS, + name: ACCOUNT_NAME, + pending: false, + derivationIndex: 0, + timeImportedMs: 123, + }, + }, + }, + } + + const v15 = migration(v14Stub) + const accounts = Object.values(v15.wallet.accounts) + // oxlint-disable-next-line typescript/no-unnecessary-condition + expect((accounts[0] as Account)?.type).toEqual(AccountType.SignerMnemonic) +} + +export function testRemoveDataApi(migration: (state: any) => any, prevSchema: any): void { + const v15Stub = { + ...prevSchema, + dataApi: {}, + } + + const v16 = migration(v15Stub) + + expect(v16.dataApi).toBeUndefined() +} + +export function testResetPushNotificationsEnabled(migration: (state: any) => any, prevSchema: any): void { + const TEST_ADDRESS = '0xTestAddress' + const ACCOUNT_NAME = 'Test Account' + const v16Stub = { + ...prevSchema, + wallet: { + ...prevSchema.wallet, + accounts: { + [TEST_ADDRESS]: { + type: 'native', + address: TEST_ADDRESS, + name: ACCOUNT_NAME, + pending: false, + derivationIndex: 0, + timeImportedMs: 123, + pushNotificationsEnabled: true, + }, + }, + }, + } + + const v17 = migration(v16Stub) + + expect(v17.wallet.accounts[TEST_ADDRESS].pushNotificationsEnabled).toEqual(false) + expect(v17.wallet.accounts[TEST_ADDRESS].type).toEqual('native') + expect(v17.wallet.accounts[TEST_ADDRESS].address).toEqual(TEST_ADDRESS) + expect(v17.wallet.accounts[TEST_ADDRESS].name).toEqual(ACCOUNT_NAME) +} + +export function testRemoveEnsState(migration: (state: any) => any, prevSchema: any): void { + const v17Stub = { + ...prevSchema, + ens: {}, + } + const v18 = migration(v17Stub) + expect(v18.ens).toBeUndefined() +} + +export function testFilterToSupportedChains(migration: (state: any) => any, prevSchema: any): void { + const ROPSTEN = 3 as UniverseChainId + const RINKEBY = 4 as UniverseChainId + const GOERLI = 5 as UniverseChainId + const KOVAN = 42 as UniverseChainId + + const TEST_ADDRESS = '0xShadowySuperCoder' + const txDetails0 = { + chainId: UniverseChainId.Mainnet, + id: '0', + from: TEST_ADDRESS, + options: { + request: { + from: '0x123', + to: '0x456', + value: '0x0', + data: '0x789', + nonce: 10, + gasPrice: BigNumber.from('10000'), + }, + }, + typeInfo: { + type: TransactionType.Approve, + tokenAddress: '0xtokenAddress', + spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', + }, + status: TransactionStatus.Pending, + addedTime: 1487076708000, + hash: '0x123', + } + + const TEST_ADDRESS_2 = '0xKingHodler' + const txDetails1 = { + chainId: GOERLI, + id: '1', + from: TEST_ADDRESS_2, + options: { + request: { + from: '0x123', + to: '0x456', + value: '0x0', + data: '0x789', + nonce: 10, + gasPrice: BigNumber.from('10000'), + }, + }, + typeInfo: { + type: TransactionType.Approve, + tokenAddress: '0xtokenAddress', + spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', + }, + status: TransactionStatus.Success, + addedTime: 1487076708000, + hash: '0x123', + } + + const transactions = { + [TEST_ADDRESS]: { + [UniverseChainId.Mainnet]: { + '0': txDetails0, + }, + [UniverseChainId.Base]: { + '0': txDetails0, + '1': txDetails1, + }, + [GOERLI]: { + '0': txDetails0, + '1': txDetails1, + }, + [ROPSTEN]: { + '0': txDetails0, + '1': txDetails1, + }, + [RINKEBY]: { + '0': txDetails1, + }, + [KOVAN]: { + '1': txDetails1, + }, + }, + [TEST_ADDRESS_2]: { + [UniverseChainId.ArbitrumOne]: { + '0': txDetails0, + }, + [UniverseChainId.Optimism]: { + '0': txDetails0, + '1': txDetails1, + }, + [ROPSTEN]: { + '0': txDetails0, + '1': txDetails1, + }, + [RINKEBY]: { + '0': txDetails1, + }, + [KOVAN]: { + '1': txDetails1, + }, + }, + } + + const blocks = { + byChainId: { + [UniverseChainId.Mainnet]: { latestBlockNumber: 123456789 }, + [UniverseChainId.Optimism]: { latestBlockNumber: 123456789 }, + [UniverseChainId.ArbitrumOne]: { latestBlockNumber: 123456789 }, + [UniverseChainId.Base]: { latestBlockNumber: 123456789 }, + [GOERLI]: { latestBlockNumber: 123456789 }, + [ROPSTEN]: { latestBlockNumber: 123456789 }, + [RINKEBY]: { latestBlockNumber: 123456789 }, + [KOVAN]: { latestBlockNumber: 123456789 }, + }, + } + + const chains = { + byChainId: { + [UniverseChainId.Mainnet]: { isActive: true }, + [UniverseChainId.Optimism]: { isActive: true }, + [UniverseChainId.ArbitrumOne]: { isActive: true }, + [UniverseChainId.Base]: { isActive: true }, + [GOERLI]: { isActive: true }, + [ROPSTEN]: { isActive: true }, + [RINKEBY]: { isActive: true }, + [KOVAN]: { isActive: true }, + }, + } + + const v18Stub = { + ...prevSchema, + transactions, + blocks, + chains, + } + + const v19 = migration(v18Stub) + + expect(v19.transactions[TEST_ADDRESS][UniverseChainId.Mainnet]).toBeDefined() + expect(v19.transactions[TEST_ADDRESS][UniverseChainId.Base]).toBeDefined() + expect(v19.transactions[TEST_ADDRESS][GOERLI]).toBeUndefined() + expect(v19.transactions[TEST_ADDRESS][ROPSTEN]).toBeUndefined() + expect(v19.transactions[TEST_ADDRESS][RINKEBY]).toBeUndefined() + expect(v19.transactions[TEST_ADDRESS][KOVAN]).toBeUndefined() + + expect(v19.transactions[TEST_ADDRESS_2][UniverseChainId.ArbitrumOne]).toBeDefined() + expect(v19.transactions[TEST_ADDRESS_2][UniverseChainId.Optimism]).toBeDefined() + expect(v19.transactions[TEST_ADDRESS_2][ROPSTEN]).toBeUndefined() + expect(v19.transactions[TEST_ADDRESS_2][RINKEBY]).toBeUndefined() + expect(v19.transactions[TEST_ADDRESS_2][KOVAN]).toBeUndefined() + + expect(v19.blocks.byChainId[UniverseChainId.Mainnet]).toBeDefined() + expect(v19.blocks.byChainId[UniverseChainId.Optimism]).toBeDefined() + expect(v19.blocks.byChainId[UniverseChainId.ArbitrumOne]).toBeDefined() + expect(v19.blocks.byChainId[UniverseChainId.Base]).toBeDefined() + expect(v19.blocks.byChainId[GOERLI]).toBeUndefined() + expect(v19.blocks.byChainId[ROPSTEN]).toBeUndefined() + expect(v19.blocks.byChainId[RINKEBY]).toBeUndefined() + expect(v19.blocks.byChainId[KOVAN]).toBeUndefined() + + expect(v19.chains.byChainId[UniverseChainId.Mainnet]).toBeDefined() + expect(v19.chains.byChainId[UniverseChainId.Optimism]).toBeDefined() + expect(v19.chains.byChainId[UniverseChainId.ArbitrumOne]).toBeDefined() + expect(v19.chains.byChainId[UniverseChainId.Base]).toBeDefined() + expect(v19.chains.byChainId[GOERLI]).toBeUndefined() + expect(v19.chains.byChainId[ROPSTEN]).toBeUndefined() + expect(v19.chains.byChainId[RINKEBY]).toBeUndefined() + expect(v19.chains.byChainId[KOVAN]).toBeUndefined() +} + +export function testResetLastTxNotificationUpdate(migration: (state: any) => any, prevSchema: any): void { + const v19Stub = { + ...prevSchema, + notifications: { + ...prevSchema.notifications, + lastTxNotificationUpdate: { 1: 122342134 }, + }, + } + + const v20 = migration(v19Stub) + expect(v20.notifications.lastTxNotificationUpdate).toEqual({}) +} + +export function testAddExperimentsSlice(migration: (state: any) => any, prevSchema: any): void { + const v20Stub = { + ...prevSchema, + } + + const v21 = migration(v20Stub) + expect(v21.experiments).toBeDefined() +} + +export function testRemoveCoingeckoApiAndTokenLists(migration: (state: any) => any, prevSchema: any): void { + const v21Stub = { + ...prevSchema, + coingeckoApi: {}, + } + const v22 = migration(v21Stub) + expect(v22.coingeckoApi).toBeUndefined() + expect(v22.tokens.watchedTokens).toBeUndefined() + expect(v22.tokens.tokenPairs).toBeUndefined() +} + +export function testResetTokensOrderByAndMetadataDisplayType(migration: (state: any) => any, prevSchema: any): void { + const v22Stub = { + ...prevSchema, + } + const v23 = migration(v22Stub) + expect(v23.wallet.settings.tokensOrderBy).toBeUndefined() + expect(v23.wallet.settings.tokensMetadataDisplayType).toBeUndefined() +} + +export function testTransformNotificationCountToStatus(migration: (state: any) => any, prevSchema: any): void { + const dummyAddress1 = '0xDumDum1' + const dummyAddress2 = '0xDumDum2' + const dummyAddress3 = '0xDumDum3' + const v23Stub = { + ...prevSchema, + notifications: { + ...prevSchema.notifications, + notificationCount: { [dummyAddress1]: 5, [dummyAddress2]: 0, [dummyAddress3]: undefined }, + }, + } + const v24 = migration(v23Stub) + expect(v24.notifications.notificationCount).toBeUndefined() + expect(v24.notifications.notificationStatus[dummyAddress1]).toBe(true) + expect(v24.notifications.notificationStatus[dummyAddress2]).toBe(false) + expect(v24.notifications.notificationStatus[dummyAddress2]).toBe(false) +} + +export function testAddPasswordLockout(migration: (state: any) => any, prevSchema: any): void { + const v24Stub = { + ...prevSchema, + } + const v25 = migration(v24Stub) + expect(v25.passwordLockout.passwordAttempts).toBe(0) +} + +export function testRemoveShowSmallBalances(migration: (state: any) => any, prevSchema: any): void { + const v25Stub = { + ...prevSchema, + } + const v26 = migration(v25Stub) + expect(v26.wallet.settings.showSmallBalances).toBeUndefined() +} + +export function testResetTokensOrderBy(migration: (state: any) => any, prevSchema: any): void { + const v26Stub = { + ...prevSchema, + } + const v27 = migration(v26Stub) + expect(v27.wallet.settings.tokensOrderBy).toBeUndefined() +} + +export function testRemoveTokensMetadataDisplayType(migration: (state: any) => any, prevSchema: any): void { + const v27Stub = { + ...prevSchema, + } + const v28 = migration(v27Stub) + expect(v28.wallet.settings.tokensMetadataDisplayType).toBeUndefined() +} + +export function testRemoveTokenListsAndCustomTokens(migration: (state: any) => any, prevSchema: any): void { + const v28Stub = { + ...prevSchema, + } + const v29 = migration(v28Stub) + expect(v29.tokenLists).toBeUndefined() + expect(v29.tokens.customTokens).toBeUndefined() +} + +export function testMigrateFiatPurchaseTransactionInfo( + migration: (state: any) => any, + prevSchema: any, + account: { address: string }, + txDetailsConfirmed: any, +): void { + const oldFiatOnRampTxDetails = { + chainId: UniverseChainId.Mainnet, + id: '0', + from: account.address, + options: { + request: {}, + }, + // expect this payload to change + typeInfo: { + type: TransactionType.FiatPurchaseDeprecated, + explorerUrl: 'explorer', + outputTokenAddress: '0xtokenAddress', + outputCurrencyAmountFormatted: 50, + outputCurrencyAmountPrice: 2, + syncedWithBackend: true, + }, + status: TransactionStatus.Pending, + addedTime: 1487076708000, + hash: '0x123', + } + const expectedTypeInfo = { + type: TransactionType.FiatPurchaseDeprecated, + explorerUrl: 'explorer', + inputCurrency: undefined, + inputCurrencyAmount: 25, + outputCurrency: { + type: 'crypto', + metadata: { + chainId: undefined, + contractAddress: '0xtokenAddress', + }, + }, + outputCurrencyAmount: undefined, + syncedWithBackend: true, + } + const transactions = { + [account.address]: { + [UniverseChainId.Mainnet]: { + '0': oldFiatOnRampTxDetails, + '1': txDetailsConfirmed, + }, + [UniverseChainId.Base]: { + '0': { ...oldFiatOnRampTxDetails, status: TransactionStatus.Failed }, + '1': txDetailsConfirmed, + }, + [UniverseChainId.ArbitrumOne]: { + '0': { ...oldFiatOnRampTxDetails, status: TransactionStatus.Failed }, + }, + }, + '0xshadowySuperCoder': { + [UniverseChainId.ArbitrumOne]: { + '0': oldFiatOnRampTxDetails, + '1': txDetailsConfirmed, + }, + [UniverseChainId.Optimism]: { + '0': oldFiatOnRampTxDetails, + '1': oldFiatOnRampTxDetails, + '2': txDetailsConfirmed, + }, + }, + '0xdeleteMe': { + [UniverseChainId.Mainnet]: { + '0': { ...oldFiatOnRampTxDetails, status: TransactionStatus.Failed }, + }, + }, + } + const v29Stub = { ...prevSchema, transactions } + + const v30 = migration(v29Stub) + + // expect fiat onramp txdetails to change + expect(v30.transactions[account.address][UniverseChainId.Mainnet]['0'].typeInfo).toEqual(expectedTypeInfo) + expect(v30.transactions[account.address][UniverseChainId.Base]['0']).toBeUndefined() + expect(v30.transactions[account.address][UniverseChainId.ArbitrumOne]).toBeUndefined() // does not create an object for chain + expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.ArbitrumOne]['0'].typeInfo).toEqual(expectedTypeInfo) + expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.Optimism]['0'].typeInfo).toEqual(expectedTypeInfo) + expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.Optimism]['1'].typeInfo).toEqual(expectedTypeInfo) + expect(v30.transactions['0xdeleteMe']).toBe(undefined) + // expect non-for txDetails to not change + expect(v30.transactions[account.address][UniverseChainId.Mainnet]['1']).toEqual(txDetailsConfirmed) + expect(v30.transactions[account.address][UniverseChainId.Base]['1']).toEqual(txDetailsConfirmed) + expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.ArbitrumOne]['1']).toEqual(txDetailsConfirmed) + expect(v30.transactions['0xshadowySuperCoder'][UniverseChainId.Optimism]['2']).toEqual(txDetailsConfirmed) +} + +export function testResetEnsApi(migration: (state: any) => any, prevSchema: any): void { + const v31Stub = { ...prevSchema, ENS: 'defined' } + + const v32 = migration(v31Stub) + + expect(v32.ENS).toBe(undefined) +} + +export function testAddReplaceAccountOptions(migration: (state: any) => any, prevSchema: any): void { + const v32Stub = { ...prevSchema } + + const v33 = migration(v32Stub) + + expect(v33.wallet.replaceAccountOptions.isReplacingAccount).toBe(false) + expect(v33.wallet.replaceAccountOptions.skipToSeedPhrase).toBe(false) +} + +export function testAddLastBalancesReport(migration: (state: any) => any, prevSchema: any): void { + const v33Stub = { ...prevSchema } + + const v34 = migration(v33Stub) + + expect(v34.telemetry.lastBalancesReport).toBe(0) +} + +export function testAddAppearanceSetting(migration: (state: any) => any, prevSchema: any): void { + const v34Stub = { ...prevSchema } + + const v35 = migration(v34Stub) + + expect(v35.appearanceSettings.selectedAppearanceSettings).toBe('system') +} + +export function testAddHiddenNfts(migration: (state: any) => any, prevSchema: any): void { + const v35Stub = { ...prevSchema } + + const v36 = migration(v35Stub) + + expect(v36.favorites.hiddenNfts).toEqual({}) +} + +export function testCorrectFailedFiatOnRampTxIds( + migration: (state: any) => any, + prevSchema: any, + account: { address: string }, + fiatOnRampTxDetailsFailed: any, + txDetailsConfirmed: any, +): void { + const id1 = '123' + const id2 = '456' + const id3 = '789' + const transactions = { + [account.address]: { + [UniverseChainId.Mainnet]: { + [id1]: { + ...fiatOnRampTxDetailsFailed, + typeInfo: { + ...fiatOnRampTxDetailsFailed.typeInfo, + id: undefined, + }, + }, + [id2]: { + ...fiatOnRampTxDetailsFailed, + typeInfo: { + ...fiatOnRampTxDetailsFailed.typeInfo, + id: undefined, + explorerUrl: undefined, + }, + }, + [id3]: txDetailsConfirmed, + }, + }, + } + + const v36Stub = { ...prevSchema, transactions } + + expect(v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id1].typeInfo.id).toBeUndefined() + expect(v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id2].typeInfo.id).toBeUndefined() + + const v37 = migration(v36Stub) + + expect(v37.transactions[account.address]?.[UniverseChainId.Mainnet][id1].typeInfo.id).toEqual( + fiatOnRampTxDetailsFailed.typeInfo.id, + ) + expect(v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id2].typeInfo.id).toBeUndefined() + expect(v36Stub.transactions[account.address]?.[UniverseChainId.Mainnet][id3]).toEqual(txDetailsConfirmed) +} + +export function testRemoveReplaceAccountOptions(migration: (state: any) => any, prevSchema: any): void { + const v37Stub = { ...prevSchema } + const v38 = migration(v37Stub) + expect(v38.wallet.replaceAccountOptions).toBeUndefined() +} + +export function testRemoveExperimentsSlice(migration: (state: any) => any, prevSchema: any): void { + const v38Stub = { ...prevSchema } + expect(v38Stub.experiments).toBeDefined() + const v39 = migration(v38Stub) + expect(v39.experiments).toBeUndefined() +} + +export function testRemovePersistedWalletConnectSlice(migration: (state: any) => any, prevSchema: any): void { + const v39Stub = { ...prevSchema } + + const v40 = migration(v39Stub) + + expect(v40.walletConnect).toBeUndefined() +} + +export function testAddLastBalancesReportValue(migration: (state: any) => any, prevSchema: any): void { + const v40Stub = { ...prevSchema } + + const v41 = migration(v40Stub) + + expect(v41.telemetry.lastBalancesReportValue).toBe(0) +} + +export function testRemoveFlashbotsEnabledFromWalletSlice(migration: (state: any) => any, prevSchema: any): void { + const v41Stub = { ...prevSchema } + + const v42 = migration(v41Stub) + + expect(v42.wallet.flashbotsenabled).toBeUndefined() +} + +export function testConvertHiddenNftsToNftsData(migration: (state: any) => any, prevSchema: any): void { + const v42Stub = { ...prevSchema } + + v42Stub.favorites.hiddenNfts = { + '0xAFa9bAb987E3D7bcD40EB510838aEC663C8b7264': { + 'nftItem.0xb96e881BD4Cd7BCCc8CB47d3aa0e254a72d2F074.3971': true, // checksummed 1 + 'nftItem.0xb96e881bd4cd7bccc8cb47d3aa0e254a72d2f074.3971': true, // not checksummed 1 + 'nftItem.0x25E503331e69EFCBbc50d2a4D661900B23D47662.2': true, // checksummed 2 + 'nftItem.0xe94abea3932576ff957a0b92190d0191aeb1a782.2': true, // not checksummed 3 + }, + } + + const v43 = migration(v42Stub) + + expect(v43.favorites.nftsData).toEqual({ + '0xAFa9bAb987E3D7bcD40EB510838aEC663C8b7264': { + 'nftItem.0xb96e881bd4cd7bccc8cb47d3aa0e254a72d2f074.3971': { isHidden: true }, // not checksummed 1 + 'nftItem.0x25e503331e69efcbbc50d2a4d661900b23d47662.2': { isHidden: true }, // not checksummed 2 + 'nftItem.0xe94abea3932576ff957a0b92190d0191aeb1a782.2': { isHidden: true }, // not checksummed 3 + }, + }) +} + +export function testRemoveProviders(migration: (state: any) => any, prevSchema: any): void { + const v43Stub = { ...prevSchema } + + v43Stub.providers = { isInitialized: true } + + const v44 = migration(v43Stub) + + expect(v44.providers).toBeUndefined() +} + +export function testAddTokensVisibility(migration: (state: any) => any, prevSchema: any): void { + const v44Stub = { ...prevSchema } + + const v45 = migration(v44Stub) + + expect(v45.favorites.tokensVisibility).toEqual({}) +} + +export function testDeleteRTKQuerySlices(migration: (state: any) => any, prevSchema: any): void { + const v45Stub = { ...prevSchema } + const v46 = migration(v45Stub) + + expect(v46.ENS).toBeUndefined() + expect(v46.ens).toBeUndefined() + expect(v46.gasApi).toBeUndefined() + expect(v46.onChainBalanceApi).toBeUndefined() + expect(v46.routingApi).toBeUndefined() + expect(v46.trmApi).toBeUndefined() +} + +export function testResetActiveChains(migration: (state: any) => any, prevSchema: any): void { + const v46Stub = { ...prevSchema } + const v47 = migration(v46Stub) + + expect(v47.chains.byChainId).toStrictEqual({ + '1': { isActive: true }, + '10': { isActive: true }, + '56': { isActive: true }, + '137': { isActive: true }, + '8453': { isActive: true }, + '42161': { isActive: true }, + }) +} + +export function testAddTweaksStartingState(migration: (state: any) => any, prevSchema: any): void { + const v47Stub = { ...prevSchema } + const v48 = migration(v47Stub) + + expect(v48.tweaks).toEqual({}) +} + +export function testAddSwapProtectionSetting(migration: (state: any) => any, prevSchema: any): void { + const v48Stub = { ...prevSchema } + const v49 = migration(v48Stub) + + expect(v49.wallet.settings.swapProtection).toEqual(SwapProtectionSetting.On) +} + +export function testDeleteChainsSlice(migration: (state: any) => any, prevSchema: any): void { + const v49Stub = { ...prevSchema } + const v50 = migration(v49Stub) + + expect(v50.chains).toBeUndefined() +} + +export function testAddLanguageSettings(migration: (state: any) => any, prevSchema: any): void { + const v50Stub = { ...prevSchema } + const v51 = migration(v50Stub) + + expect(v51.languageSettings).not.toBeUndefined() +} + +export function testAddFiatCurrencySettings(migration: (state: any) => any, prevSchema: any): void { + const v51Stub = { ...prevSchema } + const v52 = migration(v51Stub) + + expect(v52.fiatCurrencySettings).not.toBeUndefined() +} + +export function testUpdateLanguageSettings(migration: (state: any) => any, prevSchema: any): void { + const v52Stub = { ...prevSchema } + const v53 = migration(v52Stub) + + expect(v53.languageSettings).not.toBeUndefined() +} + +export function testAddWalletIsFunded(migration: (state: any) => any, prevSchema: any): void { + const v53Stub = { ...prevSchema } + const v54 = migration(v53Stub) + + expect(v54.telemetry.walletIsFunded).toBe(false) +} + +export function testAddBehaviorHistory(migration: (state: any) => any, prevSchema: any): void { + const v54Stub = { ...prevSchema } + const v55 = migration(v54Stub) + + expect(v55.behaviorHistory.hasViewedReviewScreen).toBe(false) +} + +export function testAddAllowAnalyticsSwitch(migration: (state: any) => any, prevSchema: any): void { + const v55Stub = { ...prevSchema } + const v56 = migration(v55Stub) + + expect(v56.telemetry.allowAnalytics).toBe(true) + expect(v56.telemetry.lastHeartbeat).toBe(0) +} + +export function testMoveSettingStateToGlobal(migration: (state: any) => any, prevSchema: any): void { + const v56Stub = { + ...prevSchema, + wallet: { + ...prevSchema.wallet, + accounts: [ + { + type: AccountType.Readonly, + address: '0x', + name: 'Test Account 1', + pending: false, + hideSpamTokens: true, + }, + ], + }, + } + const v57 = migration(v56Stub) + expect(v57.wallet.settings.hideSmallBalances).toBe(true) + expect(v57.wallet.settings.hideSpamTokens).toBe(true) + expect(v57.wallet.accounts[0].showSpamTokens).toBeUndefined() + expect(v57.wallet.accounts[0].showSmallBalances).toBeUndefined() +} + +export function testAddSkippedUnitagBoolean(migration: (state: any) => any, prevSchema: any): void { + const v57Stub = { ...prevSchema } + const v58 = migration(v57Stub) + + expect(v58.behaviorHistory.hasSkippedUnitagPrompt).toBe(false) +} + +export function testAddCompletedUnitagsIntroBoolean(migration: (state: any) => any, prevSchema: any): void { + const v58Stub = { ...prevSchema } + const v59 = migration(v58Stub) + + expect(v59.behaviorHistory.hasCompletedUnitagsIntroModal).toBe(false) +} + +export function testAddUniconV2IntroModalBoolean(migration: (state: any) => any, prevSchema: any): void { + const v59Stub = { ...prevSchema } + const v60 = migration(v59Stub) + + expect(v60.behaviorHistory.hasViewedUniconV2IntroModal).toBe(false) +} + +export function testFlattenTokenVisibility(migration: (state: any) => any, prevSchema: any): void { + const v60Stub = { ...prevSchema } + const address1 = '0x123' + const address2 = '0x456' + const nftKey1 = '0xNFTKey1' + const nftKey2 = '0xNFTKey2' + const nftKey3 = '0xNFTKey3' + const nftKey4 = '0xNFTKey4' + + const currency1ToVisibility = { '0xCurrency1': { isVisible: true } } + const currency2ToVisibility = { '0xCurrency2': { isVisible: false } } + const currency3ToVisibility = { '0xCurrency3': { isVisible: false } } + const nft1ToVisibility = { [nftKey1]: { isSpamIgnored: true } } + const nft2ToVisibility = { [nftKey2]: { isHidden: true } } + const nft3ToVisibility = { [nftKey3]: { isSpamIgnored: false, isHidden: false } } + const nft4ToVisibility = { [nftKey4]: { isSpamIgnored: false, isHidden: true } } + + v60Stub.favorites = { + ...v60Stub.favorites, + tokensVisibility: { + [address1]: { ...currency1ToVisibility, ...currency2ToVisibility }, + [address2]: { ...currency2ToVisibility, ...currency3ToVisibility }, + }, + nftsData: { + [address1]: { ...nft1ToVisibility, ...nft2ToVisibility, ...nft3ToVisibility }, + [address2]: { ...nft3ToVisibility, ...nft4ToVisibility }, + }, + } + + const v61 = migration(v60Stub) + + expect(v61.favorites.nftsData).toBeUndefined() + expect(v61.favorites.tokensVisibility).toMatchObject({ + ...currency1ToVisibility, + ...currency2ToVisibility, + ...currency3ToVisibility, + }) + expect(v61.favorites.nftsVisibility).toMatchObject({ + [nftKey1]: { isVisible: true }, + [nftKey2]: { isVisible: false }, + [nftKey3]: { isVisible: true }, + [nftKey4]: { isVisible: false }, + }) +} + +export function testAddExtensionOnboardingState(migration: (state: any) => any, prevSchema: any): void { + const v61Stub = { ...prevSchema } + const v62 = migration(v61Stub) + + // Removed in schema 69 + expect(v62.behaviorHistory.extensionOnboardingState).toBe('Undefined') +} + +export function testResetOnboardingStateForGA(migration: (state: any) => any, prevSchema: any): void { + const v66Stub = { ...prevSchema } + const v67 = migration(v66Stub) + + // Removed in migration 69 + expect(v67.behaviorHistory.extensionOnboardingState).toBe('Undefined') +} + +export function testDeleteOldOnRampTxData( + migration: (state: any) => any, + prevSchema: any, + account: { address: string }, + txDetailsConfirmed: any, +): void { + const oldFiatOnRampTxDetails = { + chainId: UniverseChainId.Mainnet, + id: '0', + from: account.address, + options: { + request: {}, + }, + typeInfo: { + type: TransactionType.FiatPurchaseDeprecated, + explorerUrl: 'explorer', + inputCurrencyAmount: 25, + outputSymbol: 'USDC', + }, + status: TransactionStatus.Pending, + addedTime: 1487076708000, + hash: '0x123', + } + const transactions = { + [account.address]: { + [UniverseChainId.Mainnet]: { + '0': oldFiatOnRampTxDetails, + '1': txDetailsConfirmed, + }, + [UniverseChainId.Optimism]: { + '0': oldFiatOnRampTxDetails, + '1': { + ...oldFiatOnRampTxDetails, + typeInfo: { + ...oldFiatOnRampTxDetails.typeInfo, + type: TransactionType.Send, + }, + }, + '2': { + ...oldFiatOnRampTxDetails, + typeInfo: { + ...oldFiatOnRampTxDetails.typeInfo, + type: TransactionType.Receive, + }, + }, + '3': txDetailsConfirmed, + }, + }, + } + const v73Stub = { ...prevSchema, transactions } + + const v74 = migration(v73Stub) + + expect(v74.transactions[account.address][UniverseChainId.Mainnet]['0']).toBe(undefined) + expect(v74.transactions[account.address][UniverseChainId.Mainnet]['1']).toEqual(txDetailsConfirmed) + + expect(v74.transactions[account.address][UniverseChainId.Optimism]['0']).toBe(undefined) + expect(v74.transactions[account.address][UniverseChainId.Optimism]['1'].typeInfo).toEqual({ + ...oldFiatOnRampTxDetails.typeInfo, + type: TransactionType.Send, + }) + expect(v74.transactions[account.address][UniverseChainId.Optimism]['2'].typeInfo).toEqual({ + ...oldFiatOnRampTxDetails.typeInfo, + type: TransactionType.Receive, + }) + expect(v74.transactions[account.address][UniverseChainId.Optimism]['3']).toEqual(txDetailsConfirmed) +} + +export function testAddPushNotifications(migration: (state: any) => any, prevSchema: any): void { + // v82 didn't have a new schema + const v82Stub = { ...prevSchema } + const v83 = migration(v82Stub) + + expect(v83.pushNotifications.generalUpdatesEnabled).toBe(false) + expect(v83.pushNotifications.priceAlertsEnabled).toBe(false) +} + +export function testMigrateDappRequestInfoTypes(migration: (state: any) => any, prevSchema: any): void { + /** test migration on uwulink transaction */ + const stateWithUwulinkTransaction = { + transactions: { + testAddress: { + testChainId: { + testTxnId: { + typeInfo: { + dappRequestInfo: { + name: 'testDapp', + }, + externalDappInfo: { + source: 'uwulink', + }, + }, + }, + }, + }, + }, + } + + const v86Stub = { ...prevSchema, ...stateWithUwulinkTransaction } + const v87 = migration(v86Stub) + + expect(v87.transactions).toBeDefined() + expect(v87.transactions.testAddress.testChainId.testTxnId.typeInfo).toEqual({ + dappRequestInfo: { + name: 'testDapp', + }, + externalDappInfo: { + requestType: DappRequestType.UwULink, + }, + }) + + /** test migration on walletconnect transaction */ + const stateWithWalletConnectTransaction = { + transactions: { + testAddress: { + testChainId: { + testTxnId: { + typeInfo: { + type: TransactionType.WCConfirm, + dapp: { + name: 'testDapp', + }, + externalDappInfo: { + source: 'walletconnect', + }, + }, + }, + }, + }, + }, + } + + const v86StubWalletConnect = { ...prevSchema, ...stateWithWalletConnectTransaction } + const v87WalletConnect = migration(v86StubWalletConnect) + + expect(v87WalletConnect.transactions).toBeDefined() + expect(v87WalletConnect.transactions.testAddress.testChainId.testTxnId.typeInfo).toEqual({ + type: TransactionType.WCConfirm, + dappRequestInfo: { + name: 'testDapp', + }, + externalDappInfo: { + requestType: DappRequestType.WalletConnectSessionRequest, + }, + }) +} + +export function testMigrateAndRemoveCloudBackupSlice(migration: (state: any) => any, prevSchema: any): void { + const androidCloudBackupEmail = 'test@test.com' + + const { cloudBackup: _oldCloudBackup, ...v91WithoutCloudBackup } = prevSchema + + const v91WithoutCloudBackupSlice = { + ...v91WithoutCloudBackup, + wallet: { + ...prevSchema.wallet, + activeAccountAddress: '0xabc', + }, + } + + const v91WithCloudBackup = { + ...v91WithoutCloudBackupSlice, + cloudBackup: { + backupsFound: [{ mnemonicId: '0xabc', email: androidCloudBackupEmail }], + }, + wallet: { + ...prevSchema.wallet, + activeAccountAddress: '0xabc', + }, + } + + const v91WithDifferentActiveAccountAddress = { + ...v91WithoutCloudBackupSlice, + cloudBackup: { + backupsFound: [{ mnemonicId: '0xdef', email: androidCloudBackupEmail }], + }, + wallet: { + ...prevSchema.wallet, + activeAccountAddress: '0xabc', + }, + } + + const v92 = migration(v91WithCloudBackup) + expect(v92.cloudBackup).toBeUndefined() + expect(v92.wallet.androidCloudBackupEmail).toBe(androidCloudBackupEmail) + + const v92WithoutCloudBackup = migration(v91WithoutCloudBackupSlice) + expect(v92WithoutCloudBackup.cloudBackup).toBeUndefined() + expect(v92WithoutCloudBackup.wallet.androidCloudBackupEmail).toBe(undefined) + + const v92WithDifferentActiveAccountAddress = migration(v91WithDifferentActiveAccountAddress) + expect(v92WithDifferentActiveAccountAddress.cloudBackup).toBeUndefined() + expect(v92WithDifferentActiveAccountAddress.wallet.androidCloudBackupEmail).toBe(androidCloudBackupEmail) +} + +export function testSetWalletDeviceLanguage( + migration: (state: any) => any, + prevSchema: any, + getWalletDeviceLanguageMock: jest.MockedFunction<() => Language>, +): void { + const deviceLanguage = Language.Japanese + getWalletDeviceLanguageMock.mockReturnValue(deviceLanguage) + + const stateReduxLanguageDiffersFromDevice = { + ...prevSchema, + userSettings: { + ...prevSchema.userSettings, + currentLanguage: Language.English, + }, + } + const resultWhenDifferent = migration(stateReduxLanguageDiffersFromDevice) + expect(resultWhenDifferent.userSettings?.currentLanguage).toBe(deviceLanguage) + expect(getWalletDeviceLanguageMock).toHaveBeenCalled() + getWalletDeviceLanguageMock.mockClear() + + const stateReduxLanguageMatchesDevice = { + ...prevSchema, + userSettings: { + ...prevSchema.userSettings, + currentLanguage: deviceLanguage, + }, + } + const resultWhenSame = migration(stateReduxLanguageMatchesDevice) + expect(resultWhenSame.userSettings?.currentLanguage).toBe(deviceLanguage) + expect(getWalletDeviceLanguageMock).toHaveBeenCalled() + + if (prevSchema.userSettings) { + for (const key of Object.keys(prevSchema.userSettings)) { + if (key !== 'currentLanguage') { + expect(resultWhenDifferent.userSettings[key]).toEqual(prevSchema.userSettings[key]) + expect(resultWhenSame.userSettings[key]).toEqual(prevSchema.userSettings[key]) + } + } + } +} diff --git a/apps/mobile/src/app/mobileMigrations.test.ts b/apps/mobile/src/app/mobileMigrations.test.ts new file mode 100644 index 00000000000..fba9b8019be --- /dev/null +++ b/apps/mobile/src/app/mobileMigrations.test.ts @@ -0,0 +1,1224 @@ +/** + * Isolated tests for individual migration functions. + * + * Tests each migration independently with various input states, edge cases, + * and error handling, without relying on output from previous migrations. + * + * For tests of the full migration chain, see mobileMigrationTests.ts. + */ +import { + addAllowAnalyticsSwitch, + addAppearanceSetting, + addBehaviorHistory, + addBiometricSettings, + addCloudBackup, + addCompletedUnitagsIntroBoolean, + addEnsState, + addExperimentsSlice, + addExtensionOnboardingState, + addFiatCurrencySettings, + addHiddenNfts, + addLanguageSettings, + addLastBalancesReport, + addLastBalancesReportValue, + addModalsState, + addPasswordLockout, + addPushNotifications, + addPushNotificationsEnabledToAccounts, + addReplaceAccountOptions, + addSearchHistory, + addSkippedUnitagBoolean, + addSwapProtectionSetting, + addTimeImportedAndDerivationIndex, + addTokensVisibility, + addTweaksStartingState, + addUniconV2IntroModalBoolean, + addWalletConnectPendingSessionAndSettings, + addWalletIsFunded, + changeNativeTypeToSignerMnemonic, + convertHiddenNftsToNftsData, + correctFailedFiatOnRampTxIds, + deleteChainsSlice, + deleteOldOnRampTxData, + deleteRTKQuerySlices, + emptyMigration, + filterToSupportedChains, + flattenTokenVisibility, + migrateAndRemoveCloudBackupSlice, + migrateBiometricSettings, + migrateDappRequestInfoTypes, + migrateFiatPurchaseTransactionInfo, + moveSettingStateToGlobal, + OLD_DEMO_ACCOUNT_ADDRESS, + removeCoingeckoApiAndTokenLists, + removeDataApi, + removeDemoAccount, + removeEnsState, + removeExperimentsSlice, + removeFlashbotsEnabledFromWalletSlice, + removeLocalTypeAccounts, + removeNonZeroDerivationIndexAccounts, + removePersistedWalletConnectSlice, + removeProviders, + removeReplaceAccountOptions, + removeShowSmallBalances, + removeTokenListsAndCustomTokens, + removeTokensMetadataDisplayType, + removeWalletConnectModalState, + renameFollowedAddressesToWatchedAddresses, + resetActiveChains, + resetEnsApi, + resetLastTxNotificationUpdate, + resetOnboardingStateForGA, + resetPushNotificationsEnabled, + resetTokensOrderBy, + resetTokensOrderByAndMetadataDisplayType, + restructureTransactionsAndNotifications, + setWalletDeviceLanguage, + transformNotificationCountToStatus, + updateLanguageSettings, +} from 'src/app/mobileMigrations' +import { AccountType } from 'uniswap/src/features/accounts/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' +import { Language } from 'uniswap/src/features/language/constants' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { TransactionStatus, TransactionType } from 'uniswap/src/features/transactions/types/transactionDetails' +import { getWalletDeviceLanguage } from 'uniswap/src/i18n/utils' +import { DappRequestType } from 'uniswap/src/types/walletConnect' +import { createThrowingProxy } from 'utilities/src/test/utils' +import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' + +jest.mock('uniswap/src/i18n/utils', () => ({ + getWalletDeviceLanguage: jest.fn(), +})) + +describe('restructureTransactionsAndNotifications', () => { + it('restructures transactions from byChainId to address-based format', () => { + const state = { + transactions: { + byChainId: { + '1': { + tx1: { id: 'tx1', from: '0x123', chainId: 1 }, + }, + }, + lastTxHistoryUpdate: { + '0x123': 1234567890, + }, + }, + notifications: {}, + } + const result = restructureTransactionsAndNotifications(state) + expect(result.transactions['0x123']['1'].tx1).toEqual({ id: 'tx1', from: '0x123', chainId: 1 }) + expect(result.notifications.lastTxNotificationUpdate['0x123'][UniverseChainId.Mainnet]).toBe(1234567890) + }) + + it('handles missing transaction state', () => { + const state = { notifications: {} } + const result = restructureTransactionsAndNotifications(state) + expect(result.transactions).toEqual({}) + }) + + it('falls back to empty state on error', () => { + const state = { + transactions: createThrowingProxy({}, { throwingMethods: ['*'] }), + notifications: {}, + } + const result = restructureTransactionsAndNotifications(state) + expect(result.transactions).toEqual({}) + expect(result.notifications.lastTxNotificationUpdate).toEqual({}) + }) +}) + +describe('removeWalletConnectModalState', () => { + it('removes modalState from walletConnect', () => { + const state = { walletConnect: { modalState: 'open', otherData: 'preserved' } } + const result = removeWalletConnectModalState(state) + expect(result.walletConnect.modalState).toBeUndefined() + expect(result.walletConnect.otherData).toBe('preserved') + }) + + it('handles missing walletConnect state', () => { + const state = { otherData: 'preserved' } + const result = removeWalletConnectModalState(state) + expect(result.otherData).toBe('preserved') + }) +}) + +describe('renameFollowedAddressesToWatchedAddresses', () => { + it('renames followedAddresses to watchedAddresses', () => { + const state = { favorites: { followedAddresses: ['0x123'] } } + const result = renameFollowedAddressesToWatchedAddresses(state) + expect(result.favorites.watchedAddresses).toEqual(['0x123']) + expect(result.favorites.followedAddresses).toBeUndefined() + }) + + it('handles missing favorites state', () => { + const state = { otherData: 'preserved' } + const result = renameFollowedAddressesToWatchedAddresses(state) + expect(result.otherData).toBe('preserved') + }) +}) + +describe('addSearchHistory', () => { + it('adds searchHistory with empty results', () => { + const state = { otherData: 'preserved' } + const result = addSearchHistory(state) + expect(result.searchHistory).toEqual({ results: [] }) + expect(result.otherData).toBe('preserved') + }) +}) + +describe('addTimeImportedAndDerivationIndex', () => { + it('adds timeImportedMs to all accounts and derivationIndex to native accounts', () => { + const state = { + wallet: { + accounts: { + '0x123': { type: 'native', address: '0x123' }, + '0x456': { type: 'readonly', address: '0x456' }, + }, + }, + } + const result = addTimeImportedAndDerivationIndex(state) + expect(result.wallet.accounts['0x123'].timeImportedMs).toBeDefined() + expect(result.wallet.accounts['0x123'].derivationIndex).toBe(0) + expect(result.wallet.accounts['0x456'].timeImportedMs).toBeDefined() + expect(result.wallet.accounts['0x456'].derivationIndex).toBeUndefined() + }) + + it('handles missing accounts', () => { + const state = { wallet: {} } + const result = addTimeImportedAndDerivationIndex(state) + expect(result.wallet).toBeDefined() + }) +}) + +describe('addModalsState', () => { + it('adds modals state and removes balances', () => { + const state = { balances: { someData: true } } + const result = addModalsState(state) + expect(result.modals[ModalName.WalletConnectScan]).toEqual({ isOpen: false, initialState: 0 }) + expect(result.modals[ModalName.Swap]).toEqual({ isOpen: false, initialState: undefined }) + expect(result.modals[ModalName.Send]).toEqual({ isOpen: false, initialState: undefined }) + expect(result.balances).toBeUndefined() + }) +}) + +describe('addWalletConnectPendingSessionAndSettings', () => { + it('adds pendingSession and settings', () => { + const state = { walletConnect: {}, wallet: { bluetooth: true } } + const result = addWalletConnectPendingSessionAndSettings(state) + expect(result.walletConnect.pendingSession).toBeNull() + expect(result.wallet.settings).toEqual({}) + expect(result.wallet.bluetooth).toBeUndefined() + }) +}) + +describe('removeNonZeroDerivationIndexAccounts', () => { + it('removes accounts with non-zero derivation index and sets mnemonicId', () => { + const state = { + wallet: { + accounts: { + '0x123': { type: 'native', derivationIndex: 0, address: '0x123' }, + '0x456': { type: 'native', derivationIndex: 1, address: '0x456' }, + }, + }, + } + const result = removeNonZeroDerivationIndexAccounts(state) + expect(result.wallet.accounts['0x123'].mnemonicId).toBe('0x123') + expect(result.wallet.accounts['0x456']).toBeUndefined() + }) +}) + +describe('addCloudBackup', () => { + it('adds cloudBackup with empty backupsFound', () => { + const state = { otherData: 'preserved' } + const result = addCloudBackup(state) + expect(result.cloudBackup).toEqual({ backupsFound: [] }) + }) +}) + +describe('removeLocalTypeAccounts', () => { + it('removes accounts with local type', () => { + const state = { + wallet: { + accounts: { + '0x123': { type: 'local' }, + '0x456': { type: 'native' }, + }, + }, + } + const result = removeLocalTypeAccounts(state) + expect(result.wallet.accounts['0x123']).toBeUndefined() + expect(result.wallet.accounts['0x456']).toBeDefined() + }) +}) + +describe('removeDemoAccount', () => { + it('removes the demo account', () => { + const state = { + wallet: { + accounts: { + [OLD_DEMO_ACCOUNT_ADDRESS]: { type: 'demo' }, + '0x456': { type: 'native' }, + }, + }, + } + const result = removeDemoAccount(state) + expect(result.wallet.accounts[OLD_DEMO_ACCOUNT_ADDRESS]).toBeUndefined() + expect(result.wallet.accounts['0x456']).toBeDefined() + }) + + it('handles missing demo account', () => { + const state = { wallet: { accounts: { '0x123': { type: 'native' } } } } + const result = removeDemoAccount(state) + expect(result.wallet.accounts['0x123']).toBeDefined() + }) +}) + +describe('addBiometricSettings', () => { + it('adds biometricSettings with defaults', () => { + const state = { otherData: 'preserved' } + const result = addBiometricSettings(state) + expect(result.biometricSettings).toEqual({ + requiredForAppAccess: false, + requiredForTransactions: false, + }) + }) +}) + +describe('addPushNotificationsEnabledToAccounts', () => { + it('adds pushNotificationsEnabled to all accounts', () => { + const state = { + wallet: { + accounts: { + '0x123': { address: '0x123' }, + '0x456': { address: '0x456' }, + }, + }, + } + const result = addPushNotificationsEnabledToAccounts(state) + expect(result.wallet.accounts['0x123'].pushNotificationsEnabled).toBe(false) + expect(result.wallet.accounts['0x456'].pushNotificationsEnabled).toBe(false) + }) + + it('handles missing accounts', () => { + const state = { wallet: {} } + const result = addPushNotificationsEnabledToAccounts(state) + expect(result.wallet.accounts).toEqual({}) + }) +}) + +describe('addEnsState', () => { + it('adds ens state with empty ensForAddress', () => { + const state = { otherData: 'preserved' } + const result = addEnsState(state) + expect(result.ens).toEqual({ ensForAddress: {} }) + }) +}) + +describe('migrateBiometricSettings', () => { + it('migrates isBiometricAuthEnabled to biometricSettings', () => { + const state = { wallet: { isBiometricAuthEnabled: true } } + const result = migrateBiometricSettings(state) + expect(result.biometricSettings).toEqual({ + requiredForAppAccess: true, + requiredForTransactions: true, + }) + expect(result.wallet.isBiometricAuthEnabled).toBeUndefined() + }) + + it('defaults to false when isBiometricAuthEnabled is missing', () => { + const state = { wallet: {} } + const result = migrateBiometricSettings(state) + expect(result.biometricSettings).toEqual({ + requiredForAppAccess: false, + requiredForTransactions: false, + }) + }) + + it('falls back to default settings on error', () => { + const state = { wallet: createThrowingProxy({}, { throwingMethods: ['*'] }) } + const result = migrateBiometricSettings(state) + expect(result.biometricSettings).toEqual({ + requiredForAppAccess: false, + requiredForTransactions: false, + }) + }) +}) + +describe('changeNativeTypeToSignerMnemonic', () => { + it('changes native type to SignerMnemonic', () => { + const state = { + wallet: { + accounts: { + '0x123': { type: 'native' }, + '0x456': { type: 'readonly' }, + }, + }, + } + const result = changeNativeTypeToSignerMnemonic(state) + expect(result.wallet.accounts['0x123'].type).toBe(AccountType.SignerMnemonic) + expect(result.wallet.accounts['0x456'].type).toBe('readonly') + }) +}) + +describe('removeDataApi', () => { + it('removes dataApi from state', () => { + const state = { dataApi: { someData: true }, otherData: 'preserved' } + const result = removeDataApi(state) + expect(result.dataApi).toBeUndefined() + expect(result.otherData).toBe('preserved') + }) +}) + +describe('resetPushNotificationsEnabled', () => { + it('resets pushNotificationsEnabled to false for all accounts', () => { + const state = { + wallet: { + accounts: { + '0x123': { pushNotificationsEnabled: true }, + '0x456': { pushNotificationsEnabled: true }, + }, + }, + } + const result = resetPushNotificationsEnabled(state) + expect(result.wallet.accounts['0x123'].pushNotificationsEnabled).toBe(false) + expect(result.wallet.accounts['0x456'].pushNotificationsEnabled).toBe(false) + }) + + it('returns state unchanged if no accounts', () => { + const state = { wallet: {} } + const result = resetPushNotificationsEnabled(state) + expect(result).toEqual(state) + }) +}) + +describe('removeEnsState', () => { + it('removes ens from state', () => { + const state = { ens: { someData: true }, otherData: 'preserved' } + const result = removeEnsState(state) + expect(result.ens).toBeUndefined() + expect(result.otherData).toBe('preserved') + }) +}) + +describe('filterToSupportedChains', () => { + it('filters chains, blocks, and transactions to supported chains only', () => { + const state = { + chains: { + byChainId: { + '1': { isActive: true }, + '99999': { isActive: true }, // unsupported + }, + }, + blocks: { + byChainId: { + '1': { blockNumber: 123 }, + '99999': { blockNumber: 456 }, + }, + }, + transactions: { + '0x123': { + '1': { tx1: { id: 'tx1' } }, + '99999': { tx2: { id: 'tx2' } }, + }, + }, + } + const result = filterToSupportedChains(state) + expect(result.chains.byChainId['1']).toBeDefined() + expect(result.chains.byChainId['99999']).toBeUndefined() + expect(result.blocks.byChainId['1']).toBeDefined() + expect(result.blocks.byChainId['99999']).toBeUndefined() + expect(result.transactions['0x123']['1']).toBeDefined() + expect(result.transactions['0x123']['99999']).toBeUndefined() + }) +}) + +describe('resetLastTxNotificationUpdate', () => { + it('resets lastTxNotificationUpdate to empty object', () => { + const state = { notifications: { lastTxNotificationUpdate: { '0x123': 12345 } } } + const result = resetLastTxNotificationUpdate(state) + expect(result.notifications.lastTxNotificationUpdate).toEqual({}) + }) +}) + +describe('addExperimentsSlice', () => { + it('adds experiments slice with empty experiments and featureFlags', () => { + const state = { otherData: 'preserved' } + const result = addExperimentsSlice(state) + expect(result.experiments).toEqual({ experiments: {}, featureFlags: {} }) + }) +}) + +describe('removeCoingeckoApiAndTokenLists', () => { + it('removes coingeckoApi and token-related data', () => { + const state = { + coingeckoApi: { data: true }, + tokens: { watchedTokens: [], tokenPairs: [], otherData: true }, + } + const result = removeCoingeckoApiAndTokenLists(state) + expect(result.coingeckoApi).toBeUndefined() + expect(result.tokens.watchedTokens).toBeUndefined() + expect(result.tokens.tokenPairs).toBeUndefined() + expect(result.tokens.otherData).toBe(true) + }) +}) + +describe('resetTokensOrderByAndMetadataDisplayType', () => { + it('removes tokensOrderBy and tokensMetadataDisplayType from wallet settings', () => { + const state = { + wallet: { + settings: { tokensOrderBy: 'volume', tokensMetadataDisplayType: 'full', otherSetting: true }, + }, + } + const result = resetTokensOrderByAndMetadataDisplayType(state) + expect(result.wallet.settings.tokensOrderBy).toBeUndefined() + expect(result.wallet.settings.tokensMetadataDisplayType).toBeUndefined() + expect(result.wallet.settings.otherSetting).toBe(true) + }) +}) + +describe('transformNotificationCountToStatus', () => { + it('transforms notificationCount to notificationStatus', () => { + const state = { + notifications: { + notificationCount: { '0x123': 5, '0x456': 0 }, + }, + } + const result = transformNotificationCountToStatus(state) + expect(result.notifications.notificationStatus['0x123']).toBe(true) + expect(result.notifications.notificationStatus['0x456']).toBe(false) + expect(result.notifications.notificationCount).toBeUndefined() + }) + + it('falls back to empty notificationStatus on error', () => { + const state = { + notifications: { notificationCount: createThrowingProxy({}, { throwingMethods: ['*'] }) }, + } + const result = transformNotificationCountToStatus(state) + expect(result.notifications.notificationStatus).toEqual({}) + }) +}) + +describe('addPasswordLockout', () => { + it('adds passwordLockout with zero attempts', () => { + const state = { otherData: 'preserved' } + const result = addPasswordLockout(state) + expect(result.passwordLockout).toEqual({ passwordAttempts: 0 }) + }) +}) + +describe('removeShowSmallBalances', () => { + it('removes showSmallBalances from wallet settings', () => { + const state = { wallet: { settings: { showSmallBalances: true, otherSetting: true } } } + const result = removeShowSmallBalances(state) + expect(result.wallet.settings.showSmallBalances).toBeUndefined() + expect(result.wallet.settings.otherSetting).toBe(true) + }) +}) + +describe('resetTokensOrderBy', () => { + it('removes tokensOrderBy from wallet settings', () => { + const state = { wallet: { settings: { tokensOrderBy: 'volume' } } } + const result = resetTokensOrderBy(state) + expect(result.wallet.settings.tokensOrderBy).toBeUndefined() + }) +}) + +describe('removeTokensMetadataDisplayType', () => { + it('removes tokensMetadataDisplayType from wallet settings', () => { + const state = { wallet: { settings: { tokensMetadataDisplayType: 'full' } } } + const result = removeTokensMetadataDisplayType(state) + expect(result.wallet.settings.tokensMetadataDisplayType).toBeUndefined() + }) +}) + +describe('removeTokenListsAndCustomTokens', () => { + it('removes tokenLists and customTokens', () => { + const state = { tokenLists: { lists: [] }, tokens: { customTokens: [], otherData: true } } + const result = removeTokenListsAndCustomTokens(state) + expect(result.tokenLists).toBeUndefined() + expect(result.tokens.customTokens).toBeUndefined() + expect(result.tokens.otherData).toBe(true) + }) +}) + +describe('migrateFiatPurchaseTransactionInfo', () => { + it('migrates fiat purchase transaction info format', () => { + const state = { + transactions: { + '0x123': { + '1': { + tx1: { + id: 'tx1', + status: TransactionStatus.Success, + typeInfo: { + type: TransactionType.FiatPurchaseDeprecated, + explorerUrl: 'https://example.com', + outputTokenAddress: '0xtoken', + outputCurrencyAmountFormatted: 100, + outputCurrencyAmountPrice: 2, + syncedWithBackend: true, + }, + }, + }, + }, + }, + } + const result = migrateFiatPurchaseTransactionInfo(state) + const txTypeInfo = result.transactions['0x123']['1'].tx1.typeInfo + expect(txTypeInfo.inputCurrency).toBeUndefined() + expect(txTypeInfo.outputCurrency.type).toBe('crypto') + }) + + it('removes failed fiat purchase transactions', () => { + const state = { + transactions: { + '0x123': { + '1': { + tx1: { + id: 'tx1', + status: TransactionStatus.Failed, + typeInfo: { type: TransactionType.FiatPurchaseDeprecated }, + }, + }, + }, + }, + } + const result = migrateFiatPurchaseTransactionInfo(state) + // Failed FiatPurchaseDeprecated transactions are skipped entirely + expect(result.transactions['0x123']?.['1']?.tx1).toBeUndefined() + }) + + it('falls back to empty transactions on error', () => { + const state = { + transactions: createThrowingProxy({}, { throwingMethods: ['*'] }), + otherData: 'preserved', + } + const result = migrateFiatPurchaseTransactionInfo(state) + expect(result.transactions).toEqual({}) + expect(result.otherData).toBe('preserved') + }) +}) + +describe('emptyMigration', () => { + it('returns state unchanged', () => { + const state = { someData: 'preserved' } + const result = emptyMigration(state) + expect(result).toEqual(state) + }) +}) + +describe('resetEnsApi', () => { + it('removes ENS from state', () => { + const state = { ENS: { data: true }, otherData: 'preserved' } + const result = resetEnsApi(state) + expect(result.ENS).toBeUndefined() + expect(result.otherData).toBe('preserved') + }) +}) + +describe('addReplaceAccountOptions', () => { + it('adds replaceAccountOptions to wallet', () => { + const state = { wallet: {} } + const result = addReplaceAccountOptions(state) + expect(result.wallet.replaceAccountOptions).toEqual({ + isReplacingAccount: false, + skipToSeedPhrase: false, + }) + }) +}) + +describe('addLastBalancesReport', () => { + it('adds telemetry with lastBalancesReport', () => { + const state = { otherData: 'preserved' } + const result = addLastBalancesReport(state) + expect(result.telemetry).toEqual({ lastBalancesReport: 0 }) + }) +}) + +describe('addAppearanceSetting', () => { + it('adds appearanceSettings with system default', () => { + const state = { otherData: 'preserved' } + const result = addAppearanceSetting(state) + expect(result.appearanceSettings).toEqual({ selectedAppearanceSettings: 'system' }) + }) +}) + +describe('addHiddenNfts', () => { + it('adds hiddenNfts to favorites', () => { + const state = { favorites: { tokens: [] } } + const result = addHiddenNfts(state) + expect(result.favorites.hiddenNfts).toEqual({}) + expect(result.favorites.tokens).toEqual([]) + }) +}) + +describe('correctFailedFiatOnRampTxIds', () => { + it('extracts id from explorerUrl for failed fiat purchase transactions', () => { + const state = { + transactions: { + '0x123': { + '1': { + tx1: { + id: 'tx1', + status: TransactionStatus.Failed, + typeInfo: { + type: TransactionType.FiatPurchaseDeprecated, + explorerUrl: 'https://example.com?id=extractedId123', + }, + }, + }, + }, + }, + } + const result = correctFailedFiatOnRampTxIds(state) + expect(result.transactions['0x123']['1'].tx1.typeInfo.id).toBe('extractedId123') + }) + + it('falls back to empty transactions on error', () => { + const state = { + transactions: createThrowingProxy({}, { throwingMethods: ['*'] }), + } + const result = correctFailedFiatOnRampTxIds(state) + expect(result.transactions).toEqual({}) + }) +}) + +describe('removeReplaceAccountOptions', () => { + it('removes replaceAccountOptions from wallet', () => { + const state = { wallet: { replaceAccountOptions: { isReplacingAccount: false }, otherData: true } } + const result = removeReplaceAccountOptions(state) + expect(result.wallet.replaceAccountOptions).toBeUndefined() + expect(result.wallet.otherData).toBe(true) + }) +}) + +describe('removeExperimentsSlice', () => { + it('removes experiments from state', () => { + const state = { experiments: { data: true }, otherData: 'preserved' } + const result = removeExperimentsSlice(state) + expect(result.experiments).toBeUndefined() + expect(result.otherData).toBe('preserved') + }) +}) + +describe('removePersistedWalletConnectSlice', () => { + it('removes walletConnect from state', () => { + const state = { walletConnect: { data: true }, otherData: 'preserved' } + const result = removePersistedWalletConnectSlice(state) + expect(result.walletConnect).toBeUndefined() + expect(result.otherData).toBe('preserved') + }) +}) + +describe('addLastBalancesReportValue', () => { + it('adds lastBalancesReportValue to telemetry', () => { + const state = { telemetry: { lastBalancesReport: 0 } } + const result = addLastBalancesReportValue(state) + expect(result.telemetry.lastBalancesReportValue).toBe(0) + expect(result.telemetry.lastBalancesReport).toBe(0) + }) +}) + +describe('removeFlashbotsEnabledFromWalletSlice', () => { + it('removes flashbotsEnabled from wallet', () => { + const state = { wallet: { flashbotsEnabled: true, otherData: true } } + const result = removeFlashbotsEnabledFromWalletSlice(state) + expect(result.wallet.flashbotsEnabled).toBeUndefined() + expect(result.wallet.otherData).toBe(true) + }) +}) + +describe('convertHiddenNftsToNftsData', () => { + it('converts hiddenNfts to nftsData format', () => { + const state = { + favorites: { + hiddenNfts: { + '0x123': { + 'mainnet.0xcontract.123': true, + }, + }, + }, + } + const result = convertHiddenNftsToNftsData(state) + expect(result.favorites.nftsData['0x123']).toBeDefined() + expect(result.favorites.hiddenNfts).toBeUndefined() + }) + + it('falls back to empty nftsData on error', () => { + const state = { + favorites: { hiddenNfts: createThrowingProxy({}, { throwingMethods: ['*'] }) }, + } + const result = convertHiddenNftsToNftsData(state) + expect(result.favorites.nftsData).toEqual({}) + }) +}) + +describe('removeProviders', () => { + it('removes providers from state', () => { + const state = { providers: { data: true }, otherData: 'preserved' } + const result = removeProviders(state) + expect(result.providers).toBeUndefined() + expect(result.otherData).toBe('preserved') + }) +}) + +describe('addTokensVisibility', () => { + it('adds tokensVisibility to favorites', () => { + const state = { favorites: { tokens: [] } } + const result = addTokensVisibility(state) + expect(result.favorites.tokensVisibility).toEqual({}) + }) +}) + +describe('deleteRTKQuerySlices', () => { + it('removes RTK Query slices', () => { + const state = { + ENS: {}, + ens: {}, + gasApi: {}, + onChainBalanceApi: {}, + routingApi: {}, + trmApi: {}, + otherData: 'preserved', + } + const result = deleteRTKQuerySlices(state) + expect(result.ENS).toBeUndefined() + expect(result.ens).toBeUndefined() + expect(result.gasApi).toBeUndefined() + expect(result.onChainBalanceApi).toBeUndefined() + expect(result.routingApi).toBeUndefined() + expect(result.trmApi).toBeUndefined() + expect(result.otherData).toBe('preserved') + }) +}) + +describe('resetActiveChains', () => { + it('resets byChainId to default active chains', () => { + const state = { chains: { byChainId: { '1': { isActive: false } } } } + const result = resetActiveChains(state) + expect(result.chains.byChainId['1']).toEqual({ isActive: true }) + expect(result.chains.byChainId['10']).toEqual({ isActive: true }) + expect(result.chains.byChainId['56']).toEqual({ isActive: true }) + expect(result.chains.byChainId['137']).toEqual({ isActive: true }) + expect(result.chains.byChainId['8453']).toEqual({ isActive: true }) + expect(result.chains.byChainId['42161']).toEqual({ isActive: true }) + }) +}) + +describe('addTweaksStartingState', () => { + it('adds empty tweaks object', () => { + const state = { otherData: 'preserved' } + const result = addTweaksStartingState(state) + expect(result.tweaks).toEqual({}) + }) +}) + +describe('addSwapProtectionSetting', () => { + it('adds swapProtection setting to wallet settings', () => { + const state = { wallet: { settings: {} } } + const result = addSwapProtectionSetting(state) + expect(result.wallet.settings.swapProtection).toBe(SwapProtectionSetting.On) + }) +}) + +describe('deleteChainsSlice', () => { + it('removes chains from state', () => { + const state = { chains: { byChainId: {} }, otherData: 'preserved' } + const result = deleteChainsSlice(state) + expect(result.chains).toBeUndefined() + expect(result.otherData).toBe('preserved') + }) +}) + +describe('addLanguageSettings', () => { + it('adds languageSettings with English default', () => { + const state = { otherData: 'preserved' } + const result = addLanguageSettings(state) + expect(result.languageSettings).toEqual({ currentLanguage: Language.English }) + }) +}) + +describe('addFiatCurrencySettings', () => { + it('adds fiatCurrencySettings with USD default', () => { + const state = { otherData: 'preserved' } + const result = addFiatCurrencySettings(state) + expect(result.fiatCurrencySettings).toEqual({ currentCurrency: FiatCurrency.UnitedStatesDollar }) + }) +}) + +describe('updateLanguageSettings', () => { + it('sets languageSettings to English', () => { + const state = { languageSettings: { currentLanguage: 'es' } } + const result = updateLanguageSettings(state) + expect(result.languageSettings).toEqual({ currentLanguage: Language.English }) + }) +}) + +describe('setWalletDeviceLanguage', () => { + const mockGetWalletDeviceLanguage = jest.mocked(getWalletDeviceLanguage) + + beforeEach(() => { + mockGetWalletDeviceLanguage.mockReturnValue(Language.SpanishSpain) + }) + + it('sets currentLanguage to device language when userSettings exists', () => { + const state = { + userSettings: { + currentLanguage: Language.English, + otherSetting: 'preserved', + }, + } + const result = setWalletDeviceLanguage(state) + expect(result.userSettings.currentLanguage).toBe(Language.SpanishSpain) + expect(result.userSettings.otherSetting).toBe('preserved') + }) + + it('returns state unchanged when userSettings does not exist', () => { + const state = { otherData: 'preserved' } + const result = setWalletDeviceLanguage(state) + expect(result.userSettings).toBeUndefined() + expect(result.otherData).toBe('preserved') + }) + + it('falls back to English when getWalletDeviceLanguage throws', () => { + mockGetWalletDeviceLanguage.mockImplementation(() => { + throw new Error('device locales unavailable') + }) + + const state = { + userSettings: { + currentLanguage: Language.French, + otherSetting: 'preserved', + }, + } + const result = setWalletDeviceLanguage(state) + expect(result.userSettings.currentLanguage).toBe(Language.English) + expect(result.userSettings.otherSetting).toBe('preserved') + }) +}) + +describe('addWalletIsFunded', () => { + it('adds walletIsFunded to telemetry', () => { + const state = { telemetry: { lastBalancesReport: 0 } } + const result = addWalletIsFunded(state) + expect(result.telemetry.walletIsFunded).toBe(false) + expect(result.telemetry.lastBalancesReport).toBe(0) + }) +}) + +describe('addBehaviorHistory', () => { + it('adds behaviorHistory with default values', () => { + const state = { otherData: 'preserved' } + const result = addBehaviorHistory(state) + expect(result.behaviorHistory).toEqual({ + hasViewedReviewScreen: false, + hasSubmittedHoldToSwap: false, + }) + }) +}) + +describe('addAllowAnalyticsSwitch', () => { + it('adds analytics settings to telemetry', () => { + const state = { telemetry: { walletIsFunded: false } } + const result = addAllowAnalyticsSwitch(state) + expect(result.telemetry.allowAnalytics).toBe(true) + expect(result.telemetry.lastHeartbeat).toBe(0) + expect(result.telemetry.walletIsFunded).toBe(false) + }) +}) + +describe('moveSettingStateToGlobal', () => { + it('moves showSmallBalances and showSpamTokens from accounts to global settings', () => { + const state = { + wallet: { + accounts: { + '0x123': { showSmallBalances: false, showSpamTokens: true }, + }, + settings: {}, + }, + } + const result = moveSettingStateToGlobal(state) + expect(result.wallet.settings.hideSmallBalances).toBe(true) + expect(result.wallet.settings.hideSpamTokens).toBe(false) + expect(result.wallet.accounts['0x123'].showSmallBalances).toBeUndefined() + expect(result.wallet.accounts['0x123'].showSpamTokens).toBeUndefined() + }) + + it('defaults to hiding when no accounts exist', () => { + const state = { wallet: { accounts: {}, settings: {} } } + const result = moveSettingStateToGlobal(state) + expect(result.wallet.settings.hideSmallBalances).toBe(true) + expect(result.wallet.settings.hideSpamTokens).toBe(true) + }) + + it('falls back to default settings on error', () => { + const state = { + wallet: { accounts: createThrowingProxy({}, { throwingMethods: ['*'] }), settings: {} }, + } + const result = moveSettingStateToGlobal(state) + expect(result.wallet.settings.hideSmallBalances).toBe(true) + expect(result.wallet.settings.hideSpamTokens).toBe(true) + }) +}) + +describe('addSkippedUnitagBoolean', () => { + it('adds hasSkippedUnitagPrompt to behaviorHistory', () => { + const state = { behaviorHistory: { otherFlag: true } } + const result = addSkippedUnitagBoolean(state) + expect(result.behaviorHistory.hasSkippedUnitagPrompt).toBe(false) + expect(result.behaviorHistory.otherFlag).toBe(true) + }) +}) + +describe('addCompletedUnitagsIntroBoolean', () => { + it('adds hasCompletedUnitagsIntroModal to behaviorHistory', () => { + const state = { behaviorHistory: { otherFlag: true } } + const result = addCompletedUnitagsIntroBoolean(state) + expect(result.behaviorHistory.hasCompletedUnitagsIntroModal).toBe(false) + expect(result.behaviorHistory.otherFlag).toBe(true) + }) +}) + +describe('addUniconV2IntroModalBoolean', () => { + it('adds hasViewedUniconV2IntroModal to behaviorHistory', () => { + const state = { behaviorHistory: { otherFlag: true } } + const result = addUniconV2IntroModalBoolean(state) + expect(result.behaviorHistory.hasViewedUniconV2IntroModal).toBe(false) + expect(result.behaviorHistory.otherFlag).toBe(true) + }) +}) + +describe('flattenTokenVisibility', () => { + it('flattens tokensVisibility and nftsData from account-based to flat structure', () => { + const state = { + favorites: { + tokensVisibility: { + '0x123': { token1: { isVisible: true } }, + '0x456': { token2: { isVisible: false } }, + }, + nftsData: { + '0x123': { nft1: { isHidden: false } }, + }, + }, + } + const result = flattenTokenVisibility(state) + expect(result.favorites.tokensVisibility['token1']).toEqual({ isVisible: true }) + expect(result.favorites.tokensVisibility['token2']).toEqual({ isVisible: false }) + expect(result.favorites.nftsVisibility).toBeDefined() + expect(result.favorites.nftsData).toBeUndefined() + }) + + it('falls back to empty objects on error', () => { + const state = { + favorites: { tokensVisibility: createThrowingProxy({}, { throwingMethods: ['*'] }) }, + } + const result = flattenTokenVisibility(state) + expect(result.favorites.tokensVisibility).toEqual({}) + expect(result.favorites.nftsVisibility).toEqual({}) + }) +}) + +describe('addExtensionOnboardingState', () => { + it('adds extensionOnboardingState to behaviorHistory', () => { + const state = { behaviorHistory: { otherFlag: true } } + const result = addExtensionOnboardingState(state) + expect(result.behaviorHistory.extensionOnboardingState).toBe('Undefined') + expect(result.behaviorHistory.otherFlag).toBe(true) + }) +}) + +describe('resetOnboardingStateForGA', () => { + it('resets extensionOnboardingState to Undefined', () => { + const state = { behaviorHistory: { extensionOnboardingState: 'Completed' } } + const result = resetOnboardingStateForGA(state) + expect(result.behaviorHistory.extensionOnboardingState).toBe('Undefined') + }) +}) + +describe('deleteOldOnRampTxData', () => { + it('removes FiatPurchaseDeprecated transactions', () => { + const state = { + transactions: { + '0x123': { + '1': { + tx1: { typeInfo: { type: TransactionType.FiatPurchaseDeprecated } }, + tx2: { typeInfo: { type: TransactionType.Swap } }, + }, + }, + }, + } + const result = deleteOldOnRampTxData(state) + expect(result.transactions['0x123']['1'].tx1).toBeUndefined() + expect(result.transactions['0x123']['1'].tx2).toBeDefined() + }) + + it('falls back to empty transactions on error', () => { + // Create a proxy with a non-empty target so Object.keys returns keys, + // then accessing those keys through get will throw + const state = { + transactions: { + '0x123': createThrowingProxy({ '1': {} }, { throwingMethods: ['*'] }), + }, + } + const result = deleteOldOnRampTxData(state) + expect(result.transactions).toEqual({}) + }) +}) + +describe('addPushNotifications', () => { + it('enables push notifications if any account has notifications enabled', () => { + const state = { + wallet: { + accounts: { + '0x123': { pushNotificationsEnabled: true }, + '0x456': { pushNotificationsEnabled: false }, + }, + }, + } + const result = addPushNotifications(state) + expect(result.pushNotifications.generalUpdatesEnabled).toBe(true) + expect(result.pushNotifications.priceAlertsEnabled).toBe(true) + }) + + it('disables push notifications if all accounts have notifications disabled', () => { + const state = { + wallet: { + accounts: { + '0x123': { pushNotificationsEnabled: false }, + '0x456': { pushNotificationsEnabled: false }, + }, + }, + } + const result = addPushNotifications(state) + expect(result.pushNotifications.generalUpdatesEnabled).toBe(false) + expect(result.pushNotifications.priceAlertsEnabled).toBe(false) + }) + + it('falls back to enabled on error', () => { + const state = { + wallet: { + accounts: { + '0x123': createThrowingProxy({}, { throwingMethods: ['*'] }), + }, + }, + } + const result = addPushNotifications(state) + expect(result.pushNotifications.generalUpdatesEnabled).toBe(true) + expect(result.pushNotifications.priceAlertsEnabled).toBe(true) + }) +}) + +describe('migrateDappRequestInfoTypes', () => { + it('migrates uwulink source to requestType', () => { + const state = { + transactions: { + '0x123': { + '1': { + tx1: { + typeInfo: { + externalDappInfo: { source: 'uwulink' }, + }, + }, + }, + }, + }, + } + const result = migrateDappRequestInfoTypes(state) + const dappInfo = result.transactions['0x123']['1'].tx1.typeInfo.externalDappInfo + expect(dappInfo.requestType).toBe(DappRequestType.UwULink) + expect(dappInfo.source).toBeUndefined() + }) + + it('migrates walletconnect source to requestType', () => { + const state = { + transactions: { + '0x123': { + '1': { + tx1: { + typeInfo: { + externalDappInfo: { source: 'walletconnect' }, + }, + }, + }, + }, + }, + } + const result = migrateDappRequestInfoTypes(state) + const dappInfo = result.transactions['0x123']['1'].tx1.typeInfo.externalDappInfo + expect(dappInfo.requestType).toBe(DappRequestType.WalletConnectSessionRequest) + expect(dappInfo.source).toBeUndefined() + }) + + it('migrates WCConfirm dapp to dappRequestInfo', () => { + const state = { + transactions: { + '0x123': { + '1': { + tx1: { + typeInfo: { + type: TransactionType.WCConfirm, + dapp: { name: 'TestDapp' }, + }, + }, + }, + }, + }, + } + const result = migrateDappRequestInfoTypes(state) + const typeInfo = result.transactions['0x123']['1'].tx1.typeInfo + expect(typeInfo.dappRequestInfo).toEqual({ name: 'TestDapp' }) + expect(typeInfo.dapp).toBeUndefined() + }) + + it('handles missing transactions state', () => { + const state = { otherData: 'preserved' } + const result = migrateDappRequestInfoTypes(state) + expect(result.otherData).toBe('preserved') + }) + + it('falls back to empty transactions on error', () => { + const state = { + transactions: createThrowingProxy({}, { throwingMethods: ['*'] }), + otherData: 'preserved', + } + const result = migrateDappRequestInfoTypes(state) + expect(result.transactions).toEqual({}) + expect(result.otherData).toBe('preserved') + }) +}) + +describe('migrateAndRemoveCloudBackupSlice', () => { + it('migrates cloudBackup email to wallet and removes cloudBackup', () => { + const state = { + cloudBackup: { + backupsFound: [{ email: 'test@example.com' }], + }, + wallet: {}, + } + const result = migrateAndRemoveCloudBackupSlice(state) + expect(result.wallet.androidCloudBackupEmail).toBe('test@example.com') + expect(result.cloudBackup).toBeUndefined() + }) + + it('removes cloudBackup without email if no backup has email', () => { + const state = { + cloudBackup: { backupsFound: [] }, + wallet: {}, + } + const result = migrateAndRemoveCloudBackupSlice(state) + expect(result.wallet.androidCloudBackupEmail).toBeUndefined() + expect(result.cloudBackup).toBeUndefined() + }) + + it('falls back to removing cloudBackup on error', () => { + const state = { + cloudBackup: createThrowingProxy({}, { throwingMethods: ['*'] }), + wallet: {}, + } + const result = migrateAndRemoveCloudBackupSlice(state) + expect(result.cloudBackup).toBeUndefined() + }) +}) diff --git a/apps/mobile/src/app/mobileMigrations.ts b/apps/mobile/src/app/mobileMigrations.ts new file mode 100644 index 00000000000..e38e907e92e --- /dev/null +++ b/apps/mobile/src/app/mobileMigrations.ts @@ -0,0 +1,1113 @@ +// Type information currently gets lost after a migration +/* oxlint-disable typescript/no-explicit-any -- Migration logic requires flexible typing */ +/* oxlint-disable typescript/explicit-function-return-type */ +/* oxlint-disable typescript/no-unsafe-return */ +/* oxlint-disable max-lines */ + +import dayjs from 'dayjs' +import { AccountType } from 'uniswap/src/features/accounts/types' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toSupportedChainId } from 'uniswap/src/features/chains/utils' +import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' +import { Language } from 'uniswap/src/features/language/constants' +import { getNFTAssetKey } from 'uniswap/src/features/nfts/utils' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { type TransactionsState } from 'uniswap/src/features/transactions/slice' +import { + type ChainIdToTxIdToDetails, + TransactionStatus, + TransactionType, +} from 'uniswap/src/features/transactions/types/transactionDetails' +import { getWalletDeviceLanguage } from 'uniswap/src/i18n/utils' +import { createSafeMigrationFactory } from 'uniswap/src/state/createSafeMigration' +import { DappRequestType } from 'uniswap/src/types/walletConnect' +import { type Account } from 'wallet/src/features/wallet/accounts/types' +import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' + +const createSafeMigration = createSafeMigrationFactory('mobileMigrations') + +export const OLD_DEMO_ACCOUNT_ADDRESS = '0xdd0E380579dF30E38524F9477808d9eE37E2dEa6' + +export const restructureTransactionsAndNotifications = createSafeMigration({ + name: 'restructureTransactionsAndNotifications', + migrate: (state: any) => { + const oldTransactionState = state?.transactions + const newTransactionState: any = {} + + const chainIds = Object.keys(oldTransactionState?.byChainId ?? {}) + for (const chainId of chainIds) { + const transactions = oldTransactionState.byChainId?.[chainId] ?? [] + const txIds = Object.keys(transactions) + for (const txId of txIds) { + const txDetails = transactions[txId] + const address = txDetails.from + newTransactionState[address] ??= {} + newTransactionState[address][chainId] ??= {} + newTransactionState[address][chainId][txId] = { ...txDetails } + } + } + + const oldNotificationState = state.notifications + const newNotificationState = { ...oldNotificationState, lastTxNotificationUpdate: {} } + const addresses = Object.keys(oldTransactionState?.lastTxHistoryUpdate || []) + for (const address of addresses) { + newNotificationState.lastTxNotificationUpdate[address] = { + [UniverseChainId.Mainnet]: oldTransactionState.lastTxHistoryUpdate[address], + } + } + + return { ...state, transactions: newTransactionState, notifications: newNotificationState } + }, + onError: (state: any) => ({ + ...state, + transactions: {}, + notifications: { ...state.notifications, lastTxNotificationUpdate: {} }, + }), +}) + +export function removeWalletConnectModalState(state: any) { + const newState = { ...state } + delete newState.walletConnect?.modalState + return newState +} + +export function renameFollowedAddressesToWatchedAddresses(state: any) { + const newState = { ...state } + const oldFollowingAddresses = state?.favorites?.followedAddresses + if (oldFollowingAddresses) { + newState.favorites.watchedAddresses = oldFollowingAddresses + } + delete newState?.favorites?.followedAddresses + return newState +} + +export function addSearchHistory(state: any) { + const newState = { ...state } + newState.searchHistory = { results: [] } + return newState +} + +export function addTimeImportedAndDerivationIndex(state: any) { + const newState = { ...state } + const accounts = newState?.wallet?.accounts ?? {} + let derivationIndex = 0 + for (const account of Object.keys(accounts)) { + newState.wallet.accounts[account].timeImportedMs = dayjs().valueOf() + if (newState.wallet.accounts[account].type === 'native') { + newState.wallet.accounts[account].derivationIndex = derivationIndex + derivationIndex += 1 + } + } + return newState +} + +export function addModalsState(state: any) { + const newState = { ...state } + newState.modals = { + [ModalName.WalletConnectScan]: { + isOpen: false, + initialState: 0, + }, + [ModalName.Swap]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.Send]: { + isOpen: false, + initialState: undefined, + }, + } + + delete newState?.balances + return newState +} + +export function addWalletConnectPendingSessionAndSettings(state: any) { + const newState = { ...state } + newState.walletConnect = { ...newState.walletConnect, pendingSession: null } + newState.wallet = { ...newState.wallet, settings: {} } + + delete newState?.wallet?.bluetooth + return newState +} + +export function removeNonZeroDerivationIndexAccounts(state: any) { + const newState = { ...state } + const accounts = newState?.wallet?.accounts ?? {} + const originalAccountValues = Object.keys(accounts) + for (const account of originalAccountValues) { + if (accounts[account].type === 'native' && accounts[account].derivationIndex !== 0) { + delete accounts[account] + } else if (accounts[account].type === 'native' && accounts[account].derivationIndex === 0) { + accounts[account].mnemonicId = accounts[account].address + } + } + return newState +} + +export function addCloudBackup(state: any) { + const newState = { ...state } + newState.cloudBackup = { backupsFound: [] } + return newState +} + +export function removeLocalTypeAccounts(state: any) { + const newState = { ...state } + const accounts = newState?.wallet?.accounts ?? {} + for (const account of Object.keys(accounts)) { + if (newState.wallet?.accounts?.[account]?.type === 'local') { + delete newState.wallet.accounts[account] + } + } + return newState +} + +export function removeDemoAccount(state: any) { + const newState = { ...state } + const accounts = newState?.wallet?.accounts ?? {} + + if (accounts[OLD_DEMO_ACCOUNT_ADDRESS]) { + delete accounts[OLD_DEMO_ACCOUNT_ADDRESS] + } + + return newState +} + +export function addBiometricSettings(state: any) { + const newState = { ...state } + newState.biometricSettings = { + requiredForAppAccess: false, + requiredForTransactions: false, + } + + return newState +} + +export function addPushNotificationsEnabledToAccounts(state: any) { + const accounts: Record | undefined = state?.wallet?.accounts + const newAccounts = Object.values(accounts ?? {}).map((account: Account) => { + const newAccount = { ...account } + newAccount.pushNotificationsEnabled = false + return newAccount + }) + + const newAccountObj = newAccounts.reduce>((accountObj, account) => { + accountObj[account.address] = account + return accountObj + }, {}) + + const newState = { ...state } + newState.wallet = { ...state.wallet, accounts: newAccountObj } + return newState +} + +export function addEnsState(state: any) { + const newState = { ...state } + newState.ens = { ensForAddress: {} } + return newState +} + +export const migrateBiometricSettings = createSafeMigration({ + name: 'migrateBiometricSettings', + migrate: (state: any) => { + const newState = { ...state } + newState.biometricSettings = { + requiredForAppAccess: state.wallet?.isBiometricAuthEnabled ?? false, + requiredForTransactions: state.wallet?.isBiometricAuthEnabled ?? false, + } + delete newState.wallet?.isBiometricAuthEnabled + return newState + }, + onError: (state: any) => ({ + ...state, + biometricSettings: { + requiredForAppAccess: false, + requiredForTransactions: false, + }, + }), +}) + +export function changeNativeTypeToSignerMnemonic(state: any) { + const newState = { ...state } + const accounts = newState?.wallet?.accounts ?? {} + for (const account of Object.keys(accounts)) { + if (newState.wallet.accounts[account].type === 'native') { + newState.wallet.accounts[account].type = AccountType.SignerMnemonic + } + } + return newState +} + +export function removeDataApi(state: any) { + const newState = { ...state } + delete newState.dataApi + return newState +} + +export function resetPushNotificationsEnabled(state: any) { + const accounts: Record | undefined = state?.wallet?.accounts + if (!accounts) { + return state + } + + for (const account of Object.values(accounts)) { + account.pushNotificationsEnabled = false + } + + const newState = { ...state } + newState.wallet = { ...state.wallet, accounts } + return newState +} + +export function removeEnsState(state: any) { + const newState = { ...state } + delete newState.ens + return newState +} + +export function filterToSupportedChains(state: any) { + const newState = { ...state } + + const chainState: + | { + byChainId: Partial> + } + | undefined = newState?.chains + const newChainState = Object.keys(chainState?.byChainId ?? {}).reduce<{ + byChainId: Partial> + }>( + (tempState, chainIdString) => { + const chainId = toSupportedChainId(chainIdString) + if (!chainId) { + return tempState + } + + const chainInfo = chainState?.byChainId[chainId] + if (!chainInfo) { + return tempState + } + + tempState.byChainId[chainId] = chainInfo + return tempState + }, + { byChainId: {} }, + ) + + const blockState: any | undefined = newState?.blocks + const newBlockState = Object.keys(blockState?.byChainId ?? {}).reduce( + (tempState, chainIdString) => { + const chainId = toSupportedChainId(chainIdString) + if (!chainId) { + return tempState + } + + const blockInfo = blockState?.byChainId[chainId] + if (!blockInfo) { + return tempState + } + + tempState.byChainId[chainId] = blockInfo + return tempState + }, + { byChainId: {} }, + ) + + const transactionState: TransactionsState | undefined = newState?.transactions + const newTransactionState = Object.keys(transactionState ?? {}).reduce((tempState, address) => { + const txs = transactionState?.[address] + if (!txs) { + return tempState + } + + const newAddressTxState = Object.keys(txs).reduce((tempAddressState, chainIdString) => { + const chainId = toSupportedChainId(chainIdString) + if (!chainId) { + return tempAddressState + } + + const txInfo = txs[chainId] + if (!txInfo) { + return tempAddressState + } + + tempAddressState[chainId] = txInfo + return tempAddressState + }, {}) + + tempState[address] = newAddressTxState + return tempState + }, {}) + + return { + ...newState, + chains: newChainState, + blocks: newBlockState, + transactions: newTransactionState, + } +} + +export function resetLastTxNotificationUpdate(state: any) { + const newState = { ...state } + newState.notifications = { ...state?.notifications, lastTxNotificationUpdate: {} } + return newState +} + +export function addExperimentsSlice(state: any) { + const newState = { ...state } + return { + ...newState, + experiments: { experiments: {}, featureFlags: {} }, + } +} + +export function removeCoingeckoApiAndTokenLists(state: any) { + const newState = { ...state } + delete newState.coingeckoApi + delete newState.tokens?.watchedTokens + delete newState.tokens?.tokenPairs + return newState +} + +export function resetTokensOrderByAndMetadataDisplayType(state: any) { + const newState = { ...state } + delete newState.wallet.settings?.tokensOrderBy + delete newState.wallet.settings?.tokensMetadataDisplayType + return newState +} + +export const transformNotificationCountToStatus = createSafeMigration({ + name: 'transformNotificationCountToStatus', + migrate: (state: any) => { + const newState = { ...state } + const notificationCount = state.notifications?.notificationCount + const notificationStatus = Object.keys(notificationCount ?? {}).reduce((obj, address) => { + const count = notificationCount[address] + if (count) { + return { ...obj, [address]: true } + } + + return { ...obj, [address]: false } + }, {}) + + delete newState.notifications?.notificationCount + newState.notifications = { ...newState.notifications, notificationStatus } + return newState + }, + onError: (state: any) => ({ + ...state, + notifications: { ...state?.notifications, notificationStatus: {} }, + }), +}) + +export function addPasswordLockout(state: any) { + return { + ...state, + passwordLockout: { passwordAttempts: 0 }, + } +} + +export function removeShowSmallBalances(state: any) { + const newState = { ...state } + delete newState.wallet.settings.showSmallBalances + return newState +} + +export function resetTokensOrderBy(state: any) { + const newState = { ...state } + delete newState.wallet.settings.tokensOrderBy + return newState +} + +export function removeTokensMetadataDisplayType(state: any) { + const newState = { ...state } + delete newState.wallet.settings.tokensMetadataDisplayType + return newState +} + +export function removeTokenListsAndCustomTokens(state: any) { + const newState = { ...state } + delete newState.tokenLists + delete newState.tokens?.customTokens + return newState +} + +export const migrateFiatPurchaseTransactionInfo = createSafeMigration({ + name: 'migrateFiatPurchaseTransactionInfo', + migrate: (state: any) => { + const newState = { ...state } + + const oldTransactionState = state?.transactions + const newTransactionState: any = {} + + const addresses = Object.keys(oldTransactionState ?? {}) + for (const address of addresses) { + const chainIds = Object.keys(oldTransactionState[address] ?? {}) + for (const chainId of chainIds) { + const transactions = oldTransactionState[address][chainId] + const txIds = Object.keys(transactions ?? {}) + + for (const txId of txIds) { + const txDetails = transactions[txId] + + if (!txDetails) { + continue + } + + if (txDetails.typeInfo?.type !== TransactionType.FiatPurchaseDeprecated) { + newTransactionState[address] ??= {} + newTransactionState[address][chainId] ??= {} + newTransactionState[address][chainId][txId] = txDetails + + continue + } + + if (txDetails.status === TransactionStatus.Failed) { + continue + } + + const { + explorerUrl, + outputTokenAddress, + outputCurrencyAmountFormatted, + outputCurrencyAmountPrice, + syncedWithBackend, + } = txDetails.typeInfo + + const newTypeInfo = { + type: TransactionType.FiatPurchaseDeprecated, + explorerUrl, + inputCurrency: undefined, + inputCurrencyAmount: outputCurrencyAmountFormatted / outputCurrencyAmountPrice, + outputCurrency: { + type: 'crypto', + metadata: { chainId: undefined, contractAddress: outputTokenAddress }, + }, + outputCurrencyAmount: undefined, + syncedWithBackend, + } + + newTransactionState[address] ??= {} + newTransactionState[address][chainId] ??= {} + newTransactionState[address][chainId][txId] = { ...txDetails, typeInfo: newTypeInfo } + } + } + } + + return { ...newState, transactions: newTransactionState } + }, + onError: (state: any) => ({ ...state, transactions: {} }), +}) + +export function emptyMigration(state: any) { + return state +} + +export function resetEnsApi(state: any) { + const newState = { ...state } + + delete newState.ENS + + return newState +} + +export function addReplaceAccountOptions(state: any) { + const newState = { ...state } + + newState.wallet.replaceAccountOptions = { + isReplacingAccount: false, + skipToSeedPhrase: false, + } + return newState +} + +export function addLastBalancesReport(state: any) { + const newState = { ...state } + + newState.telemetry = { + lastBalancesReport: 0, + } + return newState +} + +export function addAppearanceSetting(state: any) { + const newState = { ...state } + + newState.appearanceSettings = { + selectedAppearanceSettings: 'system', + } + return newState +} + +export function addHiddenNfts(state: any) { + const newState = { ...state } + + newState.favorites = { + ...state.favorites, + hiddenNfts: {}, + } + return newState +} + +export const correctFailedFiatOnRampTxIds = createSafeMigration({ + name: 'correctFailedFiatOnRampTxIds', + migrate: (state: any) => { + const newState = { ...state } + + const oldTransactionState = state?.transactions + const newTransactionState: any = {} + + const addresses = Object.keys(oldTransactionState ?? {}) + for (const address of addresses) { + const chainIds = Object.keys(oldTransactionState[address] ?? {}) + for (const chainId of chainIds) { + const transactions = oldTransactionState[address][chainId] + const txIds = Object.keys(transactions ?? {}) + + for (const txId of txIds) { + const txDetails = transactions[txId] + + if (!txDetails) { + continue + } + + newTransactionState[address] ??= {} + newTransactionState[address][chainId] ??= {} + newTransactionState[address][chainId][txId] = + txDetails.typeInfo?.type === TransactionType.FiatPurchaseDeprecated && + txDetails.status === TransactionStatus.Failed + ? { + ...txDetails, + typeInfo: { + ...txDetails.typeInfo, + id: txDetails.typeInfo?.explorerUrl?.split('=')?.[1], + }, + } + : txDetails + } + } + } + return { ...newState, transactions: newTransactionState } + }, + onError: (state: any) => ({ ...state, transactions: {} }), +}) + +export function removeReplaceAccountOptions(state: any) { + const newState = { ...state } + delete newState.wallet.replaceAccountOptions + return newState +} + +export function removeExperimentsSlice(state: any) { + const newState = { ...state } + delete newState.experiments + return newState +} + +export function removePersistedWalletConnectSlice(state: any) { + const newState = { ...state } + delete newState.walletConnect + return newState +} + +export function addLastBalancesReportValue(state: any) { + const newState = { ...state } + + newState.telemetry = { + ...state.telemetry, + lastBalancesReportValue: 0, + } + return newState +} + +export function removeFlashbotsEnabledFromWalletSlice(state: any) { + const newState = { ...state } + + delete newState.wallet.flashbotsEnabled + + return newState +} + +export const convertHiddenNftsToNftsData = createSafeMigration({ + name: 'convertHiddenNftsToNftsData', + migrate: (state: any) => { + const newState = { ...state } + + const accountAddresses = Object.keys(state.favorites?.hiddenNfts ?? {}) + + type AccountToNftData = Record> + + const nftsData: AccountToNftData = {} + for (const accountAddress of accountAddresses) { + // oxlint-disable-next-line typescript/no-unnecessary-condition + nftsData[accountAddress] ??= {} + const hiddenNftKeys = Object.keys(state.favorites.hiddenNfts[accountAddress]) + + for (const hiddenNftKey of hiddenNftKeys) { + const [, nftKey, tokenId] = hiddenNftKey.split('.') + + const newNftKey = nftKey && tokenId && getNFTAssetKey(nftKey, tokenId) + + const accountNftsData = nftsData[accountAddress] + + if (newNftKey) { + accountNftsData[newNftKey] = { isHidden: true } + } + } + } + + newState.favorites = { + ...state.favorites, + nftsData, + } + delete newState.favorites.hiddenNfts + return newState + }, + onError: (state: any) => ({ + ...state, + favorites: { + ...state?.favorites, + nftsData: {}, + }, + }), +}) + +export function removeProviders(state: any) { + const newState = { ...state } + + delete newState.providers + + return newState +} + +export function addTokensVisibility(state: any) { + const newState = { ...state } + + newState.favorites = { + ...state.favorites, + tokensVisibility: {}, + } + return newState +} + +export function deleteRTKQuerySlices(state: any) { + const newState = { ...state } + + delete newState.ENS + delete newState.ens + delete newState.gasApi + delete newState.onChainBalanceApi + delete newState.routingApi + delete newState.trmApi + + return newState +} + +export function resetActiveChains(state: any) { + const newState = { ...state } + + newState.chains.byChainId = { + '1': { isActive: true }, + '10': { isActive: true }, + '56': { isActive: true }, + '137': { isActive: true }, + '8453': { isActive: true }, + '42161': { isActive: true }, + } + + return newState +} + +export function addTweaksStartingState(state: any) { + const newState = { ...state } + + newState.tweaks = {} + + return newState +} + +export function addSwapProtectionSetting(state: any) { + const newState = { ...state } + newState.wallet.settings = { + ...state.wallet.settings, + swapProtection: SwapProtectionSetting.On, + } + return newState +} + +export function deleteChainsSlice(state: any) { + const newState = { ...state } + delete newState.chains + return newState +} + +export function addLanguageSettings(state: any) { + return { + ...state, + languageSettings: { currentLanguage: Language.English }, + } +} + +export function addFiatCurrencySettings(state: any) { + return { + ...state, + fiatCurrencySettings: { currentCurrency: FiatCurrency.UnitedStatesDollar }, + } +} + +export function updateLanguageSettings(state: any) { + return { + ...state, + languageSettings: { currentLanguage: Language.English }, + } +} + +export function addWalletIsFunded(state: any) { + const newState = { ...state } + + newState.telemetry = { + ...state.telemetry, + walletIsFunded: false, + } + + return newState +} + +export function addBehaviorHistory(state: any) { + const newState = { ...state } + + newState.behaviorHistory = { + hasViewedReviewScreen: false, + hasSubmittedHoldToSwap: false, + } + + return newState +} + +export function addAllowAnalyticsSwitch(state: any) { + const newState = { ...state } + + newState.telemetry = { + ...state.telemetry, + allowAnalytics: true, + lastHeartbeat: 0, + } + + return newState +} + +export const moveSettingStateToGlobal = createSafeMigration({ + name: 'moveSettingStateToGlobal', + migrate: (state: any) => { + const newState = { ...state } + + const accounts = newState?.wallet?.accounts ?? {} + const firstAccountKey = Object.keys(accounts)[0] + + const hideSmallBalances = firstAccountKey ? !accounts[firstAccountKey].showSmallBalances : true + const hideSpamTokens = firstAccountKey ? !accounts[firstAccountKey].showSpamTokens : true + + newState.wallet = { + ...newState.wallet, + settings: { + ...newState.wallet?.settings, + hideSmallBalances, + hideSpamTokens, + }, + } + + const accountKeys = Object.keys(accounts ?? {}) + for (const accountKey of accountKeys) { + delete accounts[accountKey].showSmallBalances + delete accounts[accountKey].showSpamTokens + } + + return newState + }, + onError: (state: any) => ({ + ...state, + wallet: { + ...state?.wallet, + settings: { + ...state?.wallet?.settings, + hideSmallBalances: true, + hideSpamTokens: true, + }, + }, + }), +}) + +export function addSkippedUnitagBoolean(state: any) { + const newState = { ...state } + + newState.behaviorHistory = { + ...state.behaviorHistory, + hasSkippedUnitagPrompt: false, + } + + return newState +} + +export function addCompletedUnitagsIntroBoolean(state: any) { + const newState = { ...state } + + newState.behaviorHistory = { + ...state.behaviorHistory, + hasCompletedUnitagsIntroModal: false, + } + + return newState +} + +export function addUniconV2IntroModalBoolean(state: any) { + const newState = { ...state } + + newState.behaviorHistory = { + ...state.behaviorHistory, + hasViewedUniconV2IntroModal: false, + } + + return newState +} + +export const flattenTokenVisibility = createSafeMigration({ + name: 'flattenTokenVisibility', + migrate: (state: any) => { + const newState = { ...state } + + type AccountToNftData = Record> + type NFTKeyToVisibility = Record + + type AccountToTokenVisibility = Record> + type CurrencyIdToVisibility = Record + + const tokenVisibilityByAccount: AccountToTokenVisibility = state.favorites?.tokensVisibility ?? {} + const flattenedTokenVisibility: CurrencyIdToVisibility = Object.values(tokenVisibilityByAccount).reduce( + (acc, currencyIdToVisibility) => ({ ...acc, ...currencyIdToVisibility }), + {}, + ) + + const nftDataByAccount: AccountToNftData = state.favorites?.nftsData ?? {} + const flattenedNFTData = Object.values(nftDataByAccount).reduce( + (acc, nftIdToVisibility) => ({ ...acc, ...nftIdToVisibility }), + {}, + ) + + const flattenedTransformedNFTData: NFTKeyToVisibility = Object.keys(flattenedNFTData).reduce( + (acc, nftKey) => { + const { isHidden, isSpamIgnored } = flattenedNFTData[nftKey] ?? {} + return { + ...acc, + [nftKey]: { isVisible: isHidden === false || isSpamIgnored === true }, + } + }, + {}, + ) + + newState.favorites = { + ...state.favorites, + tokensVisibility: flattenedTokenVisibility, + nftsVisibility: flattenedTransformedNFTData, + } + + delete newState.favorites.nftsData + + return newState + }, + onError: (state: any) => ({ + ...state, + favorites: { + ...state?.favorites, + tokensVisibility: {}, + nftsVisibility: {}, + }, + }), +}) + +export function addExtensionOnboardingState(state: any) { + const newState = { ...state } + + newState.behaviorHistory = { + ...state.behaviorHistory, + extensionOnboardingState: 'Undefined', + } + + return newState +} + +export function resetOnboardingStateForGA(state: any) { + const newState = { ...state } + + newState.behaviorHistory = { + ...state.behaviorHistory, + extensionOnboardingState: 'Undefined', + } + + return newState +} + +export const deleteOldOnRampTxData = createSafeMigration({ + name: 'deleteOldOnRampTxData', + migrate: (state: any) => { + const newState = { ...state } + + const transactionsState = newState.transactions + + const addresses = Object.keys(transactionsState ?? {}) + for (const address of addresses) { + const chainIds = Object.keys(transactionsState[address] ?? {}) + for (const chainId of chainIds) { + const transactions = transactionsState[address][chainId] + const txIds = Object.keys(transactions ?? {}) + for (const txId of txIds) { + if (transactions[txId]?.typeInfo?.type === TransactionType.FiatPurchaseDeprecated) { + delete transactionsState[address][chainId][txId] + } + } + } + } + + return { ...newState, transactions: transactionsState } + }, + onError: (state: any) => ({ ...state, transactions: {} }), +}) + +export const addPushNotifications = createSafeMigration({ + name: 'addPushNotifications', + migrate: (state: any) => { + const hasAllWalletNotifsDisabled = Object.values(state.wallet?.accounts ?? {}).every( + (account) => + account && + typeof account === 'object' && + 'pushNotificationsEnabled' in account && + !account.pushNotificationsEnabled, + ) + + return { + ...state, + pushNotifications: { + generalUpdatesEnabled: !hasAllWalletNotifsDisabled, + priceAlertsEnabled: !hasAllWalletNotifsDisabled, + }, + } + }, + onError: (state: any) => ({ + ...state, + pushNotifications: { + generalUpdatesEnabled: true, + priceAlertsEnabled: true, + }, + }), +}) + +export const migrateDappRequestInfoTypes = createSafeMigration({ + name: 'migrateDappRequestInfoTypes', + // oxlint-disable-next-line complexity -- biome-parity: oxlint is stricter here + migrate: (state: any) => { + const newState = { ...state } + + if (!newState?.transactions) { + return newState + } + + const newTransactionState = {} as Record + + for (const [address, chainIdToTxIdToDetails] of Object.entries(newState.transactions as Record)) { + for (const [chainId, txIdToDetails] of Object.entries(chainIdToTxIdToDetails as Record)) { + for (const [txId, details] of Object.entries(txIdToDetails as Record)) { + let newDetails = { ...details } + + if (details.typeInfo?.externalDappInfo?.source === 'uwulink') { + newDetails = { + ...details, + typeInfo: { + ...details.typeInfo, + externalDappInfo: { + ...details.typeInfo.externalDappInfo, + requestType: DappRequestType.UwULink, + }, + }, + } + } + + if (details.typeInfo?.externalDappInfo?.source === 'walletconnect') { + newDetails = { + ...details, + typeInfo: { + ...details.typeInfo, + externalDappInfo: { + ...details.typeInfo.externalDappInfo, + requestType: DappRequestType.WalletConnectSessionRequest, + }, + }, + } + } + + if (details.typeInfo?.type === TransactionType.WCConfirm && details.typeInfo?.dapp) { + newDetails.typeInfo.dappRequestInfo = { + ...details.typeInfo.dapp, + } + } + + delete newDetails.typeInfo?.dapp + delete newDetails.typeInfo?.externalDappInfo?.source + + newTransactionState[address] ??= {} + newTransactionState[address][chainId] ??= {} + newTransactionState[address][chainId][txId] = newDetails + } + } + } + + return { + ...newState, + transactions: newTransactionState, + } + }, + onError: (state: any) => ({ ...state, transactions: {} }), +}) + +export const migrateAndRemoveCloudBackupSlice = createSafeMigration({ + name: 'migrateAndRemoveCloudBackupSlice', + migrate: (state: any) => { + const newState = { ...state } + const backupEmail = newState.cloudBackup?.backupsFound?.find((backup: any) => backup.email)?.email + if (backupEmail) { + newState.wallet = { + ...newState.wallet, + androidCloudBackupEmail: backupEmail, + } + } + delete newState.cloudBackup + + return newState + }, + onError: (state: any) => { + const fallbackState = { ...state } + delete fallbackState.cloudBackup + return fallbackState + }, +}) + +export const setWalletDeviceLanguage = createSafeMigration({ + name: 'setWalletDeviceLanguage', + migrate: (state: any) => { + if (!state?.userSettings) { + return state + } + + return { + ...state, + userSettings: { + ...state.userSettings, + currentLanguage: getWalletDeviceLanguage(), + }, + } + }, + onError: (state: any) => ({ + ...state, + userSettings: { + ...state?.userSettings, + currentLanguage: Language.English, + }, + }), +}) diff --git a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx index de176e99439..aa6cb7b89f3 100644 --- a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx +++ b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx @@ -1,4 +1,5 @@ import { useIsFocused } from '@react-navigation/core' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -12,9 +13,13 @@ import { Button, Flex, Text, TouchableArea, useSporeColors } from 'ui/src' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { spacing } from 'ui/src/theme' import { AddressDisplay } from 'uniswap/src/components/accounts/AddressDisplay' +import { buildWrappedUrl } from 'uniswap/src/components/banners/shared/utils' +import { UniswapWrapped2025Card } from 'uniswap/src/components/banners/UniswapWrapped2025Card/UniswapWrapped2025Card' import { ActionSheetModal, MenuItemProp } from 'uniswap/src/components/modals/ActionSheetModal' import { Modal } from 'uniswap/src/components/modals/Modal' -import { AccountType, DisplayNameType } from 'uniswap/src/features/accounts/types' +import { UNISWAP_WEB_URL } from 'uniswap/src/constants/urls' +import { AccountType } from 'uniswap/src/features/accounts/types' +import { setHasDismissedUniswapWrapped2025Banner } from 'uniswap/src/features/behaviorHistory/slice' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { ElementName, ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' @@ -23,13 +28,15 @@ import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { ImportType, OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { MobileScreens, OnboardingScreens } from 'uniswap/src/types/screens/mobile' import { areAddressesEqual } from 'uniswap/src/utils/addresses' +import { openUri } from 'uniswap/src/utils/linking' +import { logger } from 'utilities/src/logger/logger' import { isAndroid } from 'utilities/src/platform' import { PlusCircle } from 'wallet/src/components/icons/PlusCircle' import { createOnboardingAccount } from 'wallet/src/features/onboarding/createOnboardingAccount' import { BackupType } from 'wallet/src/features/wallet/accounts/types' import { hasBackup } from 'wallet/src/features/wallet/accounts/utils' import { createAccountsActions } from 'wallet/src/features/wallet/create/createAccountsSaga' -import { useActiveAccountAddress, useDisplayName, useNativeAccountExists } from 'wallet/src/features/wallet/hooks' +import { useActiveAccountAddress, useNativeAccountExists } from 'wallet/src/features/wallet/hooks' import { selectAllAccountsSorted, selectSortedSignerMnemonicAccounts } from 'wallet/src/features/wallet/selectors' import { setAccountAsActive } from 'wallet/src/features/wallet/slice' @@ -59,9 +66,8 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme const hasImportedSeedPhrase = useNativeAccountExists() const isModalOpen = useIsFocused() const { openWalletRestoreModal, walletRestoreType } = useWalletRestore() - const displayName = useDisplayName(activeAccountAddress) - const activeAccountHasENS = displayName?.type === DisplayNameType.ENS + const isWrappedBannerEnabled = useFeatureFlag(FeatureFlags.UniswapWrapped2025) const sortedMnemonicAccounts = useSelector(selectSortedSignerMnemonicAccounts) @@ -99,6 +105,21 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme }) } + const onPressWrappedCard = useCallback(async () => { + if (!activeAccountAddress) { + return + } + + try { + const url = buildWrappedUrl(UNISWAP_WEB_URL, activeAccountAddress) + await openUri({ uri: url, openExternalBrowser: true }) + onClose() + dispatch(setHasDismissedUniswapWrapped2025Banner(true)) + } catch (error) { + logger.error(error, { tags: { file: 'AccountSwitcherModal', function: 'onPressWrappedCard' } }) + } + }, [activeAccountAddress, onClose, dispatch]) + const addWalletOptions = useMemo(() => { const createAdditionalAccount = async (): Promise => { // Generate new account @@ -270,19 +291,22 @@ export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Eleme size={spacing.spacing60 - spacing.spacing4} variant="subheading1" /> - {!activeAccountHasENS && ( + {isWrappedBannerEnabled && ( - + )} + + + void +} + +export function BackupReminderModal({ onClose: externalOnClose }: BackupReminderModalProps): JSX.Element { const { t } = useTranslation() const dispatch = useDispatch() const closedByButtonRef = useRef(false) - const { onClose } = useReactNavigationModal() + const { onClose: navigationOnClose } = useReactNavigationModal() + const onClose = externalOnClose ?? navigationOnClose const { convertFiatAmountFormatted } = useLocalizationContext() const activeAddress = useActiveAccountAddress() diff --git a/apps/mobile/src/app/modals/BridgedAssetWarningWrapper.tsx b/apps/mobile/src/app/modals/BridgedAssetWarningWrapper.tsx index 6b6c3ade50d..89cf251e362 100644 --- a/apps/mobile/src/app/modals/BridgedAssetWarningWrapper.tsx +++ b/apps/mobile/src/app/modals/BridgedAssetWarningWrapper.tsx @@ -1,11 +1,10 @@ import { AppStackScreenProp } from 'src/app/navigation/types' import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal' import { BridgedAssetModal } from 'uniswap/src/components/BridgedAsset/BridgedAssetModal' -import { checkIsBridgedAsset } from 'uniswap/src/components/BridgedAsset/utils' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { ModalName } from 'uniswap/src/features/telemetry/constants' -import { useDismissedBridgedAssetWarnings } from 'uniswap/src/features/tokens/slice/hooks' import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' +import { useDismissedBridgedAssetWarnings } from 'uniswap/src/features/tokens/warnings/slice/hooks' import { currencyIdToAddress, currencyIdToChain, isNativeCurrencyAddress } from 'uniswap/src/utils/currencyId' export function BridgedAssetWarningWrapper({ @@ -36,7 +35,7 @@ export function BridgedAssetWarningWrapper({ return null } - const isBridgedAsset = checkIsBridgedAsset(currencyInfo) + const isBridgedAsset = Boolean(currencyInfo.isBridged) // If token is not bridged or warning was dismissed and not blocked, skip warning and proceed to SwapFlow if (!isBridgedAsset || bridgedAssetWarningDismissed) { diff --git a/apps/mobile/src/app/modals/KoreaCexTransferInfoModal.tsx b/apps/mobile/src/app/modals/KoreaCexTransferInfoModal.tsx index 3625621af1e..985dd210473 100644 --- a/apps/mobile/src/app/modals/KoreaCexTransferInfoModal.tsx +++ b/apps/mobile/src/app/modals/KoreaCexTransferInfoModal.tsx @@ -1,6 +1,7 @@ -import React from 'react' +import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal' +import { useOpenReceiveModal } from 'src/features/modals/hooks/useOpenReceiveModal' import { Button, Flex, Image, Text, useIsDarkMode, useSporeColors } from 'ui/src' import { CEX_TRANSFER_MODAL_BG_DARK, CEX_TRANSFER_MODAL_BG_LIGHT } from 'ui/src/assets' import { Modal } from 'uniswap/src/components/modals/Modal' @@ -15,6 +16,12 @@ export function KoreaCexTransferInfoModal(): JSX.Element { const { t } = useTranslation() const isDarkMode = useIsDarkMode() const { onClose } = useReactNavigationModal() + const openReceiveModal = useOpenReceiveModal() + + const onPressReceive = useCallback(() => { + onClose() + openReceiveModal() + }, [onClose, openReceiveModal]) return ( @@ -33,14 +40,18 @@ export function KoreaCexTransferInfoModal(): JSX.Element { {t('fiatOnRamp.cexTransferModal.description')} - + + diff --git a/apps/mobile/src/app/modals/LazyModalRenderer.tsx b/apps/mobile/src/app/modals/LazyModalRenderer.tsx index f9fab34f1ea..b788790935b 100644 --- a/apps/mobile/src/app/modals/LazyModalRenderer.tsx +++ b/apps/mobile/src/app/modals/LazyModalRenderer.tsx @@ -1,6 +1,6 @@ import { useDispatch, useSelector } from 'react-redux' -import { ModalsState } from 'src/features/modals/ModalsState' import { closeModal } from 'src/features/modals/modalSlice' +import { ModalsState } from 'src/features/modals/ModalsState' import { selectModalState } from 'src/features/modals/selectModalState' import { ErrorBoundary } from 'wallet/src/components/ErrorBoundary/ErrorBoundary' diff --git a/apps/mobile/src/app/modals/SwapModal.test.tsx b/apps/mobile/src/app/modals/SwapModal.test.tsx index 34dbcdf1daa..b35f86a69e3 100644 --- a/apps/mobile/src/app/modals/SwapModal.test.tsx +++ b/apps/mobile/src/app/modals/SwapModal.test.tsx @@ -5,7 +5,6 @@ import { AppStackScreenProp } from 'src/app/navigation/types' import { persistedReducer } from 'src/app/store' import { preloadedMobileState } from 'src/test/fixtures' import { renderWithProviders } from 'src/test/render' -import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' import { ModalName } from 'uniswap/src/features/telemetry/constants' // Mock required modules with simpler implementation @@ -42,7 +41,7 @@ describe('SwapModal', () => { middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: false, // Disable serialization check for tests - }).concat(fiatOnRampAggregatorApi.middleware), + }), }) const tree = renderWithProviders(, { diff --git a/apps/mobile/src/app/modals/SwapModal.tsx b/apps/mobile/src/app/modals/SwapModal.tsx index 30ac6b5d8e1..5f8c19debf5 100644 --- a/apps/mobile/src/app/modals/SwapModal.tsx +++ b/apps/mobile/src/app/modals/SwapModal.tsx @@ -8,8 +8,8 @@ import { useBiometricAppSettings } from 'src/features/biometrics/useBiometricApp import { useOsBiometricAuthEnabled } from 'src/features/biometrics/useOsBiometricAuthEnabled' import { useBiometricPrompt } from 'src/features/biometricsSettings/hooks' import { useWalletRestore } from 'src/features/wallet/useWalletRestore' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' -import { clearNotificationQueue } from 'uniswap/src/features/notifications/slice/slice' +import { clearNotificationsByType } from 'uniswap/src/features/notifications/slice/slice' +import { AppNotificationType } from 'uniswap/src/features/notifications/slice/types' import { useHapticFeedback } from 'uniswap/src/features/settings/useHapticFeedback/useHapticFeedback' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { updateSwapStartTimestamp } from 'uniswap/src/features/timing/slice' @@ -17,22 +17,21 @@ import { useSwapPrefilledState } from 'uniswap/src/features/transactions/swap/fo import { logger } from 'utilities/src/logger/logger' import { WalletSwapFlow } from 'wallet/src/features/transactions/swap/WalletSwapFlow' import { invalidateAndRefetchWalletDelegationQueries } from 'wallet/src/features/transactions/watcher/transactionFinalizationSaga' -import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' export function SwapModal({ route }: AppStackScreenProp): JSX.Element { const appDispatch = useDispatch() const initialState = route.params const { hapticFeedback } = useHapticFeedback() - const signerMnemonicAccounts = useSignerAccounts() - const chains = useEnabledChains() - const accountAddresses = signerMnemonicAccounts.map((account) => account.address) - const { onClose: onCloseModal } = useReactNavigationModal() - // Clear all notification toasts when the swap modal closes + // Clear network change notification toasts when the swap modal closes const onClose = useCallback(() => { - appDispatch(clearNotificationQueue()) + appDispatch( + clearNotificationsByType({ + types: [AppNotificationType.NetworkChanged, AppNotificationType.NetworkChangedBridge], + }), + ) onCloseModal() }, [appDispatch, onCloseModal]) @@ -40,10 +39,10 @@ export function SwapModal({ route }: AppStackScreenProp): useEffect(() => { const timestamp = Date.now() appDispatch(updateSwapStartTimestamp({ timestamp })) - invalidateAndRefetchWalletDelegationQueries({ accountAddresses, chainIds: chains.chains }).catch((error) => + invalidateAndRefetchWalletDelegationQueries().catch((error) => logger.debug('SwapModal', 'useEffect', 'Failed to invalidate and refetch wallet delegation queries', error), ) - }, [appDispatch, accountAddresses, chains.chains]) + }, [appDispatch]) const { openWalletRestoreModal, walletRestoreType } = useWalletRestore() diff --git a/apps/mobile/src/app/modals/TokenWarningModalWrapper.tsx b/apps/mobile/src/app/modals/TokenWarningModalWrapper.tsx index 8fb9d9ed3fe..ea871b10104 100644 --- a/apps/mobile/src/app/modals/TokenWarningModalWrapper.tsx +++ b/apps/mobile/src/app/modals/TokenWarningModalWrapper.tsx @@ -3,9 +3,10 @@ import { useReactNavigationModal } from 'src/components/modals/useReactNavigatio import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { TokenList } from 'uniswap/src/features/dataApi/types' import { ModalName } from 'uniswap/src/features/telemetry/constants' -import { useDismissedTokenWarnings } from 'uniswap/src/features/tokens/slice/hooks' -import TokenWarningModal from 'uniswap/src/features/tokens/TokenWarningModal' import { useCurrencyInfo } from 'uniswap/src/features/tokens/useCurrencyInfo' +import { getTokenProtectionWarning } from 'uniswap/src/features/tokens/warnings/safetyUtils' +import { useDismissedTokenWarnings } from 'uniswap/src/features/tokens/warnings/slice/hooks' +import TokenWarningModal from 'uniswap/src/features/tokens/warnings/TokenWarningModal' import { currencyIdToAddress, currencyIdToChain, isNativeCurrencyAddress } from 'uniswap/src/utils/currencyId' export function TokenWarningModalWrapper({ @@ -18,11 +19,13 @@ export function TokenWarningModalWrapper({ const currencyChainId = (currencyId && currencyIdToChain(currencyId)) || defaultChainId const currencyAddress = currencyId ? currencyIdToAddress(currencyId) : undefined const currencyInfo = useCurrencyInfo(currencyId) + const tokenProtectionWarning = getTokenProtectionWarning(currencyInfo) // Get the token info only if we have a valid non-native currency const isNativeCurrency = isNativeCurrencyAddress(currencyChainId, currencyAddress) const { tokenWarningDismissed } = useDismissedTokenWarnings( isNativeCurrency || !currencyAddress ? undefined : { chainId: currencyChainId, address: currencyAddress }, + tokenProtectionWarning, ) // Return null if modal state is malformed diff --git a/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap b/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap index 701d8f51372..f21d8327dce 100644 --- a/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap +++ b/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap @@ -23,7 +23,6 @@ exports[`AccountSwitcher renders correctly 1`] = ` } > - - + - - + + + + - + @@ -172,7 +144,7 @@ exports[`AccountSwitcher renders correctly 1`] = ` style={ { "flexDirection": "column", - "flexGrow": 1, + "flexShrink": 1, "gap": 0, } } @@ -180,10 +152,8 @@ exports[`AccountSwitcher renders correctly 1`] = ` @@ -192,6 +162,7 @@ exports[`AccountSwitcher renders correctly 1`] = ` { "alignItems": "center", "flexDirection": "row", + "flexGrow": 1, "flexShrink": 1, "gap": 4, "justifyContent": "center", @@ -206,17 +177,13 @@ exports[`AccountSwitcher renders correctly 1`] = ` numberOfLines={1} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "flexShrink": 1, "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", "lineHeight": 24, + "textAlign": "center", } } suppressHighlighting={true} @@ -225,207 +192,240 @@ exports[`AccountSwitcher renders correctly 1`] = ` - - 0x​82D56A...373Fa6 - + + 0x​82D56A...373Fa6 + - - - - - + strokeWidth={8} + > + + + + @@ -445,12 +445,18 @@ exports[`AccountSwitcher renders correctly 1`] = ` @@ -513,16 +555,9 @@ exports[`AccountSwitcher renders correctly 1`] = ` line-height-disabled="true" maxFontSizeMultiplier={1.2} numberOfLines={1} - onBlur={[Function]} - onFocus={[Function]} style={ { - "color": { - "dynamic": { - "dark": "#FFFFFF", - "light": "#131313", - }, - }, + "color": "#131313", "fontFamily": "Basel Grotesk", "fontSize": 17, "fontWeight": "500", @@ -586,11 +621,35 @@ exports[`AccountSwitcher renders correctly 1`] = ` collapsable={false} focusVisibleStyle={{}} forwardedRef={[Function]} + jestAnimatedProps={ + { + "value": {}, + } + } jestAnimatedStyle={ { "value": {}, } } + jestInlineStyle={ + [ + { + "backgroundColor": "transparent", + "borderBottomLeftRadius": 12, + "borderBottomRightRadius": 12, + "borderTopLeftRadius": 12, + "borderTopRightRadius": 12, + "flexDirection": "column", + "marginTop": 16, + "opacity": 1, + "transform": [ + { + "scale": 1, + }, + ], + }, + ] + } onBlur={[Function]} onClick={[Function]} onFocus={[Function]} @@ -603,27 +662,28 @@ exports[`AccountSwitcher renders correctly 1`] = ` onStartShouldSetResponder={[Function]} role="button" style={ - { - "backgroundColor": "transparent", - "borderBottomLeftRadius": 12, - "borderBottomRightRadius": 12, - "borderTopLeftRadius": 12, - "borderTopRightRadius": 12, - "flexDirection": "column", - "marginTop": 16, - "opacity": 1, - "transform": [ - { - "scale": 1, - }, - ], - } + [ + { + "backgroundColor": "transparent", + "borderBottomLeftRadius": 12, + "borderBottomRightRadius": 12, + "borderTopLeftRadius": 12, + "borderTopRightRadius": 12, + "flexDirection": "column", + "marginTop": 16, + "opacity": 1, + "transform": [ + { + "scale": 1, + }, + ], + }, + {}, + ] } testID="account-switcher-add-wallet" > = { @@ -61,17 +58,11 @@ export const monitoredSagas: Record = { reducer: executeSwapReducer, actions: executeSwapActions, }, - [swapSagaName]: { - name: swapSagaName, - wrappedSaga: swapSaga, - reducer: swapReducer, - actions: swapActions, - }, - [tokenWrapSagaName]: { - name: tokenWrapSagaName, - wrappedSaga: tokenWrapSaga, - reducer: tokenWrapReducer, - actions: tokenWrapActions, + [executePlanSagaName]: { + name: executePlanSagaName, + wrappedSaga: executePlanSaga, + reducer: executePlanReducer, + actions: executePlanActions, }, [removeDelegationSagaName]: { name: removeDelegationSagaName, diff --git a/apps/mobile/src/app/navigation/ExploreStackNavigator.tsx b/apps/mobile/src/app/navigation/ExploreStackNavigator.tsx index cb04801094d..f6bcc947174 100644 --- a/apps/mobile/src/app/navigation/ExploreStackNavigator.tsx +++ b/apps/mobile/src/app/navigation/ExploreStackNavigator.tsx @@ -8,9 +8,7 @@ import { ExploreStackParamList } from 'src/app/navigation/types' import { HorizontalEdgeGestureTarget } from 'src/components/layout/screens/EdgeGestureTarget' import { ExploreScreen } from 'src/screens/ExploreScreen' import { ExternalProfileScreen } from 'src/screens/ExternalProfileScreen' -import { NFTCollectionScreen } from 'src/screens/NFTCollectionScreen' -import { NFTItemScreen } from 'src/screens/NFTItemScreen' -import { TokenDetailsScreen } from 'src/screens/TokenDetailsScreen' +import { TokenDetailsScreen } from 'src/screens/TokenDetailsScreen/TokenDetailsScreen' import { useSporeColors } from 'ui/src' import { MobileScreens } from 'uniswap/src/types/screens/mobile' @@ -52,10 +50,6 @@ export function ExploreStackNavigator({ {(props): JSX.Element => } - - {(props): JSX.Element => } - - diff --git a/apps/mobile/src/app/navigation/NavBar.tsx b/apps/mobile/src/app/navigation/NavBar.tsx index f0101480883..0e61a86a972 100644 --- a/apps/mobile/src/app/navigation/NavBar.tsx +++ b/apps/mobile/src/app/navigation/NavBar.tsx @@ -57,7 +57,7 @@ export function NavBar(): JSX.Element { const colors = useSporeColors() const isDarkMode = useIsDarkMode() - // biome-ignore lint/correctness/useExhaustiveDependencies: we want to ignore isNarrow because of unknown reason + // oxlint-disable-next-line react/exhaustive-deps -- we want to ignore isNarrow because of unknown reason useEffect(() => { if (isNarrow || !exploreButtonLayout?.width || !swapButtonLayout?.width) { return @@ -66,6 +66,7 @@ export function NavBar(): JSX.Element { // When the 2 buttons overflow, we set `isNarrow` to true and adjust the design accordingly. // To test this, you can use an iPhone Mini set to Spanish. setIsNarrow(exploreButtonLayout.width + swapButtonLayout.width + NAV_BAR_GAP + NAV_BAR_MARGIN_SIDES > screenWidth) + // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here }, [exploreButtonLayout?.width, swapButtonLayout?.width, screenWidth]) const onExploreLayout = useCallback((e: LayoutChangeEvent) => setExploreButtonLayout(e.nativeEvent.layout), []) @@ -121,7 +122,7 @@ type SwapTabBarButtonProps = { onSwapLayout: (event: LayoutChangeEvent) => void } -const SwapFAB = memo(function _SwapFAB({ activeScale = 0.96, onSwapLayout }: SwapTabBarButtonProps) { +const SwapFAB = memo(function SwapFABInner({ activeScale = 0.96, onSwapLayout }: SwapTabBarButtonProps) { const { t } = useTranslation() const { defaultChainId } = useEnabledChains() const { hapticFeedback } = useHapticFeedback() diff --git a/apps/mobile/src/app/navigation/NavigationContainer.tsx b/apps/mobile/src/app/navigation/NavigationContainer.tsx index 3300418d114..42ecef4f228 100644 --- a/apps/mobile/src/app/navigation/NavigationContainer.tsx +++ b/apps/mobile/src/app/navigation/NavigationContainer.tsx @@ -91,7 +91,7 @@ export const NavigationContainer: FC> = ({ children, on const useManageDeepLinks = (): void => { const dispatch = useDispatch() - const urlListener = useRef() + const urlListener = useRef(undefined) const deepLinkMutation = useMutation({ mutationFn: async () => { diff --git a/apps/mobile/src/app/navigation/components.tsx b/apps/mobile/src/app/navigation/components.tsx index 37d7c9a488a..ba872ab85c2 100644 --- a/apps/mobile/src/app/navigation/components.tsx +++ b/apps/mobile/src/app/navigation/components.tsx @@ -2,18 +2,15 @@ import { useTranslation } from 'react-i18next' import { BackButton } from 'src/components/buttons/BackButton' import { Text, TouchableArea } from 'ui/src' import { RotatableChevron } from 'ui/src/components/icons' -import { iconSizes } from 'ui/src/theme' import { ElementName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' import { TestID } from 'uniswap/src/test/fixtures/testIDs' export const renderHeaderBackButton = (): JSX.Element => ( - + ) -export const renderHeaderBackImage = (): JSX.Element => ( - -) +export const renderHeaderBackImage = (): JSX.Element => export const HeaderSkipButton = ({ onPress }: { onPress: () => void }): JSX.Element => { const { t } = useTranslation() diff --git a/apps/mobile/src/app/navigation/constants.ts b/apps/mobile/src/app/navigation/constants.ts new file mode 100644 index 00000000000..778a6d63580 --- /dev/null +++ b/apps/mobile/src/app/navigation/constants.ts @@ -0,0 +1,2 @@ +// Some pages in react native navigation require a delay before the modal is opened +export const MODAL_OPEN_WAIT_TIME = 300 diff --git a/apps/mobile/src/app/navigation/hooks.ts b/apps/mobile/src/app/navigation/hooks.ts index e58ecff8179..e49c8fbcc4a 100644 --- a/apps/mobile/src/app/navigation/hooks.ts +++ b/apps/mobile/src/app/navigation/hooks.ts @@ -62,6 +62,7 @@ export function useEagerExternalProfileRootNavigation(): EagerExternalProfileRoo ) const navigate = useEvent(async (address: string, callback?: () => void) => { + // oxlint-disable-next-line typescript/await-thenable -- biome-parity: oxlint is stricter here await rootNavigate(MobileScreens.ExternalProfile, { address }) callback?.() }) diff --git a/apps/mobile/src/app/navigation/navStackOptions.ts b/apps/mobile/src/app/navigation/navStackOptions.ts index 6b1aa993a07..55ca1cab932 100644 --- a/apps/mobile/src/app/navigation/navStackOptions.ts +++ b/apps/mobile/src/app/navigation/navStackOptions.ts @@ -1,7 +1,7 @@ import { NativeStackNavigationOptions } from '@react-navigation/native-stack' import { StackNavigationOptions } from '@react-navigation/stack' -export const navNativeStackOptions: Record = { +export const navNativeStackOptions = { noHeader: { headerShown: false }, presentationModal: { presentation: 'modal' }, presentationBottomSheet: { @@ -16,8 +16,8 @@ export const navNativeStackOptions: Record headerShown: false, animation: 'slide_from_right', }, -} +} as const satisfies Record -export const navStackOptions: Record = { +export const navStackOptions = { noHeader: { headerShown: false }, -} +} as const satisfies Record diff --git a/apps/mobile/src/app/navigation/navigation.tsx b/apps/mobile/src/app/navigation/navigation.tsx index d59a3a4d5d1..0bb90c370bf 100644 --- a/apps/mobile/src/app/navigation/navigation.tsx +++ b/apps/mobile/src/app/navigation/navigation.tsx @@ -1,6 +1,7 @@ import { NavigationContainer, NavigationIndependentTree } from '@react-navigation/native' import { createNativeStackNavigator } from '@react-navigation/native-stack' import { createStackNavigator, TransitionPresets } from '@react-navigation/stack' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useEffect } from 'react' import { DevSettings } from 'react-native' import { INCLUDE_PROTOTYPE_FEATURES, IS_E2E_TEST } from 'react-native-dotenv' @@ -23,22 +24,25 @@ import { navNativeStackOptions, navStackOptions } from 'src/app/navigation/navSt import { TabsNavigator } from 'src/app/navigation/tabs/TabsNavigator' import { startTracking, stopTracking } from 'src/app/navigation/trackingHelpers' import { - AppStackParamList, - FiatOnRampStackParamList, - OnboardingStackParamList, - SettingsStackParamList, + type AppStackParamList, + type FiatOnRampStackParamList, + type OnboardingStackParamList, + type SettingsStackParamList, useAppStackNavigation, } from 'src/app/navigation/types' +import { FiatOnRampActionModal } from 'src/components/home/FiatOnRampActionModal' import { FundWalletModal } from 'src/components/home/introCards/FundWalletModal' import { HorizontalEdgeGestureTarget } from 'src/components/layout/screens/EdgeGestureTarget' import { AdvancedSettingsModal } from 'src/components/modals/ReactNavigationModals/AdvancedSettingsModal' import { BridgedAssetModalScreen } from 'src/components/modals/ReactNavigationModals/BridgedAssetModal' import { HiddenTokenInfoModalScreen } from 'src/components/modals/ReactNavigationModals/HiddenTokenInfoModalScreen' -import { LanguageSettingsScreen } from 'src/components/modals/ReactNavigationModals/LanguageSettingsScreen' import { PasskeyHelpModalScreen } from 'src/components/modals/ReactNavigationModals/PasskeyHelpModalScreen' import { PasskeyManagementModalScreen } from 'src/components/modals/ReactNavigationModals/PasskeyManagementModalScreen' import { PermissionsSettingsScreen } from 'src/components/modals/ReactNavigationModals/PermissionsSettingsScreen' import { PortfolioBalanceSettingsScreen } from 'src/components/modals/ReactNavigationModals/PortfolioBalanceSettingsScreen' +import { ReportPortfolioDataModalScreen } from 'src/components/modals/ReactNavigationModals/ReportPortfolioDataModalScreen' +import { ReportTokenDataModalScreen } from 'src/components/modals/ReactNavigationModals/ReportTokenDataModalScreen' +import { ReportTokenIssueModalScreen } from 'src/components/modals/ReactNavigationModals/ReportTokenIssueModalScreen' import { SmartWalletEnabledModalScreen } from 'src/components/modals/ReactNavigationModals/SmartWalletEnabledModalScreen' import { SmartWalletNudgeScreen } from 'src/components/modals/ReactNavigationModals/SmartWalletNudgeScreen' import { TestnetModeModalScreen } from 'src/components/modals/ReactNavigationModals/TestnetModeModalScreen' @@ -64,6 +68,7 @@ import { EditUnitagProfileScreen } from 'src/features/unitags/EditUnitagProfileS import { UnitagChooseProfilePicScreen } from 'src/features/unitags/UnitagChooseProfilePicScreen' import { UnitagConfirmationScreen } from 'src/features/unitags/UnitagConfirmationScreen' import { AppLoadingScreen } from 'src/screens/AppLoadingScreen' +import { DebugScreensScreen } from 'src/screens/DebugScreensScreen' import { DevScreen } from 'src/screens/DevScreen' import { EducationScreen } from 'src/screens/EducationScreen' import { ExternalProfileScreen } from 'src/screens/ExternalProfileScreen' @@ -82,8 +87,6 @@ import { RestoreMethodScreen } from 'src/screens/Import/RestoreMethodScreen' import { SeedPhraseInputScreen } from 'src/screens/Import/SeedPhraseInputScreen/SeedPhraseInputScreen' import { SelectWalletScreen } from 'src/screens/Import/SelectWalletScreen' import { WatchWalletScreen } from 'src/screens/Import/WatchWalletScreen' -import { NFTCollectionScreen } from 'src/screens/NFTCollectionScreen' -import { NFTItemScreen } from 'src/screens/NFTItemScreen' import { BackupScreen } from 'src/screens/Onboarding/BackupScreen' import { CloudBackupPasswordConfirmScreen } from 'src/screens/Onboarding/CloudBackupPasswordConfirmScreen' import { CloudBackupPasswordCreateScreen } from 'src/screens/Onboarding/CloudBackupPasswordCreateScreen' @@ -99,19 +102,19 @@ import { SettingsCloudBackupPasswordCreateScreen } from 'src/screens/SettingsClo import { SettingsCloudBackupProcessingScreen } from 'src/screens/SettingsCloudBackupProcessingScreen' import { SettingsCloudBackupStatus } from 'src/screens/SettingsCloudBackupStatus' import { SettingsFiatCurrencyModal } from 'src/screens/SettingsFiatCurrencyModal' +import { SettingsLanguageModal } from 'src/screens/SettingsLanguageModal' import { SettingsNotificationsScreen } from 'src/screens/SettingsNotificationsScreen' import { SettingsPrivacyScreen } from 'src/screens/SettingsPrivacyScreen' import { SettingsScreen } from 'src/screens/SettingsScreen' import { SettingsSmartWalletScreen } from 'src/screens/SettingsSmartWalletScreen' +import { SettingsStorageScreen } from 'src/screens/SettingsStorageScreen' import { SettingsViewSeedPhraseScreen } from 'src/screens/SettingsViewSeedPhraseScreen' import { SettingsWalletManageConnection } from 'src/screens/SettingsWalletManageConnection' -import { TokenDetailsScreen } from 'src/screens/TokenDetailsScreen' +import { TokenDetailsScreen } from 'src/screens/TokenDetailsScreen/TokenDetailsScreen' import { ViewPrivateKeysScreen } from 'src/screens/ViewPrivateKeys/ViewPrivateKeysScreen' import { WebViewScreen } from 'src/screens/WebViewScreen' import { useSporeColors } from 'ui/src' import { spacing } from 'ui/src/theme' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { OnboardingEntryPoint } from 'uniswap/src/types/onboarding' @@ -120,7 +123,7 @@ import { MobileScreens, OnboardingScreens, UnitagScreens, - UnitagStackParamList, + type UnitagStackParamList, } from 'uniswap/src/types/screens/mobile' import { OnboardingContextProvider } from 'wallet/src/features/onboarding/OnboardingContext' import { selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors' @@ -158,6 +161,7 @@ function SettingsStackGroup(): JSX.Element { /> + + @@ -219,8 +224,6 @@ export function FiatOnRampStackNavigator(): JSX.Element { function OnboardingStackNavigator(): JSX.Element { const colors = useSporeColors() - const isOnboardingKeyringEnabled = useFeatureFlag(FeatureFlags.OnboardingKeyring) - return ( @@ -235,13 +238,11 @@ function OnboardingStackNavigator(): JSX.Element { animation: 'slide_from_right', }} > - {isOnboardingKeyringEnabled && ( - - )} + - - @@ -401,6 +400,7 @@ export function AppStackNavigator(): JSX.Element { + @@ -420,11 +420,15 @@ export function AppStackNavigator(): JSX.Element { + + + + @@ -435,7 +439,6 @@ export function AppStackNavigator(): JSX.Element { - @@ -448,7 +451,15 @@ export function AppStackNavigator(): JSX.Element { {__DEV__ && ((): JSX.Element => { const StorybookUIRoot = require('src/../.storybook').default - return + const { HashcashBenchmarkScreen } = require('src/screens/HashcashBenchmarkScreen') + const { SessionsDebugScreen } = require('src/screens/SessionsDebugScreen') + return ( + <> + + + + + ) })()} ) diff --git a/apps/mobile/src/app/navigation/rootNavigation.ts b/apps/mobile/src/app/navigation/rootNavigation.ts index 2463211be84..de54be4a1a6 100644 --- a/apps/mobile/src/app/navigation/rootNavigation.ts +++ b/apps/mobile/src/app/navigation/rootNavigation.ts @@ -25,7 +25,7 @@ export function navigate(...args: RootNav // Type assignment to `any` is a workaround until we figure out how to // type `createNavigationContainerRef` in a way that's compatible - // biome-ignore lint/suspicious/noExplicitAny: Navigation refs need flexible typing + // oxlint-disable-next-line typescript/no-explicit-any -- Navigation refs need flexible typing navigationRef.navigate(routeName as any, params as never) } diff --git a/apps/mobile/src/app/navigation/tabs/CustomTabBar/CustomTabBar.tsx b/apps/mobile/src/app/navigation/tabs/CustomTabBar/CustomTabBar.tsx index dc6727df6f5..d1b9cddd9bc 100644 --- a/apps/mobile/src/app/navigation/tabs/CustomTabBar/CustomTabBar.tsx +++ b/apps/mobile/src/app/navigation/tabs/CustomTabBar/CustomTabBar.tsx @@ -4,7 +4,7 @@ import type { LayoutChangeEvent } from 'react-native' import { useAnimatedStyle, useDerivedValue, withTiming } from 'react-native-reanimated' import { TAB_BAR_ANIMATION_DURATION, TAB_ITEMS } from 'src/app/navigation/tabs/CustomTabBar/constants' import { SwapButton } from 'src/app/navigation/tabs/SwapButton' -import { SwapLongPressModal } from 'src/app/navigation/tabs/SwapLongPressModal' +import { SwapLongPressOverlay } from 'src/app/navigation/tabs/SwapLongPressOverlay' import { Flex, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { iconSizes, spacing } from 'ui/src/theme' @@ -37,6 +37,7 @@ const TabItem = ({ tab, index, isFocused, onPress, colors }: TabItemProps): JSX. return ( - (null) + const longPressTimerRef = useRef(null) const hasTriggeredLongPressHaptic = useRef(false) const activeAccountAddress = useActiveAccountAddressWithThrow() diff --git a/apps/mobile/src/app/navigation/tabs/SwapLongPressModal.tsx b/apps/mobile/src/app/navigation/tabs/SwapLongPressModal.tsx deleted file mode 100644 index ef44031e018..00000000000 --- a/apps/mobile/src/app/navigation/tabs/SwapLongPressModal.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { BlurView } from 'expo-blur' -import { type ReactNode, useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { Modal, StyleSheet } from 'react-native' -import { useDispatch } from 'react-redux' -import { navigate } from 'src/app/navigation/rootNavigation' -import { ESTIMATED_BOTTOM_TABS_HEIGHT } from 'src/app/navigation/tabs/CustomTabBar/constants' -import { SwapButton } from 'src/app/navigation/tabs/SwapButton' -import { useOpenReceiveModal } from 'src/features/modals/hooks/useOpenReceiveModal' -import { openModal } from 'src/features/modals/modalSlice' -import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' -import { Bank, Buy, ReceiveAlt, SendAction } from 'ui/src/components/icons' -import { iconSizes } from 'ui/src/theme' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' -import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' -import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' -import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' -import { isAndroid } from 'utilities/src/platform' -import { useEvent } from 'utilities/src/react/hooks' - -const styles = StyleSheet.create({ - blurView: { - flex: 1, - }, -}) -interface SwapLongPressModalProps { - isVisible: boolean - onClose: () => void - onSwapLongPress: () => void -} - -export function SwapLongPressModal({ isVisible, onClose, onSwapLongPress }: SwapLongPressModalProps): JSX.Element { - const colors = useSporeColors() - const insets = useAppInsets() - const dispatch = useDispatch() - const { t } = useTranslation() - - const openReceiveModal = useOpenReceiveModal() - const { isTestnetModeEnabled } = useEnabledChains() - const disableForKorea = useFeatureFlag(FeatureFlags.DisableFiatOnRampKorea) - - const onBuyPress = useEvent(async (): Promise => { - sendAnalyticsEvent(MobileEventName.SwapLongPress, { element: 'buy' }) - - if (isTestnetModeEnabled) { - navigate(ModalName.TestnetMode, { - unsupported: true, - descriptionCopy: t('tdp.noTestnetSupportDescription'), - }) - return - } - disableForKorea - ? navigate(ModalName.KoreaCexTransferInfoModal) - : dispatch( - openModal({ - name: ModalName.FiatOnRampAggregator, - }), - ) - - onClose() - }) - - const onSellPress = useEvent(() => { - sendAnalyticsEvent(MobileEventName.SwapLongPress, { element: 'sell' }) - - if (isTestnetModeEnabled) { - navigate(ModalName.TestnetMode, { - unsupported: true, - descriptionCopy: t('tdp.noTestnetSupportDescription'), - }) - return - } - disableForKorea - ? navigate(ModalName.KoreaCexTransferInfoModal) - : dispatch( - openModal({ - name: ModalName.FiatOnRampAggregator, - initialState: { - isOfframp: true, - }, - }), - ) - - onClose() - }) - - const onReceivePress = useEvent(() => { - sendAnalyticsEvent(MobileEventName.SwapLongPress, { element: 'receive' }) - - openReceiveModal() - onClose() - }) - - const onSendPress = useEvent(() => { - sendAnalyticsEvent(MobileEventName.SwapLongPress, { element: 'send' }) - - dispatch(openModal({ name: ModalName.Send })) - onClose() - }) - - const swapMenuItems: { title: string; onPress: () => void; icon: ReactNode }[] = useMemo( - () => [ - { - title: t('common.button.receive'), - onPress: onReceivePress, - icon: , - }, - { - title: t('common.button.send'), - onPress: onSendPress, - icon: , - }, - { - title: t('common.button.buy'), - onPress: onBuyPress, - icon: , - }, - { - title: t('common.button.sell'), - onPress: onSellPress, - icon: , - }, - ], - [t, onReceivePress, onSendPress, onBuyPress, onSellPress, colors.accent1.val], - ) - - return ( - - - - - {swapMenuItems.map((item) => ( - - ))} - - {/* Swap Button as last item in the column */} - - - {t('common.button.swap')} - - - - - - - - ) -} - -function MenuItem({ title, icon, onPress }: { title: string; icon: ReactNode; onPress: () => void }): JSX.Element { - return ( - - - - {title} - - - {icon} - - - - ) -} diff --git a/apps/mobile/src/app/navigation/tabs/SwapLongPressOverlay.tsx b/apps/mobile/src/app/navigation/tabs/SwapLongPressOverlay.tsx new file mode 100644 index 00000000000..9b36790f87a --- /dev/null +++ b/apps/mobile/src/app/navigation/tabs/SwapLongPressOverlay.tsx @@ -0,0 +1,256 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' +import { BlurView } from 'expo-blur' +import { type ReactNode, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { StyleSheet, type ViewStyle } from 'react-native' +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' +import { useDispatch } from 'react-redux' +import { navigate } from 'src/app/navigation/rootNavigation' +import { ESTIMATED_BOTTOM_TABS_HEIGHT } from 'src/app/navigation/tabs/CustomTabBar/constants' +import { SwapButton } from 'src/app/navigation/tabs/SwapButton' +import { useOpenReceiveModal } from 'src/features/modals/hooks/useOpenReceiveModal' +import { openModal } from 'src/features/modals/modalSlice' +import { Flex, Text, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src' +import { Bank, Buy, ReceiveAlt, SendAction } from 'ui/src/components/icons' +import { iconSizes } from 'ui/src/theme' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' +import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' +import { isAndroid } from 'utilities/src/platform' +import { useEvent } from 'utilities/src/react/hooks' + +const ANIMATION_DURATION = 200 +const BASE_DELAY = 40 + +const AnimatedBlurView = Animated.createAnimatedComponent(BlurView) + +function AnimatedContainer({ + enteringDelay, + exitingDelay, + children, + style, + exitingDuration = ANIMATION_DURATION, +}: { + enteringDelay: number + exitingDelay: number + children: ReactNode + style?: ViewStyle + exitingDuration?: number +}): JSX.Element { + return ( + + {children} + + ) +} + +const styles = StyleSheet.create({ + blurView: { + height: '100%', + left: 0, + position: 'absolute', + top: 0, + width: '100%', + }, +}) +interface SwapLongPressOverlayProps { + isVisible: boolean + onClose: () => void + onSwapLongPress: () => void +} + +export function SwapLongPressOverlay({ + isVisible, + onClose, + onSwapLongPress, +}: SwapLongPressOverlayProps): JSX.Element | null { + const colors = useSporeColors() + const isDarkMode = useIsDarkMode() + const insets = useAppInsets() + const dispatch = useDispatch() + const { t } = useTranslation() + + const openReceiveModal = useOpenReceiveModal() + const { isTestnetModeEnabled } = useEnabledChains() + const disableForKorea = useFeatureFlag(FeatureFlags.DisableFiatOnRampKorea) + + const onBuyPress = useEvent(async (): Promise => { + sendAnalyticsEvent(MobileEventName.SwapLongPress, { element: 'buy' }) + + if (isTestnetModeEnabled) { + navigate(ModalName.TestnetMode, { + unsupported: true, + descriptionCopy: t('tdp.noTestnetSupportDescription'), + }) + return + } + disableForKorea + ? navigate(ModalName.KoreaCexTransferInfoModal) + : dispatch( + openModal({ + name: ModalName.FiatOnRampAggregator, + }), + ) + + onClose() + }) + + const onSellPress = useEvent(() => { + sendAnalyticsEvent(MobileEventName.SwapLongPress, { element: 'sell' }) + + if (isTestnetModeEnabled) { + navigate(ModalName.TestnetMode, { + unsupported: true, + descriptionCopy: t('tdp.noTestnetSupportDescription'), + }) + return + } + disableForKorea + ? navigate(ModalName.KoreaCexTransferInfoModal) + : dispatch( + openModal({ + name: ModalName.FiatOnRampAggregator, + initialState: { + isOfframp: true, + }, + }), + ) + + onClose() + }) + + const onReceivePress = useEvent(() => { + sendAnalyticsEvent(MobileEventName.SwapLongPress, { element: 'receive' }) + + openReceiveModal() + onClose() + }) + + const onSendPress = useEvent(() => { + sendAnalyticsEvent(MobileEventName.SwapLongPress, { element: 'send' }) + + dispatch(openModal({ name: ModalName.Send })) + onClose() + }) + + const swapMenuItems: { title: string; onPress: () => void; icon: ReactNode }[] = useMemo( + () => [ + { + title: t('common.button.receive'), + onPress: onReceivePress, + icon: , + }, + { + title: t('common.button.send'), + onPress: onSendPress, + icon: , + }, + { + title: t('common.button.buy'), + onPress: onBuyPress, + icon: , + }, + { + title: t('common.button.sell'), + onPress: onSellPress, + icon: , + }, + ], + [t, onReceivePress, onSendPress, onBuyPress, onSellPress, colors.accent1.val], + ) + + const NUM_OF_SWAP_MENU_ITEMS = swapMenuItems.length + const TOTAL_DELAY_FOR_EXIT_FROM_MENU_ITEMS = NUM_OF_SWAP_MENU_ITEMS * BASE_DELAY + + // Used for main container and SwapButton + const DELAY_FOR_MAIN_EXIT = TOTAL_DELAY_FOR_EXIT_FROM_MENU_ITEMS + ANIMATION_DURATION / 2 + + // We want to start animating the text out just before the main animation starts + const DELAY_FOR_SWAP_TEXT_EXIT = DELAY_FOR_MAIN_EXIT - ANIMATION_DURATION / 4 + + if (!isVisible) { + return null + } + + return ( + + + + {swapMenuItems.map((item, i) => { + const length = NUM_OF_SWAP_MENU_ITEMS + + // Wait for initial animation to complete, then animate each item in sequentially with a delay based on its position in the array. + const enteringDelay = ANIMATION_DURATION + (length - i) * BASE_DELAY + + // We want to start animating before the main animation starts + const exitingDelay = i * BASE_DELAY + + return ( + + + + ) + })} + + {/* Swap Button as last item in the column */} + + {/* We want to delay animating the text entering so the Swap button shows first, then the text animates in, followed by the actions + + Text animates out at the same time as the Swap button, so it looks like the text is animating in while the Swap button is animating out. + */} + + + {t('common.button.swap')} + + + {/* Swap Button doesn't animate in so it feels like the same button that was long pressed on HomeScreen is still there. It is the final thing to animate out (along with the Text above). */} + + + + + + + + ) +} + +function MenuItem({ title, icon, onPress }: { title: string; icon: ReactNode; onPress: () => void }): JSX.Element { + return ( + + + + {title} + + + {icon} + + + + ) +} diff --git a/apps/mobile/src/app/navigation/types.ts b/apps/mobile/src/app/navigation/types.ts index c4d0cf03be1..39715fdf221 100644 --- a/apps/mobile/src/app/navigation/types.ts +++ b/apps/mobile/src/app/navigation/types.ts @@ -21,9 +21,11 @@ import { ReceiveCryptoModalState } from 'src/screens/ReceiveCryptoModalState' import { ViewPrivateKeysScreenState } from 'src/screens/ViewPrivateKeys/ViewPrivateKeysScreenState' import { BridgedAssetModalProps } from 'uniswap/src/components/BridgedAsset/BridgedAssetModal' import { WormholeModalProps } from 'uniswap/src/components/BridgedAsset/WormholeModal' +import { ReportPortfolioDataModalProps } from 'uniswap/src/components/reporting/ReportPortfolioDataModal' +import { ReportTokenDataModalProps } from 'uniswap/src/components/reporting/ReportTokenDataModal' +import { ReportTokenModalProps } from 'uniswap/src/components/reporting/ReportTokenIssueModal' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { FORServiceProvider } from 'uniswap/src/features/fiatOnRamp/types' -import { NFTItem } from 'uniswap/src/features/nfts/types' import { PasskeyManagementModalState } from 'uniswap/src/features/passkey/PasskeyManagementModal' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { TestnetModeModalState } from 'uniswap/src/features/testnets/TestnetModeModal' @@ -41,14 +43,6 @@ import { SmartWalletEnabledModalState } from 'wallet/src/components/smartWallet/ import { SmartWalletNudgeState } from 'wallet/src/components/smartWallet/modals/SmartWalletNudge' import { ExploreOrderBy } from 'wallet/src/features/wallet/types' -type NFTItemScreenParams = { - owner?: Address - address: string - tokenId: string - isSpam?: boolean - fallbackData?: NFTItem -} - export type ExploreScreenParams = { showFavorites?: boolean orderByMetric?: ExploreOrderBy @@ -73,8 +67,6 @@ export type ExploreStackParamList = { [MobileScreens.ExternalProfile]: { address: string } - [MobileScreens.NFTItem]: NFTItemScreenParams - [MobileScreens.NFTCollection]: { collectionAddress: string } [MobileScreens.TokenDetails]: { currencyId: string } @@ -93,6 +85,7 @@ export type FiatOnRampStackParamList = { } export type SettingsStackParamList = { + [MobileScreens.DebugScreens]: undefined [MobileScreens.Dev]: undefined [MobileScreens.Settings]: undefined [MobileScreens.SettingsCloudBackupPasswordConfirm]: CloudBackupFormParams @@ -104,7 +97,8 @@ export type SettingsStackParamList = { [MobileScreens.SettingsNotifications]: undefined [MobileScreens.SettingsPrivacy]: undefined [MobileScreens.SettingsSmartWallet]: undefined - [MobileScreens.SettingsViewSeedPhrase]: { address: Address; walletNeedsRestore?: boolean } + [MobileScreens.SettingsStorage]: undefined + [MobileScreens.SettingsViewSeedPhrase]: { address?: Address; walletNeedsRestore?: boolean } | undefined [MobileScreens.SettingsWallet]: { address: Address } [MobileScreens.SettingsWalletEdit]: { address: Address } [MobileScreens.SettingsWalletManageConnection]: { address: Address } @@ -160,6 +154,8 @@ export type OnboardingStackParamList = { export type AppStackParamList = { [MobileScreens.Activity]: undefined + [MobileScreens.HashcashBenchmark]: undefined + [MobileScreens.SessionsDebug]: undefined [MobileScreens.Education]: { type: EducationContentType } & OnboardingStackBaseParams @@ -170,8 +166,6 @@ export type AppStackParamList = { [MobileScreens.TokenDetails]: { currencyId: string } - [MobileScreens.NFTItem]: NFTItemScreenParams - [MobileScreens.NFTCollection]: { collectionAddress: string } [MobileScreens.ExternalProfile]: { address: string } @@ -181,6 +175,7 @@ export type AppStackParamList = { [ModalName.Swap]: TransactionState | undefined [ModalName.Explore]: ExploreModalState | undefined [ModalName.NotificationsOSSettings]: undefined + [ModalName.FiatOnRampAction]: { entry: 'onramp' | 'offramp' } [ModalName.FundWallet]: undefined [ModalName.KoreaCexTransferInfoModal]: undefined [ModalName.ExchangeTransferModal]: { initialState: { serviceProvider: FORServiceProvider } } @@ -221,6 +216,9 @@ export type AppStackParamList = { [ModalName.ConfirmDisableSmartWalletScreen]: undefined [ModalName.BridgedAsset]: BridgedAssetModalProps [ModalName.Wormhole]: WormholeModalProps + [ModalName.ReportTokenIssue]: ReportTokenModalProps + [ModalName.ReportPortfolioData]: ReportPortfolioDataModalProps + [ModalName.ReportTokenData]: ReportTokenDataModalProps } export type AppStackNavigationProp = NativeStackNavigationProp diff --git a/apps/mobile/src/app/saga.ts b/apps/mobile/src/app/saga.ts index 90f3709a189..2e71b2b5ff6 100644 --- a/apps/mobile/src/app/saga.ts +++ b/apps/mobile/src/app/saga.ts @@ -15,7 +15,6 @@ import { signWcRequestSaga } from 'src/features/walletConnect/signWcRequestSaga' import { call, fork, join, spawn } from 'typed-redux-saga' import { waitForRehydration } from 'uniswap/src/utils/saga' import { apolloClientRef } from 'wallet/src/data/apollo/usePersistedApolloClient' -import { deviceLocaleWatcher } from 'wallet/src/features/i18n/deviceLocaleWatcherSaga' import { transactionWatcher } from 'wallet/src/features/transactions/watcher/transactionWatcherSaga' // These sagas are not persisted, so we can run them before rehydration @@ -32,7 +31,6 @@ const sagas = [ signWcRequestSaga, telemetrySaga, walletConnectSaga, - deviceLocaleWatcher, ] export function* rootMobileSaga(): SagaIterator { @@ -60,6 +58,6 @@ export function* rootMobileSaga(): SagaIterator { // Start monitored sagas for (const m of Object.values(monitoredSagas)) { - yield* spawn(m.wrappedSaga) + yield* spawn(m['wrappedSaga']) } } diff --git a/apps/mobile/src/app/schema.ts b/apps/mobile/src/app/schema.ts index b396fd537ad..198d48b96c7 100644 --- a/apps/mobile/src/app/schema.ts +++ b/apps/mobile/src/app/schema.ts @@ -1,4 +1,4 @@ -/* eslint-disable max-lines */ +/* oxlint-disable max-lines */ import { RankingType } from '@universe/api' import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' import { Language } from 'uniswap/src/features/language/constants' @@ -115,7 +115,7 @@ export const v4Schema = { ...v3Schema, } -// biome-ignore lint/correctness/noUnusedVariables: Destructuring for schema migration +// oxlint-disable-next-line no-unused-vars -- Destructuring for schema migration const { balances, ...restV4Schema } = v4Schema delete restV4Schema.favorites.followedAddresses @@ -207,7 +207,7 @@ export const v29Schema = { ...v28Schema } const v30Schema = { ...v29Schema } -// biome-ignore lint/correctness/noUnusedVariables: Destructuring for schema migration +// oxlint-disable-next-line no-unused-vars -- Destructuring for schema migration const { tokenLists, ...v31SchemaIntermediate } = { ...v30Schema } export const v31Schema = v31SchemaIntermediate @@ -267,7 +267,7 @@ delete v38SchemaIntermediate.experiments export const v39Schema = { ...v38SchemaIntermediate } -// biome-ignore lint/correctness/noUnusedVariables: walletConnect removed in schema migration +// oxlint-disable-next-line no-unused-vars -- walletConnect removed in schema migration const { walletConnect, ...v39SchemaIntermediate } = { ...v39Schema } export const v40Schema = { ...v39SchemaIntermediate } @@ -689,7 +689,7 @@ const v88SchemaIntermediate = { }, userSettings: { ...v87Schema.userSettings, - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition hapticsEnabled: v87Schema.appearanceSettings.hapticsEnabled ?? true, }, } @@ -721,8 +721,20 @@ delete v92SchemaIntermediate.cloudBackup export const v92Schema = v92SchemaIntermediate -const v93Schema = v92Schema +export const v93Schema = v92Schema + +export const v95Schema = { + ...v93Schema, + visibility: { + ...v93Schema.visibility, + activity: {}, + }, +} + +export const v96Schema = v95Schema + +const v97Schema = v96Schema // TODO: [MOB-201] use function with typed output when API reducers are removed from rootReducer // export const getSchema = (): RootState => v0Schema -export const getSchema = (): typeof v93Schema => v93Schema +export const getSchema = (): typeof v97Schema => v97Schema diff --git a/apps/mobile/src/app/store.ts b/apps/mobile/src/app/store.ts index ab4541ee681..30398fd7146 100644 --- a/apps/mobile/src/app/store.ts +++ b/apps/mobile/src/app/store.ts @@ -4,7 +4,6 @@ import { persistReducer, persistStore, Storage } from 'redux-persist' import { MOBILE_STATE_VERSION, migrations } from 'src/app/migrations' import { MobileState, mobilePersistedStateList, mobileReducer } from 'src/app/mobileReducer' import { rootMobileSaga } from 'src/app/saga' -import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' import { delegationListenerMiddleware } from 'uniswap/src/features/smartWallet/delegation/slice' import { isNonTestDev } from 'utilities/src/environment/constants' import { createDatadogReduxEnhancer } from 'utilities/src/logger/datadog/Datadog' @@ -53,11 +52,11 @@ if (isNonTestDev) { enhancers.push(reactotron.createEnhancer()) } -const middlewares: Middleware[] = [fiatOnRampAggregatorApi.middleware, delegationListenerMiddleware.middleware] +const middlewares: Middleware[] = [delegationListenerMiddleware.middleware] const setupStore = ( preloadedState?: PreloadedState, - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + // oxlint-disable-next-line typescript/explicit-function-return-type ) => { return createStore({ reducer: persistedReducer, diff --git a/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx b/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx index 0018779f299..170bb39e159 100644 --- a/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx +++ b/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx @@ -14,6 +14,7 @@ import { PriceNumberOfDigits, TokenSpotData, useTokenPriceHistory } from 'src/co import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext' import { useIsScreenNavigationReady } from 'src/utils/useIsScreenNavigationReady' import { Flex, SegmentedControl, Text } from 'ui/src' +import { useLayoutAnimationOnChange } from 'ui/src/animations' import GraphCurve from 'ui/src/assets/backgrounds/graph-curve.svg' import { spacing } from 'ui/src/theme' import { isLowVarianceRange } from 'uniswap/src/components/charts/utils' @@ -32,15 +33,20 @@ const LOW_VARIANCE_Y_PADDING = 100 type PriceTextProps = { loading: boolean - relativeChange?: SharedValue + relativeChange?: SharedValue numberOfDigits: PriceNumberOfDigits spotPrice?: SharedValue + startingPrice?: number + shouldTreatAsStablecoin?: boolean } const PriceTextSection = memo(function PriceTextSection({ loading, numberOfDigits, + relativeChange, spotPrice, + startingPrice, + shouldTreatAsStablecoin, }: PriceTextProps): JSX.Element { const price = useLineChartPrice(spotPrice) const currency = useAppFiatCurrencyInfo() @@ -62,9 +68,14 @@ const PriceTextSection = memo(function PriceTextSection({ We want both the animated number skeleton and the relative change skeleton to hide at the exact same time. When multiple skeletons hide in different order, it gives the feeling of things being slower than they actually are. */} - - + + ) }) @@ -80,7 +91,7 @@ function TimeRangeTraceWrapper({ ) } -export const PriceExplorer = memo(function _PriceExplorer(): JSX.Element { +export const PriceExplorer = memo(function PriceExplorerInner(): JSX.Element { const { isTestnetModeEnabled } = useEnabledChains() const { chartHeight, chartWidth } = useChartDimensions() @@ -88,10 +99,10 @@ export const PriceExplorer = memo(function _PriceExplorer(): JSX.Element { return } - return + return }) -const PriceExplorerInner = memo(function _PriceExplorerInner(): JSX.Element { +const PriceExplorerContent = memo(function PriceExplorerContentInner(): JSX.Element { const { currencyId, tokenColor, navigation } = useTokenDetailsContext() const isScreenNavigationReady = useIsScreenNavigationReady({ navigation }) @@ -136,6 +147,8 @@ const PriceExplorerInner = memo(function _PriceExplorerInner(): JSX.Element { return { lastPricePoint: priceHistory.length - 1, convertedPriceHistory: priceHistory } }, [data, conversionRate]) + useLayoutAnimationOnChange(convertedPriceHistory.length) + const convertedSpotValue = useDerivedValue(() => conversionRate * (data?.spot?.value.value ?? 0)) const convertedSpot = useMemo((): TokenSpotData | undefined => { return ( @@ -144,7 +157,8 @@ const PriceExplorerInner = memo(function _PriceExplorerInner(): JSX.Element { value: convertedSpotValue, } ) - }, [data, convertedSpotValue]) + // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here + }, [data]) // Zoom out y-axis for low variance assets const shouldZoomOut = useMemo(() => { @@ -177,6 +191,9 @@ const PriceExplorerInner = memo(function _PriceExplorerInner(): JSX.Element { return } + // Get the starting price for fiat delta calculation + const startingPrice = convertedPriceHistory[0]?.value + return ( @@ -185,6 +202,8 @@ const PriceExplorerInner = memo(function _PriceExplorerInner(): JSX.Element { numberOfDigits={numberOfDigits} relativeChange={convertedSpot?.relativeChange} spotPrice={convertedSpot?.value} + startingPrice={startingPrice} + shouldTreatAsStablecoin={shouldZoomOut} /> diff --git a/apps/mobile/src/components/PriceExplorer/Text.test.tsx b/apps/mobile/src/components/PriceExplorer/Text.test.tsx index c9bb1f43e76..f9f6ced4714 100644 --- a/apps/mobile/src/components/PriceExplorer/Text.test.tsx +++ b/apps/mobile/src/components/PriceExplorer/Text.test.tsx @@ -48,8 +48,8 @@ describe(PriceText, () => { const wholePart = await within(animatedText).findByTestId('wholePart') const decimalPart = await within(animatedText).findByTestId('decimalPart') - expect(wholePart.props.text).toBe(`$${amounts.sm().value}`) - expect(decimalPart.props.text).toBe(`.00`) + expect(wholePart.props['text']).toBe(`$${amounts.sm().value}`) + expect(decimalPart.props['text']).toBe(`.00`) }) }) @@ -88,7 +88,7 @@ describe(RelativeChangeText, () => { const tree = render() const text = await tree.findByTestId('relative-change-text') - expect(text.props.value).toBe(`10.00%`) + expect(text.props['text']).toBe(`10.00%`) }) }) @@ -100,6 +100,7 @@ describe(DatetimeText, () => { }) const tree = render() + expect(tree.toJSON()).toHaveStyle({ opacity: 1 }) expect(tree).toMatchSnapshot() }) @@ -110,6 +111,6 @@ describe(DatetimeText, () => { }) const tree = render() - expect(tree).toMatchSnapshot() + expect(tree.toJSON()).toHaveStyle({ opacity: 0 }) }) }) diff --git a/apps/mobile/src/components/PriceExplorer/Text.tsx b/apps/mobile/src/components/PriceExplorer/Text.tsx index 0da167fcdcd..b218fd804f8 100644 --- a/apps/mobile/src/components/PriceExplorer/Text.tsx +++ b/apps/mobile/src/components/PriceExplorer/Text.tsx @@ -1,9 +1,18 @@ -import React from 'react' -import { useAnimatedStyle } from 'react-native-reanimated' -import { useLineChartDatetime } from 'react-native-wagmi-charts' +import React, { useEffect } from 'react' +import Animated, { + cancelAnimation, + SharedValue, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + withTiming, +} from 'react-native-reanimated' +import { useLineChart, useLineChartDatetime } from 'react-native-wagmi-charts' import { AnimatedDecimalNumber } from 'src/components/PriceExplorer/AnimatedDecimalNumber' +import { useLineChartFiatDelta } from 'src/components/PriceExplorer/useFiatDelta' import { useLineChartPrice, useLineChartRelativeChange } from 'src/components/PriceExplorer/usePrice' import { AnimatedText } from 'src/components/text/AnimatedText' +import { numberToPercentWorklet } from 'src/utils/reanimated' import { Flex, Text, useSporeColors } from 'ui/src' import { AnimatedCaretChange } from 'ui/src/components/icons' import { FiatCurrency } from 'uniswap/src/features/fiatCurrency/constants' @@ -39,19 +48,94 @@ export function PriceText({ maxWidth }: { loading: boolean; maxWidth?: number }) ) } -export function RelativeChangeText({ loading }: { loading: boolean }): JSX.Element { +export function RelativeChangeText({ + loading, + spotRelativeChange, + startingPrice, + shouldTreatAsStablecoin = false, +}: { + loading: boolean + /** Price change for selected duration (used when not scrubbing chart) */ + spotRelativeChange?: SharedValue + startingPrice?: number + shouldTreatAsStablecoin?: boolean +}): JSX.Element { const colors = useSporeColors() + const { isActive } = useLineChart() - const relativeChange = useLineChartRelativeChange() + // Calculate relative change from chart data (used when scrubbing) + const calculatedRelativeChange = useLineChartRelativeChange() - const styles = useAnimatedStyle(() => ({ - color: relativeChange.value.value >= 0 ? colors.statusSuccess.val : colors.statusCritical.val, + const fiatDelta = useLineChartFiatDelta({ + startingPrice, + shouldTreatAsStablecoin, + }) + + // Decide which source to use: API's 24hr when idle, chart's when scrubbing + // This ensures the color shows immediately with correct API data + const hasSpotData = !!spotRelativeChange + const shouldUseSpotData = useDerivedValue(() => !isActive.value && hasSpotData) + + const relativeChange = useDerivedValue(() => { + return shouldUseSpotData.value + ? (spotRelativeChange?.value ?? calculatedRelativeChange.value.value) + : calculatedRelativeChange.value.value + }) + + const relativeChangeFormatted = useDerivedValue(() => { + if (shouldUseSpotData.value) { + return spotRelativeChange?.value + ? numberToPercentWorklet(spotRelativeChange.value, { precision: 2, absolute: true }) + : calculatedRelativeChange.formatted.value + } + return calculatedRelativeChange.formatted.value + }) + + // Shared value for fade-in animation; always start hidden since + // the component always mounts with loading=true + const contentOpacity = useSharedValue(0) + + useEffect(() => { + if (!loading) { + contentOpacity.value = withTiming(1, { duration: 200 }) + } else { + cancelAnimation(contentOpacity) + contentOpacity.value = 0 + } + // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here + }, [loading]) + + const animatedContentStyle = useAnimatedStyle(() => ({ + opacity: contentOpacity.value, })) + + const changeColor = useDerivedValue(() => { + // Round the range to 2 decimal places to check if is equal to 0 + const absRelativeChange = Math.round(Math.abs(relativeChange.value) * 100) + if (absRelativeChange === 0) { + return colors.neutral3.val + } + return relativeChange.value > 0 ? colors.statusSuccess.val : colors.statusCritical.val + }) + const caretStyle = useAnimatedStyle(() => ({ - color: relativeChange.value.value >= 0 ? colors.statusSuccess.val : colors.statusCritical.val, - transform: [{ rotate: relativeChange.value.value >= 0 ? '180deg' : '0deg' }], + color: changeColor.value, + transform: [ + { rotate: relativeChange.value >= 0 ? '180deg' : '0deg' }, + // fix vertical centering + { translateY: relativeChange.value >= 0 ? -1 : 1 }, + ], })) + // Combine fiat delta and percentage in a derived value + const combinedText = useDerivedValue(() => { + const delta = fiatDelta.formatted.value + if (delta) { + return `${delta} (${relativeChangeFormatted.value})` + } + return relativeChangeFormatted.value + }) + return ( - {loading ? ( + {loading && ( // We use `no-shimmer` here to speed up the first render and so that this skeleton renders // at the exact same time as the animated number skeleton. // TODO(WALL-5215): we can remove `no-shimmer` once we have a better Skeleton component. - ) : ( - <> - = 0 ? -1 : 1 }, - ]} - /> - - )} + {/* Must always mount this component to avoid stale values on initial render */} + + + + + + ) } -export function DatetimeText({ loading }: { loading: boolean }): JSX.Element | null { +export function DatetimeText({ loading }: { loading: boolean }): JSX.Element { const locale = useCurrentLocale() // `datetime` when scrubbing the chart const datetime = useLineChartDatetime({ locale }) - if (loading) { - return null - } - return ( - - + + ) } diff --git a/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap b/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap index 98694cc5409..2c9cdfe65d1 100644 --- a/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap +++ b/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap @@ -1,46 +1,67 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`DatetimeText renders loading state 1`] = `null`; - exports[`DatetimeText renders without error 1`] = ` `; @@ -57,7 +78,16 @@ exports[`PriceText renders loading state 1`] = ` `; @@ -96,7 +152,16 @@ exports[`PriceText renders without error 1`] = ` `; @@ -163,7 +285,16 @@ exports[`PriceText renders without error less than a dollar 1`] = ` `; @@ -257,12 +445,7 @@ exports[`RelativeChangeText renders loading state 1`] = ` maxFontSizeMultiplier={1.4} style={ { - "color": { - "dynamic": { - "dark": "transparent", - "light": "transparent", - }, - }, + "color": "transparent", "fontFamily": "Basel Grotesk", "fontSize": 19, "fontWeight": "400", @@ -278,12 +461,7 @@ exports[`RelativeChangeText renders loading state 1`] = ` + + + + + + + + + + `; @@ -314,88 +641,154 @@ exports[`RelativeChangeText renders without error 1`] = ` } testID="relative-price-change" > - - - + + + + + - - - + + `; diff --git a/apps/mobile/src/components/PriceExplorer/useFiatDelta.tsx b/apps/mobile/src/components/PriceExplorer/useFiatDelta.tsx new file mode 100644 index 00000000000..68098b73578 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/useFiatDelta.tsx @@ -0,0 +1,119 @@ +import { useCallback, useMemo } from 'react' +import { runOnJS, SharedValue, useAnimatedReaction, useDerivedValue, useSharedValue } from 'react-native-reanimated' +import { useLineChart } from 'react-native-wagmi-charts' +import { useFormatChartFiatDelta } from 'uniswap/src/features/fiatCurrency/hooks/useFormatChartFiatDelta' +import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' + +interface UseFiatDeltaParams { + startingPrice?: number + shouldTreatAsStablecoin?: boolean +} + +interface FiatDeltaResult { + formatted: SharedValue +} + +/** + * Hook to calculate and format fiat delta for price charts. + * Optimized to only calculate deltas on-demand during scrubbing, reducing memory usage. + */ +export function useLineChartFiatDelta({ + startingPrice, + shouldTreatAsStablecoin = false, +}: UseFiatDeltaParams): FiatDeltaResult { + const { currentIndex, data, isActive } = useLineChart() + const { conversionRate } = useLocalizationContext() + const { formatChartFiatDelta } = useFormatChartFiatDelta() + + // Shared value for the current scrubbing delta + const scrubbingDeltaSharedValue = useSharedValue('') + + // Pre-calculate only the last point's delta (for non-scrubbing state) + const lastPointDelta = useMemo(() => { + if (!startingPrice || !data || !conversionRate || data.length === 0) { + return '' + } + + const convertedStartPrice = startingPrice * conversionRate + const lastPoint = data[data.length - 1] + if (!lastPoint) { + return '' + } + const convertedEndPrice = lastPoint.value * conversionRate + + const delta = formatChartFiatDelta({ + startingPrice: convertedStartPrice, + endingPrice: convertedEndPrice, + isStablecoin: shouldTreatAsStablecoin, + }) + + return delta.formatted + }, [startingPrice, data, conversionRate, formatChartFiatDelta, shouldTreatAsStablecoin]) + + // Calculate delta for current scrubbing position + const calculateCurrentDelta = useMemo(() => { + return (index: number) => { + if (!startingPrice || !data || !conversionRate) { + return '' + } + + const currentPoint = data[index] + if (!currentPoint) { + return '' + } + + const convertedStartPrice = startingPrice * conversionRate + const convertedEndPrice = currentPoint.value * conversionRate + + const delta = formatChartFiatDelta({ + startingPrice: convertedStartPrice, + endingPrice: convertedEndPrice, + isStablecoin: shouldTreatAsStablecoin, + }) + + return delta.formatted + } + }, [startingPrice, data, conversionRate, formatChartFiatDelta, shouldTreatAsStablecoin]) + + // Callback for updating the scrubbing delta from the UI thread + const updateScrubbingDelta = useCallback( + (index: number) => { + scrubbingDeltaSharedValue.value = calculateCurrentDelta(index) + }, + // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here + [calculateCurrentDelta], + ) + + // Track current index changes with useAnimatedReaction + useAnimatedReaction( + () => { + return currentIndex.value + }, + (currentIndexValue) => { + if (data && data.length > 0) { + const safeIndex = Math.min(Math.max(0, Math.round(currentIndexValue)), data.length - 1) + runOnJS(updateScrubbingDelta)(safeIndex) + } + }, + [data, updateScrubbingDelta], + ) + + // Create a derived value that decides which delta to show + /* oxlint-disable react/exhaustive-deps -- isActive and scrubbingDeltaSharedValue are Reanimated shared values tracked automatically */ + const formatted = useDerivedValue(() => { + if (!data || data.length === 0) { + return '' + } + + // When scrubbing, use the current scrubbing delta + if (isActive.value) { + return scrubbingDeltaSharedValue.value + } + + // When not scrubbing, use the pre-calculated last point delta + return lastPointDelta + }, [lastPointDelta, data]) + /* oxlint-enable react/exhaustive-deps */ + + return { formatted } +} diff --git a/apps/mobile/src/components/PriceExplorer/usePrice.tsx b/apps/mobile/src/components/PriceExplorer/usePrice.tsx index 3b8197599c2..5d1385eca35 100644 --- a/apps/mobile/src/components/PriceExplorer/usePrice.tsx +++ b/apps/mobile/src/components/PriceExplorer/usePrice.tsx @@ -68,7 +68,8 @@ export function useLineChartPrice(currentSpot?: SharedValue): ValueAndFo formatted: priceFormatted, shouldAnimate, }), - [price, priceFormatted, shouldAnimate], + // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here + [], ) } diff --git a/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts b/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts index 4d2669249db..1937b53c64f 100644 --- a/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts +++ b/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts @@ -3,6 +3,7 @@ import { GraphQLApi } from '@universe/api' import { act } from 'react-test-renderer' import { useTokenPriceHistory } from 'src/components/PriceExplorer/usePriceHistory' import { renderHookWithProviders } from 'src/test/render' +import { USDC, USDC_ARBITRUM, USDC_BASE, USDC_OPTIMISM, USDC_POLYGON } from 'uniswap/src/constants/tokens' import { getLatestPrice, priceHistory, @@ -34,6 +35,24 @@ const mockTokenProjectsQuery = (historyPrices: number[]) => (): GraphQLApi.Token const formatPriceHistory = (history: GraphQLApi.TimestampedAmount[]): Omit[] => history.map(({ timestamp, value }) => ({ value, timestamp: timestamp * 1000 })) +/** + * Creates a USDC token project with matching priceHistory for both the aggregated market + * and the Ethereum token's market. This ensures the hook returns the expected data since + * it prefers per-chain price history over aggregated price history. + */ +const createUsdcTokenProjectWithMatchingPriceHistory = ( + history: (GraphQLApi.TimestampedAmount | undefined)[], +): GraphQLApi.TokenProject => ({ + ...usdcTokenProject({ priceHistory: history }), + tokens: [ + token({ sdkToken: USDC, market: tokenMarket({ priceHistory: history }) }), + token({ sdkToken: USDC_POLYGON }), + token({ sdkToken: USDC_ARBITRUM }), + token({ sdkToken: USDC_BASE, market: tokenMarket() }), + token({ sdkToken: USDC_OPTIMISM }), + ], +}) + describe(useTokenPriceHistory, () => { it('returns correct initial values', async () => { const { result } = renderHookWithProviders(() => useTokenPriceHistory({ currencyId: SAMPLE_CURRENCY_ID_1 })) @@ -59,7 +78,13 @@ describe(useTokenPriceHistory, () => { it('returns on-chain spot price if off-chain spot price is not available', async () => { const market = tokenMarket() const { resolvers } = queryResolvers({ - tokenProjects: () => [usdcTokenProject({ markets: undefined, tokens: [token({ market })] })], + tokenProjects: () => [ + usdcTokenProject({ + markets: undefined, + // Ensure token has the correct chain to match SAMPLE_CURRENCY_ID_1 (Ethereum) + tokens: [token({ market, chain: GraphQLApi.Chain.Ethereum })], + }), + ], }) const { result } = renderHookWithProviders(() => useTokenPriceHistory({ currencyId: SAMPLE_CURRENCY_ID_1 }), { resolvers, @@ -72,14 +97,43 @@ describe(useTokenPriceHistory, () => { await waitFor(() => { expect(result.current.data?.spot).toEqual({ - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition value: expect.objectContaining({ value: market.price?.value }), - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + // oxlint-disable-next-line typescript/no-unnecessary-condition relativeChange: expect.objectContaining({ value: market.pricePercentChange?.value }), }) }) }) + it('handles gracefully when no token matches the currencyId chain', async () => { + const aggregatedMarket = tokenProjectMarket() + const { resolvers } = queryResolvers({ + tokenProjects: () => [ + usdcTokenProject({ + markets: [aggregatedMarket], + // Provide tokens for different chains, but none matching SAMPLE_CURRENCY_ID_1 (Ethereum) + tokens: [token({ chain: GraphQLApi.Chain.Polygon }), token({ chain: GraphQLApi.Chain.Arbitrum })], + }), + ], + }) + const { result } = renderHookWithProviders(() => useTokenPriceHistory({ currencyId: SAMPLE_CURRENCY_ID_1 }), { + resolvers, + }) + + await waitFor(() => { + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(false) + }) + + // Should fall back to aggregated market data when no chain-specific token is found + await waitFor(() => { + expect(result.current.data?.spot).toEqual({ + value: expect.objectContaining({ value: aggregatedMarket.price.value }), + relativeChange: expect.objectContaining({ value: aggregatedMarket.pricePercentChange24h.value }), + }) + }) + }) + describe('correct number of digits', () => { it('for max price greater than 1', async () => { const { resolvers } = queryResolvers({ @@ -141,7 +195,7 @@ describe(useTokenPriceHistory, () => { it('properly formats price history entries', async () => { const history = priceHistory() const { resolvers } = queryResolvers({ - tokenProjects: () => [usdcTokenProject({ priceHistory: history })], + tokenProjects: () => [createUsdcTokenProjectWithMatchingPriceHistory(history)], }) const { result } = renderHookWithProviders(() => useTokenPriceHistory({ currencyId: SAMPLE_CURRENCY_ID_1 }), { resolvers, @@ -156,12 +210,9 @@ describe(useTokenPriceHistory, () => { }) it('filters out invalid price history entries', async () => { + const invalidHistory = [undefined, timestampedAmount({ value: 1 }), undefined, timestampedAmount({ value: 2 })] const { resolvers } = queryResolvers({ - tokenProjects: () => [ - usdcTokenProject({ - priceHistory: [undefined, timestampedAmount({ value: 1 }), undefined, timestampedAmount({ value: 2 })], - }), - ], + tokenProjects: () => [createUsdcTokenProjectWithMatchingPriceHistory(invalidHistory)], }) const { result } = renderHookWithProviders(() => useTokenPriceHistory({ currencyId: SAMPLE_CURRENCY_ID_1 }), { resolvers, @@ -191,15 +242,15 @@ describe(useTokenPriceHistory, () => { const monthPriceHistory = priceHistory({ duration: GraphQLApi.HistoryDuration.Month }) const yearPriceHistory = priceHistory({ duration: GraphQLApi.HistoryDuration.Year }) - const dayTokenProject = usdcTokenProject({ priceHistory: dayPriceHistory }) - const weekTokenProject = usdcTokenProject({ priceHistory: weekPriceHistory }) - const monthTokenProject = usdcTokenProject({ priceHistory: monthPriceHistory }) - const yearTokenProject = usdcTokenProject({ priceHistory: yearPriceHistory }) + const dayTokenProject = createUsdcTokenProjectWithMatchingPriceHistory(dayPriceHistory) + const weekTokenProject = createUsdcTokenProjectWithMatchingPriceHistory(weekPriceHistory) + const monthTokenProject = createUsdcTokenProjectWithMatchingPriceHistory(monthPriceHistory) + const yearTokenProject = createUsdcTokenProjectWithMatchingPriceHistory(yearPriceHistory) const { resolvers } = queryResolvers({ - // eslint-disable-next-line max-params + // oxlint-disable-next-line max-params tokenProjects: (parent, args, context, info) => { - switch (info.variableValues.duration) { + switch (info.variableValues['duration']) { case GraphQLApi.HistoryDuration.Day: return [dayTokenProject] case GraphQLApi.HistoryDuration.Week: @@ -239,10 +290,11 @@ describe(useTokenPriceHistory, () => { }) await waitFor(() => { + const ethereumToken = dayTokenProject.tokens.find((t) => t.chain === GraphQLApi.Chain.Ethereum) expect(result.current.data?.spot).toEqual({ - value: expect.objectContaining({ value: dayTokenProject.markets[0]?.price.value }), + value: expect.objectContaining({ value: ethereumToken?.market?.price?.value }), relativeChange: expect.objectContaining({ - value: dayTokenProject.markets[0]?.pricePercentChange24h.value, + value: dayTokenProject.markets?.[0]?.pricePercentChange24h?.value, }), }) }) @@ -273,7 +325,7 @@ describe(useTokenPriceHistory, () => { }) }) - it('returns correct spot price', async () => { + it('returns correct spot price with calculated percentage change', async () => { const { result } = renderHookWithProviders( () => useTokenPriceHistory({ @@ -283,10 +335,16 @@ describe(useTokenPriceHistory, () => { { resolvers }, ) await waitFor(() => { + const ethereumToken = yearTokenProject.tokens.find((t) => t.chain === GraphQLApi.Chain.Ethereum) + // For non-Day durations, relativeChange is calculated from price history + const openPrice = yearPriceHistory[0]?.value ?? 0 + const closePrice = yearPriceHistory[yearPriceHistory.length - 1]?.value ?? 0 + const calculatedChange = openPrice > 0 ? ((closePrice - openPrice) / openPrice) * 100 : 0 + expect(result.current.data?.spot).toEqual({ - value: expect.objectContaining({ value: yearTokenProject.markets[0]?.price?.value }), + value: expect.objectContaining({ value: ethereumToken?.market?.price?.value }), relativeChange: expect.objectContaining({ - value: yearTokenProject.markets[0]?.pricePercentChange24h?.value, + value: calculatedChange, }), }) }) @@ -294,18 +352,20 @@ describe(useTokenPriceHistory, () => { }) describe('when duration is changed', () => { - it('returns new price history and spot price', async () => { + it('returns new price history and spot price with correct percentage change calculation', async () => { const { result } = renderHookWithProviders(() => useTokenPriceHistory({ currencyId: SAMPLE_CURRENCY_ID_1 }), { resolvers, }) await waitFor(() => { + const ethereumToken = dayTokenProject.tokens.find((t) => t.chain === GraphQLApi.Chain.Ethereum) + // For Day duration, should use API's 24hr value expect(result.current.data).toEqual({ priceHistory: formatPriceHistory(dayPriceHistory), spot: { - value: expect.objectContaining({ value: dayTokenProject.markets[0]?.price.value }), + value: expect.objectContaining({ value: ethereumToken?.market?.price?.value }), relativeChange: expect.objectContaining({ - value: dayTokenProject.markets[0]?.pricePercentChange24h.value, + value: dayTokenProject.markets?.[0]?.pricePercentChange24h?.value, }), }, }) @@ -317,12 +377,18 @@ describe(useTokenPriceHistory, () => { }) await waitFor(() => { + const ethereumToken = weekTokenProject.tokens.find((t) => t.chain === GraphQLApi.Chain.Ethereum) + // For Week duration, should calculate from price history + const openPrice = weekPriceHistory[0]?.value ?? 0 + const closePrice = weekPriceHistory[weekPriceHistory.length - 1]?.value ?? 0 + const calculatedChange = openPrice > 0 ? ((closePrice - openPrice) / openPrice) * 100 : 0 + expect(result.current.data).toEqual({ priceHistory: formatPriceHistory(weekPriceHistory), spot: { - value: expect.objectContaining({ value: weekTokenProject.markets[0]?.price?.value }), + value: expect.objectContaining({ value: ethereumToken?.market?.price?.value }), relativeChange: expect.objectContaining({ - value: weekTokenProject.markets[0]?.pricePercentChange24h?.value, + value: calculatedChange, }), }, }) diff --git a/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts b/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts index 5f1222aaeda..2ac29e07c20 100644 --- a/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts +++ b/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts @@ -4,12 +4,15 @@ import { type Dispatch, type SetStateAction, useCallback, useMemo, useRef, useSt import { type SharedValue, useDerivedValue } from 'react-native-reanimated' import { type TLineChartData } from 'react-native-wagmi-charts' import { PollingInterval } from 'uniswap/src/constants/misc' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { toGraphQLChain } from 'uniswap/src/features/chains/utils' import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils/currencyIdToContractInput' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' +import { currencyIdToChain } from 'uniswap/src/utils/currencyId' export type TokenSpotData = { value: SharedValue - relativeChange: SharedValue + relativeChange: SharedValue } export type PriceNumberOfDigits = { @@ -20,6 +23,7 @@ export type PriceNumberOfDigits = { /** * @returns Token price history for requested duration */ +// oxlint-disable-next-line complexity -- biome-parity: oxlint is stricter here export function useTokenPriceHistory({ currencyId, initialDuration = GraphQLApi.HistoryDuration.Day, @@ -63,18 +67,48 @@ export function useTokenPriceHistory({ skip, }) + // Data source strategy for multi-chain tokens: + // - Use PER-CHAIN data (token.market) for price and price history to show the correct chain-specific view + // - Fallback to AGGREGATED data (project.markets) when per-chain history is unavailable + // - Continue using aggregated 24hr change for consistency across platforms + // Note: TokenProjectMarket is aggregated across chains, TokenMarket is per-chain const offChainData = priceData?.tokenProjects?.[0]?.markets?.[0] - const onChainData = priceData?.tokenProjects?.[0]?.tokens[0]?.market - const price = offChainData?.price?.value ?? onChainData?.price?.value ?? lastPrice.current + // We need to find the specific token for the chain we're viewing + const currentChain = toGraphQLChain(currencyIdToChain(currencyId) ?? UniverseChainId.Mainnet) + const currentChainToken = priceData?.tokenProjects?.[0]?.tokens.find((token) => token.chain === currentChain) + const onChainData = currentChainToken?.market + + // Use per-chain price to ensure correct price on each chain (e.g., USDC on Ethereum vs Polygon) + const price = onChainData?.price?.value ?? offChainData?.price?.value ?? lastPrice.current lastPrice.current = price - const priceHistory = offChainData?.priceHistory ?? onChainData?.priceHistory + + // Prefer per-chain price history so multi-chain tokens render the correct chart for the selected chain + const priceHistory = onChainData?.priceHistory ?? offChainData?.priceHistory + const pricePercentChange24h = offChainData?.pricePercentChange24h?.value ?? onChainData?.pricePercentChange24h?.value ?? 0 + // Calculate percentage change from price history for the selected duration + const calculatedPriceChange = useMemo(() => { + if (!priceHistory || priceHistory.length === 0) { + return undefined + } + const openPrice = priceHistory[0]?.value + const closePrice = priceHistory[priceHistory.length - 1]?.value + if (openPrice === undefined || closePrice === undefined || openPrice === 0) { + return undefined + } + return ((closePrice - openPrice) / openPrice) * 100 + }, [priceHistory]) + + // Use API's 24hr change for 1d, calculated change for other durations + const priceChange = duration === GraphQLApi.HistoryDuration.Day ? pricePercentChange24h : calculatedPriceChange + const spotValue = useDerivedValue(() => price ?? 0) - const spotRelativeChange = useDerivedValue(() => pricePercentChange24h) + const spotRelativeChange = useDerivedValue(() => priceChange) + // oxlint-disable-next-line react/exhaustive-deps -- ensure spot updates when price changes const spot = useMemo( () => price !== undefined @@ -83,7 +117,8 @@ export function useTokenPriceHistory({ relativeChange: spotRelativeChange, } : undefined, - [price, spotValue, spotRelativeChange], + // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here + [price, priceChange, spotValue, spotRelativeChange], ) const formattedPriceHistory = useMemo(() => { diff --git a/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx b/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx index a0680858145..df8134dba9e 100644 --- a/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx +++ b/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx @@ -1,7 +1,7 @@ +import 'react-native-reanimated' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { Alert } from 'react-native' -import 'react-native-reanimated' import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner' import { getSupportedURI, URIType } from 'src/components/Requests/ScanSheet/util' import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src' @@ -92,11 +92,11 @@ export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.E > {currentScreenState === ScannerModalState.ScanQr ? ( - + ) : ( - + )} - + {currentScreenState === ScannerModalState.ScanQr ? t('qrScanner.recipient.action.show') : t('qrScanner.recipient.action.scan')} diff --git a/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx b/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx index f629e20bb4c..bbed8ea6984 100644 --- a/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx +++ b/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx @@ -1,14 +1,14 @@ import React, { memo, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { TextInput } from 'react-native' -import { FadeIn, FadeOut } from 'react-native-reanimated' +import { KeyboardAvoidingView } from 'react-native-keyboard-controller' import { RecipientScanModal } from 'src/components/RecipientSelect/RecipientScanModal' -import { Flex, Loader, Text, TouchableArea } from 'ui/src' -import { Scan } from 'ui/src/components/icons' -import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' +import { Flex, flexStyles, Loader, Text, TouchableArea } from 'ui/src' +import { Scan, UserSearch } from 'ui/src/components/icons' import { UniverseChainId } from 'uniswap/src/features/chains/types' import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { dismissNativeKeyboard } from 'utilities/src/device/keyboard/dismissNativeKeyboard' +import { isIOS } from 'utilities/src/platform' import { useFilteredRecipientSections } from 'wallet/src/components/RecipientSearch/hooks' import { RecipientList } from 'wallet/src/components/RecipientSearch/RecipientList' import { RecipientSelectSpeedBumps } from 'wallet/src/components/RecipientSearch/RecipientSelectSpeedBumps' @@ -32,7 +32,7 @@ function QRScannerIconButton({ onPress }: { onPress: () => void }): JSX.Element ) } -function _RecipientSelect({ +function RecipientSelectInner({ onSelectRecipient, onHideRecipientSelector, recipient, @@ -80,37 +80,68 @@ function _RecipientSelect({ return ( <> - - {!renderedInModal && ( - - - {t('send.recipient.header')} - - - )} - } - hideBackButton={hideBackButton} - placeholder={t('send.recipient.input.placeholder')} - value={pattern} - onBack={recipient ? onHideRecipientSelector : undefined} - onChangeText={setPattern} - /> - {loading ? ( - - ) : !sections.length ? ( - - {t('send.recipient.results.empty')} - - {t('send.recipient.results.error')} - - - ) : ( - - )} - + + + {!renderedInModal && ( + + + {t('send.recipient.header')} + + + )} + } + hideBackButton={hideBackButton} + placeholder={t('send.recipient.input.placeholder')} + value={pattern} + onBack={recipient ? onHideRecipientSelector : undefined} + onChangeText={setPattern} + /> + {loading ? ( + + ) : !pattern && sections.length === 0 ? ( + + + + + {t('send.recipientSelect.search.empty')} + + + + + {t('qrScanner.recipient.action.scan')} + + + + ) : !sections.length ? ( + + {t('send.recipient.results.empty')} + + {t('send.recipient.results.error')} + + + ) : ( + + )} + + {showQRScanner && } [0]>> -function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Element { - const { fullHeight } = useDeviceDimensions() +function AssociatedAccountsListInner({ accounts }: { accounts: Account[] }): JSX.Element { const addresses = useMemo(() => accounts.map((account) => account.address), [accounts]) const { data, loading } = useAccountListData({ addresses, @@ -36,11 +32,6 @@ function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Ele })) .sort((a, b) => b.balance - a.balance) - 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 ( @@ -74,7 +65,7 @@ function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Ele ) } -export const AssociatedAccountsList = React.memo(_AssociatedAccountsList) +export const AssociatedAccountsList = React.memo(AssociatedAccountsListInner) function AssociatedAccountRow({ index, diff --git a/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx b/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx index f7678c80e2f..d8a320a012b 100644 --- a/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx +++ b/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx @@ -33,6 +33,7 @@ export function RemoveLastMnemonicWalletFooter({ + {isRunning && ( + + )} + + + + + {/* Current Progress */} + + + {/* Results */} + {Object.entries(groupedResults).map(([difficulty, impls]) => ( + + ))} + + {/* Empty State */} + {results.length === 0 && !isRunning && ( + + + Select difficulty and implementation, then run a benchmark. + + + )} + + {/* Operation Log */} + + + + + ) +} diff --git a/apps/mobile/src/screens/HomeScreen/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen/HomeScreen.tsx index c57c24a05d1..d20af5c62a1 100644 --- a/apps/mobile/src/screens/HomeScreen/HomeScreen.tsx +++ b/apps/mobile/src/screens/HomeScreen/HomeScreen.tsx @@ -1,14 +1,15 @@ -/* eslint-disable max-lines */ +/* oxlint-disable max-lines */ import { useApolloClient } from '@apollo/client' import { useIsFocused, useScrollToTop } from '@react-navigation/native' import { SharedQueryClient } from '@universe/api' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' +import { getIsNotificationServiceLocalOverrideEnabled } from '@universe/notifications' +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Freeze } from 'react-freeze' import { useTranslation } from 'react-i18next' import { StyleProp, View, ViewProps, ViewStyle } from 'react-native' import Animated, { FadeIn, interpolateColor, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated' import { SceneRendererProps, TabBar } from 'react-native-tab-view' -import { Video } from 'react-native-video' import { useDispatch, useSelector } from 'react-redux' import { useHomeScreenCustomAndroidBackButton } from 'src/app/navigation/hooks' import { NavBar, SWAP_BUTTON_HEIGHT } from 'src/app/navigation/NavBar' @@ -19,6 +20,7 @@ import { ActivityContent } from 'src/components/activity/ActivityContent' import { HomeExploreTab } from 'src/components/home/HomeExploreTab' import { OnboardingIntroCardStack } from 'src/components/home/introCards/OnboardingIntroCardStack' import { NftsTab } from 'src/components/home/NftsTab' +import { PortfolioOverview } from 'src/components/home/PortfolioChart/PortfolioOverview' import { TokensTab } from 'src/components/home/TokensTab' import { Screen } from 'src/components/layout/Screen' import { @@ -38,40 +40,38 @@ import { useBiometricPrompt } from 'src/features/biometricsSettings/hooks' import { selectSomeModalOpen } from 'src/features/modals/selectSomeModalOpen' import { useHideSplashScreen } from 'src/features/splashScreen/useHideSplashScreen' import { useWalletRestore } from 'src/features/wallet/useWalletRestore' +import { MobileNotificationServiceManager } from 'src/notification-service/MobileNotificationServiceManager' import { HomeScreenQuickActions } from 'src/screens/HomeScreen/HomeScreenQuickActions' import { HomeScreenTabIndex } from 'src/screens/HomeScreen/HomeScreenTabIndex' +import { SmartWalletModals } from 'src/screens/HomeScreen/SmartWalletModals' import { useHomeScreenState } from 'src/screens/HomeScreen/useHomeScreenState' import { useHomeScrollRefs } from 'src/screens/HomeScreen/useHomeScrollRefs' -import { useOpenBackupReminderModal } from 'src/utils/useOpenBackupReminderModal' -import { Flex, Image, Text, TouchableArea, useMedia, useSporeColors } from 'ui/src' -import { SMART_WALLET_UPGRADE_FALLBACK, SMART_WALLET_UPGRADE_VIDEO } from 'ui/src/assets' +import { Flex, Text, TouchableArea, useMedia, useSporeColors } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { spacing } from 'ui/src/theme' +import { buildWrappedUrl } from 'uniswap/src/components/banners/shared/utils' +import { UniswapWrapped2025Banner } from 'uniswap/src/components/banners/UniswapWrapped2025Banner/UniswapWrapped2025Banner' import { NFTS_TAB_DATA_DEPENDENCIES } from 'uniswap/src/components/nfts/constants' +import { UNISWAP_WEB_URL } from 'uniswap/src/constants/urls' import { getPortfolioQuery } from 'uniswap/src/data/rest/getPortfolio' import { getListTransactionsQuery } from 'uniswap/src/data/rest/listTransactions' import { AccountType } from 'uniswap/src/features/accounts/types' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' +import { selectHasDismissedUniswapWrapped2025Banner } from 'uniswap/src/features/behaviorHistory/selectors' +import { setHasDismissedUniswapWrapped2025Banner } from 'uniswap/src/features/behaviorHistory/slice' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useSelectAddressHasNotifications } from 'uniswap/src/features/notifications/slice/hooks' import { setNotificationStatus } from 'uniswap/src/features/notifications/slice/slice' -import { PortfolioBalance } from 'uniswap/src/features/portfolio/PortfolioBalance/PortfolioBalance' import { ModalName, SectionName } from 'uniswap/src/features/telemetry/constants' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { MobileScreens } from 'uniswap/src/types/screens/mobile' +import { openUri } from 'uniswap/src/utils/linking' import { logger } from 'utilities/src/logger/logger' import { useEvent } from 'utilities/src/react/hooks' -import { SmartWalletCreatedModal } from 'wallet/src/components/smartWallet/modals/SmartWalletCreatedModal' -import { SmartWalletUpgradeModals } from 'wallet/src/components/smartWallet/modals/SmartWalletUpgradeModal' import { useOpenSmartWalletNudgeOnCompletedSwap } from 'wallet/src/components/smartWallet/smartAccounts/hooks' -import { selectHasSeenCreatedSmartWalletModal } from 'wallet/src/features/behaviorHistory/selectors' -import { - setHasSeenSmartWalletCreatedWalletModal, - setIncrementNumPostSwapNudge, -} from 'wallet/src/features/behaviorHistory/slice' -import { useAccountCountChanged, useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' +import { setIncrementNumPostSwapNudge } from 'wallet/src/features/behaviorHistory/slice' +import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' import { setSmartWalletConsent } from 'wallet/src/features/wallet/slice' type HomeRoute = { @@ -87,7 +87,20 @@ const CONTENT_HEADER_HEIGHT_ESTIMATE = 270 */ export function WrappedHomeScreen(props: AppStackScreenProp): JSX.Element { const activeAccount = useActiveAccountWithThrow() - return + + const [isLayoutReady, setIsLayoutReady] = useState(false) + + return ( + <> + + + + ) } /** @@ -95,10 +108,18 @@ export function WrappedHomeScreen(props: AppStackScreenProp) * Manages TokensTabs and NftsTab scroll offsets when header is collapsed * Borrowed from: https://stormotion.io/blog/how-to-create-collapsing-tab-header-using-react-native/ */ -function HomeScreen(props?: AppStackScreenProp): JSX.Element { +function HomeScreen({ + isLayoutReady, + setIsLayoutReady, + ...props +}: AppStackScreenProp & { + isLayoutReady: boolean + setIsLayoutReady: Dispatch> +}): JSX.Element { const activeAccount = useActiveAccountWithThrow() const { t } = useTranslation() const colors = useSporeColors() + const darkColors = useSporeColors('dark') const media = useMedia() const insets = useAppInsets() const dimensions = useDeviceDimensions() @@ -107,20 +128,28 @@ function HomeScreen(props?: AppStackScreenProp): JSX.Element const isModalOpen = useSelector(selectSomeModalOpen) const isHomeScreenBlur = !isFocused || isModalOpen const hideSplashScreen = useHideSplashScreen() - const isSmartWalletEnabled = useFeatureFlag(FeatureFlags.SmartWallet) - const SmartWalletDisableVideo = useFeatureFlag(FeatureFlags.SmartWalletDisableVideo) const { requiredForTransactions: requiresBiometrics } = useBiometricAppSettings() const isBottomTabsEnabled = useFeatureFlag(FeatureFlags.BottomTabs) + const isPnLEnabled = useFeatureFlag(FeatureFlags.ProfitLoss) + const isWrappedBannerEnabled = useFeatureFlag(FeatureFlags.UniswapWrapped2025) + const isNotificationServiceEnabledFlag = useFeatureFlag(FeatureFlags.NotificationService) + const isNotificationServiceEnabled = + getIsNotificationServiceLocalOverrideEnabled() || isNotificationServiceEnabledFlag + + const hasDismissedWrappedBanner = useSelector(selectHasDismissedUniswapWrapped2025Banner) + const shouldShowWrappedBanner = isWrappedBannerEnabled && !hasDismissedWrappedBanner const { showEmptyWalletState, isTabsDataLoaded } = useHomeScreenState() + const [hasIntroCards, setHasIntroCards] = useState(false) + const { chains } = useEnabledChains() // opens the wallet restore modal if recovery phrase is missing after the app is opened useWalletRestore({ openModalImmediately: true }) const { trigger } = useBiometricPrompt() - const [routeTabIndex, setRouteTabIndex] = useState(props?.route.params?.tab ?? HomeScreenTabIndex.Tokens) + const [routeTabIndex, setRouteTabIndex] = useState(props.route.params?.tab ?? HomeScreenTabIndex.Tokens) // Ensures that tabIndex has the proper value between the empty state and non-empty state const tabIndex = showEmptyWalletState ? HomeScreenTabIndex.Tokens : routeTabIndex @@ -145,7 +174,9 @@ function HomeScreen(props?: AppStackScreenProp): JSX.Element const tabs: Array = [ { key: SectionName.HomeTokensTab, title: tokensTitle }, { key: SectionName.HomeNFTsTab, title: nftsTitle }, - ...(!isBottomTabsEnabled ? [{ key: SectionName.HomeActivityTab, title: activityTitle }] : []), + ...(!isBottomTabsEnabled + ? [{ key: SectionName.HomeActivityTab, title: activityTitle, enableNotificationBadge: true }] + : []), ] return tabs @@ -153,17 +184,15 @@ function HomeScreen(props?: AppStackScreenProp): JSX.Element useEffect( function syncTabIndex() { - const newTabIndex = props?.route.params?.tab + const newTabIndex = props.route.params?.tab if (newTabIndex === undefined) { return } setRouteTabIndex(newTabIndex) }, - [props?.route.params?.tab], + [props.route.params?.tab], ) - const [isLayoutReady, setIsLayoutReady] = useState(false) - const [headerHeight, setHeaderHeight] = useState(CONTENT_HEADER_HEIGHT_ESTIMATE) const headerConfig = useMemo( () => ({ @@ -175,10 +204,13 @@ function HomeScreen(props?: AppStackScreenProp): JSX.Element const { heightCollapsed, heightExpanded } = headerConfig const headerHeightDiff = heightExpanded - heightCollapsed - const handleHeaderLayout = useCallback>((event) => { - setHeaderHeight(event.nativeEvent.layout.height) - setIsLayoutReady(true) - }, []) + const handleHeaderLayout = useCallback>( + (event) => { + setHeaderHeight(event.nativeEvent.layout.height) + setIsLayoutReady(true) + }, + [setIsLayoutReady], + ) const { tokensTabScrollValue, @@ -239,7 +271,7 @@ function HomeScreen(props?: AppStackScreenProp): JSX.Element }, [dispatch, activeAccount.address, tabIndex, hasNotifications, isBottomTabsEnabled]) // If accounts are switched, we want to scroll to top and show full header - // biome-ignore lint/correctness/useExhaustiveDependencies: we want to trigger this effect also when activeAccount changes + // oxlint-disable-next-line react/exhaustive-deps -- we want to trigger this effect also when activeAccount changes useEffect(() => { resetScrollState() }, [activeAccount, resetScrollState]) @@ -280,27 +312,71 @@ function HomeScreen(props?: AppStackScreenProp): JSX.Element // Hide actions when active account isn't a signer account. const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic - // This hooks handles the logic for when to open the BackupReminderModal - useOpenBackupReminderModal(activeAccount) + // Sets isLayoutReady to false when switching a wallet + useEffect(() => { + return () => setIsLayoutReady(false) + }, [setIsLayoutReady]) const viewOnlyLabel = t('home.warning.viewOnly') + const handleDismissWrappedBanner = useCallback(() => { + dispatch(setHasDismissedUniswapWrapped2025Banner(true)) + }, [dispatch]) + + const handlePressWrappedBanner = useCallback(async () => { + try { + const url = buildWrappedUrl(UNISWAP_WEB_URL, activeAccount.address) + await openUri({ uri: url, openExternalBrowser: true }) + dispatch(setHasDismissedUniswapWrapped2025Banner(true)) + } catch (error) { + logger.error(error, { tags: { file: 'HomeScreen', function: 'handlePressWrappedBanner' } }) + } + }, [activeAccount.address, dispatch]) + + const handleIntroCardsChange = useCallback((hasCards: boolean) => { + setHasIntroCards(hasCards) + }, []) + const promoBanner = useMemo( - () => , - [showEmptyWalletState, isTabsDataLoaded], + () => + isNotificationServiceEnabled ? ( + + ) : ( + + ), + [showEmptyWalletState, isTabsDataLoaded, isNotificationServiceEnabled, handleIntroCardsChange], ) const contentHeader = useMemo(() => { return ( + {shouldShowWrappedBanner && ( + + + + + )} - - - + {isSignerAccount ? ( ) : ( @@ -316,8 +392,14 @@ function HomeScreen(props?: AppStackScreenProp): JSX.Element ) }, [ + hasIntroCards, showEmptyWalletState, isBottomTabsEnabled, + isPnLEnabled, + chains, + shouldShowWrappedBanner, + handleDismissWrappedBanner, + handlePressWrappedBanner, activeAccount.address, isSignerAccount, onPressViewOnlyLabel, @@ -325,34 +407,6 @@ function HomeScreen(props?: AppStackScreenProp): JSX.Element promoBanner, ]) - const [hasVideoError, setVideoHasError] = useState(false) - - const MemoizedVideo = useMemo(() => { - if (hasVideoError) { - return ( - - - - ) - } - - return ( - - - ) - }, [hasVideoError]) - const paddingTop = headerHeight + TAB_BAR_HEIGHT + (showEmptyWalletState ? 0 : TAB_STYLES.tabListInner.paddingTop) const paddingBottom = insets.bottom + SWAP_BUTTON_HEIGHT + TAB_STYLES.tabListInner.paddingBottom + spacing.spacing12 @@ -394,11 +448,9 @@ function HomeScreen(props?: AppStackScreenProp): JSX.Element ) const statusBarStyle = useAnimatedStyle(() => ({ - backgroundColor: interpolateColor( - currentScrollValue.value, - [0, headerHeightDiff], - [colors.surface1.val, colors.surface1.val], - ), + backgroundColor: shouldShowWrappedBanner + ? darkColors.surface1.val + : interpolateColor(currentScrollValue.value, [0, headerHeightDiff], [colors.surface1.val, colors.surface1.val]), })) const apolloClient = useApolloClient() @@ -574,45 +626,6 @@ function HomeScreen(props?: AppStackScreenProp): JSX.Element ], ) - const handleSmartWalletEnable = useCallback( - async (onComplete?: () => void): Promise => { - const successAction = (): void => { - dispatch(setSmartWalletConsent({ address: activeAccount.address, smartWalletConsent: true })) - onComplete?.() - navigate(ModalName.SmartWalletEnabledModal, { - showReconnectDappPrompt: false, - }) - } - - if (requiresBiometrics) { - await trigger({ successCallback: successAction }) - } else { - successAction() - } - }, - [dispatch, activeAccount.address, requiresBiometrics, trigger], - ) - - const hasSeenCreatedSmartWalletModal = useSelector(selectHasSeenCreatedSmartWalletModal) - const [shouldShowCreatedModal, setShouldShowCreatedModal] = useState(false) - - // Setup listener for account creation events to show the SmartWalletCreatedModal - useAccountCountChanged( - useEvent(() => { - if (hasSeenCreatedSmartWalletModal) { - return - } - setShouldShowCreatedModal(true) - }), - ) - - const shouldOpenSmartWalletCreatedModal = - isSmartWalletEnabled && - isTabsDataLoaded && - isLayoutReady && - shouldShowCreatedModal && - !hasSeenCreatedSmartWalletModal - useOpenSmartWalletNudgeOnCompletedSwap( useEvent(() => { if (!activeAccount.address || activeAccount.type !== AccountType.SignerMnemonic) { @@ -642,12 +655,14 @@ function HomeScreen(props?: AppStackScreenProp): JSX.Element return ( - + {contentHeader} {isTabsDataLoaded && isLayoutReady && ( ): JSX.Element width="100%" zIndex="$sticky" /> - - {isSmartWalletEnabled && ( - - )} - - { - setShouldShowCreatedModal(false) - dispatch(setHasSeenSmartWalletCreatedWalletModal()) - }} - /> ) } diff --git a/apps/mobile/src/screens/HomeScreen/HomeScreenQuickActions.tsx b/apps/mobile/src/screens/HomeScreen/HomeScreenQuickActions.tsx index 30cf4f84917..d41e8c308c1 100644 --- a/apps/mobile/src/screens/HomeScreen/HomeScreenQuickActions.tsx +++ b/apps/mobile/src/screens/HomeScreen/HomeScreenQuickActions.tsx @@ -1,3 +1,5 @@ +// oxlint-disable typescript/no-duplicate-type-constituents +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { FlatList, ListRenderItemInfo } from 'react-native' @@ -6,15 +8,14 @@ import { navigate } from 'src/app/navigation/rootNavigation' import { useOpenReceiveModal } from 'src/features/modals/hooks/useOpenReceiveModal' import { openModal } from 'src/features/modals/modalSlice' import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' -import { ArrowDownCircle, Bank, SendAction, SwapDotted } from 'ui/src/components/icons' +import { ArrowDownCircle, Bank, MinusCircle, PlusCircle, SendAction, SwapDotted } from 'ui/src/components/icons' import { iconSizes, spacing } from 'ui/src/theme' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useHighestBalanceNativeCurrencyId } from 'uniswap/src/features/dataApi/balances/balances' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useHapticFeedback } from 'uniswap/src/features/settings/useHapticFeedback/useHapticFeedback' import { ElementName, MobileEventName, ModalName } from 'uniswap/src/features/telemetry/constants' import { Trace } from 'uniswap/src/features/telemetry/Trace' +import { useIsPortfolioZero } from 'uniswap/src/features/transactions/swap/components/SwapFormButton/hooks/useIsPortfolioZero' import { selectFilteredChainIds } from 'uniswap/src/features/transactions/swap/state/selectors' import { prepareSwapFormState } from 'uniswap/src/features/transactions/types/transactionState' import { CurrencyField } from 'uniswap/src/types/currency' @@ -22,7 +23,13 @@ import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hoo const MIN_BUTTON_WIDTH = 102 -type IconComponent = typeof SwapDotted | typeof Bank | typeof SendAction | typeof ArrowDownCircle +type IconComponent = + | typeof SwapDotted + | typeof Bank + | typeof PlusCircle + | typeof MinusCircle + | typeof SendAction + | typeof ArrowDownCircle type ActionItem = { Icon: IconComponent label: string @@ -53,6 +60,8 @@ export function HomeScreenQuickActions(): JSX.Element { const { isTestnetModeEnabled, defaultChainId } = useEnabledChains() const disableForKorea = useFeatureFlag(FeatureFlags.DisableFiatOnRampKorea) const isBottomTabsEnabled = useFeatureFlag(FeatureFlags.BottomTabs) + const multichainTokenUxEnabled = useFeatureFlag(FeatureFlags.MultichainTokenUx) + const isPortfolioZero = useIsPortfolioZero() const activeAccountAddress = useActiveAccountAddressWithThrow() const persistedFilteredChainIds = useSelector(selectFilteredChainIds) @@ -85,29 +94,44 @@ export function HomeScreenQuickActions(): JSX.Element { await triggerHaptics() }, [openReceiveModal, triggerHaptics]) - const onPressBuy = useCallback(async (): Promise => { - await triggerHaptics() - if (isTestnetModeEnabled) { - navigate(ModalName.TestnetMode, { - unsupported: true, - descriptionCopy: t('tdp.noTestnetSupportDescription'), - }) - return - } - disableForKorea - ? navigate(ModalName.KoreaCexTransferInfoModal) - : dispatch( - openModal({ - name: ModalName.FiatOnRampAggregator, - }), - ) - }, [triggerHaptics, isTestnetModeEnabled, disableForKorea, dispatch, t]) + const onPressFORAction = useCallback( + async (entry: 'onramp' | 'offramp'): Promise => { + await triggerHaptics() + if (isTestnetModeEnabled) { + navigate(ModalName.TestnetMode, { + unsupported: true, + descriptionCopy: t('tdp.noTestnetSupportDescription'), + }) + return + } + // When multichain UX is enabled, show the interstitial sheet unless + // the user has zero balance (in which case go straight to FOR). + // Korea check is handled inside the modal and below for the direct path. + if (multichainTokenUxEnabled && !isPortfolioZero) { + navigate(ModalName.FiatOnRampAction, { entry }) + return + } + if (disableForKorea) { + navigate(ModalName.KoreaCexTransferInfoModal) + return + } + dispatch( + openModal({ + name: ModalName.FiatOnRampAggregator, + ...(entry === 'offramp' && { initialState: { isOfframp: true } }), + }), + ) + }, + [triggerHaptics, isTestnetModeEnabled, disableForKorea, multichainTokenUxEnabled, isPortfolioZero, dispatch, t], + ) // PR #4621 Necessary to declare these as direct dependencies due to race // condition with initializing react-i18next and useMemo const forLabel = t('home.label.for') const sendLabel = t('home.label.send') const receiveLabel = t('home.label.receive') + const buyLabel = t('common.buy.label') + const sellLabel = t('common.sell.label') const actions = useMemo( () => [ ...(isBottomTabsEnabled @@ -121,11 +145,11 @@ export function HomeScreenQuickActions(): JSX.Element { ] : []), { - Icon: Bank, + Icon: multichainTokenUxEnabled ? PlusCircle : Bank, eventName: MobileEventName.FiatOnRampQuickActionButtonPressed, - label: forLabel, + label: multichainTokenUxEnabled ? buyLabel : forLabel, name: ElementName.Buy, - onPress: onPressBuy, + onPress: () => onPressFORAction('onramp'), }, { Icon: SendAction, @@ -139,11 +163,34 @@ export function HomeScreenQuickActions(): JSX.Element { name: ElementName.Receive, onPress: onPressReceive, }, + ...(multichainTokenUxEnabled + ? [ + { + Icon: MinusCircle, + eventName: MobileEventName.FiatOnRampQuickActionButtonPressed, + label: sellLabel, + name: ElementName.Sell, + onPress: () => onPressFORAction('offramp'), + }, + ] + : []), + ], + [ + isBottomTabsEnabled, + onPressSwap, + multichainTokenUxEnabled, + buyLabel, + forLabel, + onPressFORAction, + sendLabel, + onPressSend, + receiveLabel, + onPressReceive, + sellLabel, ], - [isBottomTabsEnabled, onPressSwap, forLabel, onPressBuy, sendLabel, onPressSend, receiveLabel, onPressReceive], ) - // biome-ignore lint/correctness/useExhaustiveDependencies: +activeScale + // oxlint-disable-next-line react/exhaustive-deps -- +activeScale const renderItem = useCallback( ({ item: { eventName, name, label, Icon, onPress } }: ListRenderItemInfo) => ( @@ -152,6 +199,7 @@ export function HomeScreenQuickActions(): JSX.Element { mr="$spacing8" scaleTo={activeScale} dd-action-name={name} + testID={name} onPress={onPress} > {actions.map(({ eventName, name, label, Icon, onPress }) => ( - + { + if (hasSeenCreatedSmartWalletModal) { + return + } + setShouldShowCreatedModal(true) + }), + ) + + const MemoizedVideo = useMemo(() => { + if (hasVideoError) { + return ( + + + + ) + } + + return ( + + + ) + }, [hasVideoError]) + + const handleSmartWalletEnable = useCallback( + async (onComplete?: () => void): Promise => { + const successAction = (): void => { + dispatch(setSmartWalletConsent({ address: activeAccount.address, smartWalletConsent: true })) + onComplete?.() + navigate(ModalName.SmartWalletEnabledModal, { + showReconnectDappPrompt: false, + }) + } + + if (requiresBiometrics) { + await trigger({ successCallback: successAction }) + } else { + successAction() + } + }, + [dispatch, activeAccount.address, requiresBiometrics, trigger], + ) + + return ( + <> + {isSmartWalletEnabled && ( + + )} + + { + setShouldShowCreatedModal(false) + dispatch(setHasSeenSmartWalletCreatedWalletModal()) + }} + /> + + ) +} diff --git a/apps/mobile/src/screens/HomeScreen/useHomeScreenState.tsx b/apps/mobile/src/screens/HomeScreen/useHomeScreenState.tsx index ce81a1cdf43..40001820383 100644 --- a/apps/mobile/src/screens/HomeScreen/useHomeScreenState.tsx +++ b/apps/mobile/src/screens/HomeScreen/useHomeScreenState.tsx @@ -12,11 +12,11 @@ import { setHasBalanceOrActivity } from 'wallet/src/features/wallet/slice' import { WalletState } from 'wallet/src/state/walletReducer' /** - * This is the interval at which the NFTs tab will poll for new NFTs - * when the wallet is empty. Both activity and balances are updated - * in other parts of the app so we don't need to poll. + * Poll interval for checking wallet state (balances, NFTs) when the wallet + * is empty. Used to detect when the user first receives tokens so the + * empty-wallet UI can transition to the funded-wallet UI. */ -const EMPTY_WALLET_NFT_POLL_INTERVAL = 15 * ONE_SECOND_MS +const EMPTY_WALLET_POLL_INTERVAL = 15 * ONE_SECOND_MS /** * Helper hook used to determine the state of the home screen such as whether the wallet should fetch @@ -47,6 +47,7 @@ export function useHomeScreenState(): { const { data: balancesById, loading: areBalancesLoading } = usePortfolioBalances({ evmAddress: address, + pollInterval: !hasUsedWalletFromCache ? EMPTY_WALLET_POLL_INTERVAL : undefined, skip: hasUsedWalletFromCache, }) const { data: nftData, loading: areNFTsLoading } = GraphQLApi.useNftsTabQuery({ @@ -56,7 +57,7 @@ export function useHomeScreenState(): { filter: { filterSpam: true }, chains: gqlChains, }, - pollInterval: EMPTY_WALLET_NFT_POLL_INTERVAL, + pollInterval: EMPTY_WALLET_POLL_INTERVAL, notifyOnNetworkStatusChange: true, // Used to trigger network state / loading on refetch or fetchMore errorPolicy: 'all', // Suppress non-null image.url fields from backend skip: hasUsedWalletFromCache, @@ -91,7 +92,7 @@ export function useHomeScreenState(): { } return { - showEmptyWalletState: !hasUsedWallet, + showEmptyWalletState: !hasUsedWallet && isTabsDataLoaded, isTabsDataLoaded: dataLoadedRef.current || isTabsDataLoaded, } } diff --git a/apps/mobile/src/screens/HomeScreen/useHomeScrollRefs.ts b/apps/mobile/src/screens/HomeScreen/useHomeScrollRefs.ts index 23e53520d2d..746d3e45a05 100644 --- a/apps/mobile/src/screens/HomeScreen/useHomeScrollRefs.ts +++ b/apps/mobile/src/screens/HomeScreen/useHomeScrollRefs.ts @@ -4,9 +4,9 @@ import { FlatList } from 'react-native' import { useAnimatedRef, useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' import { TokenBalanceListRow } from 'uniswap/src/features/portfolio/types' -// biome-ignore lint/suspicious/noExplicitAny: Generic type needed for scroll ref +// oxlint-disable-next-line typescript/no-explicit-any -- Generic type needed for scroll ref type FlashListAnyType = FlashList -// biome-ignore lint/suspicious/noExplicitAny: Generic type needed for scroll ref +// oxlint-disable-next-line typescript/no-explicit-any -- Generic type needed for scroll ref type FlatListAnyType = FlatList type ScrollRefType = FlashListAnyType | FlatListAnyType diff --git a/apps/mobile/src/screens/Import/ImportMethodScreen.tsx b/apps/mobile/src/screens/Import/ImportMethodScreen.tsx index 572ca36bbcc..f99ac3ad5f4 100644 --- a/apps/mobile/src/screens/Import/ImportMethodScreen.tsx +++ b/apps/mobile/src/screens/Import/ImportMethodScreen.tsx @@ -1,9 +1,11 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' import { navigate } from 'src/app/navigation/rootNavigation' import { OnboardingStackParamList } from 'src/app/navigation/types' import { checkCloudBackupOrShowAlert } from 'src/components/mnemonic/cloudImportUtils' +import { useRegionalizedLineHeight } from 'src/components/text/useRegionalizedLineHeight' import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' import { OptionCard } from 'src/features/onboarding/OptionCard' import { @@ -17,8 +19,6 @@ import { Flex, SpinningLoader, Text, TouchableArea } from 'ui/src' import { Eye, WalletFilled } from 'ui/src/components/icons' import { useIsDarkMode } from 'ui/src/hooks/useIsDarkMode' import { iconSizes } from 'ui/src/theme' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { authenticateWithPasskeyForSeedPhraseExport } from 'uniswap/src/features/passkey/embeddedWallet' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' @@ -110,6 +110,8 @@ export function ImportMethodScreen({ navigation, route: { params } }: Props): JS importOptions = importOptions.filter((option) => option.name !== ElementName.OnboardingPasskey) } + const regionalizedLineHeight = useRegionalizedLineHeight() + return ( => handleOnPress(OnboardingScreens.WatchWallet, ImportType.Watch)} > {t('account.wallet.button.watch')} diff --git a/apps/mobile/src/screens/Import/OnDeviceRecoveryScreen.tsx b/apps/mobile/src/screens/Import/OnDeviceRecoveryScreen.tsx index 95c14285751..0172fcfffc9 100644 --- a/apps/mobile/src/screens/Import/OnDeviceRecoveryScreen.tsx +++ b/apps/mobile/src/screens/Import/OnDeviceRecoveryScreen.tsx @@ -1,6 +1,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation' import { SharedEventName } from '@uniswap/analytics-events' +import { DynamicConfigs, OnDeviceRecoveryConfigKey, useDynamicConfigValue } from '@universe/gating' import dayjs from 'dayjs' import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -20,8 +21,6 @@ import { iconSizes } from 'ui/src/theme' import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' import { AccountType } from 'uniswap/src/features/accounts/types' -import { DynamicConfigs, OnDeviceRecoveryConfigKey } from 'uniswap/src/features/gating/configs' -import { useDynamicConfigValue } from 'uniswap/src/features/gating/hooks' import { Platform } from 'uniswap/src/features/platforms/types/Platform' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' diff --git a/apps/mobile/src/screens/Import/OnDeviceRecoveryWalletCard.tsx b/apps/mobile/src/screens/Import/OnDeviceRecoveryWalletCard.tsx index 2d50f2708f0..f4450ec858f 100644 --- a/apps/mobile/src/screens/Import/OnDeviceRecoveryWalletCard.tsx +++ b/apps/mobile/src/screens/Import/OnDeviceRecoveryWalletCard.tsx @@ -41,11 +41,12 @@ export function OnDeviceRecoveryWalletCard({ const firstWalletInfo = targetWalletInfos[0] const remainingWalletCount = targetWalletInfos.length - 1 - // biome-ignore lint/correctness/useExhaustiveDependencies: we want to recalculate this only when loading, screenLoading changes + // oxlint-disable-next-line react/exhaustive-deps -- we want to recalculate this only when loading, screenLoading changes useEffect(() => { if (!loading && screenLoading) { onLoadComplete(significantRecoveryWalletInfos.length) } + // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here }, [loading, screenLoading]) if (screenLoading || !firstWalletInfo) { diff --git a/apps/mobile/src/screens/Import/PasskeyImportScreen.tsx b/apps/mobile/src/screens/Import/PasskeyImportScreen.tsx index 00e845449fe..77d967ddf98 100644 --- a/apps/mobile/src/screens/Import/PasskeyImportScreen.tsx +++ b/apps/mobile/src/screens/Import/PasskeyImportScreen.tsx @@ -30,7 +30,7 @@ export function PasskeyImportScreen({ navigation, route: { params } }: Props): J }) }) - // biome-ignore lint/correctness/useExhaustiveDependencies: We want to import the mnemonic only once + // oxlint-disable-next-line react/exhaustive-deps -- We want to import the mnemonic only once useEffect(() => { const importAndGenerateAccount = async (): Promise => { const mnemonic = await fetchSeedPhrase(params.passkeyCredential) @@ -54,6 +54,7 @@ export function PasskeyImportScreen({ navigation, route: { params } }: Props): J navigation.goBack() navigate(ModalName.PasskeysHelp) }) + // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here }, []) return ( diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx index 43f0df98856..6fb1cd8960f 100644 --- a/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx @@ -83,7 +83,7 @@ const BackupListItem = ({ {displayName?.name} {isUnitag && ( - + )} @@ -93,7 +93,7 @@ const BackupListItem = ({ - + diff --git a/apps/mobile/src/screens/Import/RestoreMethodScreen.tsx b/apps/mobile/src/screens/Import/RestoreMethodScreen.tsx index 994f0ce214b..d4b12c53b99 100644 --- a/apps/mobile/src/screens/Import/RestoreMethodScreen.tsx +++ b/apps/mobile/src/screens/Import/RestoreMethodScreen.tsx @@ -1,4 +1,5 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React from 'react' import { useTranslation } from 'react-i18next' import { OnboardingStackParamList } from 'src/app/navigation/types' @@ -13,8 +14,6 @@ import { useNavigationHeader } from 'src/utils/useNavigationHeader' import { Flex, Text, TouchableArea } from 'ui/src' import { WalletFilled } from 'ui/src/components/icons' import { useIsDarkMode } from 'ui/src/hooks/useIsDarkMode' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' import { TestID } from 'uniswap/src/test/fixtures/testIDs' diff --git a/apps/mobile/src/screens/Import/SeedPhraseInputScreen/SeedPhraseInput/SeedPhraseInput.tsx b/apps/mobile/src/screens/Import/SeedPhraseInputScreen/SeedPhraseInput/SeedPhraseInput.tsx index 5de1cac2780..89bec180dac 100644 --- a/apps/mobile/src/screens/Import/SeedPhraseInputScreen/SeedPhraseInput/SeedPhraseInput.tsx +++ b/apps/mobile/src/screens/Import/SeedPhraseInputScreen/SeedPhraseInput/SeedPhraseInput.tsx @@ -24,7 +24,7 @@ type SeedPhraseInputProps = NativeSeedPhraseInputProps & { navigation: NativeStackNavigationProp } -export const SeedPhraseInput = forwardRef(function _SeedPhraseInput( +export const SeedPhraseInput = forwardRef(function SeedPhraseInputInner( { navigation, ...rest }, ref, ) { diff --git a/apps/mobile/src/screens/Import/SeedPhraseInputScreen/SeedPhraseInput/types.ts b/apps/mobile/src/screens/Import/SeedPhraseInputScreen/SeedPhraseInput/types.ts index f991098ce45..0cb1c995785 100644 --- a/apps/mobile/src/screens/Import/SeedPhraseInputScreen/SeedPhraseInput/types.ts +++ b/apps/mobile/src/screens/Import/SeedPhraseInputScreen/SeedPhraseInput/types.ts @@ -1,5 +1,4 @@ import { NativeSyntheticEvent, StyleProp, ViewStyle } from 'react-native' - import { TestIDType } from 'uniswap/src/test/fixtures/testIDs' export enum StringKey { diff --git a/apps/mobile/src/screens/Import/SelectWalletScreen.tsx b/apps/mobile/src/screens/Import/SelectWalletScreen.tsx index 8e0f065ad69..666a9ede9f1 100644 --- a/apps/mobile/src/screens/Import/SelectWalletScreen.tsx +++ b/apps/mobile/src/screens/Import/SelectWalletScreen.tsx @@ -1,4 +1,5 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { ComponentProps, useCallback } from 'react' import { Trans, useTranslation } from 'react-i18next' import { ScrollView } from 'react-native' @@ -10,8 +11,6 @@ import { Button, Flex, Loader, Text, TouchableArea, useLayoutAnimationOnChange } import { WalletFilled } from 'ui/src/components/icons' import { spacing } from 'ui/src/theme' import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' import { TestID } from 'uniswap/src/test/fixtures/testIDs' @@ -109,9 +108,8 @@ export function SelectWalletScreen({ navigation, route: { params } }: Props): JS return null } return ( - + diff --git a/apps/mobile/src/screens/Import/WatchWalletScreen.tsx b/apps/mobile/src/screens/Import/WatchWalletScreen.tsx index 9a8fdff2d59..8b1ef0267d8 100644 --- a/apps/mobile/src/screens/Import/WatchWalletScreen.tsx +++ b/apps/mobile/src/screens/Import/WatchWalletScreen.tsx @@ -163,7 +163,7 @@ export function WatchWalletScreen({ navigation, route: { params } }: Props): JSX setValue(text?.trim()) } - // biome-ignore lint/correctness/useExhaustiveDependencies: Only want to reset timer on value change + // oxlint-disable-next-line react/exhaustive-deps -- Only want to reset timer on value change useEffect(() => { const delayFn = setTimeout(() => { setShowLiveCheck(true) diff --git a/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap b/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap index 2ba8b7f5615..e5a5af4bcc8 100644 --- a/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap +++ b/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap @@ -4,12 +4,7 @@ exports[`RestoreCloudBackupPasswordScreen renders correctly 1`] = ` unitagState.data?.username).join('') const unitagLoading = unitagStates.some((unitagState) => unitagState.isLoading) - // biome-ignore lint/correctness/useExhaustiveDependencies: we want to recalculate this when unitagsCombined or balancesLoading changes + // oxlint-disable-next-line react/exhaustive-deps -- we want to recalculate this when unitagsCombined or balancesLoading changes const recoveryWalletInfos = useMemo((): RecoveryWalletInfo[] => { return addressesWithIndex.map((addressWithIndex, index): RecoveryWalletInfo => { const { address, derivationIndex } = addressWithIndex @@ -140,6 +140,7 @@ export function useOnDeviceRecoveryData(mnemonicId: string | undefined): { unitag: unitagStates[derivationIndex]?.data?.username, } }) + // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here }, [addressesWithIndex, balances, balancesLoading, ensMap, unitagsCombined]) const significantRecoveryWalletInfos = useMemo( diff --git a/apps/mobile/src/screens/NFTCollectionScreen.tsx b/apps/mobile/src/screens/NFTCollectionScreen.tsx deleted file mode 100644 index 7932839ac0c..00000000000 --- a/apps/mobile/src/screens/NFTCollectionScreen.tsx +++ /dev/null @@ -1,268 +0,0 @@ -import { NetworkStatus } from '@apollo/client' -import { useScrollToTop } from '@react-navigation/native' -import { GraphQLApi, isError } from '@universe/api' -import React, { type ReactElement, useCallback, useMemo, useRef } from 'react' -import { useTranslation } from 'react-i18next' -import { type ListRenderItemInfo } from 'react-native' -import { useAnimatedScrollHandler, useSharedValue, withTiming } from 'react-native-reanimated' -import { type AppStackScreenProp, useAppStackNavigation } from 'src/app/navigation/types' -import { Screen } from 'src/components/layout/Screen' -import { ScrollHeader } from 'src/components/layout/screens/ScrollHeader' -import { Loader } from 'src/components/loading/loaders' -import { ListPriceBadge } from 'src/features/nfts/collection/ListPriceCard' -import { NFTCollectionContextMenu } from 'src/features/nfts/collection/NFTCollectionContextMenu' -import { NFT_BANNER_HEIGHT, NFTCollectionHeader } from 'src/features/nfts/collection/NFTCollectionHeader' -import { ExploreModalAwareView } from 'src/screens/ModalAwareView' -import { Flex, Text, TouchableArea } from 'ui/src' -import { AnimatedBottomSheetFlashList, AnimatedFlashList } from 'ui/src/components/AnimatedFlashList/AnimatedFlashList' -import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' -import { iconSizes, spacing } from 'ui/src/theme' -import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' -import { NFTViewer } from 'uniswap/src/components/nfts/images/NFTViewer' -import { type NFTItem } from 'uniswap/src/features/nfts/types' -import { getNFTAssetKey } from 'uniswap/src/features/nfts/utils' -import Trace from 'uniswap/src/features/telemetry/Trace' -import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' -import { MobileScreens } from 'uniswap/src/types/screens/mobile' -import { isIOS } from 'utilities/src/platform' - -const PREFETCH_ITEMS_THRESHOLD = 0.5 -const ASSET_FETCH_PAGE_SIZE = 30 -const ESTIMATED_ITEM_SIZE = 104 // heuristic provided by FlashList - -const LOADING_ITEM = 'loading' -const LOADING_BUFFER_AMOUNT = 9 -const LOADING_ITEMS_ARRAY: NFTItem[] = Array(LOADING_BUFFER_AMOUNT).fill(LOADING_ITEM) - -const keyExtractor = (item: NFTItem | string, index: number): string => - typeof item === 'string' ? `${LOADING_ITEM}-${index}` : getNFTAssetKey(item.contractAddress ?? '', item.tokenId ?? '') - -function gqlNFTAssetToNFTItem(data: GraphQLApi.NftCollectionScreenQuery | undefined): NFTItem[] | undefined { - const items = data?.nftAssets?.edges.flatMap((item) => item.node) - if (!items) { - return undefined - } - - return items.map((item): NFTItem => { - return { - name: item.name ?? undefined, - contractAddress: item.nftContract?.address ?? undefined, - tokenId: item.tokenId, - imageUrl: item.image?.url ?? undefined, - collectionName: item.collection?.name ?? undefined, - ownerAddress: item.ownerAddress ?? undefined, - imageDimensions: - item.image?.dimensions?.height && item.image.dimensions.width - ? { width: item.image.dimensions.width, height: item.image.dimensions.height } - : undefined, - listPrice: item.listings?.edges[0]?.node.price ?? undefined, - } - }) -} - -type NFTCollectionScreenProps = AppStackScreenProp & { - renderedInModal?: boolean -} - -export function NFTCollectionScreen({ - route: { - params: { collectionAddress }, - }, - renderedInModal = false, -}: NFTCollectionScreenProps): ReactElement { - const { t } = useTranslation() - const insets = useAppInsets() - const dimensions = useDeviceDimensions() - const navigation = useAppStackNavigation() - - // Collection overview data and paginated grid items - const { data, networkStatus, fetchMore, refetch } = GraphQLApi.useNftCollectionScreenQuery({ - variables: { contractAddress: collectionAddress, first: ASSET_FETCH_PAGE_SIZE }, - notifyOnNetworkStatusChange: true, - fetchPolicy: 'cache-and-network', - }) - - // Parse response for overview data and collection grid data - const collectionData = data?.nftCollections?.edges[0]?.node - const collectionItems = useMemo(() => gqlNFTAssetToNFTItem(data), [data]) - - // Fill in grid with loading boxes if we have incomplete data and are loading more - const extraLoadingItemAmount = - networkStatus === NetworkStatus.fetchMore || networkStatus === NetworkStatus.loading - ? LOADING_BUFFER_AMOUNT + (3 - ((collectionItems ?? []).length % 3)) - : undefined - - const onListEndReached = useCallback(async () => { - if (!data?.nftAssets?.pageInfo.hasNextPage) { - return - } - await fetchMore({ - variables: { - first: ASSET_FETCH_PAGE_SIZE, - after: data.nftAssets.pageInfo.endCursor, - }, - }) - }, [data?.nftAssets?.pageInfo.endCursor, data?.nftAssets?.pageInfo.hasNextPage, fetchMore]) - - // Scroll behavior for fixed scroll header - // biome-ignore lint/suspicious/noExplicitAny: FlashList ref type is complex and any is acceptable here - const listRef = useRef(null) - useScrollToTop(listRef) - const scrollY = useSharedValue(0) - const scrollHandler = useAnimatedScrollHandler( - { - onScroll: (event) => { - scrollY.value = event.contentOffset.y - }, - onEndDrag: (event) => { - scrollY.value = withTiming(event.contentOffset.y > 0 ? NFT_BANNER_HEIGHT : 0) - }, - }, - [scrollY], - ) - - const onPressItem = (asset: NFTItem): void => { - navigation.navigate(MobileScreens.NFTItem, { - address: asset.contractAddress ?? '', - tokenId: asset.tokenId ?? '', - isSpam: asset.isSpam ?? false, - fallbackData: asset, - }) - } - - /** - * @TODO: @ianlapham We can remove these styles when FLashList supports - * columnWrapperStyle prop (from FlatList). Until then, do this to preserve full width header, - * but padded list. - */ - const renderItem = ({ item, index }: ListRenderItemInfo): JSX.Element => { - const first = index % 3 === 0 - const last = index % 3 === 2 - const middle = !first && !last - const containerStyle = { - marginLeft: middle ? spacing.spacing8 : first ? spacing.spacing16 : 0, - marginRight: middle ? spacing.spacing8 : last ? spacing.spacing16 : 0, - marginBottom: spacing.spacing8, - } - const priceColor = isIOS ? '$white' : '$neutral1' - - return ( - - {typeof item === 'string' ? ( - - ) : ( - onPressItem(item)}> - - {item.listPrice && ( - - )} - - )} - - ) - } - - // Only show loading UI if no data and first request, otherwise render cached data - const headerDataLoading = networkStatus === NetworkStatus.loading && !collectionData - const gridDataLoading = networkStatus === NetworkStatus.loading && !collectionItems - - const gridDataWithLoadingElements = useMemo(() => { - if (gridDataLoading) { - return LOADING_ITEMS_ARRAY - } - - const extraLoadingItems: NFTItem[] = extraLoadingItemAmount ? Array(extraLoadingItemAmount).fill(LOADING_ITEM) : [] - - return [...(collectionItems ?? []), ...extraLoadingItems] - }, [collectionItems, extraLoadingItemAmount, gridDataLoading]) - - const traceProperties = useMemo( - () => (collectionData?.name ? { collectionAddress, collectionName: collectionData.name } : undefined), - [collectionAddress, collectionData?.name], - ) - - if (isError(networkStatus, !!data)) { - return ( - - - - - - - ) - } - - const List = renderedInModal ? AnimatedBottomSheetFlashList : AnimatedFlashList - - return ( - - - - {collectionData.name} : undefined} - listRef={listRef} - rightElement={} - scrollY={scrollY} - showHeaderScrollYDistance={NFT_BANNER_HEIGHT} - /> - - } - ListHeaderComponent={} - contentContainerStyle={{ paddingBottom: insets.bottom }} - data={gridDataWithLoadingElements} - estimatedItemSize={ESTIMATED_ITEM_SIZE} - estimatedListSize={{ - width: dimensions.fullWidth, - height: dimensions.fullHeight, - }} - keyExtractor={keyExtractor} - numColumns={3} - renderItem={renderItem} - showsVerticalScrollIndicator={false} - onEndReached={onListEndReached} - onEndReachedThreshold={PREFETCH_ITEMS_THRESHOLD} - onScroll={scrollHandler} - /> - - - - ) -} diff --git a/apps/mobile/src/screens/NFTItemScreen.tsx b/apps/mobile/src/screens/NFTItemScreen.tsx deleted file mode 100644 index 2af9b1d4be6..00000000000 --- a/apps/mobile/src/screens/NFTItemScreen.tsx +++ /dev/null @@ -1,515 +0,0 @@ -/* eslint-disable complexity, max-lines */ -import { ApolloQueryResult } from '@apollo/client' -import { GraphQLApi } from '@universe/api' -import { isAddress } from 'ethers/lib/utils' -import React, { useCallback, useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { GestureResponderEvent, StatusBar, StyleSheet } from 'react-native' -import { useDispatch } from 'react-redux' -import { AppStackScreenProp, useAppStackNavigation } from 'src/app/navigation/types' -import { HeaderScrollScreen } from 'src/components/layout/screens/HeaderScrollScreen' -import { Loader } from 'src/components/loading/loaders' -import { useIsInModal } from 'src/components/modals/useIsInModal' -import { LongMarkdownText } from 'src/components/text/LongMarkdownText' -import { PriceAmount } from 'src/features/nfts/collection/ListPriceCard' -import { BlurredImageBackground } from 'src/features/nfts/item/BlurredImageBackground' -import { CollectionPreviewCard } from 'src/features/nfts/item/CollectionPreviewCard' -import { NFTTraitList } from 'src/features/nfts/item/traits' -import { ExploreModalAwareView } from 'src/screens/ModalAwareView' -import { - Flex, - getTokenValue, - MIN_COLOR_CONTRAST_THRESHOLD, - passesContrast, - Text, - Theme, - TouchableArea, - useSporeColors, -} from 'ui/src' -import { CopyAlt, Ellipsis } from 'ui/src/components/icons' -import { colorsDark, fonts, iconSizes } from 'ui/src/theme' -import { AddressDisplay } from 'uniswap/src/components/accounts/AddressDisplay' -import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' -import { NetworkLogo } from 'uniswap/src/components/CurrencyLogo/NetworkLogo' -import { ContextMenu } from 'uniswap/src/components/menus/ContextMenuV2' -import { ContextMenuTriggerMode } from 'uniswap/src/components/menus/types' -import { NFTViewer } from 'uniswap/src/components/nfts/images/NFTViewer' -import { PollingInterval } from 'uniswap/src/constants/misc' -import { UniverseChainId } from 'uniswap/src/features/chains/types' -import { fromGraphQLChain, getChainLabel } from 'uniswap/src/features/chains/utils' -import { useNFTContextMenuItems } from 'uniswap/src/features/nfts/hooks/useNftContextMenuItems' -import { pushNotification } from 'uniswap/src/features/notifications/slice/slice' -import { AppNotificationType, CopyNotificationType } from 'uniswap/src/features/notifications/slice/types' -import { Platform } from 'uniswap/src/features/platforms/types/Platform' -import { chainIdToPlatform } from 'uniswap/src/features/platforms/utils/chains' -import { ModalName } from 'uniswap/src/features/telemetry/constants' -import Trace from 'uniswap/src/features/telemetry/Trace' -import { MobileScreens } from 'uniswap/src/types/screens/mobile' -import { areAddressesEqual } from 'uniswap/src/utils/addresses' -import { setClipboard, setClipboardImage } from 'uniswap/src/utils/clipboard' -import { useNearestThemeColorFromImageUri } from 'uniswap/src/utils/colors' -import { shortenAddress } from 'utilities/src/addresses' -import { isAndroid, isIOS } from 'utilities/src/platform' -import { useBooleanState } from 'utilities/src/react/useBooleanState' -import { useAccounts, useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' - -const MAX_NFT_IMAGE_HEIGHT = 375 - -type NFTItemScreenProps = AppStackScreenProp - -export function NFTItemScreen(props: NFTItemScreenProps): JSX.Element { - return isAndroid ? ( - // display screen with theme dependent colors on Android - - ) : ( - // put Theme above the Contents so our useSporeColors() gets the right colors - - - - ) -} - -function NFTItemScreenContents({ - route: { - // ownerFromProps needed here, when nftBalances GQL query returns a user NFT, - // but nftAssets query for this NFT returns ownerAddress === null, - params: { owner: ownerFromProps, address, tokenId, isSpam, fallbackData }, - }, -}: NFTItemScreenProps): JSX.Element { - const { t } = useTranslation() - const activeAccountAddress = useActiveAccountAddressWithThrow() - const dispatch = useDispatch() - const colors = useSporeColors() - const navigation = useAppStackNavigation() - - const { - data, - loading: nftLoading, - refetch, - } = GraphQLApi.useNftItemScreenQuery({ - variables: { - contractAddress: address, - filter: { tokenIds: [tokenId] }, - activityFilter: { - address, - tokenId, - activityTypes: [GraphQLApi.NftActivityType.Sale], - }, - }, - pollInterval: PollingInterval.Slow, - }) - const asset = data?.nftAssets?.edges[0]?.node - const owner = (ownerFromProps || asset?.ownerAddress) ?? undefined - const chainId = fromGraphQLChain(fallbackData?.chain) ?? undefined - const contractAddress = address || asset?.nftContract?.address || fallbackData?.contractAddress - - const lastSaleData = data?.nftActivity?.edges[0]?.node - const listingPrice = asset?.listings?.edges[0]?.node.price - - const name = useMemo(() => asset?.name ?? fallbackData?.name, [asset?.name, fallbackData?.name]) - const description = useMemo( - () => asset?.description ?? fallbackData?.description, - [asset?.description, fallbackData?.description], - ) - const imageUrl = useMemo( - () => asset?.image?.url ?? fallbackData?.imageUrl, - [asset?.image?.url, fallbackData?.imageUrl], - ) - const imageHeight = asset?.image?.dimensions?.height - const imageWidth = asset?.image?.dimensions?.width - const imageDimensionsExist = imageHeight && imageWidth - const imageDimensions = imageDimensionsExist ? { height: imageHeight, width: imageWidth } : undefined - const imageAspectRatio = imageDimensions ? imageDimensions.width / imageDimensions.height : 1 - const onPressCollection = (): void => { - if (contractAddress) { - navigation.navigate(MobileScreens.NFTCollection, { collectionAddress: contractAddress }) - } - } - - // Disable navigation to profile if user owns NFT or invalid owner - const platform = chainId ? chainIdToPlatform(chainId) : Platform.EVM - const disableProfileNavigation = Boolean( - owner && - (areAddressesEqual({ - addressInput1: { address: owner, platform }, - addressInput2: { address: activeAccountAddress, platform }, - }) || - !isAddress(owner)), - ) - - const onPressOwner = (): void => { - if (owner) { - navigation.navigate(MobileScreens.ExternalProfile, { - address: owner, - }) - } - } - - const inModal = useIsInModal(ModalName.Explore) - - const traceProperties: Record> = useMemo(() => { - const baseProps = { - owner, - address, - tokenId, - } - - if (asset?.collection?.name) { - return { - ...baseProps, - collectionName: asset.collection.name, - isMissingData: false, - } - } - - if (fallbackData) { - return { - ...baseProps, - collectionName: fallbackData.collectionName, - isMissingData: true, - } - } - - return { ...baseProps, isMissingData: true } - }, [address, asset?.collection?.name, fallbackData, owner, tokenId]) - - const { collectionName } = traceProperties - - const displayCollectionName = name || collectionName - - const { colorLight, colorDark } = useNearestThemeColorFromImageUri(imageUrl) - // check if colorLight passes contrast against card bg color, if not use fallback - const accentTextColor = useMemo(() => { - if ( - colorLight && - passesContrast({ - color: colorLight, - backgroundColor: colors.surface1.val, - contrastThreshold: MIN_COLOR_CONTRAST_THRESHOLD, - }) - ) { - return colorLight - } - return colors.neutral2.val - }, [colorLight, colors.neutral2, colors.surface1]) - - const onLongPressNFTImage = async (): Promise => { - await setClipboardImage(imageUrl) - - dispatch( - pushNotification({ - type: AppNotificationType.Copied, - copyType: CopyNotificationType.Image, - }), - ) - } - - const rightElement = useMemo( - () => ( - - ), - [chainId, contractAddress, isSpam, owner, tokenId], - ) - - const onPressCopyAddress = useCallback( - async (_: GestureResponderEvent) => { - if (contractAddress) { - await setClipboard(contractAddress) - dispatch( - pushNotification({ - type: AppNotificationType.Copied, - copyType: CopyNotificationType.Address, - }), - ) - } - }, - [contractAddress, dispatch], - ) - - return ( - <> - {isIOS ? : null} - - - <> - {isIOS ? ( - - ) : ( - - )} - - - - ) : displayCollectionName ? ( - - {displayCollectionName} - - ) : undefined - } - renderedInModal={inModal} - rightElement={rightElement} - > - {/* Content wrapper */} - - - - {nftLoading ? ( - - - - ) : imageUrl ? ( - - - - ) : ( - - {displayCollectionName ? ( - - {displayCollectionName} - - ) : ( - > => refetch()} - /> - )} - - )} - - - {nftLoading ? ( - - ) : displayCollectionName ? ( - - {displayCollectionName} - - ) : null} - - - - {/* Description */} - - {nftLoading ? ( - - - - ) : description ? ( - - ) : null} - - - {/* Metadata */} - - {listingPrice?.value ? ( - - } - /> - ) : null} - {chainId && ( - - - {getChainLabel(chainId)} - - - - } - /> - )} - {lastSaleData?.price?.value ? ( - - } - /> - ) : null} - - {contractAddress ? ( - - {shortenAddress({ address: contractAddress })} - - - } - /> - ) : null} - - {owner && ( - - - - } - /> - )} - - - {/* Traits */} - {asset?.traits && asset.traits.length > 0 ? ( - - - {t('tokens.nfts.details.traits')} - - - - ) : null} - - - - - - - ) -} - -function AssetMetadata({ - title, - valueComponent, - color, -}: { - title: string - valueComponent: JSX.Element - color: string -}): JSX.Element { - return ( - - - - {title} - - - {valueComponent} - - ) -} - -function RightElement({ - chainId, - contractAddress, - tokenId, - owner, - isSpam, -}: { - chainId?: UniverseChainId - contractAddress?: string - tokenId?: string - owner?: string - isSpam?: boolean -}): JSX.Element { - const accounts = useAccounts() - - const { value: contextMenuIsOpen, setFalse: closeContextMenu, setTrue: openContextMenu } = useBooleanState(false) - - const menuItems = useNFTContextMenuItems({ - contractAddress, - tokenId, - owner, - walletAddresses: Object.keys(accounts), - showNotification: true, - isSpam, - chainId, - }) - - return ( - - {menuItems.length > 0 && ( - - - - - - )} - - ) -} diff --git a/apps/mobile/src/screens/Onboarding/LandingScreen.tsx b/apps/mobile/src/screens/Onboarding/LandingScreen.tsx index e18fd5f0ac5..5c8de6bb751 100644 --- a/apps/mobile/src/screens/Onboarding/LandingScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/LandingScreen.tsx @@ -1,5 +1,6 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useCallback, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { useAnimatedStyle, useSharedValue, withDelay, withTiming } from 'react-native-reanimated' @@ -12,8 +13,6 @@ import { TermsOfService } from 'src/screens/Onboarding/TermsOfService' import { Button, Flex, Text, TouchableArea } from 'ui/src' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { setIsTestnetModeEnabled } from 'uniswap/src/features/settings/slice' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import Trace from 'uniswap/src/features/telemetry/Trace' @@ -40,7 +39,8 @@ export function LandingScreen({ navigation }: Props): JSX.Element { useEffect(() => { // disables looping animation during e2e tests which was preventing js thread from idle actionButtonsOpacity.value = withDelay(LANDING_ANIMATION_DURATION, withTiming(1, { duration: ONE_SECOND_MS })) - }, [actionButtonsOpacity]) + // oxlint-disable-next-line react/exhaustive-deps -- biome-parity: oxlint is stricter here + }, []) // Disables testnet mode on mount if enabled (eg upon removing a wallet) useEffect(() => { diff --git a/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx b/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx index 8e6235aca99..88227707d53 100644 --- a/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx @@ -1,7 +1,8 @@ +import { useFocusEffect } from '@react-navigation/core' import { NativeStackScreenProps } from '@react-navigation/native-stack' import { SharedEventName } from '@uniswap/analytics-events' import { addScreenshotListener } from 'expo-screen-capture' -import React, { useEffect, useReducer, useState } from 'react' +import React, { useCallback, useEffect, useReducer, useState } from 'react' import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' import { navigate } from 'src/app/navigation/rootNavigation' @@ -86,16 +87,18 @@ export function ManualBackupScreen({ navigation, route: { params } }: Props): JS navigate(MobileScreens.Home) } - useEffect(() => { - if (view !== View.SeedPhrase) { - return undefined - } + useFocusEffect( + useCallback(() => { + if (view !== View.SeedPhrase) { + return undefined + } - const listener = addScreenshotListener(() => - navigate(ModalName.ScreenshotWarning, { acknowledgeText: t('common.button.ok') }), - ) - return () => listener.remove() - }, [view, t]) + const listener = addScreenshotListener(() => { + navigate(ModalName.ScreenshotWarning, { acknowledgeText: t('common.button.ok') }) + }) + return () => listener.remove() + }, [view, t]), + ) useEffect(() => { if (confirmContinueButtonPressed && hasBackup(BackupType.Manual, account)) { diff --git a/apps/mobile/src/screens/Onboarding/TermsOfService.tsx b/apps/mobile/src/screens/Onboarding/TermsOfService.tsx index cf8623168c3..8913c9cb725 100644 --- a/apps/mobile/src/screens/Onboarding/TermsOfService.tsx +++ b/apps/mobile/src/screens/Onboarding/TermsOfService.tsx @@ -10,6 +10,7 @@ export function TermsOfService(): JSX.Element { components={{ highlightTerms: ( => openUri({ uri: uniswapUrls.termsOfServiceUrl })} @@ -17,6 +18,7 @@ export function TermsOfService(): JSX.Element { ), highlightPrivacy: ( => openUri({ uri: uniswapUrls.privacyPolicyUrl })} diff --git a/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx b/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx index e24c5e7baec..15553acf052 100644 --- a/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx +++ b/apps/mobile/src/screens/Onboarding/WelcomeWalletScreen.tsx @@ -9,8 +9,8 @@ import { Button, Flex, Loader, Text, useMedia, useSporeColors } from 'ui/src' import { Arrow } from 'ui/src/components/arrow/Arrow' import { Lock } from 'ui/src/components/icons' import { fonts, iconSizes, opacify } from 'ui/src/theme' -import AnimatedNumber from 'uniswap/src/components/AnimatedNumber/AnimatedNumber' import { DisplayNameText } from 'uniswap/src/components/accounts/DisplayNameText' +import AnimatedNumber from 'uniswap/src/components/AnimatedNumber/AnimatedNumber' import { AccountIcon } from 'uniswap/src/features/accounts/AccountIcon' import { DisplayNameType } from 'uniswap/src/features/accounts/types' import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' diff --git a/apps/mobile/src/screens/Onboarding/__snapshots__/BackupScreen.test.tsx.snap b/apps/mobile/src/screens/Onboarding/__snapshots__/BackupScreen.test.tsx.snap index 167f5fdd4ad..126056f6548 100644 --- a/apps/mobile/src/screens/Onboarding/__snapshots__/BackupScreen.test.tsx.snap +++ b/apps/mobile/src/screens/Onboarding/__snapshots__/BackupScreen.test.tsx.snap @@ -4,12 +4,7 @@ exports[`BackupScreen renders backup options when none are completed 1`] = ` void }): JSX.Element { } return ( - + ({ + sessionId: state.session.sessionId, + deviceId: state.session.deviceId, + uniswapIdentifier: state.session.uniswapIdentifier, + })), + ) + const challenge = useSessionsDebugStore((state) => state.challenge) + const isLoading = useSessionsDebugStore((state) => state.isLoading) + const hashcashIsRunning = useSessionsDebugStore((state) => state.hashcashProgress.isRunning) + const hashcashStartTime = useSessionsDebugStore((state) => state.hashcashProgress.startTime) + + // Actions (stable references) + const setSession = useSessionsDebugStore((state) => state.setSession) + const setChallenge = useSessionsDebugStore((state) => state.setChallenge) + const startOperation = useSessionsDebugStore((state) => state.startOperation) + const endOperation = useSessionsDebugStore((state) => state.endOperation) + const addLog = useSessionsDebugStore((state) => state.addLog) + const startHashcash = useSessionsDebugStore((state) => state.startHashcash) + const updateHashcashProgress = useSessionsDebugStore((state) => state.updateHashcashProgress) + const completeHashcash = useSessionsDebugStore((state) => state.completeHashcash) + const stopHashcash = useSessionsDebugStore((state) => state.stopHashcash) + const reset = useSessionsDebugStore((state) => state.reset) + + const sessionServiceRef = useRef(null) + + const getSessionService = useCallback((): SessionService => { + if (!sessionServiceRef.current) { + sessionServiceRef.current = provideSessionService({ + getBaseUrl: getEntryGatewayUrl, + getIsSessionServiceEnabled: () => true, // Always enabled for debug + getLogger: () => logger, + }) + } + return sessionServiceRef.current + }, []) + + const refreshSessionState = useCallback(async (): Promise => { + const driver = getStorageDriver() + const [sessionId, deviceId, uniswapIdentifier] = await Promise.all([ + driver.get(SESSION_ID_KEY), + driver.get(DEVICE_ID_KEY), + driver.get(UNISWAP_IDENTIFIER_KEY), + ]) + setSession({ + sessionId: sessionId || null, + deviceId: deviceId || null, + uniswapIdentifier: uniswapIdentifier || null, + }) + }, [setSession]) + + // Initial load - only run once on mount + useEffect(() => { + const loadInitialState = async (): Promise => { + const driver = getStorageDriver() + const [sessionId, deviceId, uniswapIdentifier] = await Promise.all([ + driver.get(SESSION_ID_KEY), + driver.get(DEVICE_ID_KEY), + driver.get(UNISWAP_IDENTIFIER_KEY), + ]) + setSession({ + sessionId: sessionId || null, + deviceId: deviceId || null, + uniswapIdentifier: uniswapIdentifier || null, + }) + } + loadInitialState().catch(() => undefined) + }, [setSession]) + + // Progress timer for hashcash + useEffect(() => { + if (hashcashIsRunning && hashcashStartTime !== null) { + const interval = setInterval(() => { + const elapsed = performance.now() - hashcashStartTime + // Estimate ~900k hashes/sec on native + const estimatedAttempts = Math.floor((elapsed / 1000) * 900000) + updateHashcashProgress(elapsed, estimatedAttempts) + }, 100) + + return (): void => { + clearInterval(interval) + } + } + return undefined + }, [hashcashIsRunning, hashcashStartTime, updateHashcashProgress]) + + const clearAllState = useCallback(async (): Promise => { + startOperation('Clearing all state...') + try { + const driver = getStorageDriver() + await Promise.all([ + driver.remove(SESSION_ID_KEY), + driver.remove(DEVICE_ID_KEY), + driver.remove(UNISWAP_IDENTIFIER_KEY), + ]) + // Reset the session service ref so a fresh one is created next time + sessionServiceRef.current = null + setChallenge(null) + reset() + addLog('Cleared all session state', 'success') + await refreshSessionState() + } catch (error) { + addLog(`Failed to clear state: ${error}`, 'error') + logger.error(error, { tags: { file: 'SessionsDebugScreen', function: 'clearAllState' } }) + } finally { + endOperation() + } + }, [startOperation, setChallenge, reset, addLog, refreshSessionState, endOperation]) + + const handleInitSession = useCallback(async (): Promise => { + startOperation('Initializing session...') + addLog('Init session started') + try { + const service = getSessionService() + const result = await service.initSession() + addLog(`Session initialized. needChallenge: ${result.needChallenge}`, 'success') + if (result.sessionId) { + addLog(`Session ID: ${truncateId(result.sessionId)}`) + } + await refreshSessionState() + } catch (error) { + addLog(`Init session failed: ${error}`, 'error') + logger.error(error, { tags: { file: 'SessionsDebugScreen', function: 'handleInitSession' } }) + } finally { + endOperation() + } + }, [startOperation, addLog, getSessionService, refreshSessionState, endOperation]) + + const handleRequestChallenge = useCallback(async (): Promise => { + startOperation('Requesting challenge...') + addLog('Request challenge started') + try { + const service = getSessionService() + const challengeResult = await service.requestChallenge() + setChallenge(challengeResult) + const challengeTypeName = ChallengeType[challengeResult.challengeType] || 'Unknown' + addLog(`Challenge received: ${challengeTypeName}`, 'success') + addLog(`Challenge ID: ${truncateId(challengeResult.challengeId)}`) + + // Parse difficulty from extra if hashcash + if (challengeResult.challengeType === ChallengeType.HASHCASH && challengeResult.extra['challengeData']) { + try { + const challengeData = JSON.parse(challengeResult.extra['challengeData']) + addLog(`Difficulty: ${challengeData.difficulty}`) + } catch { + // Ignore parse errors + } + } + } catch (error) { + addLog(`Request challenge failed: ${error}`, 'error') + logger.error(error, { tags: { file: 'SessionsDebugScreen', function: 'handleRequestChallenge' } }) + } finally { + endOperation() + } + }, [startOperation, addLog, getSessionService, setChallenge, endOperation]) + + const handleSolveChallenge = useCallback(async (): Promise => { + const currentChallenge = useSessionsDebugStore.getState().challenge + if (!currentChallenge) { + addLog('No challenge to solve. Request a challenge first.', 'error') + return + } + + if (currentChallenge.challengeType !== ChallengeType.HASHCASH) { + addLog('Only Hashcash challenges are supported on mobile', 'error') + return + } + + startOperation('Solving hashcash challenge...') + addLog('Hashcash solve started') + + // Parse difficulty for progress display + let difficulty = 0 + if (currentChallenge.extra['challengeData']) { + try { + const challengeData = JSON.parse(currentChallenge.extra['challengeData']) + difficulty = challengeData.difficulty || 0 + } catch { + // Use default + } + } + + // Start progress tracking + startHashcash(difficulty) + + try { + const solver = createHashcashSolver({ + performanceTracker: { + now: () => performance.now(), + }, + getWorkerChannel: () => createHashcashWorkerChannel(), + onSolveCompleted: (data) => { + completeHashcash(data) + }, + }) + + const solution = await solver.solve({ + challengeId: currentChallenge.challengeId, + challengeType: currentChallenge.challengeType, + extra: currentChallenge.extra, + }) + + addLog(`Challenge solved!`, 'success') + addLog(`Solution: ${truncateId(solution, 32)}`) + + // Verify with backend + startOperation('Verifying session...') + addLog('Verifying session with backend...') + const service = getSessionService() + const verifyResult = await service.verifySession({ + solution, + challengeId: currentChallenge.challengeId, + challengeType: currentChallenge.challengeType, + }) + + if (verifyResult.retry) { + addLog('Verification returned retry=true. May need another challenge.', 'info') + } else { + addLog('Session verified successfully!', 'success') + } + + setChallenge(null) + await refreshSessionState() + } catch (error) { + stopHashcash() + addLog(`Solve challenge failed: ${error}`, 'error') + logger.error(error, { tags: { file: 'SessionsDebugScreen', function: 'handleSolveChallenge' } }) + } finally { + endOperation() + } + }, [ + addLog, + startOperation, + startHashcash, + completeHashcash, + getSessionService, + setChallenge, + refreshSessionState, + stopHashcash, + endOperation, + ]) + + const copyToClipboard = useCallback( + async (value: string | null, label: string): Promise => { + if (!value) { + return + } + await setClipboard(value) + addLog(`Copied ${label} to clipboard`, 'info') + }, + [addLog], + ) + + const hasChallenge = challenge !== null + + return ( + + + + + + + Sessions Debug + + Test session initialization flow step by step. + + + {/* Session Status Section */} + + Session Status + + + + + Session ID: + + + + {truncateId(session.sessionId)} + + {session.sessionId && ( + copyToClipboard(session.sessionId, 'Session ID')}> + + + )} + + + + + + Device ID: + + + + {truncateId(session.deviceId)} + + {session.deviceId && ( + copyToClipboard(session.deviceId, 'Device ID')}> + + + )} + + + + + + Uniswap ID: + + + + {truncateId(session.uniswapIdentifier)} + + {session.uniswapIdentifier && ( + copyToClipboard(session.uniswapIdentifier, 'Uniswap ID')}> + + + )} + + + + + + Challenge Pending: + + + {hasChallenge ? 'Yes' : 'No'} + + + + + + {/* Action Buttons */} + + + + + + {/* Step-by-Step Testing */} + + Step-by-Step Testing + + + + + + + + {/* Current Operation */} + + + {/* Hashcash Progress */} + + + {/* Operation Log */} + + + + + ) +} diff --git a/apps/mobile/src/screens/SettingsCloudBackupPasswordConfirmScreen.tsx b/apps/mobile/src/screens/SettingsCloudBackupPasswordConfirmScreen.tsx index bf9c114298d..365345a5a61 100644 --- a/apps/mobile/src/screens/SettingsCloudBackupPasswordConfirmScreen.tsx +++ b/apps/mobile/src/screens/SettingsCloudBackupPasswordConfirmScreen.tsx @@ -25,6 +25,7 @@ export function SettingsCloudBackupPasswordConfirmScreen({ navigation, route: { return ( diff --git a/apps/mobile/src/screens/SettingsCloudBackupPasswordCreateScreen.tsx b/apps/mobile/src/screens/SettingsCloudBackupPasswordCreateScreen.tsx index 569b08df363..4ebfeb00276 100644 --- a/apps/mobile/src/screens/SettingsCloudBackupPasswordCreateScreen.tsx +++ b/apps/mobile/src/screens/SettingsCloudBackupPasswordCreateScreen.tsx @@ -64,7 +64,11 @@ export function SettingsCloudBackupPasswordCreateScreen({ {showCloudBackupInfoModal && ( - + setShowCloudBackupInfoModal(false)} + > diff --git a/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx b/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx index 4394cd2cad5..058c12fb626 100644 --- a/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx +++ b/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx @@ -1,5 +1,5 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack' -import React, { useState } from 'react' +import React, { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { Alert } from 'react-native' import { FlatList } from 'react-native-gesture-handler' @@ -15,14 +15,18 @@ import { Check } from 'ui/src/components/icons' import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' import { spacing } from 'ui/src/theme' import { AddressDisplay } from 'uniswap/src/components/accounts/AddressDisplay' +import { WarningSeverity } from 'uniswap/src/components/modals/WarningModal/types' import { WarningModal } from 'uniswap/src/components/modals/WarningModal/WarningModal' import { AccountType } from 'uniswap/src/features/accounts/types' +import { useLocalizationContext } from 'uniswap/src/features/language/LocalizationContext' import { ModalName } from 'uniswap/src/features/telemetry/constants' import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' +import { NumberType } from 'utilities/src/format/types' import { logger } from 'utilities/src/logger/logger' +import { useAccountListData } from 'wallet/src/features/accounts/useAccountListData' import { EditAccountAction, editAccountActions } from 'wallet/src/features/wallet/accounts/editAccountSaga' import { Account, BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' import { useAccounts } from 'wallet/src/features/wallet/hooks' @@ -40,6 +44,7 @@ export function SettingsCloudBackupStatus({ const insets = useAppInsets() const dimensions = useDeviceDimensions() const dispatch = useDispatch() + const { convertFiatAmountFormatted } = useLocalizationContext() const accounts = useAccounts() const mnemonicId = (accounts[address] as SignerMnemonicAccount).mnemonicId const androidCloudBackupEmail = useSelector(selectAndroidCloudBackupEmail) @@ -47,6 +52,24 @@ export function SettingsCloudBackupStatus({ (a) => a.type === AccountType.SignerMnemonic && a.mnemonicId === mnemonicId, ) + // Fetch balance data for associated accounts + const accountAddresses = useMemo(() => associatedAccounts.map((account) => account.address), [associatedAccounts]) + const { data: accountBalanceData, loading } = useAccountListData({ + addresses: accountAddresses, + }) + + // Create balance mapping + const balanceRecord: Record = useMemo(() => { + if (!accountBalanceData?.portfolios) { + return {} + } + return Object.fromEntries( + accountBalanceData.portfolios + .filter((portfolio): portfolio is NonNullable => Boolean(portfolio)) + .map((portfolio) => [portfolio.ownerAddress, portfolio.tokensTotalDenominatedValue?.value ?? 0]), + ) + }, [accountBalanceData]) + const [showBackupDeleteWarning, setShowBackupDeleteWarning] = useState(false) const onConfirmDeleteBackup = async (): Promise => { if (requiredForTransactions) { @@ -90,13 +113,28 @@ export function SettingsCloudBackupStatus({ navigation.goBack() } - const renderItem = ({ item, index }: { item: Account; index: number }): JSX.Element => ( - - - - ) + const renderItem = ({ item, index }: { item: Account; index: number }): JSX.Element => { + const balance = balanceRecord[item.address] ?? 0 + const formattedBalance = convertFiatAmountFormatted(balance, NumberType.PortfolioBalance) + + return ( + + + + {formattedBalance} + + + ) + } - const fullScreenContentHeight = (dimensions.fullHeight - insets.top - insets.bottom - spacing.spacing36) / 2 + const maxListHeight = (dimensions.fullHeight - insets.top - insets.bottom - spacing.spacing16) / 2.5 return ( @@ -155,21 +193,24 @@ export function SettingsCloudBackupStatus({ })} rejectText={t('common.button.close')} acknowledgeText={t('common.button.delete')} + acknowledgeButtonVariant="critical" isOpen={showBackupDeleteWarning} modalName={ModalName.ViewSeedPhraseWarning} title={t('settings.setting.backup.delete.confirm.title')} + severity={WarningSeverity.High} onClose={(): void => { setShowBackupDeleteWarning(false) }} onAcknowledge={onConfirmDeleteBackup} > {associatedAccounts.length > 1 && ( - - + + {t('settings.setting.backup.delete.confirm.message')} - + `${index}-${item.address}`} diff --git a/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx b/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx index ab778953bbc..e837e91ef18 100644 --- a/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx +++ b/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx @@ -1,76 +1,39 @@ -import React, { useCallback } from 'react' +import React from 'react' import { useTranslation } from 'react-i18next' -// TODO(WALL-7189): Explore removing FlatList. Currently using this to fix a scrolling regression. -import { FlatList } from 'react-native-gesture-handler' import { useDispatch } from 'react-redux' -import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal' -import { Flex, Text, TouchableArea } from 'ui/src' -import { Check } from 'ui/src/components/icons' -import { Modal } from 'uniswap/src/components/modals/Modal' +import { SettingsListModal } from 'src/components/Settings/lists/SettingsListModal' import { FiatCurrency, ORDERED_CURRENCIES } from 'uniswap/src/features/fiatCurrency/constants' -import { useAppFiatCurrency, useFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' +import { getFiatCurrencyCode, getFiatCurrencyName, useAppFiatCurrency } from 'uniswap/src/features/fiatCurrency/hooks' import { setCurrentFiatCurrency } from 'uniswap/src/features/settings/slice' import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { useEvent } from 'utilities/src/react/hooks' export function SettingsFiatCurrencyModal(): JSX.Element { + const dispatch = useDispatch() const { t } = useTranslation() const selectedCurrency = useAppFiatCurrency() - const { onClose } = useReactNavigationModal() - // render - const renderItem = useCallback( - ({ item: currency }: { item: FiatCurrency }) => ( - - ), - [selectedCurrency, onClose], - ) + const getCurrencyTitle = useEvent((currency: FiatCurrency) => { + return getFiatCurrencyName(t, currency).name + }) - return ( - - - {t('settings.setting.currency.title')} - - {/* When modifying this component, please test on a physical device that - scrolling the currencies list continues to work correctly. */} - item} - renderItem={renderItem} - showsVerticalScrollIndicator={false} - bounces={true} - keyboardShouldPersistTaps="always" - keyboardDismissMode="on-drag" - /> - - ) -} - -interface FiatCurrencyOptionProps { - active?: boolean - currency: FiatCurrency - onPress: () => void -} - -function FiatCurrencyOption({ active, currency, onPress }: FiatCurrencyOptionProps): JSX.Element { - const dispatch = useDispatch() - const { name, code } = useFiatCurrencyInfo(currency) + const getCurrencyCode = useEvent((currency: FiatCurrency) => { + return getFiatCurrencyCode(currency) + }) - const changeCurrency = useCallback(() => { + const onSelectCurrencyOption = useEvent(async (currency: FiatCurrency) => { dispatch(setCurrentFiatCurrency(currency)) - onPress() - }, [dispatch, onPress, currency]) + }) return ( - - - - {name} - - {code} - - - {active && } - - + ) } diff --git a/apps/mobile/src/screens/SettingsLanguageModal.tsx b/apps/mobile/src/screens/SettingsLanguageModal.tsx new file mode 100644 index 00000000000..95a47f2d680 --- /dev/null +++ b/apps/mobile/src/screens/SettingsLanguageModal.tsx @@ -0,0 +1,39 @@ +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { useDispatch } from 'react-redux' +import { SettingsListModal } from 'src/components/Settings/lists/SettingsListModal' +import { Language, mapLanguageToLocale, WALLET_SUPPORTED_LANGUAGES } from 'uniswap/src/features/language/constants' +import { getLanguageInfo, useCurrentLanguage } from 'uniswap/src/features/language/hooks' +import { setCurrentLanguage } from 'uniswap/src/features/settings/slice' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { changeLanguage } from 'uniswap/src/i18n' +import { useEvent } from 'utilities/src/react/hooks' + +export function SettingsLanguageModal(): JSX.Element { + const dispatch = useDispatch() + const { t } = useTranslation() + const selectedLanguage = useCurrentLanguage() + + const getLanguageTitle = useCallback( + (language: Language) => { + return getLanguageInfo(t, language).displayName + }, + [t], + ) + + const onSelectLanguageOption = useEvent(async (language: Language) => { + await changeLanguage(mapLanguageToLocale[language]) + dispatch(setCurrentLanguage(language)) + }) + + return ( + + ) +} diff --git a/apps/mobile/src/screens/SettingsNotificationsScreen.tsx b/apps/mobile/src/screens/SettingsNotificationsScreen.tsx index c21443f46f0..0a494ace85e 100644 --- a/apps/mobile/src/screens/SettingsNotificationsScreen.tsx +++ b/apps/mobile/src/screens/SettingsNotificationsScreen.tsx @@ -41,7 +41,7 @@ type SettingItem = { type NotificationItem = SettingItem | AccountItem -function _SettingsNotificationsScreen(): JSX.Element { +function SettingsNotificationsScreenInner(): JSX.Element { const { t } = useTranslation() const insets = useAppInsets() const { fullWidth, fullHeight } = useDeviceDimensions() @@ -117,7 +117,7 @@ function _SettingsNotificationsScreen(): JSX.Element { ) } -export const SettingsNotificationsScreen = memo(_SettingsNotificationsScreen) +export const SettingsNotificationsScreen = memo(SettingsNotificationsScreenInner) SettingsNotificationsScreen.displayName = 'SettingsNotificationsScreen' @@ -190,6 +190,7 @@ const AccountNotificationRow = memo(function AccountNotificationRow({ size={iconSizes.icon32} variant="subheading2" captionVariant="body3" + alignItems="center" /> @@ -205,7 +206,7 @@ function onPermissionChanged(enabled: boolean, type: NotificationToggleLoggingTy const PENDING_DELAY = 100 -function _AddressNotificationsSwitch({ address }: { address: string }): JSX.Element { +function AddressNotificationsSwitchInner({ address }: { address: string }): JSX.Element { const { isEnabled, isPending, toggle } = useAddressNotificationToggle({ address, onToggle: (enabled) => onPermissionChanged(enabled, 'wallet_activity'), @@ -235,6 +236,6 @@ function _AddressNotificationsSwitch({ address }: { address: string }): JSX.Elem return } -const AddressNotificationsSwitch = memo(_AddressNotificationsSwitch) +const AddressNotificationsSwitch = memo(AddressNotificationsSwitchInner) AddressNotificationsSwitch.displayName = 'AddressNotificationsSwitch' diff --git a/apps/mobile/src/screens/SettingsScreen.tsx b/apps/mobile/src/screens/SettingsScreen.tsx index 87db6bed817..b6018af0409 100644 --- a/apps/mobile/src/screens/SettingsScreen.tsx +++ b/apps/mobile/src/screens/SettingsScreen.tsx @@ -1,13 +1,15 @@ import { useNavigation } from '@react-navigation/core' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import { default as React, useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { ListRenderItemInfo } from 'react-native' -import { useDispatch, useSelector } from 'react-redux' +import { useSelector } from 'react-redux' import { OnboardingStackNavigationProp, SettingsStackNavigationProp } from 'src/app/navigation/types' import { ScreenWithHeader } from 'src/components/layout/screens/ScreenWithHeader' import { useReactNavigationModal } from 'src/components/modals/useReactNavigationModal' import { WalletRestoreType } from 'src/components/RestoreWalletModal/RestoreWalletModalState' import { FooterSettings } from 'src/components/Settings/FooterSettings' +import { ForceReduxDataLossRow } from 'src/components/Settings/ForceReduxDataLossRow' import { SettingsList } from 'src/components/Settings/lists/SettingsList' import { SectionData } from 'src/components/Settings/lists/types' import { OnboardingRow } from 'src/components/Settings/OnboardingRow' @@ -19,6 +21,7 @@ import { SettingsSectionItemComponent, } from 'src/components/Settings/SettingsRow' import { WalletSettings } from 'src/components/Settings/WalletSettings' +import { useBiometricsAlert } from 'src/features/biometrics/useBiometricsAlert' import { useBiometricsState } from 'src/features/biometrics/useBiometricsState' import { useDeviceSupportsBiometricAuth } from 'src/features/biometrics/useDeviceSupportsBiometricAuth' import { useBiometricName } from 'src/features/biometricsSettings/hooks' @@ -26,6 +29,7 @@ import { NotificationPermission, useNotificationOSPermissionsEnabled, } from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled' +import { useAdvancedSettingsMenuState } from 'src/features/settings/hooks/useAdvancedSettingsMenuState' import { useWalletRestore } from 'src/features/wallet/useWalletRestore' import { importFromCloudBackupOption, restoreFromCloudBackupOption } from 'src/screens/Import/constants' import { Flex, IconProps, Text, useSporeColors } from 'ui/src' @@ -54,22 +58,18 @@ import { } from 'ui/src/components/icons' import { iconSizes } from 'ui/src/theme' import { uniswapUrls } from 'uniswap/src/constants/urls' +import { useCurrentAppearanceSetting } from 'uniswap/src/features/appearance/hooks' import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' import { useAppFiatCurrencyInfo } from 'uniswap/src/features/fiatCurrency/hooks' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { useCurrentLanguageInfo } from 'uniswap/src/features/language/hooks' -import { setIsTestnetModeEnabled } from 'uniswap/src/features/settings/slice' import { useHapticFeedback } from 'uniswap/src/features/settings/useHapticFeedback/useHapticFeedback' -import { ModalName, WalletEventName } from 'uniswap/src/features/telemetry/constants' -import { sendAnalyticsEvent } from 'uniswap/src/features/telemetry/send' +import { ModalName } from 'uniswap/src/features/telemetry/constants' import { TestID } from 'uniswap/src/test/fixtures/testIDs' import { OnboardingEntryPoint } from 'uniswap/src/types/onboarding' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' import { isDevEnv } from 'utilities/src/environment/env' import { isAndroid } from 'utilities/src/platform' -import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks' import { selectHasCopiedPrivateKeys } from 'wallet/src/features/behaviorHistory/selectors' import { BackupType } from 'wallet/src/features/wallet/accounts/types' import { hasBackup } from 'wallet/src/features/wallet/accounts/utils' @@ -81,12 +81,12 @@ const AVOID_RENDER_DURING_ANIMATION_MS = 100 export function SettingsScreen(): JSX.Element { const navigation = useNavigation() - const dispatch = useDispatch() const colors = useSporeColors() const hasCopiedPrivateKeys = useSelector(selectHasCopiedPrivateKeys) const shouldShowPrivateKeys = useFeatureFlag(FeatureFlags.EnableExportPrivateKeys) - const { deviceSupportsBiometrics } = useBiometricsState() + const { isBiometricsDisabledInOSSettings } = useBiometricsState() const { t } = useTranslation() + const { showBiometricsAlert } = useBiometricsAlert({ t }) const { onClose } = useReactNavigationModal() // check if device supports biometric authentication, if not, hide option @@ -109,37 +109,11 @@ export function SettingsScreen(): JSX.Element { const { notificationPermissionsEnabled: notificationOSPermission } = useNotificationOSPermissionsEnabled() const { isTestnetModeEnabled } = useEnabledChains() - const handleTestnetModeToggle = useCallback((): void => { - const newIsTestnetMode = !isTestnetModeEnabled - const fireAnalytic = (): void => - sendAnalyticsEvent(WalletEventName.TestnetModeToggled, { - enabled: newIsTestnetMode, - location: 'settings', - }) - - if (isSmartWalletSettingsEnabled) { - // this assumes that we can only navigate to this toggle from the advanced settings modal - navigation.goBack() - } else { - onClose() - } - - setTimeout(() => { - // trigger before toggling on (ie disabling analytics) - if (newIsTestnetMode) { - fireAnalytic() - navigation.navigate(ModalName.TestnetMode, {}) - } - - dispatch(setIsTestnetModeEnabled(newIsTestnetMode)) - - // trigger after toggling off (ie enabling analytics) - if (!newIsTestnetMode) { - fireAnalytic() - } - }, AVOID_RENDER_DURING_ANIMATION_MS) - }, [dispatch, onClose, isSmartWalletSettingsEnabled, isTestnetModeEnabled, navigation]) + // For non-smart-wallet mode, we need to close the settings modal instead of going back + const advancedSettingsState = useAdvancedSettingsMenuState({ + onClose: isSmartWalletSettingsEnabled ? undefined : onClose, + }) // Signer account info const signerAccount = useSignerAccounts()[0] @@ -163,6 +137,7 @@ export function SettingsScreen(): JSX.Element { navigation={navigation} page={item} checkIfCanProceed={item.checkIfCanProceed} + cantProceedFallback={item.cantProceedFallback} testID={item.testID} /> ) @@ -227,7 +202,7 @@ export function SettingsScreen(): JSX.Element { }, { navigationModal: ModalName.PortfolioBalanceModal, - text: t('settings.setting.smallBalances.title'), + text: t('settings.setting.balancesActivity.title'), icon: , }, { @@ -242,13 +217,7 @@ export function SettingsScreen(): JSX.Element { navigationModal: ModalName.SmartWalletAdvancedSettingsModal, text: t('settings.setting.advanced.title'), icon: , - navigationProps: { - isTestnetEnabled: isTestnetModeEnabled, - onTestnetModeToggled: handleTestnetModeToggle, - onPressSmartWallet: (): void => { - navigation.navigate(MobileScreens.SettingsSmartWallet) - }, - }, + navigationProps: advancedSettingsState, }, ] : [ @@ -256,7 +225,7 @@ export function SettingsScreen(): JSX.Element { text: t('settings.setting.wallet.testnetMode.title'), icon: , isToggleEnabled: isTestnetModeEnabled, - onToggle: handleTestnetModeToggle, + onToggle: advancedSettingsState.handleTestnetModeToggle, }, ]), ], @@ -265,22 +234,22 @@ export function SettingsScreen(): JSX.Element { subTitle: t('settings.section.privacyAndSecurity'), isHidden: noSignerAccountImported, data: [ - ...(deviceSupportsBiometrics - ? [ - { - navigationModal: ModalName.BiometricsModal, - isHidden: !isTouchIdSupported && !isFaceIdSupported, - text: isAndroid ? t('settings.setting.biometrics.title') : biometricsMethod, - icon: isAndroid ? ( - - ) : isTouchIdSupported ? ( - - ) : ( - - ), - }, - ] - : []), + { + navigationModal: ModalName.BiometricsModal, + isHidden: !isTouchIdSupported && !isFaceIdSupported, + checkIfCanProceed: (): boolean => !isBiometricsDisabledInOSSettings, + cantProceedFallback: (): void => { + showBiometricsAlert(biometricsMethod) + }, + text: isAndroid ? t('settings.setting.biometrics.title') : biometricsMethod, + icon: isAndroid ? ( + + ) : isTouchIdSupported ? ( + + ) : ( + + ), + }, { screen: MobileScreens.SettingsViewSeedPhrase, text: t('settings.setting.recoveryPhrase.title'), @@ -407,8 +376,14 @@ export function SettingsScreen(): JSX.Element { text: 'Dev options', icon: , }, + { + screen: MobileScreens.DebugScreens, + text: 'Debug Screens', + icon: , + }, { component: }, { component: }, + { component: }, ], }, ] @@ -421,7 +396,6 @@ export function SettingsScreen(): JSX.Element { hapticsEnabled, onToggleEnableHaptics, noSignerAccountImported, - deviceSupportsBiometrics, isTouchIdSupported, isFaceIdSupported, biometricsMethod, @@ -431,12 +405,14 @@ export function SettingsScreen(): JSX.Element { hasPasskeyBackup, isTestnetModeEnabled, isSmartWalletSettingsEnabled, - handleTestnetModeToggle, + advancedSettingsState, notificationOSPermission, navigation, hasCopiedPrivateKeys, shouldShowPrivateKeys, walletRestoreType, + isBiometricsDisabledInOSSettings, + showBiometricsAlert, ]) return ( diff --git a/apps/mobile/src/screens/SettingsStorageScreen.tsx b/apps/mobile/src/screens/SettingsStorageScreen.tsx new file mode 100644 index 00000000000..ff560ddd728 --- /dev/null +++ b/apps/mobile/src/screens/SettingsStorageScreen.tsx @@ -0,0 +1,32 @@ +import { useTranslation } from 'react-i18next' +import { ScreenWithHeader } from 'src/components/layout/screens/ScreenWithHeader' +import { useAppStateResetter } from 'src/features/appState/appStateResetter' +import { Flex, Text } from 'ui/src' +import { StorageHelpIcon, StorageSettingsContent } from 'uniswap/src/features/settings/storage/StorageSettingsContent' +import { useEvent } from 'utilities/src/react/hooks' + +export function SettingsStorageScreen(): JSX.Element { + const { t } = useTranslation() + + const appStateResetter = useAppStateResetter() + const onPressClearAccountHistory = useEvent(() => appStateResetter.resetAccountHistory()) + const onPressClearUserSettings = useEvent(() => appStateResetter.resetUserSettings()) + const onPressClearCachedData = useEvent(() => appStateResetter.resetQueryCaches()) + const onPressClearAllData = useEvent(() => appStateResetter.resetAll()) + + return ( + {t('settings.setting.storage.title')}} + rightElement={} + > + + + + + ) +} diff --git a/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx b/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx index 6be051828db..77b2c3b5c7f 100644 --- a/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx +++ b/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx @@ -9,20 +9,19 @@ import { Text } from 'ui/src' import { AccountType } from 'uniswap/src/features/accounts/types' import { MobileScreens } from 'uniswap/src/types/screens/mobile' import { logger } from 'utilities/src/logger/logger' -import { useAccounts } from 'wallet/src/features/wallet/hooks' +import { useAccounts, useActiveSignerAccount } from 'wallet/src/features/wallet/hooks' type Props = NativeStackScreenProps -export function SettingsViewSeedPhraseScreen({ - navigation, - route: { - params: { address, walletNeedsRestore }, - }, -}: Props): JSX.Element { +export function SettingsViewSeedPhraseScreen({ navigation, route }: Props): JSX.Element { const { t } = useTranslation() + const { address: addressParam, walletNeedsRestore } = route.params ?? {} + // Use provided address or fall back to active signer account + const activeSignerAccount = useActiveSignerAccount() + const address = addressParam ?? activeSignerAccount?.address const accounts = useAccounts() - const account = accounts[address] + const account = address ? accounts[address] : undefined const mnemonicId = account?.type === AccountType.SignerMnemonic ? account.mnemonicId : undefined const navigateBack = (): void => { diff --git a/apps/mobile/src/screens/TokenDetailsScreen.tsx b/apps/mobile/src/screens/TokenDetailsScreen.tsx deleted file mode 100644 index 37413162e96..00000000000 --- a/apps/mobile/src/screens/TokenDetailsScreen.tsx +++ /dev/null @@ -1,422 +0,0 @@ -import { useApolloClient } from '@apollo/client' -import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation' -import { GQLQueries, GraphQLApi } from '@universe/api' -import React, { memo, useCallback, useEffect, useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import { FadeInDown, FadeOutDown } from 'react-native-reanimated' -import { navigate } from 'src/app/navigation/rootNavigation' -import type { AppStackScreenProp } from 'src/app/navigation/types' -import { HeaderScrollScreen } from 'src/components/layout/screens/HeaderScrollScreen' -import { useIsInModal } from 'src/components/modals/useIsInModal' -import { PriceExplorer } from 'src/components/PriceExplorer/PriceExplorer' -import { ContractAddressExplainerModal } from 'src/components/TokenDetails/ContractAddressExplainerModal' -import { TokenBalances } from 'src/components/TokenDetails/TokenBalances' -import { TokenDetailsActionButtons } from 'src/components/TokenDetails/TokenDetailsActionButtons' -import { TokenDetailsBridgedAssetSection } from 'src/components/TokenDetails/TokenDetailsBridgedAssetSection' -import { TokenDetailsContextProvider, useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext' -import { TokenDetailsHeader } from 'src/components/TokenDetails/TokenDetailsHeader' -import { TokenDetailsLinks } from 'src/components/TokenDetails/TokenDetailsLinks' -import { TokenDetailsStats } from 'src/components/TokenDetails/TokenDetailsStats' -import { useTokenDetailsCTAVariant } from 'src/components/TokenDetails/useTokenDetailsCTAVariant' -import { useTokenDetailsCurrentChainBalance } from 'src/components/TokenDetails/useTokenDetailsCurrentChainBalance' -import { HeaderRightElement, HeaderTitleElement } from 'src/screens/TokenDetailsHeaders' -import { useIsScreenNavigationReady } from 'src/utils/useIsScreenNavigationReady' -import { Flex, Separator } from 'ui/src' -import { ArrowDownCircle, ArrowUpCircle, Bank, SendRoundedAirplane } from 'ui/src/components/icons' -import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' -import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' -import { getBridgedAsset } from 'uniswap/src/components/BridgedAsset/utils' -import type { MenuOptionItem } from 'uniswap/src/components/menus/ContextMenuV2' -import { PollingInterval } from 'uniswap/src/constants/misc' -import { useCrossChainBalances } from 'uniswap/src/data/balances/hooks/useCrossChainBalances' -import { - useTokenBasicInfoPartsFragment, - useTokenBasicProjectPartsFragment, -} from 'uniswap/src/data/graphql/uniswap-data-api/fragments' -import { useBridgingTokenWithHighestBalance } from 'uniswap/src/features/bridging/hooks/tokens' -import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' -import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' -import { TokenList } from 'uniswap/src/features/dataApi/types' -import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils/currencyIdToContractInput' -import { useIsSupportedFiatOnRampCurrency } from 'uniswap/src/features/fiatOnRamp/hooks' -import { useOnChainNativeCurrencyBalance } from 'uniswap/src/features/portfolio/api' -import { ModalName } from 'uniswap/src/features/telemetry/constants' -import Trace from 'uniswap/src/features/telemetry/Trace' -import { TokenWarningCard } from 'uniswap/src/features/tokens/TokenWarningCard' -import TokenWarningModal from 'uniswap/src/features/tokens/TokenWarningModal' -import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' -import type { CurrencyField } from 'uniswap/src/types/currency' -import { MobileScreens } from 'uniswap/src/types/screens/mobile' -import { AddressStringFormat, normalizeAddress } from 'uniswap/src/utils/addresses' -import { buildCurrencyId, isNativeCurrencyAddress } from 'uniswap/src/utils/currencyId' -import { useEvent } from 'utilities/src/react/hooks' -import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' -import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' - -export function TokenDetailsScreen({ route, navigation }: AppStackScreenProp): JSX.Element { - const { currencyId } = route.params - const normalizedCurrencyId = normalizeAddress(currencyId, AddressStringFormat.Lowercase) - - return ( - - - - ) -} - -function TokenDetailsWrapper(): JSX.Element { - const { chainId, address, currencyId } = useTokenDetailsContext() - const { data: token } = useTokenBasicInfoPartsFragment({ currencyId }) - - const traceProperties = useMemo( - () => ({ - chain: chainId, - address, - currencyName: token.name, - }), - [address, chainId, token.name], - ) - - return ( - - - - - - ) -} - -const TokenDetailsQuery = memo(function _TokenDetailsQuery(): JSX.Element { - const { currencyId, setError } = useTokenDetailsContext() - - const { error } = GraphQLApi.useTokenDetailsScreenQuery({ - variables: currencyIdToContractInput(currencyId), - pollInterval: PollingInterval.Normal, - notifyOnNetworkStatusChange: true, - returnPartialData: true, - }) - - useEffect(() => setError(error), [error, setError]) - - return -}) - -const TokenDetails = memo(function _TokenDetails(): JSX.Element { - const centerElement = useMemo(() => , []) - const rightElement = useMemo(() => , []) - - const inModal = useIsInModal(MobileScreens.Explore, true) - - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) -}) - -const TokenDetailsErrorCard = memo(function _TokenDetailsErrorCard(): JSX.Element | null { - const apolloClient = useApolloClient() - const { error, setError } = useTokenDetailsContext() - - const onRetry = useCallback(() => { - setError(undefined) - apolloClient - .refetchQueries({ include: [GQLQueries.TokenDetailsScreen, GQLQueries.TokenPriceHistory] }) - .catch((e) => setError(e)) - }, [apolloClient, setError]) - - return error ? ( - - - - ) : null -}) - -const TokenDetailsModals = memo(function _TokenDetailsModals(): JSX.Element { - const { navigateToSwapFlow } = useWalletNavigation() - - const { - chainId, - address, - activeTransactionType, - currencyInfo, - isTokenWarningModalOpen, - isContractAddressExplainerModalOpen, - closeTokenWarningModal, - closeContractAddressExplainerModal, - copyAddressToClipboard, - } = useTokenDetailsContext() - - const onCloseTokenWarning = useEvent(() => { - closeTokenWarningModal() - }) - - const onAcknowledgeTokenWarning = useEvent(() => { - closeTokenWarningModal() - if (activeTransactionType !== undefined) { - navigateToSwapFlow({ currencyField: activeTransactionType, currencyAddress: address, currencyChainId: chainId }) - } - }) - - const onAcknowledgeContractAddressExplainer = useEvent(async (markViewed: boolean) => { - closeContractAddressExplainerModal(markViewed) - if (markViewed) { - await copyAddressToClipboard(address) - } - }) - - return ( - <> - {isTokenWarningModalOpen && currencyInfo && ( - - )} - - {isContractAddressExplainerModalOpen && ( - - )} - - ) -}) - -const TokenDetailsActionButtonsWrapper = memo(function _TokenDetailsActionButtonsWrapper(): JSX.Element | null { - const { t } = useTranslation() - const insets = useAppInsets() - const activeAddress = useActiveAccountAddressWithThrow() - const { isTestnetModeEnabled } = useEnabledChains() - - const { currencyId, chainId, address, currencyInfo, openTokenWarningModal, tokenColorLoading, navigation } = - useTokenDetailsContext() - - const { navigateToFiatOnRamp, navigateToSwapFlow, navigateToSend, navigateToReceive } = useWalletNavigation() - - const token = useTokenBasicInfoPartsFragment({ currencyId }).data - - const isBlocked = currencyInfo?.safetyInfo?.tokenList === TokenList.Blocked - - const isNativeCurrency = isNativeCurrencyAddress(chainId, address) - const nativeCurrencyAddress = getChainInfo(chainId).nativeCurrency.address - - const { balance: nativeCurrencyBalance, isLoading: isNativeCurrencyBalanceLoading } = useOnChainNativeCurrencyBalance( - chainId, - activeAddress, - ) - const hasZeroNativeBalance = nativeCurrencyBalance && nativeCurrencyBalance.equalTo('0') - - const { currency: nativeFiatOnRampCurrency, isLoading: isNativeFiatOnRampCurrencyLoading } = - useIsSupportedFiatOnRampCurrency(buildCurrencyId(chainId, nativeCurrencyAddress)) - - const currentChainBalance = useTokenDetailsCurrentChainBalance() - - const hasTokenBalance = Boolean(currentChainBalance) - - const { currency: fiatOnRampCurrency, isLoading: isFiatOnRampCurrencyLoading } = - useIsSupportedFiatOnRampCurrency(currencyId) - - const { data: bridgingTokenWithHighestBalance, isLoading: isBridgingTokenLoading } = - useBridgingTokenWithHighestBalance({ - evmAddress: activeAddress, - currencyAddress: address, - currencyChainId: chainId, - }) - - const onPressSwap = useCallback( - (currencyField: CurrencyField) => { - if (isBlocked) { - openTokenWarningModal() - } else { - navigateToSwapFlow({ currencyField, currencyAddress: address, currencyChainId: chainId }) - } - }, - [isBlocked, openTokenWarningModal, navigateToSwapFlow, address, chainId], - ) - - const onPressBuyFiatOnRamp = useCallback( - (isOfframp = false): void => { - navigateToFiatOnRamp({ prefilledCurrency: fiatOnRampCurrency, isOfframp }) - }, - [navigateToFiatOnRamp, fiatOnRampCurrency], - ) - - const onPressGet = useCallback(() => { - navigate(ModalName.BuyNativeToken, { - chainId, - currencyId, - }) - }, [chainId, currencyId]) - - const onPressSend = useCallback(() => { - navigateToSend({ currencyAddress: address, chainId }) - }, [address, chainId, navigateToSend]) - - const onPressWithdraw = useCallback(() => { - setTimeout(() => { - navigate(ModalName.Wormhole, { - currencyInfo, - }) - }, 300) // delay is needed to prevent menu from not closing properly - }, [currencyInfo]) - - const bridgedAsset = getBridgedAsset(currencyInfo) - - const isScreenNavigationReady = useIsScreenNavigationReady({ navigation }) - - const getCTAVariant = useTokenDetailsCTAVariant({ - hasTokenBalance, - isNativeCurrency, - nativeFiatOnRampCurrency, - fiatOnRampCurrency, - bridgingTokenWithHighestBalance, - hasZeroNativeBalance, - tokenSymbol: token.symbol, - onPressBuyFiatOnRamp, - onPressGet, - onPressSwap, - }) - - const actionMenuOptions: MenuOptionItem[] = useMemo(() => { - const actions: MenuOptionItem[] = [] - - if (fiatOnRampCurrency) { - actions.push({ label: t('common.button.buy'), Icon: Bank, onPress: () => onPressBuyFiatOnRamp() }) - } - - if (!!bridgedAsset && hasTokenBalance) { - actions.push({ - label: t('common.withdraw'), - Icon: ArrowUpCircle, - onPress: () => onPressWithdraw(), - subheader: t('bridgedAsset.wormhole.toNativeChain', { nativeChainName: bridgedAsset.nativeChain }), - actionType: 'external-link', - height: 56, - }) - } - - if (hasTokenBalance && fiatOnRampCurrency) { - actions.push({ label: t('common.button.sell'), Icon: ArrowUpCircle, onPress: () => onPressBuyFiatOnRamp(true) }) - } - - if (hasTokenBalance) { - actions.push({ label: t('common.button.send'), Icon: SendRoundedAirplane, onPress: onPressSend }) - } - - // All cases have a receive action - actions.push({ label: t('common.button.receive'), Icon: ArrowDownCircle, onPress: navigateToReceive }) - - return actions - }, [ - fiatOnRampCurrency, - t, - bridgedAsset, - hasTokenBalance, - onPressWithdraw, - onPressSend, - navigateToReceive, - onPressBuyFiatOnRamp, - ]) - - const hideActionButtons = - !isScreenNavigationReady || - tokenColorLoading || - isNativeCurrencyBalanceLoading || - isNativeFiatOnRampCurrencyLoading || - isFiatOnRampCurrencyLoading || - isBridgingTokenLoading - - return hideActionButtons ? null : ( - - - navigate(ModalName.TestnetMode, { - unsupported: true, - descriptionCopy: t('tdp.noTestnetSupportDescription'), - }) - : openTokenWarningModal - } - /> - - ) -}) - -const TokenBalancesWrapper = memo(function _TokenBalancesWrapper(): JSX.Element | null { - const activeAddress = useActiveAccountAddressWithThrow() - const { currencyId, isChainEnabled } = useTokenDetailsContext() - - const projectTokens = useTokenBasicProjectPartsFragment({ currencyId }).data.project?.tokens - - const crossChainTokens: Array<{ - address: string | null - chain: GraphQLApi.Chain - }> = [] - - for (const token of projectTokens ?? []) { - if (!token || !token.chain || token.address === undefined) { - continue - } - - crossChainTokens.push({ - address: token.address, - chain: token.chain, - }) - } - - const { currentChainBalance, otherChainBalances } = useCrossChainBalances({ - evmAddress: activeAddress, - currencyId, - crossChainTokens, - }) - - return isChainEnabled ? ( - - ) : null -}) - -const TokenWarningCardWrapper = memo(function _TokenWarningCardWrapper(): JSX.Element | null { - const { currencyInfo, openTokenWarningModal } = useTokenDetailsContext() - - return -}) diff --git a/apps/mobile/src/screens/TokenDetailsScreen/NetworkBalanceSheetContent.tsx b/apps/mobile/src/screens/TokenDetailsScreen/NetworkBalanceSheetContent.tsx new file mode 100644 index 00000000000..ef2472e3b0c --- /dev/null +++ b/apps/mobile/src/screens/TokenDetailsScreen/NetworkBalanceSheetContent.tsx @@ -0,0 +1,38 @@ +import { BottomSheetScrollView } from '@gorhom/bottom-sheet' +import { useTranslation } from 'react-i18next' +import { NetworkBalanceList } from 'src/components/TokenDetails/NetworkBalanceList' +import { Flex, Text } from 'ui/src' +import { spacing } from 'ui/src/theme' +import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' + +const STICKY_HEADER_INDICES = [0] +const NETWORK_SHEET_CONTENT_STYLE = { paddingBottom: spacing.spacing48 } + +interface NetworkBalanceSheetContentProps { + allChainBalances: PortfolioBalance[] + onSelectBalance: (balance: PortfolioBalance) => void +} + +export function NetworkBalanceSheetContent({ + allChainBalances, + onSelectBalance, +}: NetworkBalanceSheetContentProps): JSX.Element { + const { t } = useTranslation() + + return ( + + + + {t('token.balances.chooseNetwork')} + + + + + + + ) +} diff --git a/apps/mobile/src/screens/TokenDetailsScreen/TokenDetailsActionButtonsWrapper.tsx b/apps/mobile/src/screens/TokenDetailsScreen/TokenDetailsActionButtonsWrapper.tsx new file mode 100644 index 00000000000..0f93b9b4487 --- /dev/null +++ b/apps/mobile/src/screens/TokenDetailsScreen/TokenDetailsActionButtonsWrapper.tsx @@ -0,0 +1,352 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' +import { memo, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { FadeInDown } from 'react-native-reanimated' +import { MODAL_OPEN_WAIT_TIME } from 'src/app/navigation/constants' +import { navigate } from 'src/app/navigation/rootNavigation' +import { + TokenDetailsBuySellButtons, + TokenDetailsSwapButtons, +} from 'src/components/TokenDetails/TokenDetailsActionButtons' +import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext' +import { + useMultichainBuyVariant, + useTokenDetailsCTAVariant, +} from 'src/components/TokenDetails/useTokenDetailsCTAVariant' +import { useTokenDetailsCurrentChainBalance } from 'src/components/TokenDetails/useTokenDetailsCurrentChainBalance' +import { NetworkBalanceSheetContent } from 'src/screens/TokenDetailsScreen/NetworkBalanceSheetContent' +import { useHighestTvlChain } from 'src/screens/TokenDetailsScreen/useHighestTvlChain' +import { useNetworkBalanceSheet } from 'src/screens/TokenDetailsScreen/useNetworkBalanceSheet' +import { useIsScreenNavigationReady } from 'src/utils/useIsScreenNavigationReady' +import { ArrowDownCircle, ArrowUpCircle, Bank, QrCode, SendRoundedAirplane } from 'ui/src/components/icons' +import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' +import type { MenuOptionItem } from 'uniswap/src/components/menus/ContextMenu' +import { Modal } from 'uniswap/src/components/modals/Modal' +import { getNativeAddress } from 'uniswap/src/constants/addresses' +import { useTokenBasicInfoPartsFragment } from 'uniswap/src/data/graphql/uniswap-data-api/fragments' +import { useBridgingTokenWithHighestBalance } from 'uniswap/src/features/bridging/hooks/tokens' +import { getChainInfo } from 'uniswap/src/features/chains/chainInfo' +import { useEnabledChains } from 'uniswap/src/features/chains/hooks/useEnabledChains' +import { type PortfolioBalance, TokenList } from 'uniswap/src/features/dataApi/types' +import { useIsSupportedFiatOnRampCurrency } from 'uniswap/src/features/fiatOnRamp/hooks' +import { useChainGasToken } from 'uniswap/src/features/gas/hooks/useChainGasToken' +import { ModalName } from 'uniswap/src/features/telemetry/constants' +import { useAppInsets } from 'uniswap/src/hooks/useAppInsets' +import { CurrencyField } from 'uniswap/src/types/currency' +import { buildCurrencyId, isNativeCurrencyAddress } from 'uniswap/src/utils/currencyId' +import { useEvent } from 'utilities/src/react/hooks' +import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' + +function getHighestBalanceEntry(balances: PortfolioBalance[]): PortfolioBalance { + return balances.reduce((best, current) => ((current.balanceUSD ?? 0) > (best.balanceUSD ?? 0) ? current : best)) +} + +export const TokenDetailsActionButtonsWrapper = memo( + function TokenDetailsActionButtonsWrapperInner(): JSX.Element | null { + const { t } = useTranslation() + const insets = useAppInsets() + const activeAddress = useActiveAccountAddressWithThrow() + const { isTestnetModeEnabled } = useEnabledChains() + const multichainTokenUxEnabled = useFeatureFlag(FeatureFlags.MultichainTokenUx) + + const { currencyId, chainId, address, currencyInfo, openTokenWarningModal, tokenColorLoading, navigation } = + useTokenDetailsContext() + + const { navigateToFiatOnRamp, navigateToSwapFlow, navigateToSend, navigateToReceive } = useWalletNavigation() + + const token = useTokenBasicInfoPartsFragment({ currencyId }).data + + const isBlocked = currencyInfo?.safetyInfo?.tokenList === TokenList.Blocked + + const isNativeCurrency = isNativeCurrencyAddress(chainId, address) + const nativeCurrencyAddress = getChainInfo(chainId).nativeCurrency.address + + const { gasBalance, isLoading: isGasBalanceLoading } = useChainGasToken({ chainId, accountAddress: activeAddress }) + const hasZeroGasBalance = gasBalance && gasBalance.equalTo('0') + + const { currency: nativeFiatOnRampCurrency, isLoading: isNativeFiatOnRampCurrencyLoading } = + useIsSupportedFiatOnRampCurrency(buildCurrencyId(chainId, nativeCurrencyAddress)) + + const currentChainBalance = useTokenDetailsCurrentChainBalance() + + const { currency: fiatOnRampCurrency, isLoading: isFiatOnRampCurrencyLoading } = + useIsSupportedFiatOnRampCurrency(currencyId) + + const { data: bridgingTokenWithHighestBalance, isLoading: isBridgingTokenLoading } = + useBridgingTokenWithHighestBalance({ + evmAddress: activeAddress, + currencyAddress: address, + currencyChainId: chainId, + }) + + const { + allChainBalances, + hasMultiChainBalances, + isNetworkSheetOpen, + openSellSheet, + openSendSheet, + onCloseNetworkSheet, + onSelectNetwork, + } = useNetworkBalanceSheet({ currencyId, chainId }) + + const hasTokenBalance = multichainTokenUxEnabled ? allChainBalances.length > 0 : Boolean(currentChainBalance) + + // For multichain UX: resolve the chain with the highest balance (computed once, used by multiple handlers) + const highestBalanceEntry = useMemo(() => { + if (!multichainTokenUxEnabled || !allChainBalances.length) { + return null + } + return getHighestBalanceEntry(allChainBalances) + }, [multichainTokenUxEnabled, allChainBalances]) + + const highestBalanceCurrencyId = highestBalanceEntry?.currencyInfo.currencyId ?? currencyId + + const { currency: highestBalanceFiatCurrency } = useIsSupportedFiatOnRampCurrency(highestBalanceCurrencyId) + + const { chainId: highestTvlChainId, address: highestTvlAddress } = useHighestTvlChain({ currencyId }) + + const onPressSwap = useEvent((currencyField: CurrencyField) => { + if (isBlocked) { + openTokenWarningModal() + } else { + navigateToSwapFlow({ currencyField, currencyAddress: address, currencyChainId: chainId }) + } + }) + + const onPressBuyFiatOnRamp = useEvent((isOfframp: boolean = false): void => { + navigateToFiatOnRamp({ prefilledCurrency: fiatOnRampCurrency, isOfframp }) + }) + + const onPressGet = useEvent(() => { + navigate(ModalName.BuyNativeToken, { + chainId, + currencyId, + }) + }) + + const onPressSend = useEvent(() => { + if (multichainTokenUxEnabled && hasMultiChainBalances) { + openSendSheet() + } else { + navigateToSend({ currencyAddress: address, chainId }) + } + }) + + const onPressWithdraw = useEvent(() => { + setTimeout(() => { + navigate(ModalName.Wormhole, { + currencyInfo, + }) + }, MODAL_OPEN_WAIT_TIME) + }) + + // Chain selection priority for the Buy (swap) flow: + // 1. Chain where the user holds the highest balance (they already have a position) + // 2. Chain with the highest TVL (best liquidity for new buyers with 0 balance) + // 3. Current TDP chain (fallback when data is unavailable) + const onPressBuy = useEvent(() => { + if (isBlocked) { + openTokenWarningModal() + return + } + if (multichainTokenUxEnabled && highestBalanceEntry) { + const { currency } = highestBalanceEntry.currencyInfo + const currencyAddress = currency.isToken ? currency.address : getNativeAddress(currency.chainId) + navigateToSwapFlow({ currencyField: CurrencyField.OUTPUT, currencyAddress, currencyChainId: currency.chainId }) + } else if (multichainTokenUxEnabled && highestTvlChainId) { + const currencyAddress = highestTvlAddress ?? getNativeAddress(highestTvlChainId) + navigateToSwapFlow({ currencyField: CurrencyField.OUTPUT, currencyAddress, currencyChainId: highestTvlChainId }) + } else { + navigateToSwapFlow({ currencyField: CurrencyField.OUTPUT, currencyAddress: address, currencyChainId: chainId }) + } + }) + + const onPressSell = useEvent(() => { + if (multichainTokenUxEnabled && hasMultiChainBalances) { + openSellSheet() + } else { + onPressSwap(CurrencyField.INPUT) + } + }) + + const onPressBuyWithCash = useEvent(() => { + navigateToFiatOnRamp({ prefilledCurrency: highestBalanceFiatCurrency ?? fiatOnRampCurrency }) + }) + + const onPressSellForCash = useEvent(() => { + navigateToFiatOnRamp({ prefilledCurrency: highestBalanceFiatCurrency ?? fiatOnRampCurrency, isOfframp: true }) + }) + + const bridgedWithdrawalInfo = currencyInfo?.bridgedWithdrawalInfo + + const isScreenNavigationReady = useIsScreenNavigationReady({ navigation }) + + const getCTAVariant = useTokenDetailsCTAVariant({ + hasTokenBalance, + isNativeCurrency, + nativeFiatOnRampCurrency, + fiatOnRampCurrency, + bridgingTokenWithHighestBalance, + hasZeroGasBalance, + tokenSymbol: token.symbol, + onPressBuyFiatOnRamp, + onPressGet, + onPressSwap, + }) + + const actionMenuOptions: MenuOptionItem[] = useMemo(() => { + const actions: MenuOptionItem[] = [] + + if (fiatOnRampCurrency) { + actions.push({ + label: t('common.button.buy'), + Icon: Bank, + onPress: onPressBuyFiatOnRamp, + }) + } + + if (bridgedWithdrawalInfo && hasTokenBalance) { + actions.push({ + label: t('common.withdraw'), + Icon: ArrowUpCircle, + onPress: onPressWithdraw, + subheader: t('bridgedAsset.wormhole.toNativeChain', { nativeChainName: bridgedWithdrawalInfo.chain }), + actionType: 'external-link', + height: 56, + }) + } + + if (hasTokenBalance && fiatOnRampCurrency) { + actions.push({ + label: t('common.button.sell'), + Icon: ArrowUpCircle, + onPress: () => onPressBuyFiatOnRamp(true), + }) + } + + if (hasTokenBalance) { + actions.push({ label: t('common.button.send'), Icon: SendRoundedAirplane, onPress: onPressSend }) + } + + // All cases have a receive action + actions.push({ label: t('common.button.receive'), Icon: ArrowDownCircle, onPress: navigateToReceive }) + + return actions + }, [ + fiatOnRampCurrency, + t, + bridgedWithdrawalInfo, + hasTokenBalance, + onPressWithdraw, + onPressSend, + navigateToReceive, + onPressBuyFiatOnRamp, + ]) + + const multichainActionMenuOptions: MenuOptionItem[] = useMemo(() => { + const actions: MenuOptionItem[] = [] + + if (hasTokenBalance) { + actions.push({ label: t('common.button.send'), Icon: SendRoundedAirplane, onPress: onPressSend }) + } + + actions.push({ label: t('common.button.receive'), Icon: QrCode, onPress: navigateToReceive }) + + if (highestBalanceFiatCurrency || fiatOnRampCurrency) { + actions.push({ label: t('fiatOnRamp.action.buyWithCash'), Icon: Bank, onPress: onPressBuyWithCash }) + } + + if (hasTokenBalance && (highestBalanceFiatCurrency || fiatOnRampCurrency)) { + actions.push({ label: t('fiatOnRamp.action.sellForCash'), Icon: ArrowUpCircle, onPress: onPressSellForCash }) + } + + if (bridgedWithdrawalInfo && hasTokenBalance) { + actions.push({ + label: t('common.withdraw'), + Icon: ArrowUpCircle, + onPress: onPressWithdraw, + subheader: t('bridgedAsset.wormhole.toNativeChain', { nativeChainName: bridgedWithdrawalInfo.chain }), + actionType: 'external-link', + height: 56, + }) + } + + return actions + }, [ + t, + hasTokenBalance, + bridgedWithdrawalInfo, + highestBalanceFiatCurrency, + fiatOnRampCurrency, + onPressSend, + navigateToReceive, + onPressBuyWithCash, + onPressSellForCash, + onPressWithdraw, + ]) + + const hideActionButtons = + !isScreenNavigationReady || + tokenColorLoading || + isGasBalanceLoading || + isNativeFiatOnRampCurrencyLoading || + isFiatOnRampCurrencyLoading || + isBridgingTokenLoading + + const onPressDisabled = isTestnetModeEnabled + ? (): void => + navigate(ModalName.TestnetMode, { + unsupported: true, + descriptionCopy: t('tdp.noTestnetSupportDescription'), + }) + : openTokenWarningModal + + const multichainBuyVariant = useMultichainBuyVariant({ + hasTokenBalance, + isNativeCurrency, + nativeFiatOnRampCurrency, + fiatOnRampCurrency, + bridgingTokenWithHighestBalance, + hasZeroGasBalance, + tokenSymbol: token.symbol, + onPressBuyWithCash, + onPressGet, + onPressBuy, + }) + + return hideActionButtons ? null : ( + + {multichainTokenUxEnabled ? ( + + ) : ( + + )} + + {multichainTokenUxEnabled && isNetworkSheetOpen && ( + + + + )} + + ) + }, +) diff --git a/apps/mobile/src/screens/TokenDetailsHeaders.tsx b/apps/mobile/src/screens/TokenDetailsScreen/TokenDetailsHeaders.tsx similarity index 75% rename from apps/mobile/src/screens/TokenDetailsHeaders.tsx rename to apps/mobile/src/screens/TokenDetailsScreen/TokenDetailsHeaders.tsx index ba0b0744664..ad05c6813e9 100644 --- a/apps/mobile/src/screens/TokenDetailsHeaders.tsx +++ b/apps/mobile/src/screens/TokenDetailsScreen/TokenDetailsHeaders.tsx @@ -1,6 +1,9 @@ +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { memo } from 'react' import { useTranslation } from 'react-i18next' import { FadeIn } from 'react-native-reanimated' +import { MODAL_OPEN_WAIT_TIME } from 'src/app/navigation/constants' +import { navigate } from 'src/app/navigation/rootNavigation' import { useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext' import { TokenDetailsFavoriteButton } from 'src/components/TokenDetails/TokenDetailsFavoriteButton' import { useTokenDetailsCurrentChainBalance } from 'src/components/TokenDetails/useTokenDetailsCurrentChainBalance' @@ -9,7 +12,7 @@ import { Ellipsis } from 'ui/src/components/icons' import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' import { iconSizes, spacing } from 'ui/src/theme' import { TokenLogo } from 'uniswap/src/components/CurrencyLogo/TokenLogo' -import { ContextMenu } from 'uniswap/src/components/menus/ContextMenuV2' +import { ContextMenu } from 'uniswap/src/components/menus/ContextMenu' import { ContextMenuTriggerMode } from 'uniswap/src/components/menus/types' import { useTokenBasicInfoPartsFragment, @@ -21,7 +24,9 @@ import { TokenMenuActionType, useTokenContextMenuOptions, } from 'uniswap/src/features/portfolio/balances/hooks/useTokenContextMenuOptions' +import { ElementName, ModalName, SectionName } from 'uniswap/src/features/telemetry/constants' import { TestID } from 'uniswap/src/test/fixtures/testIDs' +import { useEvent } from 'utilities/src/react/hooks' import { useBooleanState } from 'utilities/src/react/useBooleanState' export const HeaderTitleElement = memo(function HeaderTitleElement(): JSX.Element { @@ -29,8 +34,10 @@ export const HeaderTitleElement = memo(function HeaderTitleElement(): JSX.Elemen const { currencyId } = useTokenDetailsContext() + const multichainTokenUxEnabled = useFeatureFlag(FeatureFlags.MultichainTokenUx) const token = useTokenBasicInfoPartsFragment({ currencyId }).data const project = useTokenBasicProjectPartsFragment({ currencyId }).data.project + const isMultichainToken = multichainTokenUxEnabled && (project?.tokens?.length ?? 0) > 1 const logo = project?.logoUrl ?? undefined const symbol = token.symbol @@ -42,6 +49,7 @@ export const HeaderTitleElement = memo(function HeaderTitleElement(): JSX.Elemen { + setTimeout(() => { + navigate(ModalName.ReportTokenIssue, { + source: 'token-details', + currency: currencyInfo?.currency, + isMarkedSpam: currencyInfo?.isSpam, + }) + }, MODAL_OPEN_WAIT_TIME) + }) + + const openReportDataIssueModal = useEvent(() => { + setTimeout(() => { + navigate(ModalName.ReportTokenData, { currency: currencyInfo?.currency, isMarkedSpam: currencyInfo?.isSpam }) + }, MODAL_OPEN_WAIT_TIME) + }) + const { value: isOpen, setTrue: openMenu, setFalse: closeMenu } = useBooleanState(false) const menuActions = useTokenContextMenuOptions({ excludedActions: EXCLUDED_ACTIONS, @@ -70,6 +94,8 @@ export const HeaderRightElement = memo(function HeaderRightElement(): JSX.Elemen tokenSymbolForNotification: currencyInfo?.currency.symbol, portfolioBalance: currentChainBalance, openContractAddressExplainerModal, + openReportDataIssueModal, + openReportTokenModal, copyAddressToClipboard, closeMenu: () => {}, }) @@ -77,11 +103,14 @@ export const HeaderRightElement = memo(function HeaderRightElement(): JSX.Elemen return ( { + closeTokenWarningModal() + if (activeTransactionType !== undefined) { + navigateToSwapFlow({ currencyField: activeTransactionType, currencyAddress: address, currencyChainId: chainId }) + } + }) + + const onAcknowledgeContractAddressExplainer = useEvent(async (markViewed: boolean) => { + closeContractAddressExplainerModal(markViewed) + if (markViewed) { + await copyAddressToClipboard(address) + } + }) + + const onTokenWarningReportSuccess = useEvent(() => { + dispatch( + pushNotification({ + type: AppNotificationType.Success, + title: t('common.reported'), + }), + ) + }) + + return ( + <> + {isTokenWarningModalOpen && currencyInfo && ( + + )} + + {isContractAddressExplainerModalOpen && ( + + )} + + ) +}) diff --git a/apps/mobile/src/screens/TokenDetailsScreen/TokenDetailsScreen.tsx b/apps/mobile/src/screens/TokenDetailsScreen/TokenDetailsScreen.tsx new file mode 100644 index 00000000000..1e266976135 --- /dev/null +++ b/apps/mobile/src/screens/TokenDetailsScreen/TokenDetailsScreen.tsx @@ -0,0 +1,199 @@ +import { useApolloClient } from '@apollo/client' +import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation' +import { GQLQueries, GraphQLApi } from '@universe/api' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' +import React, { memo, useEffect, useMemo } from 'react' +import { FadeInDown, FadeOutDown } from 'react-native-reanimated' +import type { AppStackScreenProp } from 'src/app/navigation/types' +import { HeaderScrollScreen } from 'src/components/layout/screens/HeaderScrollScreen' +import { useIsInModal } from 'src/components/modals/useIsInModal' +import { PriceExplorer } from 'src/components/PriceExplorer/PriceExplorer' +import { TokenBalances } from 'src/components/TokenDetails/TokenBalances' +import { TokenDetailsBridgedAssetSection } from 'src/components/TokenDetails/TokenDetailsBridgedAssetSection' +import { TokenDetailsContextProvider, useTokenDetailsContext } from 'src/components/TokenDetails/TokenDetailsContext' +import { TokenDetailsHeader } from 'src/components/TokenDetails/TokenDetailsHeader' +import { TokenDetailsLinks } from 'src/components/TokenDetails/TokenDetailsLinks' +import { TokenDetailsStats } from 'src/components/TokenDetails/TokenDetailsStats' +import { TokenPerformance } from 'src/components/TokenDetails/TokenPerformance' +import { TokenDetailsActionButtonsWrapper } from 'src/screens/TokenDetailsScreen/TokenDetailsActionButtonsWrapper' +import { HeaderRightElement, HeaderTitleElement } from 'src/screens/TokenDetailsScreen/TokenDetailsHeaders' +import { TokenDetailsModals } from 'src/screens/TokenDetailsScreen/TokenDetailsModals' +import { Flex, Separator } from 'ui/src' +import { AnimatedFlex } from 'ui/src/components/layout/AnimatedFlex' +import { BaseCard } from 'uniswap/src/components/BaseCard/BaseCard' +import { PollingInterval } from 'uniswap/src/constants/misc' +import { useCrossChainBalances } from 'uniswap/src/data/balances/hooks/useCrossChainBalances' +import { + useTokenBasicInfoPartsFragment, + useTokenBasicProjectPartsFragment, +} from 'uniswap/src/data/graphql/uniswap-data-api/fragments' +import { currencyIdToContractInput } from 'uniswap/src/features/dataApi/utils/currencyIdToContractInput' +import Trace from 'uniswap/src/features/telemetry/Trace' +import { TokenWarningCard } from 'uniswap/src/features/tokens/warnings/TokenWarningCard' +import { MobileScreens } from 'uniswap/src/types/screens/mobile' +import { AddressStringFormat, normalizeAddress } from 'uniswap/src/utils/addresses' +import { useEvent } from 'utilities/src/react/hooks' +import { useDelayedRender } from 'utilities/src/react/useDelayedRender' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' + +const CONTEXT_MENU_RENDER_DELAY_MS = 1000 + +export function TokenDetailsScreen({ route, navigation }: AppStackScreenProp): JSX.Element { + const { currencyId } = route.params + const normalizedCurrencyId = normalizeAddress(currencyId, AddressStringFormat.Lowercase) + + return ( + + + + ) +} + +function TokenDetailsWrapper(): JSX.Element { + const { chainId, address, currencyId } = useTokenDetailsContext() + const { data: token } = useTokenBasicInfoPartsFragment({ currencyId }) + + const traceProperties = useMemo( + () => ({ + chain: chainId, + address, + currencyName: token.name, + }), + [address, chainId, token.name], + ) + + return ( + + + + + + ) +} + +const TokenDetailsQuery = memo(function TokenDetailsQueryInner(): JSX.Element { + const { currencyId, setError } = useTokenDetailsContext() + const multichainTokenUxEnabled = useFeatureFlag(FeatureFlags.MultichainTokenUx) + + const { error } = GraphQLApi.useTokenDetailsScreenQuery({ + variables: { + ...currencyIdToContractInput(currencyId), + multichain: multichainTokenUxEnabled, + }, + pollInterval: PollingInterval.Normal, + notifyOnNetworkStatusChange: true, + returnPartialData: true, + }) + + useEffect(() => setError(error), [error, setError]) + + return +}) + +const TokenDetails = memo(function TokenDetailsInner(): JSX.Element { + const centerElement = useMemo(() => , []) + const rightElement = useMemo(() => , []) + const { isContentHidden } = useDelayedRender(CONTEXT_MENU_RENDER_DELAY_MS) + const multichainTokenUxEnabled = useFeatureFlag(FeatureFlags.MultichainTokenUx) + + const inModal = useIsInModal(MobileScreens.Explore, true) + + return ( + <> + + + + + + + + + + + + + + + + + {!multichainTokenUxEnabled && } + + + + + + + + + + + + + + + + ) +}) + +const TokenDetailsErrorCard = memo(function TokenDetailsErrorCardInner(): JSX.Element | null { + const apolloClient = useApolloClient() + const { error, setError } = useTokenDetailsContext() + + const onRetry = useEvent(() => { + setError(undefined) + apolloClient + .refetchQueries({ include: [GQLQueries.TokenDetailsScreen, GQLQueries.TokenPriceHistory] }) + .catch((e) => setError(e)) + }) + + return error ? ( + + + + ) : null +}) + +const TokenBalancesWrapper = memo(function TokenBalancesWrapperInner(): JSX.Element | null { + const activeAddress = useActiveAccountAddressWithThrow() + const { currencyId, isChainEnabled } = useTokenDetailsContext() + + const projectTokens = useTokenBasicProjectPartsFragment({ currencyId }).data.project?.tokens + + const crossChainTokens: Array<{ + address: string | null + chain: GraphQLApi.Chain + }> = [] + + for (const token of projectTokens ?? []) { + if (!token || !token.chain || token.address === undefined) { + continue + } + + crossChainTokens.push({ + address: token.address, + chain: token.chain, + }) + } + + const { currentChainBalance, otherChainBalances } = useCrossChainBalances({ + evmAddress: activeAddress, + currencyId, + crossChainTokens, + }) + + return isChainEnabled ? ( + + ) : null +}) + +const TokenWarningCardWrapper = memo(function TokenWarningCardWrapperInner(): JSX.Element | null { + const { currencyInfo, openTokenWarningModal } = useTokenDetailsContext() + + return +}) diff --git a/apps/mobile/src/screens/TokenDetailsScreen/useHighestTvlChain.test.ts b/apps/mobile/src/screens/TokenDetailsScreen/useHighestTvlChain.test.ts new file mode 100644 index 00000000000..a30ddc41d16 --- /dev/null +++ b/apps/mobile/src/screens/TokenDetailsScreen/useHighestTvlChain.test.ts @@ -0,0 +1,106 @@ +import { renderHook } from '@testing-library/react' +import { useHighestTvlChain } from 'src/screens/TokenDetailsScreen/useHighestTvlChain' +import { UniverseChainId } from 'uniswap/src/features/chains/types' + +const mockFragmentData = jest.fn() + +jest.mock('uniswap/src/data/graphql/uniswap-data-api/fragments', () => ({ + useTokenProjectTokensTvlPartsFragment: () => ({ data: mockFragmentData() }), +})) + +describe(useHighestTvlChain, () => { + beforeEach(() => jest.clearAllMocks()) + + it('returns the chain with the highest TVL', () => { + mockFragmentData.mockReturnValue({ + project: { + tokens: [ + { chain: 'ETHEREUM', address: '0xEthAddress', market: { totalValueLocked: { value: 500_000 } } }, + { chain: 'BASE', address: '0xBaseAddress', market: { totalValueLocked: { value: 2_000_000 } } }, + { chain: 'ARBITRUM', address: '0xArbAddress', market: { totalValueLocked: { value: 300_000 } } }, + ], + }, + }) + + const { result } = renderHook(() => useHighestTvlChain({ currencyId: '1-0xEthAddress' })) + + expect(result.current.chainId).toBe(UniverseChainId.Base) + expect(result.current.address).toBe('0xBaseAddress') + }) + + it('returns null when project tokens are empty', () => { + mockFragmentData.mockReturnValue({ project: { tokens: [] } }) + + const { result } = renderHook(() => useHighestTvlChain({ currencyId: '1-0xEthAddress' })) + + expect(result.current.chainId).toBeNull() + expect(result.current.address).toBeNull() + }) + + it('returns null when project is undefined', () => { + mockFragmentData.mockReturnValue({}) + + const { result } = renderHook(() => useHighestTvlChain({ currencyId: '1-0xEthAddress' })) + + expect(result.current.chainId).toBeNull() + expect(result.current.address).toBeNull() + }) + + it('returns null when all TVL values are 0', () => { + mockFragmentData.mockReturnValue({ + project: { + tokens: [ + { chain: 'ETHEREUM', address: '0xEthAddress', market: { totalValueLocked: { value: 0 } } }, + { chain: 'BASE', address: '0xBaseAddress', market: { totalValueLocked: { value: 0 } } }, + ], + }, + }) + + const { result } = renderHook(() => useHighestTvlChain({ currencyId: '1-0xEthAddress' })) + + expect(result.current.chainId).toBeNull() + expect(result.current.address).toBeNull() + }) + + it('returns null when market data is missing', () => { + mockFragmentData.mockReturnValue({ + project: { + tokens: [ + { chain: 'ETHEREUM', address: '0xEthAddress', market: undefined }, + { chain: 'BASE', address: '0xBaseAddress', market: null }, + ], + }, + }) + + const { result } = renderHook(() => useHighestTvlChain({ currencyId: '1-0xEthAddress' })) + + expect(result.current.chainId).toBeNull() + expect(result.current.address).toBeNull() + }) + + it('handles single-chain tokens', () => { + mockFragmentData.mockReturnValue({ + project: { + tokens: [{ chain: 'ETHEREUM', address: '0xEthAddress', market: { totalValueLocked: { value: 1_000_000 } } }], + }, + }) + + const { result } = renderHook(() => useHighestTvlChain({ currencyId: '1-0xEthAddress' })) + + expect(result.current.chainId).toBe(UniverseChainId.Mainnet) + expect(result.current.address).toBe('0xEthAddress') + }) + + it('returns null address for native tokens', () => { + mockFragmentData.mockReturnValue({ + project: { + tokens: [{ chain: 'ETHEREUM', address: undefined, market: { totalValueLocked: { value: 1_000_000 } } }], + }, + }) + + const { result } = renderHook(() => useHighestTvlChain({ currencyId: '1-0xNative' })) + + expect(result.current.chainId).toBe(UniverseChainId.Mainnet) + expect(result.current.address).toBeNull() + }) +}) diff --git a/apps/mobile/src/screens/TokenDetailsScreen/useHighestTvlChain.ts b/apps/mobile/src/screens/TokenDetailsScreen/useHighestTvlChain.ts new file mode 100644 index 00000000000..2872261571b --- /dev/null +++ b/apps/mobile/src/screens/TokenDetailsScreen/useHighestTvlChain.ts @@ -0,0 +1,48 @@ +import { useMemo } from 'react' +import { useTokenProjectTokensTvlPartsFragment } from 'uniswap/src/data/graphql/uniswap-data-api/fragments' +import type { UniverseChainId } from 'uniswap/src/features/chains/types' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' + +interface HighestTvlChainResult { + chainId: UniverseChainId | null + address: string | null +} + +/** + * Returns the chain with the highest TVL for a given token's project. + * Reads per-chain TVL data from Apollo cache (populated by the TokenDetailsScreen query). + * Returns nulls if data is unavailable or all TVL values are 0. + */ +export function useHighestTvlChain({ currencyId }: { currencyId: string }): HighestTvlChainResult { + const { data } = useTokenProjectTokensTvlPartsFragment({ currencyId }) + const projectTokens = data.project?.tokens + + return useMemo(() => { + if (!projectTokens?.length) { + return { chainId: null, address: null } + } + + let bestTvl = 0 + let bestIndex = -1 + + for (let i = 0; i < projectTokens.length; i++) { + const token = projectTokens[i] + if (!token) { + continue + } + const tvl = token.market?.totalValueLocked?.value ?? 0 + if (tvl > bestTvl) { + bestTvl = tvl + bestIndex = i + } + } + + const bestToken = bestIndex >= 0 ? projectTokens[bestIndex] : undefined + if (!bestToken) { + return { chainId: null, address: null } + } + + const chainId = fromGraphQLChain(bestToken.chain) + return { chainId: chainId ?? null, address: bestToken.address ?? null } + }, [projectTokens]) +} diff --git a/apps/mobile/src/screens/TokenDetailsScreen/useNetworkBalanceSheet.test.ts b/apps/mobile/src/screens/TokenDetailsScreen/useNetworkBalanceSheet.test.ts new file mode 100644 index 00000000000..d49d18cea9b --- /dev/null +++ b/apps/mobile/src/screens/TokenDetailsScreen/useNetworkBalanceSheet.test.ts @@ -0,0 +1,214 @@ +import { act, renderHook } from '@testing-library/react' +import { useNetworkBalanceSheet } from 'src/screens/TokenDetailsScreen/useNetworkBalanceSheet' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' +import { CurrencyField } from 'uniswap/src/types/currency' + +const mockNavigateToSwapFlow = jest.fn() +const mockNavigateToSend = jest.fn() + +jest.mock('wallet/src/contexts/WalletNavigationContext', () => ({ + // oxlint-disable-next-line typescript/explicit-function-return-type + useWalletNavigation: () => ({ + navigateToSwapFlow: mockNavigateToSwapFlow, + navigateToSend: mockNavigateToSend, + }), +})) + +jest.mock('wallet/src/features/wallet/hooks', () => ({ + // oxlint-disable-next-line typescript/explicit-function-return-type + useActiveAccountAddressWithThrow: () => '0xTestAddress', +})) + +const mockCurrentChainBalance: PortfolioBalance = { + id: 'balance-mainnet', + cacheId: 'cache-mainnet', + quantity: 100, + balanceUSD: 100, + relativeChange24: 0, + isHidden: false, + currencyInfo: { + currencyId: `${UniverseChainId.Mainnet}-0xtoken`, + currency: { + chainId: UniverseChainId.Mainnet, + address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + decimals: 6, + symbol: 'USDC', + name: 'USD Coin', + isNative: false, + isToken: true, + }, + logoUrl: null, + safetyLevel: null, + safetyInfo: null, + isSpam: false, + }, +} as unknown as PortfolioBalance + +const mockOtherChainBalance: PortfolioBalance = { + id: 'balance-base', + cacheId: 'cache-base', + quantity: 50, + balanceUSD: 50, + relativeChange24: 0, + isHidden: false, + currencyInfo: { + currencyId: `${UniverseChainId.Base}-0xtoken`, + currency: { + chainId: UniverseChainId.Base, + address: '0xBaseTokenAddress', + decimals: 6, + symbol: 'USDC', + name: 'USD Coin', + isNative: false, + isToken: true, + }, + logoUrl: null, + safetyLevel: null, + safetyInfo: null, + isSpam: false, + }, +} as unknown as PortfolioBalance + +let mockCrossChainResult = { + currentChainBalance: null as PortfolioBalance | null, + otherChainBalances: null as PortfolioBalance[] | null, +} + +jest.mock('uniswap/src/data/balances/hooks/useCrossChainBalances', () => ({ + // oxlint-disable-next-line typescript/explicit-function-return-type + useCrossChainBalances: () => mockCrossChainResult, +})) + +jest.mock('uniswap/src/data/graphql/uniswap-data-api/fragments', () => ({ + // oxlint-disable-next-line typescript/explicit-function-return-type + useTokenBasicProjectPartsFragment: () => ({ + data: { project: { tokens: [] } }, + }), +})) + +const defaultArgs = { + currencyId: `${UniverseChainId.Mainnet}-0xtoken`, + chainId: UniverseChainId.Mainnet, +} + +describe(useNetworkBalanceSheet, () => { + beforeEach(() => { + jest.clearAllMocks() + mockCrossChainResult = { + currentChainBalance: null, + otherChainBalances: null, + } + }) + + describe('allChainBalances', () => { + it('returns empty array when no balances exist', () => { + const { result } = renderHook(() => useNetworkBalanceSheet(defaultArgs)) + expect(result.current.allChainBalances).toEqual([]) + }) + + it('returns current chain balance when only one chain has balance', () => { + mockCrossChainResult = { + currentChainBalance: mockCurrentChainBalance, + otherChainBalances: null, + } + const { result } = renderHook(() => useNetworkBalanceSheet(defaultArgs)) + expect(result.current.allChainBalances).toEqual([mockCurrentChainBalance]) + }) + + it('combines current and other chain balances', () => { + mockCrossChainResult = { + currentChainBalance: mockCurrentChainBalance, + otherChainBalances: [mockOtherChainBalance], + } + const { result } = renderHook(() => useNetworkBalanceSheet(defaultArgs)) + expect(result.current.allChainBalances).toEqual([mockCurrentChainBalance, mockOtherChainBalance]) + }) + }) + + describe('hasMultiChainBalances', () => { + it('returns false when only one chain balance exists', () => { + mockCrossChainResult = { + currentChainBalance: mockCurrentChainBalance, + otherChainBalances: null, + } + const { result } = renderHook(() => useNetworkBalanceSheet(defaultArgs)) + expect(result.current.hasMultiChainBalances).toBe(false) + }) + + it('returns true when multiple chain balances exist', () => { + mockCrossChainResult = { + currentChainBalance: mockCurrentChainBalance, + otherChainBalances: [mockOtherChainBalance], + } + const { result } = renderHook(() => useNetworkBalanceSheet(defaultArgs)) + expect(result.current.hasMultiChainBalances).toBe(true) + }) + }) + + describe('sheet state', () => { + it('starts with sheet closed', () => { + const { result } = renderHook(() => useNetworkBalanceSheet(defaultArgs)) + expect(result.current.isNetworkSheetOpen).toBe(false) + }) + + it('opens sheet when openSellSheet is called', () => { + const { result } = renderHook(() => useNetworkBalanceSheet(defaultArgs)) + act(() => result.current.openSellSheet()) + expect(result.current.isNetworkSheetOpen).toBe(true) + }) + + it('opens sheet when openSendSheet is called', () => { + const { result } = renderHook(() => useNetworkBalanceSheet(defaultArgs)) + act(() => result.current.openSendSheet()) + expect(result.current.isNetworkSheetOpen).toBe(true) + }) + + it('closes sheet when onCloseNetworkSheet is called', () => { + const { result } = renderHook(() => useNetworkBalanceSheet(defaultArgs)) + act(() => result.current.openSellSheet()) + expect(result.current.isNetworkSheetOpen).toBe(true) + act(() => result.current.onCloseNetworkSheet()) + expect(result.current.isNetworkSheetOpen).toBe(false) + }) + }) + + describe('onSelectNetwork', () => { + it('navigates to send when sheet was opened via openSendSheet', () => { + const { result } = renderHook(() => useNetworkBalanceSheet(defaultArgs)) + + act(() => result.current.openSendSheet()) + act(() => result.current.onSelectNetwork(mockOtherChainBalance)) + + expect(mockNavigateToSend).toHaveBeenCalledWith({ + currencyAddress: '0xBaseTokenAddress', + chainId: UniverseChainId.Base, + }) + expect(mockNavigateToSwapFlow).not.toHaveBeenCalled() + }) + + it('navigates to swap flow when sheet was opened via openSellSheet', () => { + const { result } = renderHook(() => useNetworkBalanceSheet(defaultArgs)) + + act(() => result.current.openSellSheet()) + act(() => result.current.onSelectNetwork(mockOtherChainBalance)) + + expect(mockNavigateToSwapFlow).toHaveBeenCalledWith({ + currencyField: CurrencyField.INPUT, + currencyAddress: '0xBaseTokenAddress', + currencyChainId: UniverseChainId.Base, + }) + expect(mockNavigateToSend).not.toHaveBeenCalled() + }) + + it('closes the sheet after selection', () => { + const { result } = renderHook(() => useNetworkBalanceSheet(defaultArgs)) + + act(() => result.current.openSellSheet()) + expect(result.current.isNetworkSheetOpen).toBe(true) + + act(() => result.current.onSelectNetwork(mockOtherChainBalance)) + expect(result.current.isNetworkSheetOpen).toBe(false) + }) + }) +}) diff --git a/apps/mobile/src/screens/TokenDetailsScreen/useNetworkBalanceSheet.ts b/apps/mobile/src/screens/TokenDetailsScreen/useNetworkBalanceSheet.ts new file mode 100644 index 00000000000..e3e98f35438 --- /dev/null +++ b/apps/mobile/src/screens/TokenDetailsScreen/useNetworkBalanceSheet.ts @@ -0,0 +1,100 @@ +import { GraphQLApi } from '@universe/api' +import { useMemo, useState } from 'react' +import { getNativeAddress } from 'uniswap/src/constants/addresses' +import { useCrossChainBalances } from 'uniswap/src/data/balances/hooks/useCrossChainBalances' +import { useTokenBasicProjectPartsFragment } from 'uniswap/src/data/graphql/uniswap-data-api/fragments' +import { UniverseChainId } from 'uniswap/src/features/chains/types' +import { fromGraphQLChain } from 'uniswap/src/features/chains/utils' +import { PortfolioBalance } from 'uniswap/src/features/dataApi/types' +import { CurrencyField } from 'uniswap/src/types/currency' +import { useEvent } from 'utilities/src/react/hooks' +import { useWalletNavigation } from 'wallet/src/contexts/WalletNavigationContext' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' + +type NetworkSheetAction = 'sell' | 'send' + +interface UseNetworkBalanceSheetParams { + currencyId: string + chainId: UniverseChainId +} + +interface UseNetworkBalanceSheetResult { + allChainBalances: PortfolioBalance[] + hasMultiChainBalances: boolean + isNetworkSheetOpen: boolean + openSellSheet: () => void + openSendSheet: () => void + onCloseNetworkSheet: () => void + onSelectNetwork: (balance: PortfolioBalance) => void +} + +export function useNetworkBalanceSheet({ + currencyId, + chainId, +}: UseNetworkBalanceSheetParams): UseNetworkBalanceSheetResult { + const activeAddress = useActiveAccountAddressWithThrow() + const { navigateToSwapFlow, navigateToSend } = useWalletNavigation() + + // Cross-chain balances (Apollo-cached, no extra network requests) + const projectTokens = useTokenBasicProjectPartsFragment({ currencyId }).data.project?.tokens + const crossChainTokens = useMemo(() => { + const result: Array<{ address: string | null; chain: GraphQLApi.Chain }> = [] + for (const projectToken of projectTokens ?? []) { + if (projectToken?.chain && projectToken.address !== undefined) { + const chainIdForToken = fromGraphQLChain(projectToken.chain) + if (chainIdForToken && chainIdForToken !== chainId) { + result.push({ address: projectToken.address, chain: projectToken.chain }) + } + } + } + return result + }, [projectTokens, chainId]) + + const { currentChainBalance: crossChainCurrentBalance, otherChainBalances } = useCrossChainBalances({ + evmAddress: activeAddress, + currencyId, + crossChainTokens, + }) + + const allChainBalances = useMemo(() => { + const others = otherChainBalances ?? [] + return crossChainCurrentBalance ? [crossChainCurrentBalance, ...others] : others + }, [crossChainCurrentBalance, otherChainBalances]) + + const hasMultiChainBalances = allChainBalances.length > 1 + + // Sheet state + const [networkSheetAction, setNetworkSheetAction] = useState(null) + const isNetworkSheetOpen = networkSheetAction !== null + + const openSellSheet = useEvent(() => setNetworkSheetAction('sell')) + const openSendSheet = useEvent(() => setNetworkSheetAction('send')) + const onCloseNetworkSheet = useEvent(() => setNetworkSheetAction(null)) + + const onSelectNetwork = useEvent((balance: PortfolioBalance) => { + const action = networkSheetAction + setNetworkSheetAction(null) + const { currency } = balance.currencyInfo + const currencyAddress = currency.isToken ? currency.address : getNativeAddress(currency.chainId) + + if (action === 'send') { + navigateToSend({ currencyAddress, chainId: currency.chainId }) + } else { + navigateToSwapFlow({ + currencyField: CurrencyField.INPUT, + currencyAddress, + currencyChainId: currency.chainId, + }) + } + }) + + return { + allChainBalances, + hasMultiChainBalances, + isNetworkSheetOpen, + openSellSheet, + openSendSheet, + onCloseNetworkSheet, + onSelectNetwork, + } +} diff --git a/apps/mobile/src/screens/ViewPrivateKeys/ViewPrivateKeysScreen.tsx b/apps/mobile/src/screens/ViewPrivateKeys/ViewPrivateKeysScreen.tsx index 618d1ae0c4d..85e5f46dcd2 100644 --- a/apps/mobile/src/screens/ViewPrivateKeys/ViewPrivateKeysScreen.tsx +++ b/apps/mobile/src/screens/ViewPrivateKeys/ViewPrivateKeysScreen.tsx @@ -1,5 +1,6 @@ import { CommonActions } from '@react-navigation/core' import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { FeatureFlags, useFeatureFlag } from '@universe/gating' import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { ScrollView } from 'react-native-gesture-handler' @@ -17,8 +18,6 @@ import { AlertTriangleFilled } from 'ui/src/components/icons/AlertTriangleFilled import { HiddenWordView } from 'ui/src/components/placeholders/HiddenWordView' import { AddressDisplay } from 'uniswap/src/components/accounts/AddressDisplay' import { Modal } from 'uniswap/src/components/modals/Modal' -import { FeatureFlags } from 'uniswap/src/features/gating/flags' -import { useFeatureFlag } from 'uniswap/src/features/gating/hooks' import { ElementName, ModalName } from 'uniswap/src/features/telemetry/constants' import { Trace } from 'uniswap/src/features/telemetry/Trace' import { TestID } from 'uniswap/src/test/fixtures/testIDs' @@ -85,7 +84,7 @@ export function ViewPrivateKeysScreen({ navigation, route }: Props): JSX.Element {t('privateKeys.export.modal.speedbump.subtitle')} - + @@ -118,7 +117,7 @@ export function ViewPrivateKeysScreen({ navigation, route }: Props): JSX.Element justifyContent="space-between" p="$spacing12" borderRadius="$rounded16" - borderWidth={1} + borderWidth="$spacing1" borderColor="$surface3" gap="$spacing8" > diff --git a/apps/mobile/src/screens/components/hashcash/LogSection.tsx b/apps/mobile/src/screens/components/hashcash/LogSection.tsx new file mode 100644 index 00000000000..b08d303b518 --- /dev/null +++ b/apps/mobile/src/screens/components/hashcash/LogSection.tsx @@ -0,0 +1,50 @@ +import React, { memo } from 'react' +import { type LogEntry, useHashcashBenchmarkStore } from 'src/screens/stores/hashcashBenchmarkStore' +import { Flex, Text, TouchableArea } from 'ui/src' + +function formatTime(date: Date): string { + return date.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) +} + +const LogEntryRow = memo(function LogEntryRow({ log, index }: { log: LogEntry; index: number }): JSX.Element { + return ( + + {formatTime(log.timestamp)} - {log.message} + + ) +}) + +export const LogSection = memo(function LogSection(): JSX.Element | null { + const logs = useHashcashBenchmarkStore((state) => state.logs) + const clearLogs = useHashcashBenchmarkStore((state) => state.clearLogs) + + if (logs.length === 0) { + return null + } + + return ( + + + Operation Log + + + Clear + + + + + {logs.map((log, index) => ( + + ))} + + ) +}) diff --git a/apps/mobile/src/screens/components/hashcash/ProgressSection.tsx b/apps/mobile/src/screens/components/hashcash/ProgressSection.tsx new file mode 100644 index 00000000000..513266a30ce --- /dev/null +++ b/apps/mobile/src/screens/components/hashcash/ProgressSection.tsx @@ -0,0 +1,69 @@ +import React, { memo } from 'react' +import { useHashcashBenchmarkStore } from 'src/screens/stores/hashcashBenchmarkStore' +import { Flex, Text } from 'ui/src' +import { useShallow } from 'zustand/shallow' + +function formatDuration(ms: number): string { + if (ms < 1000) { + return `${ms.toFixed(0)}ms` + } + return `${(ms / 1000).toFixed(2)}s` +} + +export const ProgressSection = memo(function ProgressSection(): JSX.Element | null { + const progress = useHashcashBenchmarkStore( + useShallow((state) => ({ + isRunning: state.progress.isRunning, + currentImpl: state.progress.currentImpl, + difficulty: state.progress.difficulty, + startTime: state.progress.startTime, + elapsedMs: state.progress.elapsedMs, + estimatedAttempts: state.progress.estimatedAttempts, + })), + ) + + if (!progress.isRunning) { + return null + } + + return ( + + Current Progress + + + + Running: + + + {progress.currentImpl === 'native' ? 'Native' : 'JavaScript'} @ Difficulty {progress.difficulty} + + + + {progress.startTime !== null ? ( + <> + + + Elapsed: + + + {formatDuration(progress.elapsedMs)} + + + + + + Est. Attempts: + + + ~{progress.estimatedAttempts.toLocaleString()} + + + + ) : ( + + JS blocks main thread - no progress updates + + )} + + ) +}) diff --git a/apps/mobile/src/screens/components/hashcash/ResultCard.tsx b/apps/mobile/src/screens/components/hashcash/ResultCard.tsx new file mode 100644 index 00000000000..a71e2f70cd5 --- /dev/null +++ b/apps/mobile/src/screens/components/hashcash/ResultCard.tsx @@ -0,0 +1,99 @@ +import React, { memo } from 'react' +import type { BenchmarkResult } from 'src/screens/stores/hashcashBenchmarkStore' +import { Flex, Text } from 'ui/src' + +function formatDuration(ms: number): string { + if (ms < 1000) { + return `${ms.toFixed(0)}ms` + } + return `${(ms / 1000).toFixed(2)}s` +} + +export const ResultCard = memo(function ResultCard({ + difficulty, + native, + js, +}: { + difficulty: string + native: BenchmarkResult | undefined + js: BenchmarkResult | undefined +}): JSX.Element { + const speedup = native && js ? js.timeMs / native.timeMs : null + + return ( + + Difficulty {difficulty} + + {native && ( + + + + Native + + + {formatDuration(native.timeMs)} + + + + + Attempts: + + + {native.attempts.toLocaleString()} + + + + + Hash Rate: + + + {native.hashRate.toLocaleString()} h/s + + + + )} + + {js && ( + + + + JavaScript + + + {formatDuration(js.timeMs)} + + + + + Attempts: + + + {js.attempts.toLocaleString()} + + + + + Hash Rate: + + + {js.hashRate.toLocaleString()} h/s + + + + )} + + {speedup && ( + + 1 ? '$statusSuccess' : '$statusCritical'} + textAlign="center" + > + {speedup > 1 ? `Native is ${speedup.toFixed(1)}x faster` : `JS is ${(1 / speedup).toFixed(1)}x faster`} + + + )} + + ) +}) diff --git a/apps/mobile/src/screens/components/sessions/CurrentOperationSection.tsx b/apps/mobile/src/screens/components/sessions/CurrentOperationSection.tsx new file mode 100644 index 00000000000..621538491a8 --- /dev/null +++ b/apps/mobile/src/screens/components/sessions/CurrentOperationSection.tsx @@ -0,0 +1,19 @@ +import React, { memo } from 'react' +import { useSessionsDebugStore } from 'src/screens/stores/sessionsDebugStore' +import { Flex, Text } from 'ui/src' + +export const CurrentOperationSection = memo(function CurrentOperationSection(): JSX.Element | null { + const currentOperation = useSessionsDebugStore((state) => state.currentOperation) + + if (!currentOperation) { + return null + } + + return ( + + + {currentOperation} + + + ) +}) diff --git a/apps/mobile/src/screens/components/sessions/HashcashProgressSection.tsx b/apps/mobile/src/screens/components/sessions/HashcashProgressSection.tsx new file mode 100644 index 00000000000..1c4f0d96d46 --- /dev/null +++ b/apps/mobile/src/screens/components/sessions/HashcashProgressSection.tsx @@ -0,0 +1,81 @@ +import React, { memo } from 'react' +import { useSessionsDebugStore } from 'src/screens/stores/sessionsDebugStore' +import { Flex, Text } from 'ui/src' +import { useShallow } from 'zustand/shallow' + +export const HashcashProgressSection = memo(function HashcashProgressSection(): JSX.Element | null { + const progress = useSessionsDebugStore( + useShallow((state) => ({ + isRunning: state.hashcashProgress.isRunning, + difficulty: state.hashcashProgress.difficulty, + estimatedAttempts: state.hashcashProgress.estimatedAttempts, + elapsedMs: state.hashcashProgress.elapsedMs, + actualResult: state.hashcashProgress.actualResult, + })), + ) + + if (!progress.isRunning && !progress.actualResult) { + return null + } + + return ( + + Hashcash Progress + + + + Status: + + + {progress.isRunning ? 'Solving...' : 'Complete'} + + + + + + Difficulty: + + + {progress.difficulty} + + + + + + Attempts: + + + {progress.actualResult + ? (progress.actualResult.iterationCount ?? 0).toLocaleString() + : `~${progress.estimatedAttempts.toLocaleString()}`} + + + + + + Time: + + + {progress.actualResult + ? `${(progress.actualResult.durationMs / 1000).toFixed(2)}s` + : `${(progress.elapsedMs / 1000).toFixed(2)}s`} + + + + {progress.actualResult && ( + + + Hash Rate: + + + {((progress.actualResult.iterationCount ?? 0) / (progress.actualResult.durationMs / 1000)).toLocaleString( + undefined, + { maximumFractionDigits: 0 }, + )}{' '} + h/s + + + )} + + ) +}) diff --git a/apps/mobile/src/screens/components/sessions/LogSection.tsx b/apps/mobile/src/screens/components/sessions/LogSection.tsx new file mode 100644 index 00000000000..150b4f4393b --- /dev/null +++ b/apps/mobile/src/screens/components/sessions/LogSection.tsx @@ -0,0 +1,50 @@ +import React, { memo } from 'react' +import { type LogEntry, useSessionsDebugStore } from 'src/screens/stores/sessionsDebugStore' +import { Flex, Text, TouchableArea } from 'ui/src' + +function formatTime(date: Date): string { + return date.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) +} + +const LogEntryRow = memo(function LogEntryRow({ log, index }: { log: LogEntry; index: number }): JSX.Element { + return ( + + {formatTime(log.timestamp)} - {log.message} + + ) +}) + +export const LogSection = memo(function LogSection(): JSX.Element | null { + const logs = useSessionsDebugStore((state) => state.logs) + const clearLogs = useSessionsDebugStore((state) => state.clearLogs) + + if (logs.length === 0) { + return null + } + + return ( + + + Operation Log + + + Clear + + + + + {logs.map((log, index) => ( + + ))} + + ) +}) diff --git a/apps/mobile/src/screens/stores/hashcashBenchmarkStore.ts b/apps/mobile/src/screens/stores/hashcashBenchmarkStore.ts new file mode 100644 index 00000000000..8dd22d77834 --- /dev/null +++ b/apps/mobile/src/screens/stores/hashcashBenchmarkStore.ts @@ -0,0 +1,116 @@ +import { create } from 'zustand' + +export type Implementation = 'native' | 'js' | 'both' + +export interface BenchmarkResult { + implementation: 'native' | 'js' + difficulty: number + counter: string | null + attempts: number + timeMs: number + hashRate: number +} + +interface BenchmarkProgress { + isRunning: boolean + currentImpl: 'native' | 'js' | null + difficulty: number + startTime: number | null + elapsedMs: number + estimatedAttempts: number +} + +export interface LogEntry { + timestamp: Date + message: string + type: 'info' | 'success' | 'error' +} + +interface HashcashBenchmarkState { + // State + results: BenchmarkResult[] + selectedDifficulty: number + selectedImpl: Implementation + logs: LogEntry[] + progress: BenchmarkProgress + measuredHashRate: number | null + isCancelled: boolean + + // Actions + setDifficulty: (difficulty: number) => void + setImpl: (impl: Implementation) => void + addResult: (result: BenchmarkResult) => void + clearResults: () => void + addLog: (message: string, type?: 'info' | 'success' | 'error') => void + clearLogs: () => void + startBenchmark: (impl: 'native' | 'js', difficulty: number) => void + updateProgress: (elapsedMs: number, estimatedAttempts: number) => void + endBenchmark: () => void + cancel: () => void + resetCancel: () => void +} + +const initialProgress: BenchmarkProgress = { + isRunning: false, + currentImpl: null, + difficulty: 0, + startTime: null, + elapsedMs: 0, + estimatedAttempts: 0, +} + +export const useHashcashBenchmarkStore = create((set) => ({ + results: [], + selectedDifficulty: 2, + selectedImpl: 'both', + logs: [], + progress: initialProgress, + measuredHashRate: null, + isCancelled: false, + + setDifficulty: (difficulty): void => set({ selectedDifficulty: difficulty }), + + setImpl: (impl): void => set({ selectedImpl: impl }), + + addResult: (result): void => + set((state) => ({ + results: [...state.results, result], + // Auto-update measured hash rate from native results + measuredHashRate: + result.implementation === 'native' && result.hashRate > 0 ? result.hashRate : state.measuredHashRate, + })), + + clearResults: (): void => set({ results: [] }), + + addLog: (message, type = 'info'): void => + set((state) => ({ + logs: [...state.logs.slice(-19), { timestamp: new Date(), message, type }], + })), + + clearLogs: (): void => set({ logs: [] }), + + startBenchmark: (impl, difficulty): void => + set({ + isCancelled: false, + progress: { + isRunning: true, + currentImpl: impl, + difficulty, + // null for JS because it blocks the thread - no progress updates possible + startTime: impl === 'native' ? performance.now() : null, + elapsedMs: 0, + estimatedAttempts: 0, + }, + }), + + updateProgress: (elapsedMs, estimatedAttempts): void => + set((state) => ({ + progress: { ...state.progress, elapsedMs, estimatedAttempts }, + })), + + endBenchmark: (): void => set({ progress: initialProgress }), + + cancel: (): void => set({ isCancelled: true, progress: initialProgress }), + + resetCancel: (): void => set({ isCancelled: false }), +})) diff --git a/apps/mobile/src/screens/stores/sessionsDebugStore.ts b/apps/mobile/src/screens/stores/sessionsDebugStore.ts new file mode 100644 index 00000000000..4f90a1dc836 --- /dev/null +++ b/apps/mobile/src/screens/stores/sessionsDebugStore.ts @@ -0,0 +1,112 @@ +import type { ChallengeResponse, HashcashSolveAnalytics } from '@universe/sessions' +import { create } from 'zustand' + +interface SessionState { + sessionId: string | null + deviceId: string | null + uniswapIdentifier: string | null +} + +export interface LogEntry { + timestamp: Date + message: string + type: 'info' | 'success' | 'error' +} + +interface HashcashProgress { + isRunning: boolean + difficulty: number + estimatedAttempts: number + elapsedMs: number + startTime: number | null + actualResult: HashcashSolveAnalytics | null +} + +interface SessionsDebugState { + // State + session: SessionState + challenge: ChallengeResponse | null + isLoading: boolean + currentOperation: string | null + logs: LogEntry[] + hashcashProgress: HashcashProgress + + // Actions + setSession: (session: SessionState) => void + setChallenge: (challenge: ChallengeResponse | null) => void + startOperation: (operation: string) => void + endOperation: () => void + addLog: (message: string, type?: 'info' | 'success' | 'error') => void + clearLogs: () => void + startHashcash: (difficulty: number) => void + updateHashcashProgress: (elapsedMs: number, estimatedAttempts: number) => void + completeHashcash: (result: HashcashSolveAnalytics) => void + stopHashcash: () => void + reset: () => void +} + +const initialHashcashProgress: HashcashProgress = { + isRunning: false, + difficulty: 0, + estimatedAttempts: 0, + elapsedMs: 0, + startTime: null, + actualResult: null, +} + +const initialState = { + session: { sessionId: null, deviceId: null, uniswapIdentifier: null }, + challenge: null, + isLoading: false, + currentOperation: null, + logs: [] as LogEntry[], + hashcashProgress: initialHashcashProgress, +} + +export const useSessionsDebugStore = create((set) => ({ + ...initialState, + + setSession: (session): void => set({ session }), + + setChallenge: (challenge): void => set({ challenge }), + + startOperation: (operation): void => set({ isLoading: true, currentOperation: operation }), + + endOperation: (): void => set({ isLoading: false, currentOperation: null }), + + addLog: (message, type = 'info'): void => + set((state) => ({ + logs: [...state.logs.slice(-19), { timestamp: new Date(), message, type }], + })), + + clearLogs: (): void => set({ logs: [] }), + + startHashcash: (difficulty): void => + set({ + hashcashProgress: { + isRunning: true, + difficulty, + estimatedAttempts: 0, + elapsedMs: 0, + startTime: performance.now(), + actualResult: null, + }, + }), + + updateHashcashProgress: (elapsedMs, estimatedAttempts): void => + set((state) => ({ + hashcashProgress: { ...state.hashcashProgress, elapsedMs, estimatedAttempts }, + })), + + completeHashcash: (result): void => + set((state) => ({ + hashcashProgress: { ...state.hashcashProgress, isRunning: false, actualResult: result }, + })), + + stopHashcash: (): void => + set((state) => ({ + hashcashProgress: { ...state.hashcashProgress, isRunning: false }, + })), + + reset: (): void => set(initialState), +})) diff --git a/apps/mobile/src/test/fixtures/redux.ts b/apps/mobile/src/test/fixtures/redux.ts index 1b1d8b45b36..4122efaa4b9 100644 --- a/apps/mobile/src/test/fixtures/redux.ts +++ b/apps/mobile/src/test/fixtures/redux.ts @@ -1,7 +1,7 @@ import { PreloadedState } from 'redux' import { MobileState } from 'src/app/mobileReducer' -import { ModalsState } from 'src/features/modals/ModalsState' import { initialModalsState } from 'src/features/modals/modalSlice' +import { ModalsState } from 'src/features/modals/ModalsState' import { createFixture } from 'uniswap/src/test/utils' import { Account } from 'wallet/src/features/wallet/accounts/types' import { preloadedWalletPackageState } from 'wallet/src/test/fixtures' @@ -14,7 +14,14 @@ type PreloadedMobileStateOptions = { account: Account | undefined } -export const preloadedMobileState = createFixture, PreloadedMobileStateOptions>({ +type PreloadedMobileStateFactory = ( + overrides?: Partial & PreloadedMobileStateOptions>, +) => PreloadedState + +export const preloadedMobileState: PreloadedMobileStateFactory = createFixture< + PreloadedState, + PreloadedMobileStateOptions +>({ account: undefined, })(({ account }) => ({ ...preloadedWalletPackageState({ account }), diff --git a/apps/mobile/src/test/render.tsx b/apps/mobile/src/test/render.tsx index 3af71d6b99b..496d80081d7 100644 --- a/apps/mobile/src/test/render.tsx +++ b/apps/mobile/src/test/render.tsx @@ -11,13 +11,12 @@ import { } from '@testing-library/react-native' import { GraphQLApi } from '@universe/api' import React, { PropsWithChildren } from 'react' -import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider' import type { MobileState } from 'src/app/mobileReducer' +import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider' import { navigationRef } from 'src/app/navigation/navigationRef' import { store as appStore, persistedReducer } from 'src/app/store' import { UniswapProvider } from 'uniswap/src/contexts/UniswapContext' import { BlankUrlProvider } from 'uniswap/src/contexts/UrlContext' -import { fiatOnRampAggregatorApi } from 'uniswap/src/features/fiatOnRamp/api' import { AutoMockedApolloProvider } from 'uniswap/src/test/mocks' import { mockUniswapContext } from 'uniswap/src/test/render' import { SharedWalletProvider } from 'wallet/src/providers/SharedWalletProvider' @@ -48,7 +47,7 @@ export function renderWithProviders( store = configureStore({ reducer: persistedReducer, preloadedState, - middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(fiatOnRampAggregatorApi.middleware), + middleware: (getDefaultMiddleware) => getDefaultMiddleware(), }), ...renderOptions }: ExtendedRenderOptions = {}, @@ -118,7 +117,7 @@ export function renderHookWithProviders( store = configureStore({ reducer: persistedReducer, preloadedState, - middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(fiatOnRampAggregatorApi.middleware), + middleware: (getDefaultMiddleware) => getDefaultMiddleware(), }), ...renderOptions } = (hookOptions ?? {}) as ExtendedRenderHookOptions

diff --git a/apps/mobile/src/utils/hooks.ts b/apps/mobile/src/utils/hooks.ts index 157e3e630de..1bb4493e8a0 100644 --- a/apps/mobile/src/utils/hooks.ts +++ b/apps/mobile/src/utils/hooks.ts @@ -37,7 +37,7 @@ export function useFunctionAfterNavigationTransitionEndWithDelay(fn: () => void, const navigation = useAppStackNavigation() useEffect(() => { - let timeout: NodeJS.Timeout | null = null + let timeout: NodeJS.Timeout | number | null = null const unsubscribe = navigation.addListener('transitionEnd', () => { timeout = setTimeout(fn, timeoutMs) diff --git a/apps/mobile/src/utils/reanimated.ts b/apps/mobile/src/utils/reanimated.ts index de1d1f9353b..863cb8d9a64 100644 --- a/apps/mobile/src/utils/reanimated.ts +++ b/apps/mobile/src/utils/reanimated.ts @@ -1,4 +1,4 @@ -/* eslint-disable max-lines */ +/* oxlint-disable max-lines */ /** * Util to format numbers inside reanimated worklets. * @@ -267,6 +267,7 @@ const currencyFormatMap = { 'zh-Hant': 'pre', } +// oxlint-disable-next-line typescript/no-duplicate-type-constituents -- biome-parity: oxlint is stricter here export type Language = keyof typeof currencyFormatMap | keyof typeof transformForLocale const currencySymbols: { [key: string]: string } = { diff --git a/apps/mobile/src/utils/useNavigationHeader.tsx b/apps/mobile/src/utils/useNavigationHeader.tsx index d57dd422903..19304509dc7 100644 --- a/apps/mobile/src/utils/useNavigationHeader.tsx +++ b/apps/mobile/src/utils/useNavigationHeader.tsx @@ -3,7 +3,6 @@ import React, { ReactNode, useEffect } from 'react' import { HeaderSkipButton } from 'src/app/navigation/components' import { OnboardingStackParamList } from 'src/app/navigation/types' import { BackButton } from 'src/components/buttons/BackButton' -import { iconSizes } from 'ui/src/theme' import { UnitagStackParamList } from 'uniswap/src/types/screens/mobile' /** @@ -16,7 +15,7 @@ export function useNavigationHeader( ): void { useEffect((): void => { navigation.setOptions({ - headerLeft: () => , + headerLeft: () => , headerRight: onSkip ? (_props): ReactNode => : undefined, }) }, [navigation, onSkip]) diff --git a/apps/mobile/src/utils/useOpenBackupReminderModal.ts b/apps/mobile/src/utils/useOpenBackupReminderModal.ts deleted file mode 100644 index 761eeb6565a..00000000000 --- a/apps/mobile/src/utils/useOpenBackupReminderModal.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { useNavigation } from '@react-navigation/native' -import { useEffect } from 'react' -import { useSelector } from 'react-redux' -import { AccountType } from 'uniswap/src/features/accounts/types' -import { usePortfolioTotalValue } from 'uniswap/src/features/dataApi/balances/balancesRest' -import { ModalName } from 'uniswap/src/features/telemetry/constants' -import { ONE_DAY_MS } from 'utilities/src/time/time' -import { selectBackupReminderLastSeenTs } from 'wallet/src/features/behaviorHistory/selectors' -import { Account } from 'wallet/src/features/wallet/accounts/types' -import { hasExternalBackup } from 'wallet/src/features/wallet/accounts/utils' -import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks' - -const MIN_PORTFOLIO_VALUE_FOR_BACKUP_REMINDER = 100 // $100 USD -const BACKUP_REMINDER_COOLDOWN_MS = ONE_DAY_MS - -export function useOpenBackupReminderModal(activeAccount: Account): void { - const navigation = useNavigation() - const activeAddress = useActiveAccountAddress() - const { data: portfolioData } = usePortfolioTotalValue({ - evmAddress: activeAddress ?? undefined, - }) - - const isBackupReminderModalOpen = navigation - .getState() - ?.routes.some((route) => route.name === ModalName.BackupReminder) - const isBackupReminderWarningModalOpen = navigation - .getState() - ?.routes.some((route) => route.name === ModalName.BackupReminderWarning) - - const backupReminderLastSeenTs = useSelector(selectBackupReminderLastSeenTs) - const externalBackups = hasExternalBackup(activeAccount) - - const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic - - // Check portfolio value threshold - const portfolioValue = portfolioData?.balanceUSD ?? 0 - const hasMinimumPortfolioValue = portfolioValue >= MIN_PORTFOLIO_VALUE_FOR_BACKUP_REMINDER - - // Check if 24 hours have passed since last seen - const now = Date.now() - const timeSinceLastSeen = backupReminderLastSeenTs ? now - backupReminderLastSeenTs : Infinity - const has24HoursPassed = timeSinceLastSeen >= BACKUP_REMINDER_COOLDOWN_MS - - const shouldOpenBackupReminderModal = - !isBackupReminderModalOpen && - !isBackupReminderWarningModalOpen && - isSignerAccount && - !externalBackups && - hasMinimumPortfolioValue && - has24HoursPassed - - useEffect(() => { - if (shouldOpenBackupReminderModal) { - const timeoutId = setTimeout(() => { - navigation.navigate(ModalName.BackupReminder as never) - }, 1000) - - return () => clearTimeout(timeoutId) - } - - return undefined - }, [shouldOpenBackupReminderModal, navigation]) -} diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json index 0f4eef55ad2..59614477d3b 100644 --- a/apps/mobile/tsconfig.json +++ b/apps/mobile/tsconfig.json @@ -4,6 +4,21 @@ "extends": "../../config/tsconfig/expo.json", "exclude": [".storybook/storybook.requires.ts"], "references": [ + { + "path": "../../packages/sessions" + }, + { + "path": "../../packages/notifications" + }, + { + "path": "../../packages/hashcash-native" + }, + { + "path": "../../packages/gating" + }, + { + "path": "../../packages/api" + }, { "path": "../../packages/wallet" }, @@ -11,15 +26,17 @@ "path": "../../packages/utilities" }, { - "path": "../../packages/uniswap" + "path": "../../packages/ui" }, { - "path": "../../packages/api" + "path": "../../packages/uniswap" } ], "compilerOptions": { - "baseUrl": "./", - "types": ["jest", "node"] + "types": ["jest", "node"], + "paths": { + "src/*": ["./src/*"] + } }, - "include": ["**/*.ts", "**/*.tsx", "global.d.ts"] + "include": ["**/*.ts", "**/*.tsx", "src/polyfills/**/*.js", "global.d.ts"] } diff --git a/apps/mobile/tsconfig.eslint.json b/apps/mobile/tsconfig.lint.json similarity index 100% rename from apps/mobile/tsconfig.eslint.json rename to apps/mobile/tsconfig.lint.json diff --git a/apps/web/.depcheckrc b/apps/web/.depcheckrc index 0469d010fbb..d43c039c348 100644 --- a/apps/web/.depcheckrc +++ b/apps/web/.depcheckrc @@ -22,9 +22,11 @@ ignores: [ '@vitest/coverage-v8', 'expo-crypto', '@datadog/datadog-ci', + 'typescript', + '@typescript/native-preview', + 'playwright', # Dependencies that depcheck thinks are missing but are actually present or never used 'stories', - 'porto', ## package.json scripts 'esbuild-register', ## GraphQL @@ -39,7 +41,6 @@ ignores: [ 'resize-observer-polyfill', ## Linting and Babel '@babel/preset-env', - 'eslint-plugin-import', 'terser-webpack-plugin', ## Storybook '@svgr/webpack', @@ -54,7 +55,6 @@ ignores: [ 'storybook-addon-pseudo-states', 'wait-on', 'detect-package-manager', - 'eslint-plugin-storybook', 'prop-types', # Storybook requires react-scripts 'react-scripts', @@ -69,36 +69,8 @@ ignores: [ 'utilities', 'ui', ## Top level local file paths - 'abis', - 'analytics', - 'appGraphql', - 'assets', - 'components', - 'connection', - 'constants', - 'dev', - 'featureFlags', - 'features', - 'hooks', - 'lib', - 'locales', - 'nft', - 'pages', - 'polyfills', - 'rpc', - 'shared-cloud', + '~', 'functions', - 'src', - 'state', - 'test-utils', - 'theme', - 'tracing', - 'types', - 'utils', - 'i18n', - 'tamagui.config', - 'setupRive', - 'sideEffects', 'global.css', # needed for ci 'dd-trace', diff --git a/apps/web/.env b/apps/web/.env index 0b43a06d734..047cb12d8c0 100644 --- a/apps/web/.env +++ b/apps/web/.env @@ -1,5 +1,4 @@ # These API keys are intentionally public. Please do not report them - thank you for your concern. -ESLINT_NO_DEV_ERRORS=true REACT_APP_AMPLITUDE_PROXY_URL="https://interface.gateway.uniswap.org/v1/amplitude-proxy" REACT_APP_AWS_API_ENDPOINT="https://beta.gateway.uniswap.org/v1/graphql" REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847" @@ -11,7 +10,6 @@ REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz" REACT_APP_STATSIG_API_KEY="statsig_api_key" REACT_APP_STATSIG_PROXY_URL="https://interface.gateway.uniswap.org/v1/statsig-proxy" REACT_APP_TEMP_API_URL="https://temp.gateway.uniswap.org/v1" -REACT_APP_UNISWAP_BASE_API_URL="https://interface.gateway.uniswap.org" REACT_APP_UNISWAP_GATEWAY_DNS="https://interface.gateway.uniswap.org/v2" REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395" REACT_APP_TRADING_API_KEY="TRADING_API_KEY" diff --git a/apps/web/.env.production b/apps/web/.env.production index 5da7d9e1165..37c42eae4e1 100644 --- a/apps/web/.env.production +++ b/apps/web/.env.production @@ -3,3 +3,6 @@ REACT_APP_AWS_API_ENDPOINT="https://interface.gateway.uniswap.org/v1/graphql" REACT_APP_JUPITER_PROXY_URL="https://entry-gateway.backend-prod.api.uniswap.org/jupiter" REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1" REACT_APP_ANALYTICS_ENABLED=true +VITE_ENABLE_ENTRY_GATEWAY_PROXY=false +PRIVY_APP_ID="cmm59god700a40bibrazsxp7n" + diff --git a/apps/web/.env.staging b/apps/web/.env.staging index d22934ffff9..a816b432f36 100644 --- a/apps/web/.env.staging +++ b/apps/web/.env.staging @@ -1,3 +1,6 @@ # These API keys are intentionally public. Please do not report them - thank you for your concern. REACT_APP_JUPITER_PROXY_URL="https://entry-gateway.backend-prod.api.uniswap.org/jupiter" +ENTRY_GATEWAY_API_URL_OVERRIDE="https://entry-gateway.api.corn-staging.com" +VITE_ENABLE_ENTRY_GATEWAY_PROXY=true +PRIVY_APP_ID="cmm59g8g900f20cjr21tbqtmd" diff --git a/apps/web/.eslintignore b/apps/web/.eslintignore deleted file mode 100644 index 5563bf10ecb..00000000000 --- a/apps/web/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -.eslintrc.js -babel.config.js -metro.config.js -node_modules -.storybook/stories diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js deleted file mode 100644 index 331eff80908..00000000000 --- a/apps/web/.eslintrc.js +++ /dev/null @@ -1,169 +0,0 @@ -/* eslint-env node */ -require('@uniswap/eslint-config/load') - -module.exports = { - root: true, - extends: ['@uniswap/eslint-config/interface', 'plugin:storybook/recommended'], - parserOptions: { - project: 'tsconfig.eslint.json', - tsconfigRootDir: __dirname, - ecmaFeatures: { - jsx: true, - }, - }, - rules: { - // let biome do things: - semi: 0, - quotes: 0, - 'comma-dangle': 0, - 'no-trailing-spaces': 0, - 'no-extra-semi': 0, - }, - - overrides: [ - { - files: [ - 'src/index.tsx', - 'src/tracing/index.ts', - 'src/state/index.ts', - 'src/state/explore/index.tsx', - 'src/components/**', - 'src/nft/**', - 'src/theme/**', - 'src/pages/**', - ], - rules: { - 'check-file/no-index': 'off', - }, - }, - { - files: ['src/**/*.ts', 'src/**/*.tsx'], - rules: { - 'no-relative-import-paths/no-relative-import-paths': [ - 'error', - { - allowSameFolder: false, - rootDir: 'src', - }, - ], - }, - }, - { - files: ['**/*'], - rules: { - 'multiline-comment-style': ['error', 'separate-lines'], - }, - }, - { - // Configuration/typings typically export objects/definitions that are used outside of the transpiled package - // (eg not captured by the tsconfig). Because it's typical and not exceptional, this is turned off entirely. - files: ['**/*.config.*', '**/*.d.ts'], - rules: { - 'import/no-unused-modules': 'off', - }, - }, - { - files: ['**/*.ts', '**/*.tsx'], - rules: { - 'import/no-restricted-paths': [ - 'error', - { - zones: [ - { - target: ['src/**/*[!.test].ts', 'src/**/*[!.test].tsx'], - from: 'src/test-utils', - }, - ], - }, - ], - 'no-restricted-syntax': [ - 'error', - { - selector: ':matches(ExportAllDeclaration)', - message: 'Barrel exports bloat the bundle size by preventing tree-shaking.', - }, - { - selector: `:matches(Literal[value='NATIVE'])`, - message: - "Don't use the string 'NATIVE' directly. Use the NATIVE_CHAIN_ID variable from constants/tokens instead.", - }, - { - selector: - 'ImportDeclaration[source.value="src/nft/components/icons"], ImportDeclaration[source.value="nft/components/icons"]', - message: 'Please import icons from nft/components/iconExports instead of directly from icons.tsx', - }, - // TODO(WEB-4251) - remove useWeb3React rules once web3 react is removed - { - selector: `VariableDeclarator[id.type='ObjectPattern'][init.callee.name='useWeb3React'] > ObjectPattern > Property[key.name='account']`, - message: - "Do not use account directly from useWeb3React. Use the useAccount hook from 'hooks/useAccount' instead.", - }, - { - selector: `VariableDeclarator[id.type='ObjectPattern'][init.callee.name='useWeb3React'] > ObjectPattern > Property[key.name='chainId']`, - message: - "Do not use chainId directly from useWeb3React. Use the useAccount hook from 'hooks/useAccount' and access account.chainId instead.", - }, - { - selector: `VariableDeclarator[id.type='ObjectPattern'][init.callee.name='useAccount'] > ObjectPattern > Property[key.name='address']`, - message: - "Do not use address directly from useWeb3React. Use the useAccount hook from 'hooks/useAccount' and access account.address instead.", - }, - { - selector: `TSTypeAssertion[typeAnnotation.typeName.name='Address'], TSAsExpression[typeAnnotation.typeName.name='Address'], TSAsExpression[typeAnnotation.type='TSUnionType'] TSTypeReference[typeName.name='Address'], TSTypeAssertion[typeAnnotation.type='TSUnionType'] TSTypeReference[typeName.name='Address']`, - message: - 'Do not use type assertions with "

". Use `assumeOxAddress` to treat a string as an address, or isAddress/getAddress from viem to validate a string as an Address.', - }, - ], - }, - }, - { - files: ['**/*.e2e.test.ts'], - rules: { - 'no-restricted-syntax': [ - 'error', - { - selector: 'CallExpression[callee.property.name="getByTestId"] > Literal', - message: - 'Use TestID enum from uniswap/src/test/fixtures/testIDs instead of string literals with getByTestId (e.g. TestID.SwapSettings)', - }, - ], - }, - }, - { - // Enforce anvil test separation - anvil tests must only be in *.anvil.e2e.test.ts files - files: ['**/*.e2e.test.ts'], - excludedFiles: ['**/*.anvil.e2e.test.ts'], - rules: { - 'no-restricted-syntax': [ - 'error', - // Block getTest({ withAnvil: true }) - { - selector: - 'CallExpression[callee.name="getTest"] > ObjectExpression > Property[key.name="withAnvil"][value.value=true]', - message: - 'Anvil tests must be in *.anvil.e2e.test.ts files. Move this test to a file with .anvil.e2e.test.ts extension.', - }, - // Block anvil fixture usage (anvil.setErc20Balance, etc.) - { - selector: 'MemberExpression[object.name="anvil"]', - message: - 'Anvil fixture usage must be in *.anvil.e2e.test.ts files. Move this test to a file with .anvil.e2e.test.ts extension.', - }, - ], - }, - }, - { - files: ['**/*.ts', '**/*.tsx'], - excludedFiles: ['src/analytics/*'], - rules: {}, - }, - { - files: ['*.mts'], - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - project: './tsconfig.eslint.json', - }, - }, - ], -} diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 9f93a7a3c96..dcaf9d606dd 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -21,6 +21,7 @@ /.swc /test-results /src/playwright-report +anvil-test-*.log bundlemeta.json # builds @@ -62,3 +63,5 @@ package-lock.json # Storybook *storybook.log storybook-static/ + +coverage/ diff --git a/apps/web/.oxlintrc.fast.json b/apps/web/.oxlintrc.fast.json new file mode 100644 index 00000000000..9d9c29e1502 --- /dev/null +++ b/apps/web/.oxlintrc.fast.json @@ -0,0 +1,18 @@ +{ + "extends": ["./.oxlintrc.json", "../../.oxlintrc.fast.json"], + "ignorePatterns": [ + "playwright/**", + "cypress/**", + "public/**", + "functions/**", + "vite/**", + "scripts/**", + "twist-configs/**", + "test-results/**", + ".storybook/**", + "setupTests.ts", + "**/test-utils/**/*", + "**/*.config.*", + "**/*.d.ts" + ] +} diff --git a/apps/web/.oxlintrc.json b/apps/web/.oxlintrc.json new file mode 100644 index 00000000000..a39d367a058 --- /dev/null +++ b/apps/web/.oxlintrc.json @@ -0,0 +1,196 @@ +{ + "extends": ["../../.oxlintrc.json"], + "rules": { + "typescript/no-floating-promises": "off" + }, + "ignorePatterns": [ + "playwright/**", + "cypress/**", + "public/**", + "functions/**", + "vite/**", + "scripts/**", + "twist-configs/**", + "test-results/**", + ".storybook/**", + "setupTests.ts", + "**/test-utils/**/*", + "**/*.config.*", + "**/*.d.ts" + ], + "overrides": [ + { + "files": ["src/**/*.ts", "src/**/*.tsx"], + "rules": { + "universe-custom/no-relative-import-paths": [ + "error", + { + "allowSameFolder": false, + "rootDir": "src" + } + ] + } + }, + { + "files": ["src/pages/Portfolio/**"], + "rules": { + "no-restricted-syntax": [ + "error", + { + "selector": "CallExpression[callee.name='useAccount']", + "message": "Do not call 'useAccount' in portfolio pages. Use 'pages/Portfolio/hooks/usePortfolioAddress' instead." + } + ] + } + }, + { + "files": ["src/**/*.ts", "src/**/*.tsx"], + "rules": { + "no-restricted-syntax": [ + "error", + { + "selector": ":matches(ExportAllDeclaration)", + "message": "Barrel exports bloat the bundle size by preventing tree-shaking." + }, + { + "selector": ":matches(Literal[value='NATIVE'])", + "message": "Don't use the string 'NATIVE' directly. Use the NATIVE_CHAIN_ID variable from constants/tokens instead." + }, + { + "selector": "ImportDeclaration[source.value='src/nft/components/icons'], ImportDeclaration[source.value='nft/components/icons']", + "message": "Please import icons from nft/components/iconExports instead of directly from icons.tsx" + }, + { + "selector": "VariableDeclarator[id.type='ObjectPattern'][init.callee.name='useWeb3React'] > ObjectPattern > Property[key.name='account']", + "message": "Do not use account directly from useWeb3React. Use the useAccount hook from 'hooks/useAccount' instead." + }, + { + "selector": "VariableDeclarator[id.type='ObjectPattern'][init.callee.name='useWeb3React'] > ObjectPattern > Property[key.name='chainId']", + "message": "Do not use chainId directly from useWeb3React. Use the useAccount hook instead." + }, + { + "selector": "VariableDeclarator[id.type='ObjectPattern'][init.callee.name='useAccount'] > ObjectPattern > Property[key.name='address']", + "message": "Do not use address directly from useAccount. Access account.address instead." + }, + { + "selector": "TSTypeAssertion[typeAnnotation.typeName.name='Address'], TSAsExpression[typeAnnotation.typeName.name='Address']", + "message": "Do not use type assertions with Address. Use assumeOxAddress or isAddress/getAddress from viem." + } + ] + } + }, + { + "files": ["**/*.e2e.test.ts"], + "rules": { + "no-restricted-syntax": [ + "error", + { + "selector": "CallExpression[callee.property.name='getByTestId'] > Literal", + "message": "Use TestID enum instead of string literals with getByTestId." + } + ] + } + }, + { + "files": ["**/*.e2e.test.ts"], + "rules": { + "no-restricted-syntax": [ + "error", + { + "selector": "CallExpression[callee.name='getTest'] > ObjectExpression > Property[key.name='withAnvil'][value.value=true]", + "message": "Anvil tests must be in *.anvil.e2e.test.ts files." + }, + { + "selector": "MemberExpression[object.name='anvil']", + "message": "Anvil fixture usage must be in *.anvil.e2e.test.ts files." + } + ] + } + }, + { + "files": ["vite.config.*", "vite/**", "functions/**", ".storybook/**", "playwright.config.ts", "public/**"], + "rules": { + "no-console": "off", + "typescript/no-explicit-any": "off", + "no-unused-vars": "off" + } + }, + { + "files": ["src/**"], + "rules": { + "typescript/no-explicit-any": "off", + "no-bitwise": "off", + "typescript/no-non-null-assertion": "off", + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "src/nft/components/icons", + "message": "Please import icons from nft/components/iconExports instead of directly from icons.tsx" + }, + { + "name": "nft/components/icons", + "message": "Please import icons from nft/components/iconExports instead of directly from icons.tsx" + }, + { + "name": "@playwright/test", + "message": "Import test and expect from playwright/fixtures instead." + }, + { + "name": "styled-components", + "message": "Styled components is deprecated, please use Flex or styled from \"ui/src\" instead." + }, + { + "name": "ethers", + "message": "Please import from '@ethersproject/module' directly to support tree-shaking." + }, + { + "name": "ui/src/components/icons", + "message": "Please import icons directly from their respective files to avoid importing the entire icons folder." + }, + { + "name": "utilities/src/platform", + "importNames": ["isIOS", "isAndroid"], + "message": "Use isWebIOS and isWebAndroid instead." + }, + { + "name": "src/test-utils", + "message": "test-utils should not be imported in non-test files" + }, + { + "name": "wagmi", + "importNames": [ + "useChainId", + "useAccount", + "useConnect", + "useDisconnect", + "useBlockNumber", + "useWatchBlockNumber" + ], + "message": "Import wrapped utilities from internal hooks instead." + }, + { + "name": "uniswap/src/features/chains/chainInfo", + "importNames": ["UNIVERSE_CHAIN_INFO"], + "message": "Use useChainInfo or helpers in packages/uniswap/src/features/chains/utils.ts when possible!" + } + ] + } + ] + } + }, + { + "files": ["src/playwright/**"], + "rules": { + "no-console": "off" + } + }, + { + "files": ["**/*.e2e.test.ts", "**/*.anvil.e2e.test.ts"], + "rules": { + "no-restricted-imports": "off" + } + } + ] +} diff --git a/apps/web/.storybook/__mocks__/tty.js b/apps/web/.storybook/__mocks__/tty.js new file mode 100644 index 00000000000..60d53bca0ed --- /dev/null +++ b/apps/web/.storybook/__mocks__/tty.js @@ -0,0 +1,10 @@ +// Mock for Node.js tty module to work in browser environment +// Used by @storybook/instrumenter + +module.exports = { + isatty: function () { + return false + }, + ReadStream: function () {}, + WriteStream: function () {}, +} diff --git a/apps/web/.storybook/main.ts b/apps/web/.storybook/main.ts index d5325632a17..c4069721c5a 100644 --- a/apps/web/.storybook/main.ts +++ b/apps/web/.storybook/main.ts @@ -1,9 +1,8 @@ -import type { StorybookConfig } from '@storybook/react-webpack5' import { dirname, join, resolve } from 'path' +import type { StorybookConfig } from '@storybook/react-webpack5' +import TerserPlugin from 'terser-webpack-plugin' import { DefinePlugin } from 'webpack' -const isDev = process.env.NODE_ENV === 'development' - /** * 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. @@ -40,7 +39,8 @@ const config: StorybookConfig = { config.plugins.push( new DefinePlugin({ - __DEV__: isDev, + __DEV__: process.env.NODE_ENV === 'development', + 'process.env.IS_UNISWAP_EXTENSION': JSON.stringify(process.env.STORYBOOK_EXTENSION || 'false'), }), ) @@ -66,32 +66,196 @@ const config: StorybookConfig = { use: ['@svgr/webpack'], }) + // Add babel-loader for TypeScript/JavaScript transpilation + // @storybook/preset-create-react-app removes TypeScript rules + config?.module?.rules && + config.module.rules.push({ + test: /\.(tsx?|jsx?)$/, + // Exclude node_modules except for expo packages and related modules + exclude: /node_modules\/(?!(expo-.*|@expo|@react-native|@uniswap\/.*)\/).*/, + use: { + loader: 'babel-loader', + options: { + presets: [ + '@babel/preset-env', + ['@babel/preset-react', { runtime: 'automatic' }], + '@babel/preset-typescript', + ], + cacheDirectory: true, + }, + }, + }) + + // Add babel-loader for React Native packages in node_modules + config?.module?.rules && + config.module.rules.push({ + test: /\.(tsx?|jsx?)$/, + // Exclude node_modules except for expo packages and related modules + exclude: /node_modules\/(?!(expo-.*|@expo|@react-native|@uniswap\/.*)\/).*/, + use: { + loader: 'babel-loader', + options: { + presets: [ + '@babel/preset-env', + ['@babel/preset-react', { runtime: 'automatic' }], + '@babel/preset-typescript', + ], + cacheDirectory: true, + }, + }, + }) + + // Add babel-loader for React Native packages in node_modules config?.module?.rules && config.module.rules.push({ - test: /\.tsx?$/, - use: 'ts-loader', - exclude: /node_modules/, + test: /\.(tsx?|jsx?)$/, + include: [ + /node_modules\/react-native-reanimated/, + /node_modules\/react-native-gesture-handler/, + /node_modules\/@react-native/, + /node_modules\/react-native\//, + ], + use: { + loader: 'babel-loader', + options: { + presets: ['@babel/preset-react', '@babel/preset-typescript'], + plugins: [], + cacheDirectory: true, + }, + }, }) config.resolve ??= {} - // Add fallback for Node.js 'os' module + // Configure resolve extensions to prefer .web files + config.resolve.extensions = ['.web.tsx', '.web.ts', '.web.jsx', '.web.js', '.tsx', '.ts', '.jsx', '.js'] + + // Add fallback for Node.js modules not available in browser config.resolve.fallback = { ...(config.resolve.fallback || {}), os: false, + tty: require.resolve('./__mocks__/tty.js'), + fs: false, + path: false, + util: false, } + // Configure webpack aliases for React Native and compatibility config.resolve = { ...config.resolve, alias: { ...config?.resolve?.alias, 'react-native$': 'react-native-web', 'expo-blur': require.resolve('./__mocks__/expo-blur.jsx'), + '~': resolve(__dirname, '../src'), }, } config.resolve.modules = [resolve(__dirname, '../src'), 'node_modules'] + // Configure optimization - disable minimization in dev to prevent Storybook errors + if (process.env.NODE_ENV === 'production') { + config.optimization = { + ...config.optimization, + minimize: true, + minimizer: [ + new TerserPlugin({ + parallel: 2, // Reduce from default (~8 CPU cores) to prevent memory exhaustion + terserOptions: { + compress: { + drop_console: true, + drop_debugger: true, + pure_funcs: ['console.log', 'console.info'], + }, + mangle: true, + output: { + comments: false, + }, + }, + extractComments: false, + }), + ], + splitChunks: { + chunks: 'all', + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', + priority: 10, + }, + tamagui: { + test: /[\\/]node_modules[\\/]tamagui[\\/]/, + name: 'tamagui', + priority: 20, + }, + reactNative: { + test: /[\\/]node_modules[\\/]react-native/, + name: 'react-native', + priority: 20, + }, + }, + maxSize: 500000, // 500KB chunks to reduce memory footprint + }, + } + } else { + // In development, explicitly disable minimization + config.optimization = { + ...config.optimization, + minimize: false, + minimizer: [], + splitChunks: { + chunks: 'all', + cacheGroups: { + vendor: { + test: /[\\/]node_modules[\\/]/, + name: 'vendors', + priority: 10, + }, + tamagui: { + test: /[\\/]node_modules[\\/]tamagui[\\/]/, + name: 'tamagui', + priority: 20, + }, + reactNative: { + test: /[\\/]node_modules[\\/]react-native/, + name: 'react-native', + priority: 20, + }, + }, + maxSize: 500000, + }, + } + } + + // Disable source maps for production Storybook builds to save memory + if (process.env.NODE_ENV === 'production') { + config.devtool = false + } + + // Remove ForkTsCheckerWebpackPlugin - it checks entire app, not just stories + const tsCheckerPlugin = + config.plugins && config.plugins.find((plugin) => plugin?.constructor.name === 'ForkTsCheckerWebpackPlugin') + + if (tsCheckerPlugin) { + config.plugins = config.plugins?.filter((p) => p !== tsCheckerPlugin) + } + + // Enable webpack persistent caching for faster rebuilds + config.cache = { + type: 'filesystem', + cacheDirectory: resolve(__dirname, '../node_modules/.cache/storybook'), + buildDependencies: { + config: [__filename], + }, + } + + // Configure performance hints + config.performance = { + maxAssetSize: 512000 * 2, + maxEntrypointSize: 512000 * 2, + hints: 'warning', + } + return config }, } diff --git a/apps/web/.storybook/preview.tsx b/apps/web/.storybook/preview.tsx index 2c8b0e68fae..1aa9219ecc9 100644 --- a/apps/web/.storybook/preview.tsx +++ b/apps/web/.storybook/preview.tsx @@ -1,13 +1,12 @@ +import '@reach/dialog/styles.css' +import '../src/global.css' +import '../src/polyfills' import type { Preview } from '@storybook/react' import { Provider } from 'react-redux' -import store from 'state' +import { MemoryRouter } from 'react-router' import { ReactRouterUrlProvider } from 'uniswap/src/contexts/UrlContext' import { TamaguiProvider } from '../src/theme/tamaguiProvider' - -import '@reach/dialog/styles.css' -import { MemoryRouter } from 'react-router' -import '../src/global.css' -import '../src/polyfills' +import store from '~/state' const preview: Preview = { decorators: [ diff --git a/apps/web/CLAUDE.md b/apps/web/CLAUDE.md deleted file mode 100644 index a5256033071..00000000000 --- a/apps/web/CLAUDE.md +++ /dev/null @@ -1,140 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -This is the Uniswap Web Interface - a React-based decentralized exchange application that's part of the Uniswap Universe monorepo. The web app enables users to swap tokens, provide liquidity, and interact with the Uniswap protocol across multiple blockchain networks. - -## Essential Development Commands - -### Daily Development - -```bash -# Start development server -bun dev - -# Run type checking -bun typecheck - -# Run linting -bun lint -bun lint:fix - -# Run tests -bun run test # Run all tests -bun run test:watch # Watch mode -bun run test:set1 # Components only -bun run test:set2 # Pages and state -bun run test:set3 # Hooks, NFT, utils -bun run test:set4 # Remaining tests - -# Run E2E tests -bun playwright:test - -# Build for production -bun build:production -``` - -### Monorepo Commands (from root) - -```bash -# Initial setup -bun lfg # Full setup with env vars - -# Global checks (all packages) -bun g:typecheck -bun g:lint -bun g:test -bun g:build - -# Quick checks (changed files only) -bun g:lint:changed -bun g:typecheck:changed -bun g:format:changed -``` - -## Architecture & Code Organization - -### Key Technologies - -- **Framework**: React with TypeScript -- **Build**: Vite (primary) with experimental Rolldown, Craco/webpack (legacy) -- **State**: Redux Toolkit, React Query, Jotai -- **Styling**: Styled Components, Tamagui UI framework -- **Web3**: Wagmi, Ethers.js, Web3-React -- **Testing**: Vitest (unit), Playwright (E2E), Storybook (visual) - -### Directory Structure - -```tree -apps/web/src/ -├── components/ # Reusable UI components -├── pages/ # Route-based page components -├── state/ # Redux slices and state logic -├── hooks/ # Custom React hooks -├── connection/ # Web3 wallet connection logic -├── lib/ # External library integrations -├── nft/ # NFT marketplace features -├── utils/ # Utility functions -└── constants/ # App-wide constants -``` - -### Key Architectural Patterns - -1. **Feature-based Organization**: Code is organized by feature/domain rather than by file type -2. **Shared UI Library**: Uses `@uniswap/ui` package from `packages/ui` for consistent components -3. **Strong TypeScript**: Strict typing with comprehensive type definitions -4. **GraphQL Code Generation**: Auto-generated types from GraphQL schemas -5. **Test Colocation**: Unit tests live alongside source files as `.test.ts(x)` - -### Testing Strategy - -- **Unit Tests**: Use Vitest, focus on logic and hooks -- **Component Tests**: Use React Testing Library with TestID enum -- **E2E Tests**: Playwright tests with `.e2e.test.ts` extension -- **Visual Tests**: Storybook for component documentation and testing - -### Important Development Notes - -1. **Environment Variables**: Managed via 1Password CLI - run `bun lfg` for setup -2. **Node Version**: Must use Node at the version specified in @.nvmrc -3. **Imports**: Use absolute imports within the app (enforced by ESLint) -4. **TestIDs**: Use the TestID enum instead of string literals for test selectors -5. **GitHub Actions**: External actions must be pinned to commit hashes with version comments - -### Common Workflows - -**Adding a new feature:** - -1. Create feature directory under appropriate section -2. Follow existing patterns for component structure -3. Add Redux slice if state management needed -4. Write tests alongside implementation -5. Update GraphQL queries if needed - -**Debugging blockchain interactions:** - -```bash -# Start local Ethereum fork -bun anvil:mainnet - -# Start local Base fork -bun anvil:base -``` - -**Working with translations:** - -```bash -bun i18n:extract # Extract new strings -bun i18n:upload # Upload to Crowdin -bun i18n:download # Download translations -``` - -### Code Style Guidelines - -- Follow existing patterns in neighboring files -- No unnecessary comments unless explicitly needed -- Use Tamagui components from `@uniswap/ui` when available -- Maintain consistency with Web3 naming conventions -- Always check existing imports before adding new dependencies diff --git a/apps/web/README.md b/apps/web/README.md index 27b01ea11b6..35ee3b99b03 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -6,45 +6,65 @@ To access the Uniswap Interface, use an IPFS gateway link from the [latest release](https://github.com/Uniswap/uniswap-interface/releases/latest), or visit [app.uniswap.org](https://app.uniswap.org). -## Running the interface locally +## Tech Stack + +- **Build**: Vite with experimental Rolldown support +- **Deployment**: Cloudflare Workers via `@cloudflare/vite-plugin` +- **Edge Functions**: Hono.js for SSR meta tags and OG image generation + +## Prerequisites + +- **Node.js version** - Use the version specified in `.nvmrc`. Run `nvm use` to switch. +- **Bun** - Package manager +- **1Password CLI** - Required for environment variables (run `bun lfg` from monorepo root for full setup) + +## Running Locally ```bash bun install -bun web start +bun web dev ``` +The dev server runs on port 3000 by default. + +Using a different port may cause CORS errors for certain Uniswap Backend services. + +## Development Commands + +| Command | Description | +|---------|-------------| +| `bun web dev` | Start development server | +| `bun web build:production` | Production build | +| `bun web preview` | Preview production build locally | +| `bun web typecheck` | Run type checking | +| `bun web test` | Run unit tests | +| `bun web e2e` | Run E2E Playwright tests with prod build | +| `bun web e2e:dev` | Run E2E Playwright tests with dev build | + ## Translations To get translations to work you'll need to set up 1Password, and then: -``` +```bash eval $(op signin) ``` Sign into 1Password, then: -``` +```bash bun mobile env:local:download ``` -Which downs a `.env.defaults.local` file at the root. Finally: +Which downloads a `.env.defaults.local` file at the root. Finally: -``` +```bash bun web i18n:download ``` Which will download the translations to `./apps/web/src/i18n/locales/translations`. -## Accessing Uniswap V2 - -The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2. - -- Swap on Uniswap V2: -- View V2 liquidity: -- Add V2 liquidity: -- Migrate V2 liquidity to V3: +## Further Documentation -## Accessing Uniswap V1 +See [CLAUDE.md](./CLAUDE.md) for detailed development guidance, architecture patterns, and workflows. -The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways -linked from the [v1.0.0 release](https://github.com/Uniswap/uniswap-interface/releases/tag/v1.0.0). +See [the e2e skill](../../.claude/skills/web-e2e/SKILL.md) for information about creating and running e2e tests. diff --git a/apps/web/functions/api/image/pools.tsx b/apps/web/functions/api/image/pools.tsx index 79b1f698a04..d002d7e3162 100644 --- a/apps/web/functions/api/image/pools.tsx +++ b/apps/web/functions/api/image/pools.tsx @@ -1,13 +1,14 @@ -// biome-ignore-all lint/correctness/noRestrictedElements: ignoring for the whole file +/* oxlint-disable react/forbid-elements -- ignoring for the whole file */ -import { GraphQLApi } from '@universe/api' +import { ProtocolVersion } from '@universe/api/src/clients/graphql/__generated__/schema-types' import { ImageResponse } from '@vercel/og' import { WATERMARK_URL } from 'functions/constants' +import { Data, PositionStatus } from 'functions/utils/cache' import getFont from 'functions/utils/getFont' import getNetworkLogoUrl from 'functions/utils/getNetworkLogoURL' import getPool from 'functions/utils/getPool' import { getRequest } from 'functions/utils/getRequest' -import { Context } from 'hono' +import { type Context } from 'hono' function UnknownTokenImage({ symbol }: { symbol?: string }) { const ticker = symbol?.slice(0, 3) @@ -32,7 +33,7 @@ function UnknownTokenImage({ symbol }: { symbol?: string }) { ) } -function PoolImage({ +export function PoolImage({ token0ImageUrl, token1ImageUrl, tokenSymbol0, @@ -95,111 +96,114 @@ function PoolImage({ ) } -export async function poolImageHandler(c: Context) { - try { - const { networkName, poolAddress } = c.req.param() - const origin = new URL(c.req.url).origin - - const cacheUrl = origin + '/pools/' + networkName + '/' + poolAddress - const data = await getRequest({ - url: cacheUrl, - getData: () => getPool({ networkName, poolAddress, url: cacheUrl }), - validateData: (data): data is NonNullable>> => Boolean(data.title), - }) - - if (!data) { - return new Response('Pool not found.', { status: 404 }) - } +const positionStatusConfig: Record = { + in_range: { color: '#40B66B', label: 'In range' }, + out_of_range: { color: '#FF5F52', label: 'Out of range' }, + closed: { color: '#9B9B9B', label: 'Closed' }, +} - const [fontData] = await Promise.all([getFont(origin, c.env)]) - const networkLogo = getNetworkLogoUrl(networkName.toUpperCase(), origin) +export async function renderPoolOgImage({ + data, + networkName, + c, + versionBadge, +}: { + data: Data + networkName: string + c: Context + versionBadge?: string +}): Promise { + const origin = new URL(c.req.url).origin + const [fontData] = await Promise.all([getFont(origin, c.env)]) + const networkLogo = getNetworkLogoUrl(networkName.toUpperCase(), origin) - return new ImageResponse( + return new ImageResponse( +
-
- - {networkLogo !== '' && ( - - )} - -
-
+ {networkLogo !== '' && ( + + )} + +
+
+
+ {data.name} +
+ {versionBadge && (
- {data.name} + {versionBadge}
- {data.poolData?.protocolVersion === GraphQLApi.ProtocolVersion.V2 && ( -
- {data.poolData?.protocolVersion} -
- )} -
-
+ )} +
+
+
{data.poolData?.feeTier}
- Uniswap + {data.positionStatus && ( +
+
+
+ {positionStatusConfig[data.positionStatus].label} +
+
+ )}
+ Uniswap
-
, - { - width: 1200, - height: 630, - fonts: [ - { - name: 'Inter', - data: fontData, - style: 'normal', - }, - ], - }, - ) as Response +
+
, + { + width: 1200, + height: 630, + fonts: [ + { + name: 'Inter', + data: fontData, + style: 'normal', + }, + ], + }, + ) as Response +} + +export async function poolImageHandler(c: Context) { + try { + const { networkName, poolAddress } = c.req.param() + const origin = new URL(c.req.url).origin + + const cacheUrl = origin + '/pools/' + networkName + '/' + poolAddress + const data = await getRequest({ + url: cacheUrl, + getData: () => getPool({ networkName, poolAddress, url: cacheUrl }), + validateData: (data): data is NonNullable>> => Boolean(data.title), + }) + + if (!data) { + return new Response('Pool not found.', { status: 404 }) + } + + return renderPoolOgImage({ + data, + networkName, + c, + versionBadge: data.poolData?.protocolVersion === ProtocolVersion.V2 ? data.poolData.protocolVersion : undefined, + }) } catch (error: any) { return new Response(error.message || error.toString(), { status: 500 }) } diff --git a/apps/web/functions/api/image/positions.tsx b/apps/web/functions/api/image/positions.tsx new file mode 100644 index 00000000000..2a806e59710 --- /dev/null +++ b/apps/web/functions/api/image/positions.tsx @@ -0,0 +1,38 @@ +import { renderPoolOgImage } from 'functions/api/image/pools' +import getPosition from 'functions/utils/getPosition' +import { getRequest } from 'functions/utils/getRequest' +import { type Context } from 'hono' + +export async function positionImageHandler(c: Context) { + try { + const { version, chainName, identifier } = c.req.param() + const origin = new URL(c.req.url).origin + + const cacheUrl = `${origin}/positions/${version}/${chainName}/${identifier}` + const data = await getRequest({ + url: cacheUrl, + getData: () => + getPosition({ + version: version as 'v2' | 'v3' | 'v4', + chainName, + identifier, + url: cacheUrl, + }), + validateData: (data): data is NonNullable>> => Boolean(data.title), + }) + + if (!data) { + return new Response('Position not found.', { status: 404 }) + } + + return renderPoolOgImage({ + data, + networkName: chainName, + c, + versionBadge: data.poolData?.protocolVersion, + }) + } catch (error: unknown) { + const message = error instanceof Error ? error.message : String(error) + return new Response(message, { status: 500 }) + } +} diff --git a/apps/web/functions/api/image/tokens.tsx b/apps/web/functions/api/image/tokens.tsx index fad14a0d3b2..8b94fa2ad73 100644 --- a/apps/web/functions/api/image/tokens.tsx +++ b/apps/web/functions/api/image/tokens.tsx @@ -1,4 +1,4 @@ -// biome-ignore-all lint/correctness/noRestrictedElements: ignoring for the whole file +/* oxlint-disable react/forbid-elements -- ignoring for the whole file */ import { ImageResponse } from '@vercel/og' import { WATERMARK_URL } from 'functions/constants' diff --git a/apps/web/functions/app.test.ts b/apps/web/functions/app.test.ts new file mode 100644 index 00000000000..613bb1cb913 --- /dev/null +++ b/apps/web/functions/app.test.ts @@ -0,0 +1,45 @@ +import { createApp } from 'functions/app' + +const mockHtml = `Uniswap` + +function buildApp() { + return createApp({ + fetchSpaHtml: async () => new Response(mockHtml, { headers: { 'content-type': 'text/html' } }), + getEntryGatewayUrl: () => 'https://entry-gateway.backend-prod.api.uniswap.org', + getWebSocketUrl: () => 'https://websockets.backend-prod.api.uniswap.org', + getTrustedClientIp: () => undefined, + }) +} + +describe('frame protection headers', () => { + it('sets frame-ancestors CSP header on SPA routes', async () => { + const app = buildApp() + const res = await app.request('/') + + expect(res.headers.get('Content-Security-Policy')).toBe("frame-ancestors 'self' https://app.safe.global") + }) + + it('sets X-Frame-Options header on SPA routes', async () => { + const app = buildApp() + const res = await app.request('/') + + expect(res.headers.get('X-Frame-Options')).toBe('SAMEORIGIN') + }) + + it('sets frame headers on /swap route', async () => { + const app = buildApp() + const res = await app.request('/swap') + + expect(res.headers.get('Content-Security-Policy')).toBe("frame-ancestors 'self' https://app.safe.global") + expect(res.headers.get('X-Frame-Options')).toBe('SAMEORIGIN') + }) + + it('does not include other CSP directives in the frame-ancestors header', async () => { + const app = buildApp() + const res = await app.request('/') + + const csp = res.headers.get('Content-Security-Policy') + expect(csp).not.toContain('default-src') + expect(csp).not.toContain('script-src') + }) +}) diff --git a/apps/web/functions/app.ts b/apps/web/functions/app.ts new file mode 100644 index 00000000000..3aeee72c431 --- /dev/null +++ b/apps/web/functions/app.ts @@ -0,0 +1,156 @@ +import { poolImageHandler } from 'functions/api/image/pools' +import { positionImageHandler } from 'functions/api/image/positions' +import { tokenImageHandler } from 'functions/api/image/tokens' +import { metaTagInjectionMiddleware } from 'functions/components/metaTagInjector' +import { rewriteProxiedCookies } from 'functions/cookie-utils' +import { Context, Hono } from 'hono' +import { proxy } from 'hono/proxy' + +type Bindings = { + ASSETS?: { fetch: typeof fetch } // Only present on Cloudflare Workers +} + +/** Platform-specific dependencies injected by each entry point. */ +interface AppConfig { + fetchSpaHtml: (c: Context) => Promise + getEntryGatewayUrl: (c: Context) => string + getWebSocketUrl: (c: Context) => string + getTrustedClientIp: (c: Context) => string | undefined +} + +// ── Frame protection ───────────────────────────────────────────────── +// frame-ancestors cannot be enforced via CSP tags (W3C spec) — it +// must be an HTTP response header. Cloudflare Workers returns responses +// with immutable headers, so we clone into a mutable Response. +function withFrameProtection(res: Response): Response { + const headers = new Headers(res.headers) + headers.set('Content-Security-Policy', "frame-ancestors 'self' https://app.safe.global") + headers.set('X-Frame-Options', 'SAMEORIGIN') + return new Response(res.body, { status: res.status, statusText: res.statusText, headers }) +} + +// ── Shared constants ───────────────────────────────────────────────── +export const ENTRY_GATEWAY_URLS = { + development: 'https://entry-gateway.backend-staging.api.uniswap.org', + staging: 'https://entry-gateway.backend-staging.api.uniswap.org', + production: 'https://entry-gateway.backend-prod.api.uniswap.org', +} as const + +// Statsig proxy via Cloudflare gateway — the URL is constant for the web app +// (platform prefix "interface", service prefix "gating") +const STATSIG_PROXY_TARGET = 'https://gating.interface.gateway.uniswap.org' + +export const WEBSOCKET_URLS = { + development: 'https://websockets.backend-staging.api.uniswap.org', + staging: 'https://websockets.backend-staging.api.uniswap.org', + production: 'https://websockets.backend-prod.api.uniswap.org', +} as const + +// ── Cache-Control middleware for image routes ─────────────────────────── +function cacheControl(maxAge: number) { + return async (c: Context, next: () => Promise) => { + await next() + if (c.res.ok) { + c.res.headers.set('Cache-Control', `public, max-age=${maxAge}`) + } + } +} + +export function createApp({ fetchSpaHtml, getEntryGatewayUrl, getWebSocketUrl, getTrustedClientIp }: AppConfig) { + const app = new Hono<{ Bindings: Bindings }>() + + // ── OG image routes ──────────────────────────────────────────────────── + app.get('/api/image/tokens/:networkName/:tokenAddress', cacheControl(604800), tokenImageHandler) + + app.get('/api/image/pools/:networkName/:poolAddress', cacheControl(604800), poolImageHandler) + + app.get('/api/image/positions/:version/:chainName/:identifier', cacheControl(604800), positionImageHandler) + + // ── BFF proxy: entry-gateway ───────────────────────────────────────── + app.all('/entry-gateway/*', async (c) => { + const backendUrl = getEntryGatewayUrl(c) + const path = c.req.path.slice('/entry-gateway'.length) || '/' + const query = new URL(c.req.url).search + + // Forward the real client IP so the EGW authorizer (and downstream + // providers like Coinbase) see the user's IP, not the proxy's IP. + // Each platform provides a trusted IP source — Cloudflare sets + // cf-connecting-ip automatically; Vercel sets x-real-ip at the TCP + // level. We always overwrite cf-connecting-ip from the trusted source + // to prevent clients from spoofing it (especially on Vercel where + // there's no Cloudflare to sanitize headers). + const clientIp = getTrustedClientIp(c) + + const targetUrl = `${backendUrl}${path}${query}` + // redirect:'manual' prevents SSRF via 3xx redirects to internal services + const response = await proxy(targetUrl, { + ...c.req, + headers: { + ...c.req.header(), + host: undefined, + ...(clientIp ? { 'cf-connecting-ip': clientIp } : {}), + }, + redirect: 'manual', + }) + + // Rewrite Set-Cookie headers so cookies work on non-.uniswap.org domains + // (Vercel previews, staging, etc.) + return rewriteProxiedCookies(response) + }) + + // ── BFF proxy: config ────────────────────────────────────────────── + app.all('/config/*', async (c) => { + const path = c.req.path.replace(/^\/config/, '/v1/statsig-proxy') + const query = new URL(c.req.url).search + + return proxy(`${STATSIG_PROXY_TARGET}${path}${query}`, { + ...c.req, + headers: { + ...c.req.header(), + host: undefined, + }, + redirect: 'manual', + }) + }) + + // ── BFF proxy: WebSocket ──────────────────────────────────────────── + // In production, clients connect directly to the backend WebSocket + // service — see getWebSocketUrl() in packages/api/src/getWebSocketUrl.ts. + // This proxy is used in local dev (Vite + @cloudflare/vite-plugin) and + // on Cloudflare Workers staging, where the CF Workers runtime handles + // the WebSocket upgrade natively via fetch(). + // Headers are stripped to avoid forwarding the local dev origin/host + // to the backend, which would reject them. + app.get('/ws', async (c) => { + const wsUrl = getWebSocketUrl(c) + const headers = new Headers(c.req.raw.headers) + headers.delete('host') + headers.delete('origin') + try { + return await fetch(wsUrl, { headers }) + } catch (err) { + return c.text(`WebSocket proxy error: ${err}`, 502) + } + }) + + // ── Catch-all: SPA serving + meta tag injection ──────────────────────── + app.all('*', async (c: Context) => { + const url = new URL(c.req.url) + + const next = async () => { + const response = await fetchSpaHtml(c) + c.res = response + } + + // API routes should not be processed by meta tag injection + if (url.pathname.startsWith('/api/')) { + await next() + return withFrameProtection(c.res) + } + + // For non-API routes, use meta tag injection middleware + return withFrameProtection(await metaTagInjectionMiddleware(c, next)) + }) + + return app +} diff --git a/apps/web/functions/components/metaTagInjector.ts b/apps/web/functions/components/metaTagInjector.ts index 759528546ae..cd936f5722b 100644 --- a/apps/web/functions/components/metaTagInjector.ts +++ b/apps/web/functions/components/metaTagInjector.ts @@ -1,16 +1,17 @@ import { Data } from 'functions/utils/cache' import getPool from 'functions/utils/getPool' +import getPosition from 'functions/utils/getPosition' import { getRequest } from 'functions/utils/getRequest' import getToken from 'functions/utils/getToken' import { Context, Next } from 'hono' import { encode } from 'html-entities' -import { MetaTagInjectorInput } from 'shared-cloud/metatags' -import { paths } from 'src/pages/paths' +import { paths } from '~/pages/paths' +import { MetaTagInjectorInput } from '~/shared-cloud/metatags' function doesMatchPath(path: string): boolean { const regexPaths = paths.map((p) => '^' + p.replace(/:[^/]+/g, '[^/]+').replace(/\*/g, '.*') + '$') // These come from a constant we define (paths.ts), so we don't need to worry about them being malicious. - // eslint-disable-next-line security/detect-non-literal-regexp + // oxlint-disable-next-line security/detect-non-literal-regexp return regexPaths.some((regex) => new RegExp(regex).test(path)) } @@ -34,7 +35,21 @@ function parseExplorePath(pathname: string): { type: 'token' | 'pool'; networkNa return null } -// eslint-disable-next-line max-params +function parsePositionPath( + pathname: string, +): { version: 'v2' | 'v3' | 'v4'; chainName: string; identifier: string } | null { + const match = pathname.match(/^\/positions\/(v2|v3|v4)\/([^/]+)\/([^/]+)$/) + if (match) { + return { + version: match[1] as 'v2' | 'v3' | 'v4', + chainName: match[2], + identifier: match[3], + } + } + return null +} + +// oxlint-disable-next-line max-params function append(tags: string, attribute: string, content: string): string { return tags + `\n` } @@ -100,6 +115,30 @@ async function fetchExploreData({ return data ? { title: data.title, image: data.image, url: requestUrl } : null } +async function fetchPositionData({ + version, + chainName, + identifier, + origin, + requestUrl, +}: { + version: 'v2' | 'v3' | 'v4' + chainName: string + identifier: string + origin: string + requestUrl: string +}): Promise { + const cacheUrl = `${origin}/positions/${version}/${chainName}/${identifier}` + + const data = await getRequest({ + url: cacheUrl, + getData: () => getPosition({ version, chainName, identifier, url: cacheUrl }), + validateData: (data): data is NonNullable>> => Boolean(data.title), + }) + + return data ? { title: data.title, image: data.image, url: requestUrl } : null +} + export async function metaTagInjectionMiddleware(c: Context, next: Next): Promise { const requestURL = new URL(c.req.url) @@ -122,6 +161,7 @@ export async function metaTagInjectionMiddleware(c: Context, next: Next): Promis const html = await clonedResponse.text() const exploreData = parseExplorePath(requestURL.pathname) + const positionData = parsePositionPath(requestURL.pathname) let data: MetaTagInjectorInput if (exploreData) { @@ -139,6 +179,21 @@ export async function metaTagInjectionMiddleware(c: Context, next: Next): Promis } data = exploreMeta + } else if (positionData) { + const origin = requestURL.origin + const positionMeta = await fetchPositionData({ + version: positionData.version, + chainName: positionData.chainName, + identifier: positionData.identifier, + origin, + requestUrl: c.req.url, + }) + + if (!positionMeta) { + return originalResponse + } + + data = positionMeta } else { const imageUri = requestURL.origin + '/images/1200x630_Rich_Link_Preview_Image.png' data = { diff --git a/apps/web/functions/cookie-utils.ts b/apps/web/functions/cookie-utils.ts new file mode 100644 index 00000000000..8e0e0802f08 --- /dev/null +++ b/apps/web/functions/cookie-utils.ts @@ -0,0 +1,78 @@ +/** + * Cookie utilities for rewriting Set-Cookie headers from the Entry Gateway proxy. + * + * The backend sets cookies with Domain=.uniswap.org and __Host-/__Secure- prefixes. + * On Vercel previews (*.vercel.app) or staging domains, the browser silently drops + * these cookies because the domain doesn't match. This causes session-based flows + * (InitSession -> RequestChallenge) to fail with "no session id provided". + * + * Based on apps/dev-portal/app/lib/entry-gateway/cookie-utils.ts + */ + +/** + * Rewrites a single Set-Cookie header value for the proxy. + * + * Transformations: + * 1. Strips __Host- and __Secure- prefixes from cookie names + * 2. Removes Domain attribute (lets browser default to request origin) + * 3. Ensures SameSite=Lax + * 4. Keeps Secure flag (both Vercel and Cloudflare serve over HTTPS) + */ +export function rewriteProxiedCookie(cookie: string): string { + let rewritten = cookie + + // Strip __Host- and __Secure- prefixes from cookie name only. + // Only touch the name portion (before first '=') to avoid corrupting values. + const nameEndIndex = rewritten.indexOf('=') + if (nameEndIndex > 0) { + const cookieName = rewritten.substring(0, nameEndIndex) + const strippedName = cookieName.replace(/^(__Host-|__Secure-)/, '') + if (strippedName !== cookieName) { + rewritten = strippedName + rewritten.substring(nameEndIndex) + } + } + + // Remove Domain attribute (e.g., Domain=.uniswap.org) + rewritten = rewritten.replace(/Domain=[^;]+;?\s?/gi, '') + + // Handle SameSite attribute — ensure Lax + if (rewritten.includes('SameSite=')) { + rewritten = rewritten.replace(/SameSite=\w+/gi, 'SameSite=Lax') + } else if (rewritten.includes('Path=')) { + rewritten = rewritten.replace(/(Path=[^;]+)/, 'SameSite=Lax; $1') + } else { + rewritten = `${rewritten}; SameSite=Lax` + } + + return rewritten +} + +/** + * Rewrites Set-Cookie headers on a proxied Response. + * + * If the response has no Set-Cookie headers, returns it unchanged. + * Otherwise, creates a new Response with rewritten cookies. + */ +export function rewriteProxiedCookies(response: Response): Response { + const setCookies = response.headers.getSetCookie() + + if (!setCookies.length) { + return response + } + + const rewritten = setCookies.map(rewriteProxiedCookie) + + // Clone the response with new headers — we need to rebuild Set-Cookie + // since Headers.set() would collapse multiple values into one. + const newHeaders = new Headers(response.headers) + newHeaders.delete('Set-Cookie') + for (const cookie of rewritten) { + newHeaders.append('Set-Cookie', cookie) + } + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: newHeaders, + }) +} diff --git a/apps/web/functions/explore/pools/__snapshots__/pool.test.ts.snap b/apps/web/functions/explore/pools/__snapshots__/pool.test.ts.snap index f8c131ca4a2..4f56733432b 100644 --- a/apps/web/functions/explore/pools/__snapshots__/pool.test.ts.snap +++ b/apps/web/functions/explore/pools/__snapshots__/pool.test.ts.snap @@ -31,6 +31,10 @@ window.$RefreshSig$ = () => (type) => type; + + + + - + - + - + - + diff --git a/apps/web/functions/explore/pools/pool.test.ts b/apps/web/functions/explore/pools/pool.test.ts index 803b6369f2f..7511682a029 100644 --- a/apps/web/functions/explore/pools/pool.test.ts +++ b/apps/web/functions/explore/pools/pool.test.ts @@ -14,7 +14,7 @@ const pools = [ { address: '0xD1F1baD4c9E6c44DeC1e9bF3B94902205c5Cd6C3', network: 'optimism', - name: 'USDC/WLD', + name: 'USDC.e/WLD', image: 'http://localhost:3000/api/image/pools/optimism/0xD1F1baD4c9E6c44DeC1e9bF3B94902205c5Cd6C3', }, ] diff --git a/apps/web/functions/explore/tokens/__snapshots__/token.test.ts.snap b/apps/web/functions/explore/tokens/__snapshots__/token.test.ts.snap index d7b5907a687..eff13f3ab5d 100644 --- a/apps/web/functions/explore/tokens/__snapshots__/token.test.ts.snap +++ b/apps/web/functions/explore/tokens/__snapshots__/token.test.ts.snap @@ -31,6 +31,10 @@ window.$RefreshSig$ = () => (type) => type; + + + + -