feat(mobile): upgrade Expo SDK to v55 (#7433)

* feat(mobile): upgrade Expo SDK to v55

Upgrade Expo SDK from v54 to v55, along with React Native 0.83.4,
React 19.2, and all related dependencies.

Key changes:
- Refactored @safe-global/theme to use sub-path exports to fix RN
  import resolution issues
- Added react-native patch for Appearance.setColorScheme bug
  (facebook/react-native#08d176453) not yet in 0.83.4 release
- Replaced Tamagui Skeleton with custom SafeSkeleton component
- Updated expo config plugins for SDK 55 compatibility

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* fix: downgrade eslint-config-next to v15 to match next.js version

eslint-config-next v16 requires next v16 but the project uses next
v15.5.8. The v16 package tries to import next/dist/compiled/babel/
eslint-parser which doesn't exist in next v15.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* fix: migrate packages away from eslint-config-next

eslint-config-next requires next at runtime for its babel parser,
but next is only installed in apps/web. After the Expo SDK v55
upgrade changed dependency hoisting, the parser became unreachable.

- Create shared config/eslint/base.mjs using direct plugin imports
- Migrate store, theme, utils, and tx-builder to the shared config
- Add packageExtensions in .yarnrc.yml so eslint-config-next in
  apps/web can still resolve next as a peer dependency

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* chore: align react 19.2 across workspaces and fix MUI peer dep

- Bump react/react-dom to 19.2.0 in web and tx-builder to match
  mobile
- Add @mui/material as optional peer dependency in theme package to
  avoid duplicate installations causing type conflicts

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* chore(mobile): update storybook on mobile to v10

* fix(web): resolve @emotion/react hoisting issue after Expo SDK 55 upgrade

Yarn 4's node-modules hoisting shifted after dependency graph changes,
leaving @emotion/react only in apps/web/node_modules while @mui/styled-engine
remained at root. Add webpack resolve aliases to ensure Next.js finds emotion
packages from the web workspace.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* fix(web): resolve @emotion hoisting issue in Jest config

Same hoisting fix as next.config.mjs but for Jest's module resolution.
Add moduleNameMapper entries for @emotion/react and @emotion/styled.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* fix(tx-builder): prettier

* fix: resolve type-check errors after Expo SDK 55 upgrade

- Fix BlurView tint prop: map ColorSchemeName to valid BlurTint values
- Replace @storybook/test imports with storybook/test (merged in v10)
- Align @types/react (~19.2.10) and @types/react-dom (~19.2.0) across
  all workspaces to prevent duplicate @mui/material type mismatches
- Spread visuallyHidden into sx prop for @types/react 19.2.x compat
- Add @types/react, @types/react-dom, storybook to yarn version checker

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

* chore: align more dependencies

* chore(web): update storybook snapshot

* fix(mobile): normalize color scheme and deduplicate MUI/emotion packages

- Normalize useTheme colorScheme to 'light' | 'dark', never 'unspecified'
  so TamaguiProvider always receives a valid theme name
- Update ColorScheme type to exclude 'unspecified'
- Remove redundant ?? 'light' fallbacks in SafeThemeProvider
- Align @emotion/* and @mui/icons-material versions across workspaces
  to prevent Yarn from creating duplicate lockfile resolutions
- Remove emotion webpack/Jest aliases (no longer needed after dedup)
- Add @emotion/react, @emotion/styled, @mui/material, @mui/icons-material
  to yarn version consistency checker

Co-authored-by: Hanzo Dev <dev@hanzo.ai>

---------

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
This commit is contained in:
Daniel Dimitrov
2026-03-18 09:29:16 +01:00
committed by GitHub
co-authored by Hanzo Dev
parent d167bad14f
commit defd17df3b
81 changed files with 2237 additions and 2999 deletions
@@ -0,0 +1,18 @@
diff --git a/Libraries/Utilities/Appearance.js b/Libraries/Utilities/Appearance.js
index 9b7758c3afa3c9874c911045e258abf5219e8af5..56e54167670befc79ee371a871138e4af2528313 100644
--- a/Libraries/Utilities/Appearance.js
+++ b/Libraries/Utilities/Appearance.js
@@ -99,7 +99,12 @@ export function setColorScheme(colorScheme: ColorSchemeName): void {
if (NativeAppearance != null) {
NativeAppearance.setColorScheme(colorScheme);
state.appearance = {
- colorScheme,
+ // When setting to 'unspecified', get the actual system color scheme.
+ // Fall back to the passed value if getColorScheme() returns null.
+ colorScheme:
+ colorScheme === 'unspecified'
+ ? (NativeAppearance.getColorScheme() ?? colorScheme)
+ : colorScheme,
};
}
}
+8
View File
@@ -8,6 +8,14 @@ enableScripts: false
nodeLinker: node-modules
# eslint-config-next requires next at runtime but doesn't declare it as
# a peer dependency. Without this, Yarn hoists eslint-config-next to root
# where it can't resolve next (which lives in apps/web/node_modules/).
packageExtensions:
eslint-config-next@*:
peerDependencies:
next: '*'
# Set cool down period for npm packages to 24 hours
# This prevents the installation of npm packages if
# they are updated within the last 24 hours
+4 -3
View File
@@ -1,9 +1,13 @@
import type { StorybookConfig as WebStorybookConfig } from '@storybook/react-webpack5'
import type { StorybookConfig as RNStorybookConfig } from '@storybook/react-native'
import path from 'path'
import { fileURLToPath } from 'url'
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
import { globSync } from 'glob'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
let config: WebStorybookConfig | RNStorybookConfig
const isWeb = process.env.STORYBOOK_WEB
@@ -29,8 +33,6 @@ if (isWeb) {
config = {
stories: [...getStories()],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-interactions',
{
name: '@storybook/addon-react-native-web',
options: {
@@ -38,7 +40,6 @@ if (isWeb) {
modulesToTranspile: [],
},
},
'@storybook/addon-webpack5-compiler-babel',
],
/**
* Use standard framework configuration instead of path resolution.
+2 -1
View File
@@ -1,7 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "commonjs"
"module": "nodenext",
"moduleResolution": "nodenext"
},
"include": ["../src/**/*", "./**/*"]
}
+3 -9
View File
@@ -32,7 +32,6 @@ const config: ExpoConfig = {
icon: './assets/images/icon.png',
scheme: 'myapp',
userInterfaceStyle: 'automatic',
newArchEnabled: true,
ios: {
config: {
usesNonExemptEncryption: false,
@@ -74,7 +73,6 @@ const config: ExpoConfig = {
'android.permission.FOREGROUND_SERVICE',
'android.permission.WAKE_LOCK',
],
edgeToEdgeEnabled: true,
allowBackup: false,
},
web: {
@@ -159,6 +157,8 @@ const config: ExpoConfig = {
enableBase64ShareAndroid: true,
},
],
'@react-native-community/datetimepicker',
'expo-image',
'expo-task-manager',
'expo-web-browser',
[
@@ -185,13 +185,7 @@ const config: ExpoConfig = {
],
experiments: {
typedRoutes: true,
},
notification: {
icon: './assets/images/icon.png',
color: '#FFFFFF',
androidMode: 'default',
androidCollapsedTitle: 'Updates from Safe Wallet',
iosDisplayInForeground: true,
reactCompiler: true,
},
}
@@ -1,5 +1,5 @@
/* eslint-disable no-undef */
const { withPodfile, withAppDelegate, withMainApplication, withDangerousMod } = require('@expo/config-plugins')
const { withPodfile, withAppDelegate, withMainApplication, withDangerousMod } = require('expo/config-plugins')
const fs = require('fs')
const path = require('path')
// SSL Pinning Configuration will be passed from app.config.js
@@ -1,6 +1,6 @@
const fs = require('fs')
const path = require('path')
const { withDangerousMod } = require('@expo/config-plugins')
const { withDangerousMod } = require('expo/config-plugins')
const androidFolderPath = ['app', 'src', 'main', 'res', 'drawable']
@@ -1,4 +1,4 @@
const { withAndroidManifest } = require('@expo/config-plugins')
const { withAndroidManifest } = require('expo/config-plugins')
function addNotificationIconMetadata(androidManifest) {
const application = androidManifest.manifest.application[0]
+3 -13
View File
@@ -2,26 +2,18 @@ const path = require('path')
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDatadogExpoConfig } = require('@datadog/mobile-react-native/metro')
const withStorybook = require('@storybook/react-native/metro/withStorybook')
const { withStorybook } = require('@storybook/react-native/metro/withStorybook')
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDatadogExpoConfig(__dirname)
config.resolver.resolveRequest = (context, moduleName, platform) => {
const defaultResolveResult = context.resolveRequest(context, moduleName, platform)
if (process.env.STORYBOOK_ENABLED !== 'true' && defaultResolveResult?.filePath?.includes?.('.storybook/')) {
return {
type: 'empty',
}
}
if (moduleName === 'crypto') {
// when importing crypto, resolve to react-native-quick-crypto
return context.resolveRequest(context, 'react-native-quick-crypto', platform)
}
return defaultResolveResult
return context.resolveRequest(context, moduleName, platform)
}
if (process.env.RN_SRC_EXT) {
@@ -29,8 +21,6 @@ if (process.env.RN_SRC_EXT) {
}
module.exports = withStorybook(config, {
// Set to false to remove storybook specific options
enabled: true,
// Path to your storybook config
enabled: process.env.STORYBOOK_ENABLED === 'true',
configPath: path.resolve(__dirname, './.storybook'),
})
+47 -52
View File
@@ -25,7 +25,7 @@
"type-check": "tsc --noEmit",
"prettier": "prettier --check . --config ../../.prettierrc --ignore-path ../../.prettierignore",
"prettier:fix": "prettier --write . --config ../../.prettierrc --ignore-path ../../.prettierignore",
"storybook-generate": "sb-rn-get-stories",
"storybook-generate": "sb-rn-get-stories --config-path ./.storybook",
"generate:icons": "node ./scripts/generateIconTypes.js",
"prepare": "husky",
"build-storybook": "STORYBOOK_WEB='true' storybook build",
@@ -41,14 +41,13 @@
"@cowprotocol/app-data": "^3.1.0",
"@datadog/mobile-react-native": "^3.1.2",
"@ethersproject/shims": "^5.7.0",
"@expo/config-plugins": "~54.0.1",
"@expo/vector-icons": "^15.1.1",
"@formatjs/intl-displaynames": "^6.8.11",
"@formatjs/intl-getcanonicallocales": "^2.5.4",
"@formatjs/intl-locale": "^4.2.10",
"@formatjs/intl-numberformat": "^8.15.3",
"@formatjs/intl-pluralrules": "^5.4.3",
"@gorhom/bottom-sheet": "^5.2.6",
"@gorhom/bottom-sheet": "^5.2.8",
"@hookform/resolvers": "^4.1.3",
"@ledgerhq/context-module": "^1.7.0",
"@ledgerhq/device-management-kit": "^0.9.0",
@@ -57,8 +56,8 @@
"@notifee/react-native": "^9.1.8",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-clipboard/clipboard": "^1.16.3",
"@react-native-community/datetimepicker": "8.4.4",
"@react-native-community/slider": "5.0.1",
"@react-native-community/datetimepicker": "8.6.0",
"@react-native-community/slider": "5.1.2",
"@react-native-firebase/analytics": "23.3.1",
"@react-native-firebase/app": "23.3.1",
"@react-native-firebase/crashlytics": "23.3.1",
@@ -66,15 +65,16 @@
"@react-native-firebase/remote-config": "23.3.1",
"@react-native-masked-view/masked-view": "0.3.2",
"@react-native-menu/menu": "^2.0.0",
"@react-navigation/native": "^7.1.17",
"@react-navigation/elements": "^2.9.10",
"@react-navigation/native": "^7.1.28",
"@react-navigation/native-stack": "^7.14.4",
"@reduxjs/toolkit": "^2.11.0",
"@safe-global/protocol-kit": "^5.2.25",
"@safe-global/store": "workspace:^",
"@safe-global/theme": "workspace:^",
"@safe-global/utils": "workspace:^",
"@shopify/flash-list": "^2.2.0",
"@storybook/addon-react-native-web": "^0.0.26",
"@storybook/addon-webpack5-compiler-babel": "^3.0.3",
"@shopify/flash-list": "^2.3.0",
"@storybook/addon-react-native-web": "^0.0.29",
"@tamagui/animations-moti": "^1.126.3",
"@tamagui/babel-plugin": "^1.126.3",
"@tamagui/config": "^1.126.3",
@@ -86,56 +86,56 @@
"date-fns": "^4.1.0",
"deepmerge": "^4.3.1",
"ethers": "6.14.3",
"expo": "^54.0.0",
"expo-application": "~7.0.7",
"expo-blur": "~15.0.7",
"expo-build-properties": "~1.0.9",
"expo-constants": "~18.0.9",
"expo-datadog": "^54.0.0",
"expo-dev-client": "~6.0.12",
"expo-device": "~8.0.8",
"expo-document-picker": "~14.0.7",
"expo-file-system": "~19.0.15",
"expo-font": "~14.0.8",
"expo-image": "~3.0.8",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.8",
"expo-router": "~6.0.8",
"expo-splash-screen": "~31.0.10",
"expo-status-bar": "~3.0.8",
"expo-system-ui": "~6.0.7",
"expo-task-manager": "~14.0.7",
"expo-web-browser": "~15.0.7",
"expo": "^55.0.6",
"expo-application": "~55.0.9",
"expo-blur": "~55.0.9",
"expo-build-properties": "~55.0.9",
"expo-constants": "~55.0.7",
"expo-datadog": "^55.0.0",
"expo-dev-client": "~55.0.16",
"expo-device": "~55.0.9",
"expo-document-picker": "~55.0.8",
"expo-file-system": "~55.0.10",
"expo-font": "~55.0.4",
"expo-image": "~55.0.6",
"expo-linear-gradient": "~55.0.8",
"expo-linking": "~55.0.7",
"expo-router": "~55.0.5",
"expo-splash-screen": "~55.0.10",
"expo-status-bar": "~55.0.4",
"expo-system-ui": "~55.0.9",
"expo-task-manager": "~55.0.9",
"expo-web-browser": "~55.0.9",
"freerasp-react-native": "^4.2.4",
"lodash": "^4.17.23",
"moti": "^0.29.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.54.2",
"react-native": "0.81.4",
"react-native": "patch:react-native@npm%3A0.83.4#~/.yarn/patches/react-native-npm-0.83.4-77634a290c.patch",
"react-native-ble-plx": "patch:react-native-ble-plx@npm%3A3.5.0#~/.yarn/patches/react-native-ble-plx+3.5.0.patch",
"react-native-capture-protection": "^2.3.1",
"react-native-collapsible-tab-view": "v9.0.0-rc.0",
"react-native-device-crypto": "patch:react-native-device-crypto@npm%3A0.1.7#~/.yarn/patches/react-native-device-crypto-npm-0.1.7-dbd2698fc4.patch",
"react-native-device-info": "^14.0.1",
"react-native-draggable-flatlist": "^4.0.1",
"react-native-gesture-handler": "~2.28.0",
"react-native-keyboard-controller": "1.18.5",
"react-native-gesture-handler": "~2.30.0",
"react-native-keyboard-controller": "1.20.7",
"react-native-keychain": "^10.0.0",
"react-native-mmkv": "^3.3.3",
"react-native-pager-view": "6.9.1",
"react-native-pager-view": "8.0.0",
"react-native-permissions": "^5.4.2",
"react-native-progress": "^5.0.1",
"react-native-qrcode-styled": "^0.4.0",
"react-native-quick-crypto": "^0.7.11",
"react-native-reanimated": "~4.1.0",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-reanimated": "4.2.1",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.23.0",
"react-native-share": "^12.2.0",
"react-native-svg": "^15.13.0",
"react-native-svg": "15.15.3",
"react-native-vision-camera": "^4.7.2",
"react-native-web": "^0.21.0",
"react-native-worklets": "^0.5.1",
"react-native-worklets": "0.7.2",
"react-redux": "^9.1.2",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
@@ -147,7 +147,6 @@
"zod": "^3.24.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.26.0",
"@babel/preset-react": "^7.26.3",
"@datadog/datadog-ci": "^5.9.0",
@@ -157,24 +156,20 @@
"@rtk-query/codegen-openapi": "^2.0.0",
"@safe-global/test": "workspace:^",
"@safe-global/types-kit": "^1.0.5",
"@storybook/addon-essentials": "^8.4.6",
"@storybook/addon-interactions": "^8.4.6",
"@storybook/addon-actions": "^9.0.8",
"@storybook/addon-onboarding": "^10.2.6",
"@storybook/addon-ondevice-actions": "^8.4.4",
"@storybook/addon-ondevice-controls": "^8.4.2",
"@storybook/addon-webpack5-compiler-swc": "^1.0.5",
"@storybook/blocks": "^8.4.6",
"@storybook/addon-ondevice-actions": "10.3.0-next.6",
"@storybook/addon-ondevice-controls": "10.3.0-next.6",
"@storybook/react": "^10.2.6",
"@storybook/react-native": "^8.4.2",
"@storybook/react-webpack5": "^8.4.6",
"@storybook/test": "^8.4.6",
"@storybook/react-native": "10.3.0-next.6",
"@storybook/react-webpack5": "^10.2.6",
"@testing-library/react-native": "^13.2.0",
"@types/eslint__js": "^8.42.3",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.13",
"@types/node": "^22.13.1",
"@types/qrcode": "^1.5.5",
"@types/react": "~19.1.10",
"@types/react": "~19.2.10",
"@types/semver": "^7.7.0",
"babel-loader": "^10.0.0",
"eslint": "^9.29.0",
@@ -182,7 +177,7 @@
"glob": "^13.0.6",
"globals": "^15.14.0",
"jest": "^29.7.0",
"jest-expo": "~54.0.12",
"jest-expo": "~55.0.9",
"prettier": "^3.6.2",
"redux-devtools-expo-dev-plugin": "^2.0.0",
"storybook": "^10.2.7",
+2 -6
View File
@@ -173,10 +173,6 @@ function RootLayout() {
)
}
let AppEntryPoint = RootLayout
import StorybookUI from '../../.storybook'
if (isStorybookEnv) {
AppEntryPoint = require('../../.storybook').default
}
export default AppEntryPoint
export default isStorybookEnv ? StorybookUI : RootLayout
@@ -40,7 +40,7 @@ export const BlurredIdenticonBackground = ({ address, height = 125, children }:
height: height,
},
]}
tint={colorScheme || 'dark'}
tint={colorScheme === 'light' ? 'light' : 'dark'}
/>
{children}
@@ -1,6 +1,6 @@
import React from 'react'
import { Text, View, XStack, YStack } from 'tamagui'
import { Skeleton } from 'moti/skeleton'
import { SafeSkeleton } from '@/src/components/SafeSkeleton'
import { Logo } from '@/src/components/Logo'
import { Fiat } from '@/src/components/Fiat'
import { useAppSelector } from '@/src/store/hooks'
@@ -53,7 +53,7 @@ export const ChainIndicator = ({
const noChains = !allChains || allChains.length === 0
const finalChainConfig = chainConfig || (showUnknown ? fallbackChainConfig : null)
if (isLoading || noChains) {
return <Skeleton width={onlyLogo ? 24 : 115} height={22} radius={4} />
return <SafeSkeleton width={onlyLogo ? 24 : 115} height={22} radius={4} />
}
if (!finalChainConfig) {
+3 -2
View File
@@ -1,5 +1,6 @@
import React from 'react'
import { ColorSchemeName, ImageSourcePropType, Platform, StyleSheet } from 'react-native'
import { ImageSourcePropType, Platform, StyleSheet } from 'react-native'
import { ColorScheme } from '@/src/types/theme'
import { H2, Image, Text, getTokenValue, View } from 'tamagui'
import { SafeButton } from '@/src/components/SafeButton'
import { WINDOW_HEIGHT } from '@/src/store/constants'
@@ -23,7 +24,7 @@ interface OptInProps {
testID?: string
isVisible?: boolean
isLoading?: boolean
colorScheme: ColorSchemeName
colorScheme: ColorScheme
infoMessage?: string
}
@@ -1,7 +1,7 @@
// Storybook stories for SafeButton component - test mobile screenshots workflow
import type { Meta, StoryObj } from '@storybook/react'
import { SafeButton } from '@/src/components/SafeButton'
import { action } from '@storybook/addon-actions'
import { action } from 'storybook/actions'
import { YStack, Text, XStack, ScrollView } from 'tamagui'
import { SafeFontIcon } from '@/src/components/SafeFontIcon'
import React from 'react'
@@ -0,0 +1,20 @@
import { Skeleton } from 'moti/skeleton'
import type { ComponentProps } from 'react'
import { useTheme } from '@/src/theme/hooks/useTheme'
type SafeSkeletonProps = Omit<ComponentProps<typeof Skeleton>, 'colorMode'>
/**
* Thin wrapper around Moti's Skeleton that automatically syncs with the app theme.
*
* Moti only accepts 'light' | 'dark' for colorMode and defaults to 'dark'
* when the value is unrecognized. This wrapper maps 'unspecified' to 'light'.
*/
export function SafeSkeleton(props: SafeSkeletonProps) {
const { colorScheme } = useTheme()
const colorMode = colorScheme === 'dark' ? 'dark' : 'light'
return <Skeleton colorMode={colorMode} {...props} />
}
SafeSkeleton.Group = Skeleton.Group
@@ -0,0 +1 @@
export { SafeSkeleton } from './SafeSkeleton'
+32 -22
View File
@@ -1,8 +1,8 @@
import React, { ReactElement } from 'react'
import React, { ReactElement, useMemo } from 'react'
import { TabBarProps, Tabs } from 'react-native-collapsible-tab-view'
import { safeTabItem } from './types'
import { SafeTabBar } from './SafeTabBar'
import { Theme } from 'tamagui'
import { Theme, useTheme } from 'tamagui'
import { StyleProp, ViewStyle } from 'react-native'
import { View } from 'tamagui'
@@ -16,7 +16,7 @@ interface SafeTabProps<T> {
rightNode?: (activeTabLabel: string) => React.ReactNode
}
export function SafeTab<T extends object>({
function SafeTabInner<T extends object>({
renderHeader,
headerHeight,
items,
@@ -25,31 +25,41 @@ export function SafeTab<T extends object>({
onIndexChange,
rightNode,
}: SafeTabProps<T>) {
const theme = useTheme()
const headerContainerStyle = useMemo(
() => ({ backgroundColor: theme.background.get(), shadowColor: 'transparent' }),
[theme.background],
)
return (
<Tabs.Container
containerStyle={containerStyle}
renderHeader={renderHeader}
headerContainerStyle={headerContainerStyle}
headerHeight={headerHeight}
renderTabBar={(props) => <SafeTabBar rightNode={rightNode} {...props} />}
onIndexChange={onIndexChange}
initialTabName={items[0].label}
>
{items.map(({ label, testID, Component }, index) => (
<Tabs.Tab name={label} key={`${label}-${index}`}>
<View testID={testID ?? `tab-content-${label}-${index}`} flex={1}>
<Component {...(containerProps as T)} />
</View>
</Tabs.Tab>
))}
</Tabs.Container>
)
}
export function SafeTab<T extends object>(props: SafeTabProps<T>) {
return (
<Theme name={'tab'}>
<Tabs.Container
containerStyle={containerStyle}
renderHeader={renderHeader}
headerContainerStyle={headerContainerStyle}
headerHeight={headerHeight}
renderTabBar={(props) => <SafeTabBar rightNode={rightNode} {...props} />}
onIndexChange={onIndexChange}
initialTabName={items[0].label}
>
{items.map(({ label, testID, Component }, index) => (
<Tabs.Tab name={label} key={`${label}-${index}`}>
<View testID={testID ?? `tab-content-${label}-${index}`} flex={1}>
<Component {...(containerProps as T)} />
</View>
</Tabs.Tab>
))}
</Tabs.Container>
<SafeTabInner {...props} />
</Theme>
)
}
const headerContainerStyle = { backgroundColor: '$background', shadowColor: 'transparent' }
SafeTab.FlashList = Tabs.FlashList
SafeTab.FlatList = Tabs.FlatList
SafeTab.ScrollView = Tabs.ScrollView
@@ -1,6 +1,6 @@
import React from 'react'
import { Badge } from '@/src/components/Badge'
import { Skeleton } from 'moti/skeleton'
import { SafeSkeleton } from '@/src/components/SafeSkeleton'
import { BadgeThemeTypes } from '@/src/components/Badge/Badge'
import { useAppSelector } from '@/src/store/hooks'
import { selectSignerByAddress } from '@/src/store/signersSlice'
@@ -28,7 +28,7 @@ export const SignerTypeBadge = ({
const signer = useAppSelector((state) => selectSignerByAddress(state, address))
if (isLoading) {
return <Skeleton colorMode="dark" radius="round" height={size} width={size} />
return <SafeSkeleton radius="round" height={size} width={size} />
}
if (signer?.type === 'ledger') {
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react'
import { PendingTransactions } from '@/src/components/StatusBanners/PendingTransactions'
import { action } from '@storybook/addon-actions'
import { action } from 'storybook/actions'
const meta: Meta<typeof PendingTransactions> = {
title: 'StatusBanners/PendingTransactions',
@@ -1,6 +1,6 @@
import React from 'react'
import { Badge } from '@/src/components/Badge'
import { Skeleton } from 'moti/skeleton'
import { SafeSkeleton } from '@/src/components/SafeSkeleton'
interface ThresholdBadgeProps {
threshold: number
@@ -20,7 +20,7 @@ export const ThresholdBadge = ({
testID,
}: ThresholdBadgeProps) => {
if (isLoading) {
return <Skeleton colorMode="dark" radius="round" height={size} width={size} />
return <SafeSkeleton radius="round" height={size} width={size} />
}
const content = `${threshold}/${ownersCount}`
@@ -1,9 +1,8 @@
import React from 'react'
import { View } from 'tamagui'
import { Skeleton } from 'moti/skeleton'
import { SafeSkeleton } from '@/src/components/SafeSkeleton'
import { Container } from '@/src/components/Container'
import random from 'lodash/random'
import { useTheme } from '@/src/theme/hooks/useTheme'
interface TransactionSkeletonProps {
count?: number
@@ -12,8 +11,6 @@ interface TransactionSkeletonProps {
}
export const TransactionSkeletonItem = () => {
const { colorScheme } = useTheme()
// Memoize random widths to prevent re-renders and maintain consistent skeleton appearance
const widths = React.useMemo(
() => ({
@@ -29,20 +26,20 @@ export const TransactionSkeletonItem = () => {
<View flexDirection="row" width="100%" alignItems="center" justifyContent="space-between">
<View flexDirection="row" maxWidth="55%" alignItems="center" gap="$3">
{/* Left icon skeleton */}
<Skeleton colorMode={colorScheme} radius="round" height={32} width={32} />
<SafeSkeleton radius="round" height={32} width={32} />
<View flex={1} gap="$2">
{/* Transaction type skeleton */}
<Skeleton colorMode={colorScheme} height={10} width={widths.transactionType} />
<SafeSkeleton height={10} width={widths.transactionType} />
{/* Transaction label skeleton */}
<Skeleton colorMode={colorScheme} height={18} width={widths.transactionLabel} />
<SafeSkeleton height={18} width={widths.transactionLabel} />
</View>
</View>
{/* Right side skeleton - value, status, or buttons */}
<View alignItems="flex-end" gap="$2">
<Skeleton colorMode={colorScheme} height={16} width={widths.rightSide} />
<SafeSkeleton height={16} width={widths.rightSide} />
</View>
</View>
</Container>
@@ -54,26 +51,20 @@ export const TransactionSkeleton = ({
showSections = true,
sectionTitles = ['Recent transactions'],
}: TransactionSkeletonProps) => {
const { colorScheme } = useTheme()
// For pending transactions, we typically have 2 sections (Next, In queue)
// For history, we typically have date-based sections
const sections = showSections ? sectionTitles : ['']
const itemsPerSection = Math.ceil(count / sections.length)
return (
<Skeleton.Group show={true}>
<SafeSkeleton.Group show={true}>
<View>
{sections.map((sectionTitle, sectionIndex) => (
<View key={sectionIndex} gap="$4">
{/* Section header skeleton - only show if we have a title */}
{showSections && sectionTitle && (
<View marginBottom="$2">
<Skeleton
colorMode={colorScheme}
height={20}
width={sectionTitle === 'Recent transactions' ? 120 : random(80, 120)}
/>
<SafeSkeleton height={20} width={sectionTitle === 'Recent transactions' ? 120 : random(80, 120)} />
</View>
)}
@@ -84,6 +75,6 @@ export const TransactionSkeleton = ({
</View>
))}
</View>
</Skeleton.Group>
</SafeSkeleton.Group>
)
}
@@ -109,6 +109,7 @@ exports[`StakingTxDepositCard renders correctly 1`] = `
onLoadStart={[Function]}
onProgress={[Function]}
placeholder={[]}
sfEffect={null}
source={
[
{
@@ -122,6 +123,8 @@ exports[`StakingTxDepositCard renders correctly 1`] = `
"flex": 1,
}
}
symbolSize={null}
symbolWeight={null}
testID="logo-image"
transition={null}
/>
@@ -109,6 +109,7 @@ exports[`StakingTxExitCard matches snapshot 1`] = `
onLoadStart={[Function]}
onProgress={[Function]}
placeholder={[]}
sfEffect={null}
source={
[
{
@@ -122,6 +123,8 @@ exports[`StakingTxExitCard matches snapshot 1`] = `
"flex": 1,
}
}
symbolSize={null}
symbolWeight={null}
testID="logo-image"
transition={null}
/>
@@ -109,6 +109,7 @@ exports[`StakingTxWithdrawCard matches snapshot 1`] = `
onLoadStart={[Function]}
onProgress={[Function]}
placeholder={[]}
sfEffect={null}
source={
[
{
@@ -122,6 +123,8 @@ exports[`StakingTxWithdrawCard matches snapshot 1`] = `
"flex": 1,
}
}
symbolSize={null}
symbolWeight={null}
testID="logo-image"
transition={null}
/>
@@ -100,6 +100,7 @@ exports[`VaultTxDepositCard renders correctly 1`] = `
onLoadStart={[Function]}
onProgress={[Function]}
placeholder={[]}
sfEffect={null}
source={
[
{
@@ -113,6 +114,8 @@ exports[`VaultTxDepositCard renders correctly 1`] = `
"flex": 1,
}
}
symbolSize={null}
symbolWeight={null}
testID="logo-image"
transition={null}
/>
@@ -100,6 +100,7 @@ exports[`VaultTxRedeemCard renders correctly 1`] = `
onLoadStart={[Function]}
onProgress={[Function]}
placeholder={[]}
sfEffect={null}
source={
[
{
@@ -113,6 +114,8 @@ exports[`VaultTxRedeemCard renders correctly 1`] = `
"flex": 1,
}
}
symbolSize={null}
symbolWeight={null}
testID="logo-image"
transition={null}
/>
@@ -2,7 +2,7 @@ import { AccountItem } from './AccountItem'
import { Meta, StoryObj } from '@storybook/react'
import { mockedActiveSafeInfo, mockedChains } from '@/src/store/constants'
import { Chain } from '@safe-global/store/gateway/AUTO_GENERATED/chains'
import { action } from '@storybook/addon-actions'
import { action } from 'storybook/actions'
import { Address } from '@/src/types/address'
const meta: Meta<typeof AccountItem> = {
@@ -2,10 +2,9 @@ import React from 'react'
import { View } from 'tamagui'
import { Fiat } from '@/src/components/Fiat'
import { Skeleton } from 'moti/skeleton'
import { SafeSkeleton } from '@/src/components/SafeSkeleton'
import { useAppSelector } from '@/src/store/hooks'
import { selectCurrency } from '@/src/store/settingsSlice'
import { useTheme } from '@/src/theme/hooks/useTheme'
interface BalanceProps {
isLoading: boolean
@@ -13,20 +12,19 @@ interface BalanceProps {
}
export function Balance({ isLoading, balanceAmount }: BalanceProps) {
const { colorScheme } = useTheme()
const currency = useAppSelector(selectCurrency)
const showSkeleton = isLoading || !balanceAmount
return (
<View alignItems="center" justifyContent="center" paddingVertical="$4" width="100%">
<Skeleton.Group show={showSkeleton}>
<Skeleton colorMode={colorScheme} width={220}>
<SafeSkeleton.Group show={showSkeleton}>
<SafeSkeleton width={220}>
<View alignItems="center">
<Fiat value={balanceAmount} currency={currency} precise />
</View>
</Skeleton>
</Skeleton.Group>
</SafeSkeleton>
</SafeSkeleton.Group>
</View>
)
}
@@ -106,6 +106,7 @@ exports[`PositionItem renders correctly 1`] = `
onLoadStart={[Function]}
onProgress={[Function]}
placeholder={[]}
sfEffect={null}
source={
[
{
@@ -119,6 +120,8 @@ exports[`PositionItem renders correctly 1`] = `
"flex": 1,
}
}
symbolSize={null}
symbolWeight={null}
testID="logo-image"
transition={null}
/>
@@ -132,6 +132,7 @@ exports[`StakingDeposit matches snapshot 1`] = `
onLoadStart={[Function]}
onProgress={[Function]}
placeholder={[]}
sfEffect={null}
source={
[
{
@@ -145,6 +146,8 @@ exports[`StakingDeposit matches snapshot 1`] = `
"flex": 1,
}
}
symbolSize={null}
symbolWeight={null}
testID="logo-image"
transition={null}
/>
@@ -132,6 +132,7 @@ exports[`StakingExit matches snapshot 1`] = `
onLoadStart={[Function]}
onProgress={[Function]}
placeholder={[]}
sfEffect={null}
source={
[
{
@@ -145,6 +146,8 @@ exports[`StakingExit matches snapshot 1`] = `
"flex": 1,
}
}
symbolSize={null}
symbolWeight={null}
testID="logo-image"
transition={null}
/>
@@ -132,6 +132,7 @@ exports[`StakingWithdrawRequest matches snapshot 1`] = `
onLoadStart={[Function]}
onProgress={[Function]}
placeholder={[]}
sfEffect={null}
source={
[
{
@@ -145,6 +146,8 @@ exports[`StakingWithdrawRequest matches snapshot 1`] = `
"flex": 1,
}
}
symbolSize={null}
symbolWeight={null}
testID="logo-image"
transition={null}
/>
@@ -1,9 +1,9 @@
import React from 'react'
import { Text, YStack, Image, styled, H2, H5, getTokenValue } from 'tamagui'
import { SafeButton } from '@/src/components/SafeButton'
import { ColorScheme } from '@/src/types/theme'
import TransferOldAppDark from '@/assets/images/transfer-old-app-dark.png'
import TransferOldAppLight from '@/assets/images/transfer-old-app-light.png'
import { ColorSchemeName } from 'react-native'
import { GradientText } from '@/src/components/GradientText'
const StyledText = styled(Text, {
@@ -12,7 +12,7 @@ const StyledText = styled(Text, {
})
interface DataTransferViewProps {
colorScheme: ColorSchemeName
colorScheme: ColorScheme
bottomInset: number
onPressTransferData: () => void
onPressStartFresh: () => void
@@ -1,9 +1,10 @@
import React from 'react'
import { Text, YStack, Image, styled, H2 } from 'tamagui'
import { SafeButton } from '@/src/components/SafeButton'
import { ColorScheme } from '@/src/types/theme'
import ImportDataSelectFilesDark from '@/assets/images/import-data-select-files-dark.png'
import ImportDataSelectFilesLight from '@/assets/images/import-data-select-files-light.png'
import { ColorSchemeName, TouchableOpacity } from 'react-native'
import { TouchableOpacity } from 'react-native'
const StyledText = styled(Text, {
fontSize: '$4',
@@ -12,7 +13,7 @@ const StyledText = styled(Text, {
})
interface FileSelectionViewProps {
colorScheme: ColorSchemeName
colorScheme: ColorScheme
bottomInset: number
onFileSelect: () => void
onImagePress: () => void
@@ -1,8 +1,7 @@
import React from 'react'
import { Text, View } from 'tamagui'
import { router } from 'expo-router'
import { Skeleton } from 'moti/skeleton'
import { useTheme } from '@/src/theme/hooks/useTheme'
import { SafeSkeleton } from '@/src/components/SafeSkeleton'
import { useAppSelector } from '@/src/store/hooks'
import { selectActiveChain } from '@/src/store/chains'
import { ExecutionMethod } from '@/src/features/HowToExecuteSheet/types'
@@ -27,7 +26,6 @@ export const EstimatedNetworkFee = ({
willFail,
}: EstimatedNetworkFeeProps) => {
const chain = useAppSelector(selectActiveChain)
const { colorScheme } = useTheme()
const activeSafe = useDefinedActiveSafe()
const { currentData: relaysRemaining, isLoading: isLoadingRelays } = useRelayGetRelaysRemainingV1Query({
@@ -47,7 +45,7 @@ export const EstimatedNetworkFee = ({
<Text color="$textSecondaryLight">Est. network fee</Text>
{isLoadingFees || isLoadingRelays ? (
<Skeleton colorMode={colorScheme} height={16} width={100} />
<SafeSkeleton height={16} width={100} />
) : executionMethod === ExecutionMethod.WITH_RELAY ? (
<RelayFee
willFail={willFail}
@@ -1,8 +1,7 @@
import { GradientText } from '@/src/components/GradientText'
import { useTheme } from '@/src/theme/hooks/useTheme'
import { SafeSkeleton } from '@/src/components/SafeSkeleton'
import { RelaysRemaining } from '@safe-global/store/gateway/AUTO_GENERATED/relay'
import { Skeleton } from 'moti/skeleton'
import { getTokenValue, Text, View } from 'tamagui'
import { CanNotEstimate } from '../CanNotEstimate'
import { TouchableOpacity } from 'react-native'
@@ -15,8 +14,6 @@ interface RelayFeeProps {
}
export const RelayFee = ({ isLoadingRelays, willFail, relaysRemaining, onFailTextPress }: RelayFeeProps) => {
const { colorScheme } = useTheme()
return (
<View alignItems="flex-end" flexDirection="row" justifyContent="center" gap="$2">
{willFail ? (
@@ -32,7 +29,7 @@ export const RelayFee = ({ isLoadingRelays, willFail, relaysRemaining, onFailTex
</View>
{isLoadingRelays ? (
<Skeleton colorMode={colorScheme} height={16} width={80} />
<SafeSkeleton height={16} width={80} />
) : (
relaysRemaining && <Text fontWeight={700}>{relaysRemaining.remaining} left / day</Text>
)}
@@ -7,8 +7,7 @@ import { SelectExecutor } from '@/src/components/SelectExecutor'
import { EstimatedNetworkFee } from '../EstimatedNetworkFee'
import { Container } from '@/src/components/Container'
import { ExecutionMethod } from '@/src/features/HowToExecuteSheet/types'
import { Skeleton } from 'moti/skeleton'
import { useTheme } from '@/src/theme/hooks/useTheme'
import { SafeSkeleton } from '@/src/components/SafeSkeleton'
import { getSubmitButtonText } from './helpers'
import { Alert } from '@/src/components/Alert'
import { SafeFontIcon } from '@/src/components/SafeFontIcon'
@@ -44,7 +43,6 @@ export function ReviewExecuteFooter({
onConfirmPress,
}: ReviewExecuteFooterProps) {
const insets = useSafeAreaInsets()
const { colorScheme } = useTheme()
const isButtonDisabled = !hasSufficientFunds || isExecuting
const buttonText = isExecuting ? 'Executing...' : getSubmitButtonText(hasSufficientFunds)
@@ -79,9 +77,9 @@ export function ReviewExecuteFooter({
</Container>
{isCheckingFunds ? (
<Skeleton.Group show>
<Skeleton colorMode={colorScheme} height={44} width="100%" radius={12} />
</Skeleton.Group>
<SafeSkeleton.Group show>
<SafeSkeleton height={44} width="100%" radius={12} />
</SafeSkeleton.Group>
) : (
<SafeButton onPress={onConfirmPress} width="100%" disabled={isButtonDisabled} loading={isExecuting}>
{buttonText}
@@ -1,16 +1,14 @@
import React from 'react'
import { Stack, View } from 'tamagui'
import { Skeleton } from 'moti/skeleton'
import { SafeSkeleton } from '@/src/components/SafeSkeleton'
import { Container } from '@/src/components/Container'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useTheme } from '@/src/theme/hooks/useTheme'
export function ReviewExecuteFooterSkeleton() {
const insets = useSafeAreaInsets()
const { colorScheme } = useTheme()
return (
<Skeleton.Group show={true}>
<SafeSkeleton.Group show={true}>
<Stack paddingHorizontal="$4" space="$3" paddingBottom={insets.bottom ? insets.bottom : '$4'}>
<Container
backgroundColor="transparent"
@@ -22,19 +20,19 @@ export function ReviewExecuteFooterSkeleton() {
{/* EstimatedNetworkFee skeleton */}
<View flexDirection="row" alignItems="center" justifyContent="space-between" paddingHorizontal="$4">
<View gap="$2">
<Skeleton colorMode={colorScheme} height={14} width={100} />
<Skeleton colorMode={colorScheme} height={12} width={140} />
<SafeSkeleton height={14} width={100} />
<SafeSkeleton height={12} width={140} />
</View>
<View alignItems="flex-end" gap="$2">
<Skeleton colorMode={colorScheme} height={16} width={60} />
<Skeleton colorMode={colorScheme} height={12} width={80} />
<SafeSkeleton height={16} width={60} />
<SafeSkeleton height={12} width={80} />
</View>
</View>
</Container>
{/* Button skeleton */}
<Skeleton colorMode={colorScheme} height={48} width="100%" radius={12} />
<SafeSkeleton height={48} width="100%" radius={12} />
</Stack>
</Skeleton.Group>
</SafeSkeleton.Group>
)
}
@@ -1,10 +1,9 @@
import { Skeleton } from 'moti/skeleton'
import React from 'react'
import { View, Text } from 'tamagui'
import { RelaysRemaining } from '@safe-global/store/gateway/AUTO_GENERATED/relay'
import { ExecutionMethod } from '@/src/features/HowToExecuteSheet/types'
import { SafeFontIcon } from '@/src/components/SafeFontIcon'
import { useTheme } from '@/src/theme/hooks/useTheme'
import { SafeSkeleton } from '@/src/components/SafeSkeleton'
interface RelayAvailableProps {
isLoadingRelays: boolean
@@ -13,8 +12,6 @@ interface RelayAvailableProps {
}
export const RelayAvailable = ({ isLoadingRelays, relaysRemaining, executionMethod }: RelayAvailableProps) => {
const { colorScheme } = useTheme()
return (
<View width="100%" flexDirection="row" justifyContent="space-between" alignItems="center">
<View flex={1}>
@@ -26,7 +23,7 @@ export const RelayAvailable = ({ isLoadingRelays, relaysRemaining, executionMeth
We pay transactions fees for you
</Text>
{isLoadingRelays ? (
<Skeleton colorMode={colorScheme} height={16} width={80} />
<SafeSkeleton height={16} width={80} />
) : (
relaysRemaining && <Text fontSize="$4">{relaysRemaining.remaining} left / day</Text>
)}
@@ -8,7 +8,7 @@ import { View, Text, ScrollView, YStack, getTokenValue } from 'tamagui'
import { useScrollableHeader } from '@/src/navigation/useScrollableHeader'
import { NavBarTitle } from '@/src/components/Title'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { Controller, useFormContext } from 'react-hook-form'
import { Controller, useFormContext, useWatch } from 'react-hook-form'
import type { FormValues } from '@/src/features/ImportReadOnly/types'
import SafeAccountInput from '@/src/components/SafeAccountInput'
@@ -21,10 +21,9 @@ export const ImportAccountFormView: React.FC<ImportAccountFormViewProps> = ({ is
const {
control,
formState: { errors, isValid, dirtyFields },
watch,
} = useFormContext<FormValues>()
const { top, bottom } = useSafeAreaInsets()
const result = watch('importedSafeResult')
const result = useWatch({ name: 'importedSafeResult' })
const { handleScroll } = useScrollableHeader({
children: <NavBarTitle paddingRight={5}>Import Safe account</NavBarTitle>,
@@ -1,13 +1,12 @@
import React from 'react'
import { Text, XStack, YStack } from 'tamagui'
import { Skeleton } from 'moti/skeleton'
import type { ThreatAnalysisResults } from '@safe-global/utils/features/safe-shield/types'
import type { AsyncResult } from '@safe-global/utils/hooks/useAsync'
import { InfoSheet } from '@/src/components/InfoSheet'
import { SafeFontIcon } from '@/src/components/SafeFontIcon'
import { SafeSkeleton } from '@/src/components/SafeSkeleton'
import { BalanceChangeItem } from './BalanceChangeItem'
import { Container } from '@/src/components/Container'
import { useTheme } from '@/src/theme/hooks/useTheme'
interface BalanceChangeBlockProps {
threat?: AsyncResult<ThreatAnalysisResults>
@@ -17,7 +16,6 @@ const BALANCE_CHANGE_INFO =
'The balance change gives an overview of the implications of a transaction. You can see which assets will be sent and received after the transaction is executed.'
export function BalanceChangeBlock({ threat }: BalanceChangeBlockProps) {
const { colorScheme } = useTheme()
const [threatData, threatError, threatLoading = false] = threat || []
const balanceChanges = threatData?.BALANCE_CHANGE || []
@@ -27,7 +25,7 @@ export function BalanceChangeBlock({ threat }: BalanceChangeBlockProps) {
if (threatLoading) {
return (
<YStack gap="$2" marginTop="$2">
<Skeleton colorMode={colorScheme} height={24} radius={4} width="100%" />
<SafeSkeleton height={24} radius={4} width="100%" />
</YStack>
)
}
@@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react'
import { WidgetAction } from './WidgetAction'
import { Severity } from '@safe-global/utils/features/safe-shield/types'
import { SEVERITY_TO_TITLE } from '@safe-global/utils/features/safe-shield/constants'
import { action } from '@storybook/addon-actions'
import { action } from 'storybook/actions'
import { View } from 'tamagui'
const meta: Meta<typeof WidgetAction> = {
@@ -1,17 +1,16 @@
import { Skeleton } from 'moti/skeleton'
import React from 'react'
import { SafeSkeleton } from '@/src/components/SafeSkeleton'
import { WidgetDisplayWrapper } from '../WidgetDisplayWrapper'
import { useTheme } from '@/src/theme/hooks/useTheme'
export function LoadingWidget() {
const { colorScheme } = useTheme()
return (
<WidgetDisplayWrapper>
<Skeleton.Group show={true}>
<Skeleton colorMode={colorScheme} height={20} radius={4} width={'100%'} />
<Skeleton colorMode={colorScheme} height={20} radius={4} width={'100%'} />
<Skeleton colorMode={colorScheme} height={20} radius={4} width={'100%'} />
<Skeleton colorMode={colorScheme} height={60} width={'100%'} />
</Skeleton.Group>
<SafeSkeleton.Group show={true}>
<SafeSkeleton height={20} radius={4} width={'100%'} />
<SafeSkeleton height={20} radius={4} width={'100%'} />
<SafeSkeleton height={20} radius={4} width={'100%'} />
<SafeSkeleton height={60} width={'100%'} />
</SafeSkeleton.Group>
</WidgetDisplayWrapper>
)
}
+9 -12
View File
@@ -2,9 +2,8 @@ import React, { useCallback } from 'react'
import { H2, ScrollView, Text, Theme, View, XStack, YStack } from 'tamagui'
import { SafeFontIcon } from '@/src/components/SafeFontIcon/SafeFontIcon'
import { SafeListItem } from '@/src/components/SafeListItem'
import { Skeleton } from 'moti/skeleton'
import { SafeSkeleton } from '@/src/components/SafeSkeleton'
import { Pressable, TouchableOpacity } from 'react-native'
import { useTheme } from '@/src/theme/hooks/useTheme'
import { EthAddress } from '@/src/components/EthAddress'
import { SafeState } from '@safe-global/store/gateway/AUTO_GENERATED/safes'
import { Address } from '@/src/types/address'
@@ -44,8 +43,6 @@ export const Settings = ({
const activeSafe = useDefinedActiveSafe()
const copy = useCopyAndDispatchToast()
const { owners = [], threshold, implementation } = data
const { colorScheme } = useTheme()
const onPressAddressCopy = useCallback(() => {
copy(activeSafe.address)
}, [activeSafe.address])
@@ -64,7 +61,7 @@ export const Settings = ({
}}
>
<YStack flex={1} paddingTop={'$10'}>
<Skeleton.Group show={!owners.length}>
<SafeSkeleton.Group show={!owners.length}>
<YStack alignItems="center" space="$3" marginBottom="$6">
<BadgeWrapper
badge={
@@ -105,11 +102,11 @@ export const Settings = ({
marginRight={'$2'}
>
<View width={30}>
<Skeleton colorMode={colorScheme}>
<SafeSkeleton>
<Text fontWeight="bold" textAlign="center" fontSize={'$4'}>
{owners.length}
</Text>
</Skeleton>
</SafeSkeleton>
</View>
<Text color="$colorHover" fontSize={'$3'}>
Signers
@@ -125,11 +122,11 @@ export const Settings = ({
width={80}
>
<View width={30}>
<Skeleton colorMode={colorScheme}>
<SafeSkeleton>
<Text fontWeight="bold" textAlign="center" fontSize={'$4'}>
{threshold}/{owners.length}
</Text>
</Skeleton>
</SafeSkeleton>
</View>
<Text color="$colorHover" fontSize={'$3'}>
Threshold
@@ -154,11 +151,11 @@ export const Settings = ({
leftNode={<SafeFontIcon name={'owners'} color={'$colorSecondary'} />}
rightNode={
<View flexDirection={'row'} alignItems={'center'} justifyContent={'center'}>
<Skeleton colorMode={colorScheme} height={17}>
<SafeSkeleton height={17}>
<Text minWidth={15} marginRight={'$3'} color={'$colorSecondary'}>
{owners.length}
</Text>
</Skeleton>
</SafeSkeleton>
<View>
<SafeFontIcon name={'chevron-right'} />
</View>
@@ -206,7 +203,7 @@ export const Settings = ({
</View>
)}
</YStack>
</Skeleton.Group>
</SafeSkeleton.Group>
{/* Footer */}
<Pressable
+2
View File
@@ -274,6 +274,8 @@ jest.mock('@react-native-firebase/crashlytics', () => {
jest.mock('@datadog/mobile-react-native', () => require('@datadog/mobile-react-native/jest'))
jest.mock('expo-datadog', () => require('@datadog/mobile-react-native/jest'))
jest.mock('react-native-worklets', () => require('react-native-worklets/src/mock'))
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
@@ -60,14 +60,14 @@ describe('useTheme', () => {
},
)
it('should return undefined when OS color scheme is null and themePreference is auto', () => {
mockUseColorScheme.mockReturnValue(null)
it('should normalize unspecified OS color scheme to dark when themePreference is auto', () => {
mockUseColorScheme.mockReturnValue('unspecified')
mockUseAppSelector.mockReturnValue('auto')
const { result } = renderHook(() => useTheme())
expect(result.current.colorScheme).toBe(undefined)
expect(result.current.isDark).toBe(false)
expect(result.current.colorScheme).toBe('dark')
expect(result.current.isDark).toBe(true)
expect(result.current.themePreference).toBe('auto')
})
+4 -5
View File
@@ -3,14 +3,12 @@ import { useColorScheme } from 'react-native'
import { updateSettings } from '@/src/store/settingsSlice'
import { selectSettings } from '@/src/store/settingsSlice'
import { useAppDispatch, useAppSelector } from '@/src/store/hooks'
import { ThemePreference } from '@/src/types/theme'
import type { ColorScheme, ThemePreference } from '@/src/types/theme'
export const useTheme = () => {
const dispatch = useAppDispatch()
// The logical OR is intentional to ensure colorSchemeOS is never `null`.
// This makes it easier to use in the rest of the codebase without having to check for null.
const colorSchemeOS = useColorScheme() || undefined
const colorSchemeOS = useColorScheme()
const themePreference = useAppSelector(
(state) => selectSettings(state, 'themePreference') ?? 'auto',
@@ -23,7 +21,8 @@ export const useTheme = () => {
[dispatch],
)
const colorScheme = themePreference === 'auto' ? colorSchemeOS : themePreference
const colorSchemeRaw = themePreference === 'auto' ? colorSchemeOS : themePreference
const colorScheme: ColorScheme = colorSchemeRaw === 'light' ? 'light' : 'dark'
return {
themePreference,
+8 -7
View File
@@ -17,12 +17,13 @@ interface SafeThemeProviderProps {
export const SafeThemeProvider = ({ children }: SafeThemeProviderProps) => {
const { colorScheme, isDark, themePreference } = useTheme()
// Sync native iOS appearance with the app theme so native components
// (RefreshControl, context menus, etc.) match the app's color scheme.
// In auto mode, pass null to let the OS control the appearance.
// Sync native iOS appearance so native components (RefreshControl, context
// menus, etc.) match the app theme. In auto mode, pass 'unspecified' to
// follow the OS. Fixed by .yarn/patches/react-native-npm-0.83.4-* which
// resolves the actual OS scheme instead of storing 'unspecified' as-is.
useEffect(() => {
Appearance.setColorScheme(themePreference === 'auto' ? null : (colorScheme ?? null))
}, [colorScheme, themePreference])
Appearance.setColorScheme(themePreference === 'auto' ? 'unspecified' : themePreference)
}, [themePreference])
const themeProvider = isStorybookEnv ? (
<View
@@ -37,8 +38,8 @@ export const SafeThemeProvider = ({ children }: SafeThemeProviderProps) => {
return (
<FontProvider>
<TamaguiProvider config={config} defaultTheme={colorScheme ?? 'light'}>
<View testID={`theme-${colorScheme ?? 'light'}`} style={{ flex: 1 }}>
<TamaguiProvider config={config} defaultTheme={colorScheme}>
<View testID={`theme-${colorScheme}`} style={{ flex: 1 }}>
{themeProvider}
</View>
</TamaguiProvider>
+3 -1
View File
@@ -1,6 +1,8 @@
import { createTokens } from 'tamagui'
import { zIndex } from '@tamagui/themes'
import { generateTamaguiColorTokens, radius, generateTamaguiFontSizes, spacingMobile } from '@safe-global/theme'
import { generateTamaguiColorTokens, generateTamaguiFontSizes } from '@safe-global/theme/generators/tamagui'
import { radius } from '@safe-global/theme/tokens/radius'
import { spacingMobile } from '@safe-global/theme/tokens/spacing'
// Generate color tokens from unified palettes
const colors = generateTamaguiColorTokens()
+2
View File
@@ -1 +1,3 @@
export type ThemePreference = 'light' | 'dark' | 'auto'
export type ColorScheme = 'light' | 'dark'
+1
View File
@@ -4,6 +4,7 @@
"strict": true,
"paths": {
"@/*": ["./*"],
"@safe-global/theme/*": ["../../packages/theme/src/*"],
"@safe-global/store/*": ["../../packages/store/src/*"],
"@safe-global/utils/*": ["../../packages/utils/src/*"],
"@safe-global/test/*": ["../../config/test/*"],
+5 -73
View File
@@ -1,78 +1,11 @@
import js from '@eslint/js'
import { FlatCompat } from '@eslint/eslintrc'
import nextVitals from 'eslint-config-next/core-web-vitals'
import typescriptEslint from '@typescript-eslint/eslint-plugin'
import baseConfig from '../../config/eslint/base.mjs'
import tsParser from '@typescript-eslint/parser'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
})
const typeScriptRules = {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-unnecessary-type-constraint': 'warn',
}
const withTypeScriptRules = (configs) => {
let hasTypeScriptPlugin = false
const updatedConfigs = configs.map((config) => {
if (!config.plugins || !config.plugins['@typescript-eslint']) {
return config
}
hasTypeScriptPlugin = true
return {
...config,
languageOptions: {
...config.languageOptions,
parser: tsParser,
ecmaVersion: config.languageOptions?.ecmaVersion ?? 2020,
sourceType: config.languageOptions?.sourceType ?? 'module',
parserOptions: {
...config.languageOptions?.parserOptions,
},
},
rules: {
...config.rules,
...typeScriptRules,
},
}
})
if (hasTypeScriptPlugin) {
return updatedConfigs
}
return [
...updatedConfigs,
{
plugins: {
'@typescript-eslint': typescriptEslint,
},
languageOptions: {
parser: tsParser,
ecmaVersion: 2020,
sourceType: 'module',
},
rules: typeScriptRules,
},
]
}
export default [
{
ignores: ['**/node_modules/', '**/build/', '**/vite.config.ts', '**/jest.config.cjs', '**/src/__mocks__/**'],
},
...withTypeScriptRules(nextVitals),
...baseConfig,
{
languageOptions: {
parser: tsParser,
@@ -83,12 +16,11 @@ export default [
},
},
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-unnecessary-type-constraint': 'warn',
'react/display-name': 'off',
'react/no-unescaped-entities': 'off',
'react-hooks/incompatible-library': 'off',
'react-hooks/refs': 'off',
'react-hooks/set-state-in-effect': 'off',
'react-hooks/use-memo': 'off',
'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
},
},
+7 -6
View File
@@ -32,8 +32,8 @@
"ethers": "6.14.3",
"evm-proxy-detection": "^1.0.0",
"localforage": "^1.10.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-hook-form": "^7.54.2",
"react-is": "^19.1.0",
"react-media": "^1.10.0",
@@ -43,7 +43,6 @@
"zod": "^3.24.1"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.18.0",
"@faker-js/faker": "^9.3.0",
"@hookform/devtools": "^4.3.2",
@@ -52,21 +51,23 @@
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^22.10.7",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@types/react": "~19.2.10",
"@types/react-dom": "~19.2.0",
"@types/styled-components": "^5.1.34",
"@typescript-eslint/eslint-plugin": "^8.31.1",
"@typescript-eslint/parser": "^8.31.1",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.29.0",
"eslint-config-next": "^16.0.5",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"msw": "^2.7.3",
"prettier": "^3.6.2",
"ts-jest": "^29.2.5",
"typescript": "~5.9.2",
"typescript-eslint": "^8.31.1",
"vite": "^6.0.7",
"vite-plugin-svgr": "^4.3.0"
}
+8 -8
View File
@@ -57,16 +57,16 @@
"@cowprotocol/widget-react": "0.14.6",
"@datadog/browser-rum": "^6.24.1",
"@ducanh2912/next-pwa": "^10.2.9",
"@emotion/cache": "^11.13.5",
"@emotion/react": "^11.13.5",
"@emotion/cache": "^11.14.0",
"@emotion/react": "^11.14.0",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.13.5",
"@emotion/styled": "^11.14.0",
"@gnosis.pm/zodiac": "^4.0.3",
"@ledgerhq/context-module": "^1.8.0",
"@ledgerhq/device-management-kit": "^0.9.1",
"@ledgerhq/device-signer-kit-ethereum": "^1.8.0",
"@ledgerhq/device-transport-kit-web-hid": "^1.2.0",
"@mui/icons-material": "^6.1.6",
"@mui/icons-material": "^6.5.0",
"@mui/material": "^6.5.0",
"@mui/x-date-pickers": "^7.23.3",
"@next/third-parties": "^15.2.0",
@@ -110,9 +110,9 @@
"papaparse": "^5.3.2",
"postcss": "^8.5.6",
"qrcode.react": "^3.1.0",
"react": "19.1.0",
"react": "19.2.0",
"react-day-picker": "^9.13.0",
"react-dom": "19.1.0",
"react-dom": "19.2.0",
"react-dropzone": "^14.2.3",
"react-hook-form": "7.41.1",
"react-papaparse": "^4.0.2",
@@ -169,8 +169,8 @@
"@types/mdx": "^2.0.13",
"@types/node": "22.13.1",
"@types/qrcode": "^1.5.5",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/react": "~19.2.10",
"@types/react-dom": "~19.2.0",
"@types/semver": "^7.3.10",
"@typescript-eslint/eslint-plugin": "^7.6.0",
"@typescript-eslint/parser": "^8.18.1",
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react'
import { fn } from '@storybook/test'
import { fn } from 'storybook/test'
import { createMockStory } from '@/stories/mocks'
import HiddenTokenButton from './index'
@@ -13,8 +13,8 @@ exports[`./DatePickerInput.stories AllowFutureDates 1`] = `
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined MuiFormLabel-colorPrimary MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined mui-style-ll8nw6-MuiFormLabel-root-MuiInputLabel-root"
data-shrink="false"
for="«r1»"
id="«r1»-label"
for="_r_1_"
id="_r_1_-label"
>
Expiry date
</label>
@@ -25,7 +25,7 @@ exports[`./DatePickerInput.stories AllowFutureDates 1`] = `
aria-invalid="false"
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputAdornedEnd mui-style-f423sj-MuiInputBase-input-MuiOutlinedInput-input"
id="«r1»"
id="_r_1_"
inputmode="text"
name="expiryDate"
placeholder="DD/MM/YYYY"
@@ -85,8 +85,8 @@ exports[`./DatePickerInput.stories Default 1`] = `
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined MuiFormLabel-colorPrimary MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined mui-style-ll8nw6-MuiFormLabel-root-MuiInputLabel-root"
data-shrink="false"
for="«r5»"
id="«r5»-label"
for="_r_5_"
id="_r_5_-label"
>
Birth date
</label>
@@ -97,7 +97,7 @@ exports[`./DatePickerInput.stories Default 1`] = `
aria-invalid="false"
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputAdornedEnd mui-style-f423sj-MuiInputBase-input-MuiOutlinedInput-input"
id="«r5»"
id="_r_5_"
inputmode="text"
name="birthDate"
placeholder="DD/MM/YYYY"
@@ -157,8 +157,8 @@ exports[`./DatePickerInput.stories DisableFutureDates 1`] = `
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined MuiFormLabel-colorPrimary MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined mui-style-ll8nw6-MuiFormLabel-root-MuiInputLabel-root"
data-shrink="false"
for="«r9»"
id="«r9»-label"
for="_r_9_"
id="_r_9_-label"
>
Created date
</label>
@@ -169,7 +169,7 @@ exports[`./DatePickerInput.stories DisableFutureDates 1`] = `
aria-invalid="false"
autocomplete="off"
class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputAdornedEnd mui-style-f423sj-MuiInputBase-input-MuiOutlinedInput-input"
id="«r9»"
id="_r_9_"
inputmode="text"
name="createdDate"
placeholder="DD/MM/YYYY"
@@ -111,7 +111,7 @@ function EnhancedTableHead(props: EnhancedTableHeadProps) {
>
{headCell.label}
{orderBy === headCell.id ? (
<Box component="span" sx={visuallyHidden}>
<Box component="span" sx={{ ...visuallyHidden }}>
{order === 'desc' ? 'sorted descending' : 'sorted ascending'}
</Box>
) : null}
@@ -1,6 +1,6 @@
import { BRAND_NAME, IS_PRODUCTION, IS_BEHIND_IAP } from '@/config/constants'
import { ContentSecurityPolicy, StrictTransportSecurity } from '@/config/securityHeaders'
import { lightPalette, darkPalette } from '@safe-global/theme'
import { lightPalette, darkPalette } from '@safe-global/theme/palettes'
const descriptionText = `${BRAND_NAME} is the most trusted smart account wallet on Ethereum with over $100B secured.`
const titleText = BRAND_NAME
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react'
import { fn } from '@storybook/test'
import { fn } from 'storybook/test'
import { Button, DialogActions, DialogContent, Typography } from '@mui/material'
import { StoreDecorator } from '@/stories/storeDecorator'
import ModalDialog from './index'
@@ -13,8 +13,8 @@ exports[`./NameInput.stories Default 1`] = `
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined MuiFormLabel-colorPrimary MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined mui-style-ll8nw6-MuiFormLabel-root-MuiInputLabel-root"
data-shrink="false"
for="«r1»"
id="«r1»-label"
for="_r_1_"
id="_r_1_-label"
>
Owner name
</label>
@@ -24,7 +24,7 @@ exports[`./NameInput.stories Default 1`] = `
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input mui-style-1eoo0u4-MuiInputBase-input-MuiOutlinedInput-input"
id="«r1»"
id="_r_1_"
name="ownerName"
type="text"
/>
@@ -59,8 +59,8 @@ exports[`./NameInput.stories Disabled 1`] = `
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined MuiFormLabel-colorPrimary Mui-disabled MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined mui-style-ll8nw6-MuiFormLabel-root-MuiInputLabel-root"
data-shrink="false"
for="«r3»"
id="«r3»-label"
for="_r_3_"
id="_r_3_-label"
>
Owner name
</label>
@@ -71,7 +71,7 @@ exports[`./NameInput.stories Disabled 1`] = `
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input Mui-disabled mui-style-1eoo0u4-MuiInputBase-input-MuiOutlinedInput-input"
disabled=""
id="«r3»"
id="_r_3_"
name="ownerName"
type="text"
/>
@@ -106,8 +106,8 @@ exports[`./NameInput.stories Required 1`] = `
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined MuiFormLabel-colorPrimary Mui-required MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined mui-style-ll8nw6-MuiFormLabel-root-MuiInputLabel-root"
data-shrink="false"
for="«r5»"
id="«r5»-label"
for="_r_5_"
id="_r_5_-label"
>
Owner name
<span
@@ -124,7 +124,7 @@ exports[`./NameInput.stories Required 1`] = `
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input mui-style-1eoo0u4-MuiInputBase-input-MuiOutlinedInput-input"
id="«r5»"
id="_r_5_"
name="ownerName"
required=""
type="text"
@@ -162,8 +162,8 @@ exports[`./NameInput.stories WithPlaceholder 1`] = `
<label
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined MuiFormLabel-colorPrimary MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeMedium MuiInputLabel-outlined mui-style-ll8nw6-MuiFormLabel-root-MuiInputLabel-root"
data-shrink="false"
for="«r7»"
id="«r7»-label"
for="_r_7_"
id="_r_7_-label"
>
Owner name
</label>
@@ -173,7 +173,7 @@ exports[`./NameInput.stories WithPlaceholder 1`] = `
<input
aria-invalid="false"
class="MuiInputBase-input MuiOutlinedInput-input mui-style-1eoo0u4-MuiInputBase-input-MuiOutlinedInput-input"
id="«r7»"
id="_r_7_"
name="ownerName"
placeholder="Enter owner name"
type="text"
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { Box } from '@mui/system'
import { lightPalette } from '@safe-global/theme'
import { lightPalette } from '@safe-global/theme/palettes'
import css from './styles.module.css'
import { Card, LinearProgress, CardHeader, Avatar, Typography, CardContent } from '@mui/material'
import type { TxStepperProps } from './useCardStepper'
@@ -4,7 +4,7 @@ import type { NewSafeFormData } from '@/components/new-safe/create'
import { getRedirect } from '@/components/new-safe/create/logic'
import StatusMessage from '@/components/new-safe/create/steps/StatusStep/StatusMessage'
import useUndeployedSafe from '@/components/new-safe/create/steps/StatusStep/useUndeployedSafe'
import { lightPalette } from '@safe-global/theme'
import { lightPalette } from '@safe-global/theme/palettes'
import { AppRoutes } from '@/config/routes'
import { safeCreationPendingStatuses } from '@/features/counterfactual'
import { SafeCreationEvent, safeCreationSubscribe, isPredictedSafeProps } from '@/features/counterfactual/services'
@@ -1,7 +1,7 @@
import { useState } from 'react'
import { Box, Checkbox, FormControlLabel, Typography } from '@mui/material'
import WarningAmberOutlinedIcon from '@mui/icons-material/WarningAmberOutlined'
import { lightPalette } from '@safe-global/theme'
import { lightPalette } from '@safe-global/theme/palettes'
import Domain from './Domain'
type UnknownAppWarningProps = {
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react'
import { fn } from '@storybook/test'
import { fn } from 'storybook/test'
import { createMockStory } from '@/stories/mocks'
import QrModal from './QrModal'
+3 -2
View File
@@ -1,6 +1,7 @@
import type { PaletteMode } from '@mui/material'
// This import includes MUI type extensions via side-effect
import { generateMuiTheme } from '@safe-global/theme'
// MUI type extensions (module augmentation for custom palette colors, button variants, etc.)
import '@safe-global/theme/generators/mui-extensions'
import { generateMuiTheme } from '@safe-global/theme/generators/mui'
/**
* Create Safe-themed MUI theme for the given mode.
+1
View File
@@ -21,6 +21,7 @@
"@gnosis.pm/zodiac/*": ["../../node_modules/@gnosis.pm/zodiac/*"],
"@cowprotocol/app-data": ["../../node_modules/@cowprotocol/app-data"],
"@/public/*": ["./public/*"],
"@safe-global/theme/*": ["../../packages/theme/src/*"],
"@safe-global/store/*": ["../../packages/store/src/*"],
"@safe-global/utils/*": ["../../packages/utils/src/*"],
"@safe-global/test/*": ["../../config/test/*"],
+33
View File
@@ -0,0 +1,33 @@
import pluginJs from '@eslint/js'
import tseslint from 'typescript-eslint'
import pluginReact from 'eslint-plugin-react'
import reactHooks from 'eslint-plugin-react-hooks'
/**
* Shared ESLint base config for packages that use React + TypeScript
* but do not need Next.js-specific rules.
*/
export default [
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
pluginReact.configs.flat['jsx-runtime'],
reactHooks.configs['recommended-latest'],
{
settings: { react: { version: 'detect' } },
rules: {
// Disable prop-types — TypeScript handles type checking
'react/prop-types': 'off',
// Match previous eslint-config-next leniency for these rules
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/ban-ts-comment': 'warn',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-empty-object-type': 'warn',
'@typescript-eslint/no-namespace': 'warn',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'warn',
'@typescript-eslint/no-unnecessary-type-constraint': 'warn',
'no-useless-escape': 'warn',
},
},
]
@@ -5,7 +5,7 @@ import {
withEntitlementsPlist,
withInfoPlist,
withXcodeProject,
} from '@expo/config-plugins'
} from 'expo/config-plugins'
import * as fs from 'fs'
import * as path from 'path'
import {
@@ -1,4 +1,4 @@
import { ConfigPlugin } from '@expo/config-plugins'
import { ConfigPlugin } from 'expo/config-plugins'
import { validateProps } from './utils'
import { NotifeeExpoPluginProps } from './types'
import NotifeeIos from './ios'
+2 -1
View File
@@ -27,7 +27,8 @@
"@ethersproject/signing-key/elliptic": "^6.6.1",
"stylus": "0.64.0",
"viem": "2.21.55",
"webpack": "5.97.1"
"webpack": "5.97.1",
"@tamagui/static/react-native-web": "0.21.1"
},
"devDependencies": {
"@storybook/builder-webpack5": "^10.2.6",
+3 -13
View File
@@ -1,18 +1,8 @@
import nextVitals from 'eslint-config-next/core-web-vitals'
import baseConfig from '../../config/eslint/base.mjs'
const eslintConfig = [
...nextVitals,
{
rules: {
'@next/next/no-html-link-for-pages': 'off',
'react-hooks/set-state-in-effect': 'off',
'react-hooks/preserve-manual-memoization': 'off',
'react-hooks/refs': 'off',
},
},
export default [
...baseConfig,
{
ignores: ['**/node_modules/'],
},
]
export default eslintConfig
+5 -2
View File
@@ -17,13 +17,16 @@
"build:local": "NODE_ENV=local yarn fetch-schema && yarn generate-api"
},
"devDependencies": {
"@eslint/js": "^9.18.0",
"@safe-global/test": "workspace:^",
"@types/jest": "^29.5.14",
"eslint": "^9.29.0",
"eslint-config-next": "^16.0.5",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"jest": "^29.7.0",
"prettier": "^3.6.2",
"typescript": "~5.9.2"
"typescript": "~5.9.2",
"typescript-eslint": "^8.31.1"
},
"dependencies": {
"jest-fixed-jsdom": "^0.0.10",
+3 -10
View File
@@ -1,15 +1,8 @@
import nextVitals from 'eslint-config-next/core-web-vitals'
import baseConfig from '../../config/eslint/base.mjs'
const eslintConfig = [
...nextVitals,
{
rules: {
'@next/next/no-html-link-for-pages': 'off',
},
},
export default [
...baseConfig,
{
ignores: ['**/node_modules/'],
},
]
export default eslintConfig
+9 -2
View File
@@ -17,22 +17,29 @@
"prettier:fix": "prettier --write . --config ../../.prettierrc --ignore-path ../../.prettierignore"
},
"peerDependencies": {
"@mui/material": "^6.5.0",
"tamagui": "^1.x"
},
"peerDependenciesMeta": {
"@mui/material": {
"optional": true
},
"tamagui": {
"optional": true
}
},
"devDependencies": {
"@eslint/js": "^9.18.0",
"@mui/material": "^6.5.0",
"@safe-global/test": "workspace:^",
"@types/jest": "^29.5.14",
"eslint": "^9.29.0",
"eslint-config-next": "^16.0.5",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"jest": "^29.7.0",
"prettier": "^3.6.2",
"typescript": "~5.9.2"
"typescript": "~5.9.2",
"typescript-eslint": "^8.31.1"
},
"dependencies": {
"jest-fixed-jsdom": "^0.0.10"
+10 -25
View File
@@ -1,26 +1,11 @@
// Main exports for @safe-global/theme package
// This package uses sub-path imports exclusively.
// Import from specific modules instead of this barrel file:
//
// import { lightPalette } from '@safe-global/theme/palettes/light'
// import { generateMuiTheme } from '@safe-global/theme/generators/mui'
// import { generateTamaguiColorTokens } from '@safe-global/theme/generators/tamagui'
// import { spacingMobile } from '@safe-global/theme/tokens/spacing'
//
// See package.json "exports" field for available sub-paths.
// MUI type extensions (side-effect import for TypeScript module augmentation)
import './generators/mui-extensions'
// Palettes
export { default as lightPalette } from './palettes/light'
export { default as darkPalette } from './palettes/dark'
export { default as staticColors } from './palettes/static'
export type { ColorPalette, StaticColors } from './palettes/types'
// Tokens
export * from './tokens'
// Generators
export { generateMuiTheme } from './generators/mui'
export {
generateTamaguiColorTokens,
generateTamaguiTokens,
generateTamaguiThemes,
generateTamaguiFontSizes,
} from './generators/tamagui'
export { generateCSSVars } from './generators/css-vars'
// Utilities
export { flattenPalette } from './utils/flatten'
export {}
+4
View File
@@ -0,0 +1,4 @@
export { default as lightPalette } from './light'
export { default as darkPalette } from './dark'
export { default as staticColors } from './static'
export type { ColorPalette, StaticColors } from './types'
+3 -10
View File
@@ -1,14 +1,9 @@
import nextVitals from 'eslint-config-next/core-web-vitals'
import baseConfig from '../../config/eslint/base.mjs'
const eslintConfig = [
...nextVitals,
export default [
...baseConfig,
{
rules: {
'@next/next/no-html-link-for-pages': 'off',
'react-hooks/set-state-in-effect': 'off',
'react-hooks/preserve-manual-memoization': 'off',
'react-hooks/refs': 'off',
'react-hooks/exhaustive-deps': [
'warn',
{
@@ -21,5 +16,3 @@ const eslintConfig = [
ignores: ['**/node_modules/', '**/src/types/contracts/'],
},
]
export default eslintConfig
+5 -2
View File
@@ -13,14 +13,17 @@
"test:coverage": "jest --coverage"
},
"devDependencies": {
"@eslint/js": "^9.18.0",
"@faker-js/faker": "^9.0.3",
"@types/jest": "^29.5.14",
"eslint": "^9.29.0",
"eslint-config-next": "^16.0.5",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"ethers": "6.14.3",
"jest": "^29.7.0",
"prettier": "^3.6.2",
"typescript": "~5.9.2"
"typescript": "~5.9.2",
"typescript-eslint": "^8.31.1"
},
"dependencies": {
"@cowprotocol/app-data": "^3.1.0",
+7
View File
@@ -17,6 +17,13 @@ const DEPS_TO_CHECK = [
'@ledgerhq/device-management-kit',
'@ledgerhq/device-signer-kit-ethereum',
'@types/jest',
'@types/react',
'@types/react-dom',
'storybook',
'@emotion/react',
'@emotion/styled',
'@mui/material',
'@mui/icons-material',
'@safe-global/protocol-kit',
'@safe-global/safe-apps-sdk',
'@safe-global/safe-deployments',
+1833 -2589
View File
File diff suppressed because it is too large Load Diff