chore(android): target API 36 with Expo 54

Upgrade the mobile runtime and tracked Android project to compile and target API 36. Align Expo modules, React Native, Node, Gradle, and Android build tooling while preserving the custom widget sources.\n\nAdapt file-system, notification, animation, test, and Metro compatibility for the SDK 54 runtime.
This commit is contained in:
jjbrunton committed 2026-08-28 20:01:15 +01:00
1 parent 1be54175e4
commit f35114dc77
30 files changed
+4061 -5634

No files matched your search

+1 -1
View File
@@ -1 +1 @@
20.10.0 20.19.4
+2 -2
View File
@@ -44,8 +44,8 @@
"tailwind-merge": "^2.2.0" "tailwind-merge": "^2.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.0", "@types/react": "~19.1.10",
"@types/react-dom": "^18.2.0", "@types/react-dom": "~19.1.7",
"@vitejs/plugin-react": "^4.2.0", "@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"postcss": "^8.4.32", "postcss": "^8.4.32",
+15 -10
View File
@@ -14,6 +14,7 @@ react {
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc" hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile() codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
enableBundleCompression = (findProperty('android.enableBundleCompression') ?: false).toBoolean()
// Use Expo CLI to bundle the app, this ensures the Metro config // Use Expo CLI to bundle the app, this ensures the Metro config
// works correctly with Expo projects. // works correctly with Expo projects.
cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim()) cliFile = new File(["node", "--print", "require.resolve('@expo/cli', { paths: [require.resolve('expo/package.json')] })"].execute(null, rootDir).text.trim())
@@ -63,9 +64,9 @@ react {
} }
/** /**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode. * Set this to true in release builds to optimize the app using R8.
*/ */
def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean() def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBuilds') ?: false).toBoolean()
/** /**
* The preferred build flavor of JavaScriptCore (JSC) * The preferred build flavor of JavaScriptCore (JSC)
@@ -78,7 +79,7 @@ def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInRelea
* give correct results when using with locales other than en-US. Note that * give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default. * this variant is about 6MiB larger per architecture than default.
*/ */
def jscFlavor = 'org.webkit:android-jsc:+' def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
android { android {
ndkVersion rootProject.ext.ndkVersion ndkVersion rootProject.ext.ndkVersion
@@ -93,6 +94,7 @@ android {
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 46 versionCode 46
versionName "1.0.5" versionName "1.0.5"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
} }
signingConfigs { signingConfigs {
debug { debug {
@@ -110,15 +112,18 @@ android {
// Caution! In production, you need to generate your own keystore file. // Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android. // see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false) def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false'
minifyEnabled enableProguardInReleaseBuilds shrinkResources enableShrinkResources.toBoolean()
minifyEnabled enableMinifyInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true) def enablePngCrunchInRelease = findProperty('android.enablePngCrunchInReleaseBuilds') ?: 'true'
crunchPngs enablePngCrunchInRelease.toBoolean()
} }
} }
packagingOptions { packagingOptions {
jniLibs { jniLibs {
useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false) def enableLegacyPackaging = findProperty('expo.useLegacyPackaging') ?: 'false'
useLegacyPackaging enableLegacyPackaging.toBoolean()
} }
} }
androidResources { androidResources {
@@ -156,15 +161,15 @@ dependencies {
if (isGifEnabled) { if (isGifEnabled) {
// For animated gif support // For animated gif support
implementation("com.facebook.fresco:animated-gif:${reactAndroidLibs.versions.fresco.get()}") implementation("com.facebook.fresco:animated-gif:${expoLibs.versions.fresco.get()}")
} }
if (isWebpEnabled) { if (isWebpEnabled) {
// For webp support // For webp support
implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}") implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
if (isWebpAnimatedEnabled) { if (isWebpAnimatedEnabled) {
// Animated webp support // Animated webp support
implementation("com.facebook.fresco:animated-webp:${reactAndroidLibs.versions.fresco.get()}") implementation("com.facebook.fresco:animated-webp:${expoLibs.versions.fresco.get()}")
} }
} }
@@ -2,6 +2,10 @@
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
@@ -17,7 +21,7 @@
<data android:scheme="https"/> <data android:scheme="https"/>
</intent> </intent>
</queries> </queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:requestLegacyExternalStorage="true" android:fullBackupContent="@xml/secure_store_backup_rules" android:dataExtractionRules="@xml/secure_store_data_extraction_rules"> <application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" android:allowBackup="true" android:theme="@style/AppTheme" android:supportsRtl="true" android:requestLegacyExternalStorage="true" android:fullBackupContent="@xml/secure_store_backup_rules" android:dataExtractionRules="@xml/secure_store_data_extraction_rules" android:enableOnBackInvokedCallback="false">
<meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color"/> <meta-data android:name="com.google.firebase.messaging.default_notification_color" android:resource="@color/notification_icon_color"/>
<meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon"/> <meta-data android:name="com.google.firebase.messaging.default_notification_icon" android:resource="@drawable/notification_icon"/>
<meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/> <meta-data android:name="expo.modules.notifications.default_notification_color" android:resource="@color/notification_icon_color"/>
@@ -8,10 +8,10 @@ import com.facebook.react.ReactApplication
import com.facebook.react.ReactNativeHost import com.facebook.react.ReactNativeHost
import com.facebook.react.ReactPackage import com.facebook.react.ReactPackage
import com.facebook.react.ReactHost import com.facebook.react.ReactHost
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultReactNativeHost import com.facebook.react.defaults.DefaultReactNativeHost
import com.facebook.react.soloader.OpenSourceMergedSoMapping import com.facebook.react.common.ReleaseLevel
import com.facebook.soloader.SoLoader
import expo.modules.ApplicationLifecycleDispatcher import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper import expo.modules.ReactNativeHostWrapper
@@ -42,11 +42,12 @@ class MainApplication : Application(), ReactApplication {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
SoLoader.init(this, OpenSourceMergedSoMapping) DefaultNewArchitectureEntryPoint.releaseLevel = try {
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())
// If you opted-in for the New Architecture, we load the native entry point for this app. } catch (e: IllegalArgumentException) {
load() ReleaseLevel.STABLE
} }
loadReactNative(this)
ApplicationLifecycleDispatcher.onApplicationCreate(this) ApplicationLifecycleDispatcher.onApplicationCreate(this)
} }
@@ -1,5 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar"> <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:enforceNavigationBarContrast" tools:targetApi="29">true</item>
<item name="android:textColor">@android:color/black</item> <item name="android:textColor">@android:color/black</item>
<item name="android:editTextStyle">@style/ResetEditText</item> <item name="android:editTextStyle">@style/ResetEditText</item>
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item> <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
+2 -19
View File
@@ -1,15 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext {
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '35.0.0'
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '24')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '35')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '34')
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.9.25'
ndkVersion = "26.1.10909125"
}
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
@@ -19,23 +10,15 @@ buildscript {
classpath 'com.google.gms:google-services:4.4.1' classpath 'com.google.gms:google-services:4.4.1'
classpath('com.android.tools.build:gradle') classpath('com.android.tools.build:gradle')
classpath('com.facebook.react:react-native-gradle-plugin') classpath('com.facebook.react:react-native-gradle-plugin')
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
} }
} }
apply plugin: "expo-root-project"
apply plugin: "com.facebook.react.rootproject" apply plugin: "com.facebook.react.rootproject"
allprojects { allprojects {
repositories { repositories {
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android'))
}
maven {
// Android JSC is installed from npm
url(new File(['node', '--print', "require.resolve('jsc-android/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim(), '../dist'))
}
google() google()
mavenCentral() mavenCentral()
maven { url 'https://www.jitpack.io' } maven { url 'https://www.jitpack.io' }
+13 -3
View File
@@ -35,7 +35,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# your application. You should enable this flag either if you want # your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that # to write custom TurboModules/Fabric components OR use libraries that
# are providing them. # are providing them.
newArchEnabled=false newArchEnabled=true
# Use this property to enable or disable the Hermes JS engine. # Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead. # If set to false, you will be using JSC instead.
@@ -55,6 +55,16 @@ EX_DEV_CLIENT_NETWORK_INSPECTOR=true
# Use legacy packaging to compress native libraries in the resulting APK. # Use legacy packaging to compress native libraries in the resulting APK.
expo.useLegacyPackaging=false expo.useLegacyPackaging=false
android.targetSdkVersion=35 android.targetSdkVersion=36
android.kotlinVersion=1.9.25
android.extraMavenRepos=[] android.extraMavenRepos=[]
# Specifies whether the app is configured to use edge-to-edge via the app config or plugin
# WARNING: This property has been deprecated and will be removed in Expo SDK 55. Use `edgeToEdgeEnabled` or `react.edgeToEdgeEnabled` to determine whether the project is using edge-to-edge.
expo.edgeToEdgeEnabled=true
# Use this property to enable edge-to-edge display support.
# This allows your app to draw behind system bars for an immersive UI.
# Note: Only works with ReactActivity and should not be used with custom Activity.
edgeToEdgeEnabled=true
android.compileSdkVersion=36
android.buildToolsVersion=36.0.0
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
+25 -24
View File
@@ -1,38 +1,39 @@
pluginManagement { pluginManagement {
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile().toString()) def reactNativeGradlePlugin = new File(
providers.exec {
workingDir(rootDir)
commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
}.standardOutput.asText.get().trim()
).getParentFile().absolutePath
includeBuild(reactNativeGradlePlugin)
def expoPluginsPath = new File(
providers.exec {
workingDir(rootDir)
commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
}.standardOutput.asText.get().trim(),
"../android/expo-gradle-plugin"
).absolutePath
includeBuild(expoPluginsPath)
}
plugins {
id("com.facebook.react.settings")
id("expo-autolinking-settings")
} }
plugins { id("com.facebook.react.settings") }
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex -> extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') { if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
ex.autolinkLibrariesFromCommand() ex.autolinkLibrariesFromCommand()
} else { } else {
def command = [ ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
'node',
'--no-warnings',
'--eval',
'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo/package.json\')] }))(process.argv.slice(1))',
'react-native-config',
'--json',
'--platform',
'android'
].toList()
ex.autolinkLibrariesFromCommand(command)
} }
} }
rootProject.name = 'Sauci' rootProject.name = 'Sauci'
dependencyResolutionManagement { expoAutolinking.useExpoModules()
versionCatalogs { expoAutolinking.useExpoVersionCatalog()
reactAndroidLibs {
from(files(new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), "../gradle/libs.versions.toml")))
}
}
}
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle");
useExpoModules()
include ':app' include ':app'
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })"].execute(null, rootDir).text.trim()).getParentFile()) includeBuild(expoAutolinking.reactNativeGradlePlugin)
+5 -4
View File
@@ -7,7 +7,7 @@
"icon": "./assets/icon.png", "icon": "./assets/icon.png",
"scheme": "app.sauci", "scheme": "app.sauci",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "automatic",
"newArchEnabled": false, "newArchEnabled": true,
"splash": { "splash": {
"image": "./assets/splash-icon.png", "image": "./assets/splash-icon.png",
"resizeMode": "contain", "resizeMode": "contain",
@@ -35,7 +35,7 @@
"NSPhotoLibraryAddUsageDescription": "Sauci saves photos and videos from your partner's messages to your photo library when you tap the save button.", "NSPhotoLibraryAddUsageDescription": "Sauci saves photos and videos from your partner's messages to your photo library when you tap the save button.",
"NSMicrophoneUsageDescription": "Sauci uses your microphone to record audio when you capture videos to share privately with your partner." "NSMicrophoneUsageDescription": "Sauci uses your microphone to record audio when you capture videos to share privately with your partner."
}, },
"privacyManifest": { "privacyManifests": {
"NSPrivacyTracking": false, "NSPrivacyTracking": false,
"NSPrivacyCollectedDataTypes": [ "NSPrivacyCollectedDataTypes": [
{ {
@@ -205,8 +205,9 @@
"expo-build-properties", "expo-build-properties",
{ {
"android": { "android": {
"targetSdkVersion": 35, "buildToolsVersion": "36.0.0",
"kotlinVersion": "1.9.25", "compileSdkVersion": 36,
"targetSdkVersion": 36,
"extraProguardRules": "-keep class com.facebook.hermes.unicode.** { *; }", "extraProguardRules": "-keep class com.facebook.hermes.unicode.** { *; }",
"useLegacyPackaging": false, "useLegacyPackaging": false,
"manifestPlaceholders": { "manifestPlaceholders": {
+1 -1
View File
@@ -65,7 +65,7 @@ export default function AppLayout() {
const { initializeRevenueCat, refreshSubscriptionStatus } = useSubscriptionStore(); const { initializeRevenueCat, refreshSubscriptionStatus } = useSubscriptionStore();
const { fetchStreak } = useStreakStore(); const { fetchStreak } = useStreakStore();
const messageToastAnim = useRef(new Animated.Value(-100)).current; const messageToastAnim = useRef(new Animated.Value(-100)).current;
const messageTimeoutRef = useRef<NodeJS.Timeout | null>(null); const messageTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [isLocked, setIsLocked] = useState(false); const [isLocked, setIsLocked] = useState(false);
const hasCheckedInitialBiometric = useRef(false); const hasCheckedInitialBiometric = useRef(false);
const wentToBackgroundAt = useRef<number | null>(null); const wentToBackgroundAt = useRef<number | null>(null);
+3 -3
View File
@@ -5,19 +5,19 @@
}, },
"build": { "build": {
"development": { "development": {
"node": "20.10.0", "node": "20.19.4",
"developmentClient": true, "developmentClient": true,
"distribution": "internal" "distribution": "internal"
}, },
"preview": { "preview": {
"node": "20.10.0", "node": "20.19.4",
"distribution": "internal", "distribution": "internal",
"env": { "env": {
"EXPO_PUBLIC_API_URL": "https://api.preprod.sauci.app" "EXPO_PUBLIC_API_URL": "https://api.preprod.sauci.app"
} }
}, },
"production": { "production": {
"node": "20.10.0", "node": "20.19.4",
"autoIncrement": true, "autoIncrement": true,
"env": { "env": {
"EXPO_PUBLIC_API_URL": "https://api.sauci.app", "EXPO_PUBLIC_API_URL": "https://api.sauci.app",
+1 -31
View File
@@ -1,33 +1,3 @@
const { getDefaultConfig } = require('expo/metro-config'); const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');
// Find the project and workspace directories module.exports = getDefaultConfig(__dirname);
const projectRoot = __dirname;
const workspaceRoot = path.resolve(projectRoot, '../..');
const config = getDefaultConfig(projectRoot);
// 1. Watch all files within the monorepo
// 1. Watch only relevant folders to improve performance on Windows
// watching the entire workspaceRoot is too slow
config.watchFolders = [
path.resolve(workspaceRoot, 'node_modules'),
path.resolve(workspaceRoot, 'packages/shared')
];
// 2. Let Metro know where to resolve packages and in which order
config.resolver.nodeModulesPaths = [
path.resolve(projectRoot, 'node_modules'),
path.resolve(workspaceRoot, 'node_modules'),
];
// 3. Force Metro to resolve (sub)dependencies from the `node_modules`
// of the workspace root, unless they are installed in the project root.
config.resolver.disableHierarchicalLookup = true;
// 4. Keep extraNodeModules in case we need to add monorepo overrides later
config.resolver.extraNodeModules = {
...config.resolver.extraNodeModules,
};
module.exports = config;
+44 -43
View File
@@ -23,8 +23,8 @@
"@bacons/apple-targets": "^0.1.6", "@bacons/apple-targets": "^0.1.6",
"@bittingz/expo-widgets": "^3.0.2", "@bittingz/expo-widgets": "^3.0.2",
"@expo/ngrok": "^4.1.0", "@expo/ngrok": "^4.1.0",
"@expo/vector-icons": "~14.0.4", "@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "1.23.1", "@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/cli": "^20.1.1", "@react-native-community/cli": "^20.1.1",
"@react-native-firebase/analytics": "^23.8.2", "@react-native-firebase/analytics": "^23.8.2",
"@react-native-firebase/app": "^23.8.2", "@react-native-firebase/app": "^23.8.2",
@@ -33,63 +33,64 @@
"@react-navigation/bottom-tabs": "^7.0.0", "@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^7.0.0", "@react-navigation/native": "^7.0.0",
"@react-navigation/native-stack": "^7.0.0", "@react-navigation/native-stack": "^7.0.0",
"@shopify/react-native-skia": "^1.12.4", "@shopify/react-native-skia": "2.2.12",
"@supabase/supabase-js": "^2.39.0", "@supabase/supabase-js": "^2.39.0",
"@tanstack/react-query": "^5.17.0", "@tanstack/react-query": "^5.17.0",
"babel-preset-expo": "~12.0.0", "babel-preset-expo": "~54.0.12",
"base64-arraybuffer": "^1.0.2", "base64-arraybuffer": "^1.0.2",
"expo": "~52.0.0", "expo": "~54.0.0",
"expo-apple-authentication": "~7.1.3", "expo-apple-authentication": "~8.0.8",
"expo-asset": "~11.0.5", "expo-asset": "~12.0.13",
"expo-av": "~15.0.2", "expo-av": "~16.0.8",
"expo-blur": "~14.0.3", "expo-blur": "~15.0.8",
"expo-build-properties": "^0.13.3", "expo-build-properties": "~1.0.10",
"expo-clipboard": "~7.0.1", "expo-clipboard": "~8.0.8",
"expo-constants": "~17.0.0", "expo-constants": "~18.0.14",
"expo-dev-client": "~5.0.20", "expo-dev-client": "~6.0.21",
"expo-file-system": "~18.0.12", "expo-file-system": "~19.0.24",
"expo-font": "~13.0.4", "expo-font": "~14.0.12",
"expo-haptics": "~14.0.1", "expo-haptics": "~15.0.8",
"expo-image": "~2.0.7", "expo-image": "~3.0.11",
"expo-image-picker": "~16.0.6", "expo-image-picker": "~17.0.11",
"expo-linear-gradient": "~14.0.2", "expo-linear-gradient": "~15.0.8",
"expo-linking": "~7.0.0", "expo-linking": "~8.0.12",
"expo-local-authentication": "~15.0.2", "expo-local-authentication": "~17.0.9",
"expo-media-library": "~17.0.6", "expo-media-library": "~18.2.1",
"expo-notifications": "~0.29.0", "expo-notifications": "~0.32.17",
"expo-router": "~4.0.0", "expo-router": "~6.0.24",
"expo-secure-store": "~14.0.0", "expo-secure-store": "~15.0.8",
"expo-sharing": "~13.0.1", "expo-sharing": "~14.0.8",
"expo-status-bar": "~2.0.0", "expo-status-bar": "~3.0.9",
"react": "18.3.1", "react": "19.1.0",
"react-dom": "18.3.1", "react-dom": "19.1.0",
"react-native": "0.76.9", "react-native": "0.81.5",
"react-native-android-widget": "^0.17.2", "react-native-android-widget": "^0.22.1",
"react-native-compressor": "^1.13.0", "react-native-compressor": "^1.13.0",
"react-native-gesture-handler": "~2.20.0", "react-native-gesture-handler": "~2.28.0",
"react-native-purchases": "^9.6.12", "react-native-purchases": "^9.6.12",
"react-native-reanimated": "~3.16.0", "react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "4.12.0", "react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.4.0", "react-native-screens": "~4.16.0",
"react-native-svg": "15.8.0", "react-native-svg": "15.12.1",
"react-native-url-polyfill": "^2.0.0", "react-native-url-polyfill": "^2.0.0",
"react-native-view-shot": "^4.0.3", "react-native-view-shot": "^4.0.3",
"react-native-web": "~0.19.13", "react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1",
"zustand": "^4.4.7" "zustand": "^4.4.7"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.24.0", "@babel/core": "^7.24.0",
"@testing-library/jest-native": "^5.4.3", "@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^13.3.3", "@testing-library/react-native": "^13.3.3",
"@types/jest": "^30.0.0", "@types/jest": "29.5.14",
"@types/react": "~18.3.12", "@types/react": "~19.1.10",
"@types/react-test-renderer": "^19.1.0", "@types/react-test-renderer": "^19.1.0",
"dotenv-cli": "^11.0.0", "dotenv-cli": "^11.0.0",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-expo": "~8.0.1", "eslint-config-expo": "~10.0.0",
"jest": "^29.7.0", "jest": "^29.7.0",
"jest-expo": "~52.0.0", "jest-expo": "~54.0.18",
"react-test-renderer": "^18.3.1", "react-test-renderer": "19.1.0",
"typescript": "^5.3.3" "typescript": "~5.9.2"
} }
} }
@@ -1,7 +1,7 @@
import { renderHook, act, waitFor } from '@testing-library/react-native'; import { renderHook, act, waitFor } from '@testing-library/react-native';
import { Alert } from 'react-native'; import { Alert } from 'react-native';
import * as MediaLibrary from 'expo-media-library'; import * as MediaLibrary from 'expo-media-library';
import * as FileSystem from 'expo-file-system'; import * as FileSystem from 'expo-file-system/legacy';
import { useMediaSaver } from '@/hooks/useMediaSaver'; import { useMediaSaver } from '@/hooks/useMediaSaver';
// Mock expo-media-library // Mock expo-media-library
@@ -11,7 +11,7 @@ jest.mock('expo-media-library', () => ({
})); }));
// Mock expo-file-system with all required methods // Mock expo-file-system with all required methods
jest.mock('expo-file-system', () => ({ jest.mock('expo-file-system/legacy', () => ({
readAsStringAsync: jest.fn(async () => ''), readAsStringAsync: jest.fn(async () => ''),
downloadAsync: jest.fn(), downloadAsync: jest.fn(),
deleteAsync: jest.fn(), deleteAsync: jest.fn(),
@@ -23,7 +23,7 @@ import { Ionicons } from "@expo/vector-icons";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import ViewShot from "react-native-view-shot"; import ViewShot from "react-native-view-shot";
import * as Sharing from "expo-sharing"; import * as Sharing from "expo-sharing";
import * as FileSystem from "expo-file-system"; import * as FileSystem from "expo-file-system/legacy";
import { colors, gradients, radius, typography, spacing, blur } from "../../theme"; import { colors, gradients, radius, typography, spacing, blur } from "../../theme";
const logo = require("../../../assets/logo.png"); const logo = require("../../../assets/logo.png");
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { View, StyleSheet } from 'react-native'; import { View, StyleSheet } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import Animated, { type AnimatedStyleProp } from 'react-native-reanimated'; import Animated, { type AnimatedStyle } from 'react-native-reanimated';
import type { ViewStyle } from 'react-native'; import type { ViewStyle } from 'react-native';
import type { IntensityLevel } from '@/types'; import type { IntensityLevel } from '@/types';
import { HEAT_COLORS } from './constants'; import { HEAT_COLORS } from './constants';
@@ -9,7 +9,7 @@ import { styles } from './styles';
interface IntensitySliderTrackProps { interface IntensitySliderTrackProps {
value: IntensityLevel; value: IntensityLevel;
progressStyle: AnimatedStyleProp<ViewStyle>; progressStyle: AnimatedStyle<ViewStyle>;
} }
export function IntensitySliderTrack({ value, progressStyle }: IntensitySliderTrackProps) { export function IntensitySliderTrack({ value, progressStyle }: IntensitySliderTrackProps) {
@@ -23,7 +23,7 @@ import { Ionicons } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import ViewShot from 'react-native-view-shot'; import ViewShot from 'react-native-view-shot';
import * as Sharing from 'expo-sharing'; import * as Sharing from 'expo-sharing';
import * as FileSystem from 'expo-file-system'; import * as FileSystem from 'expo-file-system/legacy';
import { colors, gradients, radius, typography, spacing, blur } from '../../../theme'; import { colors, gradients, radius, typography, spacing, blur } from '../../../theme';
const logo = require('../../../../assets/logo.png'); const logo = require('../../../../assets/logo.png');
@@ -1,7 +1,7 @@
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import type { SkImage } from '@shopify/react-native-skia'; import type { SkImage } from '@shopify/react-native-skia';
import * as MediaLibrary from 'expo-media-library'; import * as MediaLibrary from 'expo-media-library';
import * as FileSystem from 'expo-file-system'; import * as FileSystem from 'expo-file-system/legacy';
interface UseCanvasCaptureReturn { interface UseCanvasCaptureReturn {
makeSnapshot: React.MutableRefObject<(() => SkImage | null) | null>; makeSnapshot: React.MutableRefObject<(() => SkImage | null) | null>;
+1 -1
View File
@@ -1,7 +1,7 @@
import { useState, useCallback, useRef } from 'react'; import { useState, useCallback, useRef } from 'react';
import { Alert, Platform, ActionSheetIOS } from 'react-native'; import { Alert, Platform, ActionSheetIOS } from 'react-native';
import * as ImagePicker from 'expo-image-picker'; import * as ImagePicker from 'expo-image-picker';
import * as FileSystem from 'expo-file-system'; import * as FileSystem from 'expo-file-system/legacy';
import { uploadMedia } from '../lib/mediaApi'; import { uploadMedia } from '../lib/mediaApi';
export interface UseAvatarPickerOptions { export interface UseAvatarPickerOptions {
+1 -1
View File
@@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { Alert } from 'react-native'; import { Alert } from 'react-native';
import * as MediaLibrary from 'expo-media-library'; import * as MediaLibrary from 'expo-media-library';
import * as FileSystem from 'expo-file-system'; import * as FileSystem from 'expo-file-system/legacy';
export function useMediaSaver() { export function useMediaSaver() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
+1 -1
View File
@@ -1,5 +1,5 @@
import { getMediaUrl, mediaId } from './mediaApi'; import { getMediaUrl, mediaId } from './mediaApi';
import * as FileSystem from 'expo-file-system'; import * as FileSystem from 'expo-file-system/legacy';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
// Cache for signed URLs with expiration tracking // Cache for signed URLs with expiration tracking
+1 -1
View File
@@ -1,5 +1,5 @@
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import * as FileSystem from 'expo-file-system'; import * as FileSystem from 'expo-file-system/legacy';
import { decode } from 'base64-arraybuffer'; import { decode } from 'base64-arraybuffer';
import { ApiError, apiClient, authenticatedFetch } from './apiClient'; import { ApiError, apiClient, authenticatedFetch } from './apiClient';
+19 -2
View File
@@ -14,6 +14,8 @@ import { useMatchStore } from "../store/matchStore";
Notifications.setNotificationHandler({ Notifications.setNotificationHandler({
handleNotification: async () => ({ handleNotification: async () => ({
shouldShowAlert: false, shouldShowAlert: false,
shouldShowBanner: false,
shouldShowList: false,
shouldPlaySound: false, shouldPlaySound: false,
shouldSetBadge: false, shouldSetBadge: false,
}), }),
@@ -27,6 +29,21 @@ export interface NotificationData {
count?: number; count?: number;
} }
const notificationTypes = new Set<NotificationData["type"]>([
"match",
"message",
"match_digest",
"partner_activity",
"nudge",
"weekly_summary",
]);
function isNotificationData(value: unknown): value is NotificationData {
if (!value || typeof value !== "object") return false;
const type = (value as { type?: unknown }).type;
return typeof type === "string" && notificationTypes.has(type as NotificationData["type"]);
}
/** /**
* Request notification permissions and get Expo push token. * Request notification permissions and get Expo push token.
@@ -219,9 +236,9 @@ export async function checkAndRegisterPushToken(userId: string): Promise<boolean
export function handleNotificationResponse( export function handleNotificationResponse(
response: Notifications.NotificationResponse response: Notifications.NotificationResponse
): void { ): void {
const data = response.notification.request.content.data as NotificationData; const data = response.notification.request.content.data;
if (!data) { if (!isNotificationData(data)) {
return; return;
} }
+14 -1
View File
@@ -103,13 +103,26 @@ jest.mock('expo-image-picker', () => ({
}, },
})); }));
jest.mock('expo-file-system', () => ({ jest.mock('expo-file-system/legacy', () => ({
readAsStringAsync: jest.fn(async () => ''), readAsStringAsync: jest.fn(async () => ''),
EncodingType: { EncodingType: {
Base64: 'base64', Base64: 'base64',
}, },
})); }));
jest.mock('expo-av', () => ({
Audio: {
requestPermissionsAsync: jest.fn(async () => ({ status: 'granted' })),
setAudioModeAsync: jest.fn(async () => undefined),
Recording: {
createAsync: jest.fn(),
},
RecordingOptionsPresets: {
HIGH_QUALITY: {},
},
},
}));
jest.mock('react-native-compressor', () => ({ jest.mock('react-native-compressor', () => ({
Video: { Video: {
compress: jest.fn(async (uri: string) => uri), compress: jest.fn(async (uri: string) => uri),
+2 -2
View File
@@ -20,8 +20,8 @@
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.0.0", "@types/node": "^20.0.0",
"@types/react": "^18.3.0", "@types/react": "~19.1.10",
"@types/react-dom": "^18.3.0", "@types/react-dom": "~19.1.7",
"autoprefixer": "^10.4.0", "autoprefixer": "^10.4.0",
"eslint": "^8.57.1", "eslint": "^8.57.1",
"eslint-config-next": "14.2.35", "eslint-config-next": "14.2.35",
+3853 -5466
View File
File diff suppressed because it is too large. Load diff
+12 -2
View File
@@ -34,8 +34,18 @@
"clean": "turbo clean", "clean": "turbo clean",
"typecheck": "turbo typecheck" "typecheck": "turbo typecheck"
}, },
"dependencies": {
"expo-router": "~6.0.24",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0"
},
"devDependencies": { "devDependencies": {
"@types/react": "~19.1.10",
"@types/react-dom": "~19.1.7",
"patch-package": "^8.0.1", "patch-package": "^8.0.1",
"react-test-renderer": "19.1.0",
"supabase": "^2.39.2", "supabase": "^2.39.2",
"pg": "^8.17.2", "pg": "^8.17.2",
"turbo": "^2.3.3", "turbo": "^2.3.3",
@@ -48,8 +58,8 @@
}, },
"overrides": { "overrides": {
"rollup": "4.54.0", "rollup": "4.54.0",
"react-native": "0.76.9", "react-native": "0.81.5",
"expo": "~52.0.0" "expo": "~54.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-linux-arm64-musl": "4.54.0", "@rollup/rollup-linux-arm64-musl": "4.54.0",
+23
View File
@@ -15,6 +15,7 @@ const path = require('path');
const ROOT = path.resolve(__dirname, '..'); const ROOT = path.resolve(__dirname, '..');
const MOBILE_NM = path.join(ROOT, 'apps', 'mobile', 'node_modules'); const MOBILE_NM = path.join(ROOT, 'apps', 'mobile', 'node_modules');
const WEB_NM = path.join(ROOT, 'apps', 'web', 'node_modules');
const ROOT_NM = path.join(ROOT, 'node_modules'); const ROOT_NM = path.join(ROOT, 'node_modules');
// Packages that must live in apps/mobile/node_modules, not root // Packages that must live in apps/mobile/node_modules, not root
@@ -40,3 +41,25 @@ for (const pkg of PACKAGES_TO_FIX) {
fs.renameSync(rootPkg, localPkg); fs.renameSync(rootPkg, localPkg);
console.log(`fix-hoisting: moved ${pkg} → apps/mobile/node_modules/`); console.log(`fix-hoisting: moved ${pkg} → apps/mobile/node_modules/`);
} }
// Next and its React-facing tooling must resolve the web workspace's React 18
// runtime, not the React 19 runtime hoisted for Expo SDK 54 / React Native.
for (const pkg of ['next', 'styled-jsx', 'eslint-config-next']) {
const rootPkg = path.join(ROOT_NM, pkg);
const localPkg = path.join(WEB_NM, pkg);
if (!fs.existsSync(rootPkg) || fs.existsSync(localPkg)) continue;
fs.mkdirSync(WEB_NM, { recursive: true });
fs.renameSync(rootPkg, localPkg);
console.log(`fix-hoisting: moved ${pkg} → apps/web/node_modules/`);
}
const localNext = path.join(WEB_NM, 'next');
if (fs.existsSync(localNext)) {
const localBinDir = path.join(WEB_NM, '.bin');
const localBin = path.join(localBinDir, 'next');
fs.mkdirSync(localBinDir, { recursive: true });
if (!fs.existsSync(localBin)) {
fs.symlinkSync(path.join('..', 'next', 'dist', 'bin', 'next'), localBin);
}
}