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

457 lines
17 KiB
TypeScript

import { NotifeeExpoPluginProps } from './types'
import {
ConfigPlugin,
withDangerousMod,
withEntitlementsPlist,
withInfoPlist,
withXcodeProject,
} from 'expo/config-plugins'
import * as fs from 'fs'
import * as path from 'path'
import {
APP_VERSION_STRING,
APP_GROUPS_PLACEHOLDER,
BACKGROUND_MODES_TO_ENABLE,
BUNDLE_IDENTIFIER_STRING,
DEFAULT_APP_VERSION,
DEFAULT_IOS_BUILD_NUMBER,
DEFAULT_IOS_DEPLOYMENT_TARGET,
EXTENSION_SERVICE_FILE,
EXTENSION_SERVICE_NAME,
FILES_TO_ADD,
IOS_BUILD_NUMBER_STRING,
NOTIFICATION_APP_GROUP_IDENTIFIER_STRING,
PODFILE_MODIF_NEEDED,
PODFILE_TARGET_STRING,
TARGET_DEVICES,
USER_ACTIVITY_TYPES_KEYS,
} from './config'
import { log, logError, throwError } from './utils'
/**
* Adds Notifee to the iOS Podfile within an Expo project configuration.
*
* @param {object} c - The Expo configuration object.
* @returns {object} - The updated Expo configuration object after modifying the Podfile.
*/
const addNotifeeToPodfile: ConfigPlugin<NotifeeExpoPluginProps> = (c) => {
return withDangerousMod(c, [
'ios',
async (c) => {
const pathToPodfile = path.join(c.modRequest.projectRoot, 'ios', 'Podfile')
try {
const podfile = fs.readFileSync(pathToPodfile, 'utf8')
const hasAlreadyNeededChanges = podfile.includes(PODFILE_TARGET_STRING)
//Add at end of podfile
if (!hasAlreadyNeededChanges) {
fs.appendFileSync(pathToPodfile, PODFILE_MODIF_NEEDED)
}
log('Added Notifee to Podfile')
} catch {
throwError('Error when trying to add Notifee to Podfile')
}
return c
},
])
}
/**
* Adds necessary notification service files to the iOS project for Notifee configuration.
*
* @param {object} c - The Expo configuration object.
* @param {NotifeeExpoPluginProps} props - The properties required for configuring Notifee-Expo-Plugin.
* @returns {object} - The updated Expo configuration object after adding the notification service files.
*/
const addNotificationServiceFilesToProject: ConfigPlugin<NotifeeExpoPluginProps> = (c, props) => {
const serviceExtensionFilesFolderPath = path.join(
require.resolve('@safe-global/notification-service-ios/package.json'),
'../ios-notification-service-files/',
)
const updatedConfig = withDangerousMod(c, [
'ios',
async (config) => {
const p = path.join(config.modRequest.projectRoot, 'ios')
try {
//Create folders
fs.mkdirSync(path.join(p, EXTENSION_SERVICE_NAME), { recursive: true })
if (!config.version) {
logError("You need to define 'version' in the expo config!")
}
const appVersion = !!config.version ? config.version : DEFAULT_APP_VERSION
if (!c.ios || !c.ios.bundleIdentifier) {
logError("You need to define 'bundleIdentifier' in the ios object of the expo config!")
}
const bundleIdentifier = 'group.' + config.ios?.bundleIdentifier + '.notifee'
if (!c.ios || !c.ios.bundleIdentifier) {
logError("You need to define 'buildNumber' in the ios object of the expo config!")
}
const buildNumber = !!config.ios?.buildNumber ? config.ios.buildNumber : DEFAULT_IOS_BUILD_NUMBER
//Transfer files & Edit necessary values
for (const fileName of FILES_TO_ADD) {
const pathToFileToRead = path.join(serviceExtensionFilesFolderPath, fileName)
const pathWhereToWrite = path.join(p, EXTENSION_SERVICE_NAME, fileName)
let file = fs.readFileSync(pathToFileToRead, 'utf8')
if (fileName === EXTENSION_SERVICE_NAME + '-Info.plist') {
file = file.replace(APP_VERSION_STRING, appVersion)
file = file.replace(IOS_BUILD_NUMBER_STRING, buildNumber)
} else if (fileName === EXTENSION_SERVICE_NAME + '.entitlements') {
// Build the app groups array for entitlements
const defaultNotifeeGroup = bundleIdentifier
const mainAppGroup = props.appGroupIdentifier || `group.${config.ios?.bundleIdentifier}`
const additionalGroups = props.additionalAppGroups || []
// Combine all app groups (notifee group + main app group + additional groups)
const allAppGroups = [defaultNotifeeGroup, mainAppGroup, ...additionalGroups]
// Remove duplicates
const uniqueAppGroups = Array.from(new Set(allAppGroups))
// Generate the XML string for app groups
const appGroupsXml = uniqueAppGroups.map((group) => `\t\t<string>${group}</string>`).join('\n')
file = file.replace(APP_GROUPS_PLACEHOLDER, appGroupsXml)
}
fs.writeFileSync(pathWhereToWrite, file)
}
// Copy Utils folder if it exists
const sourceUtilsPath = path.join(serviceExtensionFilesFolderPath, 'Utils')
const destUtilsPath = path.join(p, EXTENSION_SERVICE_NAME, 'Utils')
if (fs.existsSync(sourceUtilsPath)) {
// Create Utils directory if it doesn't exist
fs.mkdirSync(destUtilsPath, { recursive: true })
// Copy all files from Utils directory
const utilsFiles = fs.readdirSync(sourceUtilsPath)
for (const utilFile of utilsFiles) {
const sourcePath = path.join(sourceUtilsPath, utilFile)
const destPath = path.join(destUtilsPath, utilFile)
fs.copyFileSync(sourcePath, destPath)
}
log('Copied Utils folder successfully!')
}
const notificationServicePath = !!props.customNotificationServiceFilePath
? props.customNotificationServiceFilePath
: path.join(serviceExtensionFilesFolderPath, EXTENSION_SERVICE_FILE)
const pathWhereToWriteNotificationService = path.join(p, EXTENSION_SERVICE_NAME, EXTENSION_SERVICE_FILE)
// Determine the app group identifier to use
const appGroupIdentifier = props.appGroupIdentifier || `group.${config.ios?.bundleIdentifier}`
let notificationServiceFile = fs.readFileSync(notificationServicePath, 'utf8')
// Replace the app group identifier placeholder
notificationServiceFile = notificationServiceFile.replaceAll(
NOTIFICATION_APP_GROUP_IDENTIFIER_STRING,
appGroupIdentifier,
)
fs.writeFileSync(pathWhereToWriteNotificationService, notificationServiceFile)
log('Added NotificationService files!')
} catch {
logError('serviceExtensionFilesFolderPath: ' + serviceExtensionFilesFolderPath)
logError('Error while copying notification service files')
}
return config
},
])
//Make files added before available in xcode project
return withXcodeProject(updatedConfig, (nc) => {
const x = nc.modResults
// Create the main service extension group
const mainGroup = x.addPbxGroup(
[...FILES_TO_ADD, EXTENSION_SERVICE_FILE],
EXTENSION_SERVICE_NAME,
EXTENSION_SERVICE_NAME,
)
// Create Utils subgroup if files exist
const sourceUtilsPath = path.join(serviceExtensionFilesFolderPath, 'Utils')
if (fs.existsSync(sourceUtilsPath)) {
const utilsFiles = fs.readdirSync(sourceUtilsPath)
if (utilsFiles.length > 0) {
// Create a Utils subgroup within the main group
const utilsGroup = x.addPbxGroup(utilsFiles, 'Utils', 'Utils')
// Add the Utils group to the main group
x.addToPbxGroup(utilsGroup.uuid, mainGroup.uuid)
// Add Utils files to Sources build phase
const target = x.pbxTargetByName(EXTENSION_SERVICE_NAME)
if (target) {
const sourcesBuildPhase = x.buildPhaseObject('PBXSourcesBuildPhase', 'Sources', target.uuid)
if (sourcesBuildPhase) {
for (const utilFile of utilsFiles) {
if (utilFile.endsWith('.swift')) {
x.addToPbxBuildFileSection(`Utils/${utilFile}`)
x.addToPbxSourcesBuildPhase(`Utils/${utilFile}`, sourcesBuildPhase)
}
}
}
}
}
}
// Add the main group to the root group
const pbxs = x.hash.project.objects['PBXGroup']
Object.keys(pbxs).forEach(function (v) {
if (typeof pbxs[v] === 'object' && !pbxs[v].name && !pbxs[v].path) {
x.addToPbxGroup(mainGroup.uuid, v)
}
})
return nc
})
}
/**
* Signs the main iOS app target and the notification service extension target with the specified Apple development team ID.
*
* @param {object} c - The current Expo configuration object.
* @param {NotifeeExpoPluginProps} props - The properties containing the Apple development team ID.
* @returns {object} - The updated Expo configuration object after signing targets.
*/
const signAppAndNotificationServiceExtension: ConfigPlugin<NotifeeExpoPluginProps> = (c, props) => {
if (!props.appleDevTeamId) {
return c
}
return withXcodeProject(c, (nc) => {
const xcodeProject = nc.modResults
//Sign main target
const mainTarget = xcodeProject.pbxTargetByName(c.name)
if (mainTarget) {
xcodeProject.addTargetAttribute('DevelopmentTeam', props.appleDevTeamId, mainTarget)
}
//Sign notification service extension target
const target = xcodeProject.pbxTargetByName(EXTENSION_SERVICE_NAME)
if (target) {
xcodeProject.addTargetAttribute('DevelopmentTeam', props.appleDevTeamId, target)
}
log('Signed the main app and notification service extension targets with: ' + props.appleDevTeamId)
return nc
})
}
/**
* Sets the APS Environment Entitlement in the app's entitlements plist file to specify whether to use the development or production Apple Push Notification service (APNs).
*
* @param {object} c - The current Expo configuration object.
* @param {NotifeeExpoPluginProps} props - The properties containing the APS environment mode (production or development).
* @returns {object} - The updated Expo configuration object after setting the APS environment.
*/
const setAPSEnvironment: ConfigPlugin<NotifeeExpoPluginProps> = (c, props) => {
return withEntitlementsPlist(c, (nc) => {
nc.modResults['aps-environment'] = props.apsEnvMode
log('Set aps-environment to: ' + props.apsEnvMode)
return nc
})
}
/**
* Adds the application group entitlement necessary for Notifee to the iOS project's entitlements plist.
*
* @param {object} c - The Expo configuration object.
* @returns {object} - The updated Expo configuration object with added application group entitlement.
*/
const addNotificationServiceGroup: ConfigPlugin<NotifeeExpoPluginProps> = (c) => {
return withEntitlementsPlist(c, (nc) => {
const g = 'com.apple.security.application-groups'
if (!Array.isArray(nc.modResults[g])) {
nc.modResults[g] = []
}
const gName = `group.${nc.ios?.bundleIdentifier}`
const modResults = nc.modResults[g]
if (!modResults.includes(gName)) {
modResults.push(gName)
}
log(`Added '${gName} to com.apple.security.application-groups`)
return nc
})
}
/**
* Adds required background modes to the iOS project's Info.plist for Notifee functionality.
*
* @param {object} c - The Expo configuration object.
* @returns {object} - The updated Expo configuration object with added background modes.
*/
const addBackgroundModes: ConfigPlugin<NotifeeExpoPluginProps> = (c, props) => {
return withInfoPlist(c, (nc) => {
//Added this condition so it doesn't add background modes capability without anything selected when the user wants no background modes.
if (props.backgroundModes && props.backgroundModes.length === 0) {
return nc
}
if (!Array.isArray(nc.modResults.UIBackgroundModes)) {
nc.modResults.UIBackgroundModes = []
}
if (!props.backgroundModes) {
props.backgroundModes = BACKGROUND_MODES_TO_ENABLE
}
for (const mode of props.backgroundModes) {
if (!nc.modResults.UIBackgroundModes.includes(mode)) {
nc.modResults.UIBackgroundModes.push(mode)
}
}
log('Added background modes (' + props.backgroundModes.join(', ') + ')')
return nc
})
}
/**
* Enables communication notifications capability for the iOS project if specified in props.
* This includes setting entitlements and adding necessary keys to Info.plist.
*
* @param {object} c - The Expo configuration object.
* @param {NotifeeExpoPluginProps} props - The properties object containing configuration options.
* @returns {object} - The updated Expo configuration object with added communication notifications capability.
*/
const addCommunicationNotificationsCapability: ConfigPlugin<NotifeeExpoPluginProps> = (c, props) => {
if (!props.enableCommunicationNotifications) {
return c
}
const updatedConfig = withEntitlementsPlist(c, (nc) => {
if (props.enableCommunicationNotifications) {
nc.modResults['com.apple.developer.usernotifications.communication'] = true
}
log('Added communication notifications capability')
return nc
})
return withInfoPlist(updatedConfig, (nc) => {
if (!Array.isArray(nc.modResults.NSUserActivityTypes)) {
nc.modResults.NSUserActivityTypes = []
}
for (const v of USER_ACTIVITY_TYPES_KEYS) {
if (!nc.modResults.NSUserActivityTypes.includes(v)) {
nc.modResults.NSUserActivityTypes.push(v)
}
}
log('Added INSendMessageIntent to NSUserActivityTypes for communication notifications')
return nc
})
}
/**
* Creates and adds a notification service extension target to the Xcode project if it doesn't already exist.
* Configures necessary settings and build phases for the extension target.
*
* @param {object} c - The Expo configuration object.
* @param {NotifeeExpoPluginProps} props - The properties object containing configuration options.
* @returns {object} - The updated Expo configuration object with the added notification service extension target.
*/
const createAndAddNotificationServiceExtensionTarget: ConfigPlugin<NotifeeExpoPluginProps> = (c, props) => {
return withXcodeProject(c, (nc) => {
const x = nc.modResults
if (!!x.pbxTargetByName(EXTENSION_SERVICE_NAME)) {
return nc
}
/**
* Needed or project with one target won't add notification extension service target
* correctly and it will throw cannot install podfiles
*/
const po = x.hash.project.objects
po['PBXContainerItemProxy'] = po['PBXTargetDependency'] ?? {}
po['PBXTargetDependency'] = po['PBXTargetDependency'] ?? {}
// Create a new target for the notification service extension
const newTargetBundleIdentifier = c.ios?.bundleIdentifier + '.' + EXTENSION_SERVICE_NAME
const nt = x.addTarget(EXTENSION_SERVICE_NAME, 'app_extension', EXTENSION_SERVICE_NAME, newTargetBundleIdentifier)
// Add necessary files to the new target
x.addBuildPhase([], 'PBXFrameworksBuildPhase', 'Frameworks', nt.uuid)
x.addBuildPhase([], 'PBXResourcesBuildPhase', 'Resources', nt.uuid)
x.addBuildPhase(['NotificationService.swift'], 'PBXSourcesBuildPhase', 'Sources', nt.uuid)
// Set the info of notification service extension target
const config = x.pbxXCBuildConfigurationSection()
for (const v in config) {
if (!!config[v].buildSettings && config[v].buildSettings.PRODUCT_NAME === `"${EXTENSION_SERVICE_NAME}"`) {
config[v].buildSettings = {
...config[v].buildSettings,
TARGETED_DEVICE_FAMILY: TARGET_DEVICES,
IPHONEOS_DEPLOYMENT_TARGET: props.iosDeploymentTarget ?? DEFAULT_IOS_DEPLOYMENT_TARGET,
DEVELOPMENT_TEAM: props.appleDevTeamId,
CODE_SIGN_ENTITLEMENTS: `${EXTENSION_SERVICE_NAME}/${EXTENSION_SERVICE_NAME}.entitlements`,
CODE_SIGN_STYLE: 'Automatic',
SWIFT_VERSION: '5.0',
}
} else if (!!config[v].buildSettings && config[v].buildSettings.PRODUCT_NAME === `"${c.name}"`) {
config[v].buildSettings = {
...config[v].buildSettings,
DEVELOPMENT_TEAM: props.appleDevTeamId,
}
}
}
log(`Created Notification Service Extension (${newTargetBundleIdentifier})`)
return nc
})
}
/**
* Adds the Notifee target to the Expo app extensions configuration for EAS builds.
* Configures the target name, bundle identifier, and entitlements for the Notifee extension.
*
* @param {object} c - The Expo configuration object.
* @returns {object} - The updated Expo configuration object with Notifee target added to app extensions.
*/
const addNotifeeTargetToExpoAppExtensions: ConfigPlugin<NotifeeExpoPluginProps> = (c) => {
const bundleIdentifier = c.ios?.bundleIdentifier + '.' + EXTENSION_SERVICE_NAME
const expoAppExtension = {
targetName: EXTENSION_SERVICE_NAME,
bundleIdentifier,
entitlements: {
'com.apple.security.application-groups': [
`group.${c.ios?.bundleIdentifier}.notifee`,
`group.${c.ios?.bundleIdentifier}`,
],
},
}
return {
...c,
extra: {
...c.extra,
eas: {
...c.extra?.eas,
build: {
...c.extra?.eas?.build,
experimental: {
...c.extra?.eas?.build?.experimental,
ios: {
...c.extra?.eas?.build?.experimental?.ios,
appExtensions: [...(c.extra?.eas?.build?.experimental?.ios?.appExtensions ?? []), expoAppExtension],
},
},
},
},
},
}
}
export default {
setAPSEnvironment,
addCommunicationNotificationsCapability,
addBackgroundModes,
addNotificationServiceFilesToProject,
addNotifeeToPodfile,
signAppAndNotificationServiceExtension,
createAndAddNotificationServiceExtensionTarget,
addNotifeeTargetToExpoAppExtensions,
addNotificationServiceGroup,
}