Files
01c352636c 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

* 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.

* 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

* 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

* 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.

* 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.

* 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

* 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 <[email protected]>
2026-03-18 09:29:16 +01:00

188 lines
6.1 KiB
JavaScript

/* eslint-disable no-undef */
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
function withIOSSSLPinning(config, { domains }) {
// Add TrustKit to Podfile
config = withPodfile(config, (config) => {
const podfileContent = config.modResults.contents
// Check if TrustKit is already added
if (!podfileContent.includes('TrustKit')) {
// Add TrustKit pod
const podfileLines = podfileContent.split('\n')
const targetIndex = podfileLines.findIndex((line) => line.includes('use_expo_modules!'))
if (targetIndex !== -1) {
podfileLines.splice(targetIndex + 1, 0, " pod 'TrustKit'")
config.modResults.contents = podfileLines.join('\n')
}
}
return config
})
// Modify AppDelegate.swift to include SSL pinning
config = withAppDelegate(config, (config) => {
const appDelegateContent = config.modResults.contents
const domainConfigs = Object.entries(domains)
.map(([domain, hashes]) => {
const hashesString = hashes.map((hash) => `"${hash}"`).join(', ')
return ` "${domain}": [
kTSKPublicKeyHashes: [${hashesString}],
kTSKEnforcePinning: true,
kTSKIncludeSubdomains: true,
kTSKReportUris: []
]`
})
.join(',\n')
const trustKitConfig = `
// SSL Pinning Configuration
let trustKitConfig: [String: Any] = [
kTSKSwizzleNetworkDelegates: true,
kTSKPinnedDomains: [
${domainConfigs}
]
]
TrustKit.initSharedInstance(withConfiguration: trustKitConfig)
print("SSL Pinning initialized successfully")
`
// Add TrustKit import if not present
if (!appDelegateContent.includes('import TrustKit')) {
config.modResults.contents = appDelegateContent.replace(
'import ReactAppDependencyProvider',
'import ReactAppDependencyProvider\nimport TrustKit',
)
}
// Add TrustKit initialization before React Native starts
if (!appDelegateContent.includes('TrustKit.initSharedInstance')) {
// Primary injection point: before factory.startReactNative
const startReactNativePattern = /([ ]+)(factory\.startReactNative\(\s*withModuleName:)/
if (startReactNativePattern.test(appDelegateContent)) {
config.modResults.contents = config.modResults.contents.replace(
startReactNativePattern,
`${trustKitConfig}
$1$2`,
)
} else {
// Fallback: after window initialization
const windowPattern = /(window = UIWindow\(frame: UIScreen\.main\.bounds\))/
if (windowPattern.test(appDelegateContent)) {
config.modResults.contents = config.modResults.contents.replace(windowPattern, `$1${trustKitConfig}`)
} else {
console.warn('⚠️ Could not find suitable injection point for SSL Pinning in AppDelegate.swift')
}
}
}
return config
})
return config
}
function withAndroidSSLPinning(config, { domains }) {
// Create SSLPinningFactory.kt
config = withDangerousMod(config, [
'android',
async (config) => {
const packageName = config.android?.package || 'global.safe.mobileapp'
const packagePath = packageName.replace(/\./g, '/')
const kotlinDir = path.join(config.modRequest.platformProjectRoot, 'app/src/main/java', packagePath)
// Ensure directory exists
if (!fs.existsSync(kotlinDir)) {
fs.mkdirSync(kotlinDir, { recursive: true })
}
// domains passed from configuration
const pinningConfig = Object.entries(domains)
.map(([domain, hashes]) => {
const hashesString = hashes.map((hash) => `"sha256/${hash}"`).join(', ')
return ` .add("${domain}", ${hashesString})`
})
.join('\n')
const sslPinningFactoryContent = `package ${packageName}
import com.facebook.react.modules.network.OkHttpClientFactory
import com.facebook.react.modules.network.ReactCookieJarContainer
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
class SSLPinningFactory : OkHttpClientFactory {
override fun createNewNetworkModuleClient(): OkHttpClient {
val certificatePinner = CertificatePinner.Builder()
${pinningConfig}
.build()
return OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.cookieJar(ReactCookieJarContainer())
.build()
}
}`
const sslPinningFactoryPath = path.join(kotlinDir, 'SSLPinningFactory.kt')
fs.writeFileSync(sslPinningFactoryPath, sslPinningFactoryContent)
return config
},
])
// Modify MainApplication.kt
config = withMainApplication(config, (config) => {
const mainApplicationContent = config.modResults.contents
const packageName = config.android?.package || 'global.safe.mobileapp'
// Add imports if not present
if (!mainApplicationContent.includes('import com.facebook.react.modules.network.OkHttpClientProvider')) {
config.modResults.contents = mainApplicationContent.replace(
'import com.facebook.react.ReactApplication',
`import com.facebook.react.ReactApplication
import com.facebook.react.modules.network.OkHttpClientProvider
import ${packageName}.SSLPinningFactory`,
)
}
// Add SSL pinning initialization in onCreate
if (!mainApplicationContent.includes('OkHttpClientProvider.setOkHttpClientFactory')) {
const onCreatePattern = /(override fun onCreate\(\) {\s*super\.onCreate\(\))/s
config.modResults.contents = config.modResults.contents.replace(
onCreatePattern,
`$1
// Initialize SSL Pinning
OkHttpClientProvider.setOkHttpClientFactory(SSLPinningFactory())`,
)
}
return config
})
return config
}
module.exports = function withSSLPinning(config, options = {}) {
const { domains = {} } = options
// Apply iOS SSL pinning
config = withIOSSSLPinning(config, { domains })
// Apply Android SSL pinning
config = withAndroidSSLPinning(config, { domains })
return config
}