chore(android): target API 36 with Expo 54

Upgrade the mobile native toolchain and Expo dependencies for Android 16 compatibility while preserving web and admin runtime isolation.
This commit is contained in:
jjbrunton committed 2026-08-28 20:28:31 +01:00
1 parent aacb4f8731
commit 61ec3692b7
27 files changed
+3856 -5524

No files matched your search

+1 -1
View File
@@ -1 +1 @@
20.10.0 20.19.4
+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<number | 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),
+3675 -5360
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",