Merge origin/staging (Expo 54 / Android API 36) into feat/dares-loop

This commit is contained in:
jjbrunton committed 2026-08-28 20:12:55 +01:00
commit 028e21e54c
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"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react": "~19.1.10",
"@types/react-dom": "~19.1.7",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"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"
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
// 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())
@@ -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)
@@ -78,7 +79,7 @@ def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInRelea
* give correct results when using with locales other than en-US. Note that
* 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 {
ndkVersion rootProject.ext.ndkVersion
@@ -93,6 +94,7 @@ android {
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 46
versionName "1.0.5"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
signingConfigs {
debug {
@@ -110,15 +112,18 @@ android {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
minifyEnabled enableProguardInReleaseBuilds
def enableShrinkResources = findProperty('android.enableShrinkResourcesInReleaseBuilds') ?: 'false'
shrinkResources enableShrinkResources.toBoolean()
minifyEnabled enableMinifyInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
crunchPngs (findProperty('android.enablePngCrunchInReleaseBuilds')?.toBoolean() ?: true)
def enablePngCrunchInRelease = findProperty('android.enablePngCrunchInReleaseBuilds') ?: 'true'
crunchPngs enablePngCrunchInRelease.toBoolean()
}
}
packagingOptions {
jniLibs {
useLegacyPackaging (findProperty('expo.useLegacyPackaging')?.toBoolean() ?: false)
def enableLegacyPackaging = findProperty('expo.useLegacyPackaging') ?: 'false'
useLegacyPackaging enableLegacyPackaging.toBoolean()
}
}
androidResources {
@@ -156,15 +161,15 @@ dependencies {
if (isGifEnabled) {
// 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) {
// For webp support
implementation("com.facebook.fresco:webpsupport:${reactAndroidLibs.versions.fresco.get()}")
implementation("com.facebook.fresco:webpsupport:${expoLibs.versions.fresco.get()}")
if (isWebpAnimatedEnabled) {
// 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.MODIFY_AUDIO_SETTINGS"/>
<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.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
@@ -17,7 +21,7 @@
<data android:scheme="https"/>
</intent>
</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_icon" android:resource="@drawable/notification_icon"/>
<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.ReactPackage
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.soloader.OpenSourceMergedSoMapping
import com.facebook.soloader.SoLoader
import com.facebook.react.common.ReleaseLevel
import expo.modules.ApplicationLifecycleDispatcher
import expo.modules.ReactNativeHostWrapper
@@ -42,11 +42,12 @@ class MainApplication : Application(), ReactApplication {
override fun onCreate() {
super.onCreate()
SoLoader.init(this, OpenSourceMergedSoMapping)
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
load()
DefaultNewArchitectureEntryPoint.releaseLevel = try {
ReleaseLevel.valueOf(BuildConfig.REACT_NATIVE_RELEASE_LEVEL.uppercase())
} catch (e: IllegalArgumentException) {
ReleaseLevel.STABLE
}
loadReactNative(this)
ApplicationLifecycleDispatcher.onApplicationCreate(this)
}
@@ -1,5 +1,6 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<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:editTextStyle">@style/ResetEditText</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.
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 {
google()
mavenCentral()
@@ -19,23 +10,15 @@ buildscript {
classpath 'com.google.gms:google-services:4.4.1'
classpath('com.android.tools.build:gradle')
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"
allprojects {
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()
mavenCentral()
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
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
newArchEnabled=true
# Use this property to enable or disable the Hermes JS engine.
# 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.
expo.useLegacyPackaging=false
android.targetSdkVersion=35
android.kotlinVersion=1.9.25
android.targetSdkVersion=36
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
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
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+25 -24
View File
@@ -1,38 +1,39 @@
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 ->
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
ex.autolinkLibrariesFromCommand()
} else {
def command = [
'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)
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
}
}
rootProject.name = 'Sauci'
dependencyResolutionManagement {
versionCatalogs {
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()
expoAutolinking.useExpoModules()
expoAutolinking.useExpoVersionCatalog()
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",
"scheme": "app.sauci",
"userInterfaceStyle": "automatic",
"newArchEnabled": false,
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"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.",
"NSMicrophoneUsageDescription": "Sauci uses your microphone to record audio when you capture videos to share privately with your partner."
},
"privacyManifest": {
"privacyManifests": {
"NSPrivacyTracking": false,
"NSPrivacyCollectedDataTypes": [
{
@@ -205,8 +205,9 @@
"expo-build-properties",
{
"android": {
"targetSdkVersion": 35,
"kotlinVersion": "1.9.25",
"buildToolsVersion": "36.0.0",
"compileSdkVersion": 36,
"targetSdkVersion": 36,
"extraProguardRules": "-keep class com.facebook.hermes.unicode.** { *; }",
"useLegacyPackaging": false,
"manifestPlaceholders": {
+1 -1
View File
@@ -65,7 +65,7 @@ export default function AppLayout() {
const { initializeRevenueCat, refreshSubscriptionStatus } = useSubscriptionStore();
const { fetchStreak } = useStreakStore();
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 hasCheckedInitialBiometric = useRef(false);
const wentToBackgroundAt = useRef<number | null>(null);
+3 -3
View File
@@ -5,19 +5,19 @@
},
"build": {
"development": {
"node": "20.10.0",
"node": "20.19.4",
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"node": "20.10.0",
"node": "20.19.4",
"distribution": "internal",
"env": {
"EXPO_PUBLIC_API_URL": "https://api.preprod.sauci.app"
}
},
"production": {
"node": "20.10.0",
"node": "20.19.4",
"autoIncrement": true,
"env": {
"EXPO_PUBLIC_API_URL": "https://api.sauci.app",
+1 -31
View File
@@ -1,33 +1,3 @@
const { getDefaultConfig } = require('expo/metro-config');
const path = require('path');
// Find the project and workspace directories
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;
module.exports = getDefaultConfig(__dirname);
+44 -43
View File
@@ -23,8 +23,8 @@
"@bacons/apple-targets": "^0.1.6",
"@bittingz/expo-widgets": "^3.0.2",
"@expo/ngrok": "^4.1.0",
"@expo/vector-icons": "~14.0.4",
"@react-native-async-storage/async-storage": "1.23.1",
"@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/cli": "^20.1.1",
"@react-native-firebase/analytics": "^23.8.2",
"@react-native-firebase/app": "^23.8.2",
@@ -33,63 +33,64 @@
"@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/native": "^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",
"@tanstack/react-query": "^5.17.0",
"babel-preset-expo": "~12.0.0",
"babel-preset-expo": "~54.0.12",
"base64-arraybuffer": "^1.0.2",
"expo": "~52.0.0",
"expo-apple-authentication": "~7.1.3",
"expo-asset": "~11.0.5",
"expo-av": "~15.0.2",
"expo-blur": "~14.0.3",
"expo-build-properties": "^0.13.3",
"expo-clipboard": "~7.0.1",
"expo-constants": "~17.0.0",
"expo-dev-client": "~5.0.20",
"expo-file-system": "~18.0.12",
"expo-font": "~13.0.4",
"expo-haptics": "~14.0.1",
"expo-image": "~2.0.7",
"expo-image-picker": "~16.0.6",
"expo-linear-gradient": "~14.0.2",
"expo-linking": "~7.0.0",
"expo-local-authentication": "~15.0.2",
"expo-media-library": "~17.0.6",
"expo-notifications": "~0.29.0",
"expo-router": "~4.0.0",
"expo-secure-store": "~14.0.0",
"expo-sharing": "~13.0.1",
"expo-status-bar": "~2.0.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.9",
"react-native-android-widget": "^0.17.2",
"expo": "~54.0.0",
"expo-apple-authentication": "~8.0.8",
"expo-asset": "~12.0.13",
"expo-av": "~16.0.8",
"expo-blur": "~15.0.8",
"expo-build-properties": "~1.0.10",
"expo-clipboard": "~8.0.8",
"expo-constants": "~18.0.14",
"expo-dev-client": "~6.0.21",
"expo-file-system": "~19.0.24",
"expo-font": "~14.0.12",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-image-picker": "~17.0.11",
"expo-linear-gradient": "~15.0.8",
"expo-linking": "~8.0.12",
"expo-local-authentication": "~17.0.9",
"expo-media-library": "~18.2.1",
"expo-notifications": "~0.32.17",
"expo-router": "~6.0.24",
"expo-secure-store": "~15.0.8",
"expo-sharing": "~14.0.8",
"expo-status-bar": "~3.0.9",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-android-widget": "^0.22.1",
"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-reanimated": "~3.16.0",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-svg": "15.8.0",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
"react-native-url-polyfill": "^2.0.0",
"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"
},
"devDependencies": {
"@babel/core": "^7.24.0",
"@testing-library/jest-native": "^5.4.3",
"@testing-library/react-native": "^13.3.3",
"@types/jest": "^30.0.0",
"@types/react": "~18.3.12",
"@types/jest": "29.5.14",
"@types/react": "~19.1.10",
"@types/react-test-renderer": "^19.1.0",
"dotenv-cli": "^11.0.0",
"eslint": "^8.57.0",
"eslint-config-expo": "~8.0.1",
"eslint-config-expo": "~10.0.0",
"jest": "^29.7.0",
"jest-expo": "~52.0.0",
"react-test-renderer": "^18.3.1",
"typescript": "^5.3.3"
"jest-expo": "~54.0.18",
"react-test-renderer": "19.1.0",
"typescript": "~5.9.2"
}
}
@@ -1,7 +1,7 @@
import { renderHook, act, waitFor } from '@testing-library/react-native';
import { Alert } from 'react-native';
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';
// Mock expo-media-library
@@ -11,7 +11,7 @@ jest.mock('expo-media-library', () => ({
}));
// Mock expo-file-system with all required methods
jest.mock('expo-file-system', () => ({
jest.mock('expo-file-system/legacy', () => ({
readAsStringAsync: jest.fn(async () => ''),
downloadAsync: jest.fn(),
deleteAsync: jest.fn(),
@@ -23,7 +23,7 @@ import { Ionicons } from "@expo/vector-icons";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import ViewShot from "react-native-view-shot";
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";
const logo = require("../../../assets/logo.png");
@@ -1,7 +1,7 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
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 { IntensityLevel } from '@/types';
import { HEAT_COLORS } from './constants';
@@ -9,7 +9,7 @@ import { styles } from './styles';
interface IntensitySliderTrackProps {
value: IntensityLevel;
progressStyle: AnimatedStyleProp<ViewStyle>;
progressStyle: AnimatedStyle<ViewStyle>;
}
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 ViewShot from 'react-native-view-shot';
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';
const logo = require('../../../../assets/logo.png');
@@ -1,7 +1,7 @@
import { useCallback, useRef } from 'react';
import type { SkImage } from '@shopify/react-native-skia';
import * as MediaLibrary from 'expo-media-library';
import * as FileSystem from 'expo-file-system';
import * as FileSystem from 'expo-file-system/legacy';
interface UseCanvasCaptureReturn {
makeSnapshot: React.MutableRefObject<(() => SkImage | null) | null>;
+1 -1
View File
@@ -1,7 +1,7 @@
import { useState, useCallback, useRef } from 'react';
import { Alert, Platform, ActionSheetIOS } from 'react-native';
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';
export interface UseAvatarPickerOptions {
+1 -1
View File
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { Alert } from 'react-native';
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() {
const [saving, setSaving] = useState(false);
+1 -1
View File
@@ -1,5 +1,5 @@
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';
// Cache for signed URLs with expiration tracking
+1 -1
View File
@@ -1,5 +1,5 @@
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 { ApiError, apiClient, authenticatedFetch } from './apiClient';
+19 -2
View File
@@ -14,6 +14,8 @@ import { useMatchStore } from "../store/matchStore";
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: false,
shouldShowBanner: false,
shouldShowList: false,
shouldPlaySound: false,
shouldSetBadge: false,
}),
@@ -27,6 +29,21 @@ export interface NotificationData {
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.
@@ -219,9 +236,9 @@ export async function checkAndRegisterPushToken(userId: string): Promise<boolean
export function handleNotificationResponse(
response: Notifications.NotificationResponse
): void {
const data = response.notification.request.content.data as NotificationData;
const data = response.notification.request.content.data;
if (!data) {
if (!isNotificationData(data)) {
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 () => ''),
EncodingType: {
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', () => ({
Video: {
compress: jest.fn(async (uri: string) => uri),
+2 -2
View File
@@ -20,8 +20,8 @@
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/react": "~19.1.10",
"@types/react-dom": "~19.1.7",
"autoprefixer": "^10.4.0",
"eslint": "^8.57.1",
"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",
"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": {
"@types/react": "~19.1.10",
"@types/react-dom": "~19.1.7",
"patch-package": "^8.0.1",
"react-test-renderer": "19.1.0",
"supabase": "^2.39.2",
"pg": "^8.17.2",
"turbo": "^2.3.3",
@@ -48,8 +58,8 @@
},
"overrides": {
"rollup": "4.54.0",
"react-native": "0.76.9",
"expo": "~52.0.0"
"react-native": "0.81.5",
"expo": "~54.0.0"
},
"optionalDependencies": {
"@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 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');
// 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);
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);
}
}