about summary refs log tree commit diff
path: root/patches
diff options
context:
space:
mode:
authorHailey <me@haileyok.com>2024-12-06 09:52:08 -0800
committerGitHub <noreply@github.com>2024-12-06 17:52:08 +0000
commit1f6acc11abec91972e6e04abd55e09b2a9dc1433 (patch)
tree3d6a9b1e8a3aa6f06a4700e9b40519439fcb37da /patches
parent3ab6c435df5dc3d17fe3e2531231ccf012a4860c (diff)
downloadvoidsky-1f6acc11abec91972e6e04abd55e09b2a9dc1433.tar.zst
clean rn 0.76 upgrade (#6887)
* package upgrades

* upgrade system ui

* update patches

* rename patch

* rm

* use .set/.set

* resolve yarnlock

* fix accidentally removed package

* fix use permissions hook

* fix some type errors

* type fixes

* more tweaking

* clean

* Discard changes to src/screens/Onboarding/StepProfile/index.tsx

* oops

* fix splash

* use ios/android in config

* Fix tests

* add back patch

* add to rn patch

* fullscreen?

* Revert "add to rn patch"

This reverts commit 4716d2c643a29fc77b871ca8555d8d78cb4ac427.

* try this

* test with revert

* test

* maybe this

* fix config

* Bump @react-native-picker/picker

* Bump some packages

* Rm unused

* Update lockfile

* Rename expo-notifications+0.29.8.patch.md to expo-notifications+0.29.10.patch.md

* Update react-native+0.76.3.patch.md

* Update react-native+0.76.3.patch.md

* Inline splash configs

Jumping around the file is annoying and makes it harder to understand how this is structured.

* Start fixing Android splash

* Downgrade compressor

This version isn't building for me due to https://github.com/numandev1/react-native-compressor/issues/322.

* Make Android splash empty for now

* Work around a bug

* Bump the compressor

* Bump again

* Include splash fixes

* Try updating

* No custom Android splash

* Revert to using icons

* welp

* Fix sizes

* Make sizing work

* Bump size

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
Diffstat (limited to 'patches')
-rw-r--r--patches/@lingui+core+4.14.1.patch (renamed from patches/@lingui+core+4.5.0.patch)0
-rw-r--r--patches/@sentry+react-native+5.24.3.patch (renamed from patches/@sentry+react-native+5.32.0.patch)0
-rw-r--r--patches/expo-haptics+14.0.0.md (renamed from patches/expo-haptics+12.8.1.md)0
-rw-r--r--patches/expo-haptics+14.0.0.patch (renamed from patches/expo-haptics+13.0.1.patch)0
-rw-r--r--patches/expo-image-picker+15.0.5.patch18
-rw-r--r--patches/expo-modules-core+1.12.11.md5
-rw-r--r--patches/expo-modules-core+1.12.11.patch26
-rw-r--r--patches/expo-notifications+0.28.7.patch216
-rw-r--r--patches/expo-notifications+0.29.10.patch169
-rw-r--r--patches/expo-notifications+0.29.10.patch.md (renamed from patches/expo-notifications-0.28.7.patch.md)0
-rw-r--r--patches/expo-updates+0.26.9.patch (renamed from patches/expo-updates+0.25.14.patch)0
-rw-r--r--patches/expo-updates+0.26.9.patch.md (renamed from patches/expo-updates+0.25.11.patch.md)0
-rw-r--r--patches/metro+0.80.4.patch44
-rw-r--r--patches/metro-runtime+0.80.4.patch50
-rw-r--r--patches/metro-transform-worker+0.80.4.patch41
-rw-r--r--patches/react-native+0.74.1.patch283
-rw-r--r--patches/react-native+0.76.3.patch331
-rw-r--r--patches/react-native+0.76.3.patch.md (renamed from patches/react-native+0.74.1.patch.md)7
-rw-r--r--patches/react-native-image-crop-picker+0.41.2.patch39
-rw-r--r--patches/react-native-svg+15.9.0.patch (renamed from patches/react-native-svg+15.3.0.patch)0
20 files changed, 502 insertions, 727 deletions
diff --git a/patches/@lingui+core+4.5.0.patch b/patches/@lingui+core+4.14.1.patch
index 8ace93a74..8ace93a74 100644
--- a/patches/@lingui+core+4.5.0.patch
+++ b/patches/@lingui+core+4.14.1.patch
diff --git a/patches/@sentry+react-native+5.32.0.patch b/patches/@sentry+react-native+5.24.3.patch
index a5ccecc21..a5ccecc21 100644
--- a/patches/@sentry+react-native+5.32.0.patch
+++ b/patches/@sentry+react-native+5.24.3.patch
diff --git a/patches/expo-haptics+12.8.1.md b/patches/expo-haptics+14.0.0.md
index afa7395bc..afa7395bc 100644
--- a/patches/expo-haptics+12.8.1.md
+++ b/patches/expo-haptics+14.0.0.md
diff --git a/patches/expo-haptics+13.0.1.patch b/patches/expo-haptics+14.0.0.patch
index 9c7b9a666..9c7b9a666 100644
--- a/patches/expo-haptics+13.0.1.patch
+++ b/patches/expo-haptics+14.0.0.patch
diff --git a/patches/expo-image-picker+15.0.5.patch b/patches/expo-image-picker+15.0.5.patch
deleted file mode 100644
index 962b36612..000000000
--- a/patches/expo-image-picker+15.0.5.patch
+++ /dev/null
@@ -1,18 +0,0 @@
-diff --git a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/exporters/RawImageExporter.kt b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/exporters/RawImageExporter.kt
-index f339e5f..fa35e82 100644
---- a/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/exporters/RawImageExporter.kt
-+++ b/node_modules/expo-image-picker/android/src/main/java/expo/modules/imagepicker/exporters/RawImageExporter.kt
-@@ -16,7 +16,12 @@ class RawImageExporter : ImageExporter {
-     copyFile(source, output, contentResolver)
-     val exifInterface = ExifInterface(output.absolutePath)
-     val imageRotation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, 0)
--    val isRotatedLandscape = (imageRotation == ExifInterface.ORIENTATION_ROTATE_90 || imageRotation == ExifInterface.ORIENTATION_ROTATE_270)
-+    val isRotatedLandscape = (
-+      imageRotation == ExifInterface.ORIENTATION_ROTATE_90 ||
-+      imageRotation == ExifInterface.ORIENTATION_ROTATE_270 ||
-+      imageRotation == ExifInterface.ORIENTATION_TRANSPOSE ||
-+      imageRotation == ExifInterface.ORIENTATION_TRANSVERSE
-+    )
-     val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
- 
-     BitmapFactory.decodeFile(output.absolutePath, options)
diff --git a/patches/expo-modules-core+1.12.11.md b/patches/expo-modules-core+1.12.11.md
deleted file mode 100644
index a690c8609..000000000
--- a/patches/expo-modules-core+1.12.11.md
+++ /dev/null
@@ -1,5 +0,0 @@
-## expo-modules-core Patch
-
-This patch fixes crashes seen in some Android clients when using intents to open the app. See https://github.com/expo/expo/pull/29513.
-
-Do not remove this patch until that PR lands in Expo and is released.
diff --git a/patches/expo-modules-core+1.12.11.patch b/patches/expo-modules-core+1.12.11.patch
deleted file mode 100644
index ea26b821d..000000000
--- a/patches/expo-modules-core+1.12.11.patch
+++ /dev/null
@@ -1,26 +0,0 @@
-diff --git a/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java b/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
-index bb74e80..0aa0202 100644
---- a/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
-+++ b/node_modules/expo-modules-core/android/src/main/java/expo/modules/adapters/react/NativeModulesProxy.java
-@@ -90,8 +90,8 @@ public class NativeModulesProxy extends ReactContextBaseJavaModule {
-     mModuleRegistry.ensureIsInitialized();
- 
-     KotlinInteropModuleRegistry kotlinModuleRegistry = getKotlinInteropModuleRegistry();
--    kotlinModuleRegistry.emitOnCreate();
-     kotlinModuleRegistry.installJSIInterop();
-+    kotlinModuleRegistry.emitOnCreate();
- 
-     Map<String, Object> constants = new HashMap<>(3);
-     constants.put(MODULES_CONSTANTS_KEY, new HashMap<>());
-diff --git a/node_modules/expo-modules-core/build/uuid/uuid.js b/node_modules/expo-modules-core/build/uuid/uuid.js
-index 109d3fe..c7fce9e 100644
---- a/node_modules/expo-modules-core/build/uuid/uuid.js
-+++ b/node_modules/expo-modules-core/build/uuid/uuid.js
-@@ -1,5 +1,7 @@
- import bytesToUuid from './lib/bytesToUuid';
- import { Uuidv5Namespace } from './uuid.types';
-+import { ensureNativeModulesAreInstalled } from '../ensureNativeModulesAreInstalled';
-+ensureNativeModulesAreInstalled();
- const nativeUuidv4 = globalThis?.expo?.uuidv4;
- const nativeUuidv5 = globalThis?.expo?.uuidv5;
- function uuidv4() {
diff --git a/patches/expo-notifications+0.28.7.patch b/patches/expo-notifications+0.28.7.patch
deleted file mode 100644
index b71da2ebb..000000000
--- a/patches/expo-notifications+0.28.7.patch
+++ /dev/null
@@ -1,216 +0,0 @@
-diff --git a/node_modules/expo-notifications/android/build.gradle b/node_modules/expo-notifications/android/build.gradle
-index b863077..8b5209e 100644
---- a/node_modules/expo-notifications/android/build.gradle
-+++ b/node_modules/expo-notifications/android/build.gradle
-@@ -32,6 +32,7 @@ dependencies {
-   api 'com.google.firebase:firebase-messaging:22.0.0'
- 
-   api 'me.leolin:ShortcutBadger:1.1.22@aar'
-+  implementation project(':expo-background-notification-handler')
- 
-   if (project.findProject(':expo-modules-test-core')) {
-     testImplementation project(':expo-modules-test-core')
-diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/badge/BadgeHelper.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/badge/BadgeHelper.kt
-index 63a46c5..f911834 100644
---- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/badge/BadgeHelper.kt
-+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/badge/BadgeHelper.kt
-@@ -1,5 +1,6 @@
- package expo.modules.notifications.badge
- 
-+import android.app.NotificationManager
- import android.content.Context
- import android.util.Log
- import me.leolin.shortcutbadger.ShortcutBadgeException
-@@ -12,7 +13,12 @@ object BadgeHelper {
- 
-   fun setBadgeCount(context: Context, badgeCount: Int): Boolean {
-     return try {
--      ShortcutBadger.applyCountOrThrow(context.applicationContext, badgeCount)
-+      if (badgeCount == 0) {
-+        val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-+        notificationManager.cancelAll()
-+      } else {
-+        ShortcutBadger.applyCountOrThrow(context.applicationContext, badgeCount)
-+      }
-       BadgeHelper.badgeCount = badgeCount
-       true
-     } catch (e: ShortcutBadgeException) {
-diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java
-index 0af7fe0..8f2c8d8 100644
---- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java
-+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/JSONNotificationContentBuilder.java
-@@ -14,6 +14,7 @@ import expo.modules.notifications.notifications.enums.NotificationPriority;
- import expo.modules.notifications.notifications.model.NotificationContent;
- 
- public class JSONNotificationContentBuilder extends NotificationContent.Builder {
-+  private static final String CHANNEL_ID_KEY = "channelId";
-   private static final String TITLE_KEY = "title";
-   private static final String TEXT_KEY = "message";
-   private static final String SUBTITLE_KEY = "subtitle";
-@@ -36,6 +37,7 @@ public class JSONNotificationContentBuilder extends NotificationContent.Builder
- 
-   public NotificationContent.Builder setPayload(JSONObject payload) {
-     this.setTitle(getTitle(payload))
-+      .setChannelId(getChannelId(payload))
-       .setSubtitle(getSubtitle(payload))
-       .setText(getText(payload))
-       .setBody(getBody(payload))
-@@ -60,6 +62,14 @@ public class JSONNotificationContentBuilder extends NotificationContent.Builder
-     return this;
-   }
- 
-+  protected String getChannelId(JSONObject payload) {
-+    try {
-+      return payload.getString(CHANNEL_ID_KEY);
-+    } catch (JSONException e) {
-+      return null;
-+    }
-+  }
-+
-   protected String getTitle(JSONObject payload) {
-     try {
-       return payload.getString(TITLE_KEY);
-diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
-index f1fed19..012757b 100644
---- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
-+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
-@@ -20,6 +20,7 @@ import expo.modules.notifications.notifications.enums.NotificationPriority;
-  * should be created using {@link NotificationContent.Builder}.
-  */
- public class NotificationContent implements Parcelable, Serializable {
-+  private String mChannelId;
-   private String mTitle;
-   private String mText;
-   private String mSubtitle;
-@@ -50,6 +51,11 @@ public class NotificationContent implements Parcelable, Serializable {
-     }
-   };
- 
-+  @Nullable
-+  public String getChannelId() {
-+    return mChannelId;
-+  }
-+
-   @Nullable
-   public String getTitle() {
-     return mTitle;
-@@ -121,6 +127,7 @@ public class NotificationContent implements Parcelable, Serializable {
-   }
- 
-   protected NotificationContent(Parcel in) {
-+    mChannelId = in.readString();
-     mTitle = in.readString();
-     mText = in.readString();
-     mSubtitle = in.readString();
-@@ -146,6 +153,7 @@ public class NotificationContent implements Parcelable, Serializable {
- 
-   @Override
-   public void writeToParcel(Parcel dest, int flags) {
-+    dest.writeString(mChannelId);
-     dest.writeString(mTitle);
-     dest.writeString(mText);
-     dest.writeString(mSubtitle);
-@@ -166,6 +174,7 @@ public class NotificationContent implements Parcelable, Serializable {
-   private static final long serialVersionUID = 397666843266836802L;
- 
-   private void writeObject(java.io.ObjectOutputStream out) throws IOException {
-+    out.writeObject(mChannelId);
-     out.writeObject(mTitle);
-     out.writeObject(mText);
-     out.writeObject(mSubtitle);
-@@ -190,6 +199,7 @@ public class NotificationContent implements Parcelable, Serializable {
-   }
- 
-   private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
-+    mChannelId = (String) in.readObject();
-     mTitle = (String) in.readObject();
-     mText = (String) in.readObject();
-     mSubtitle = (String) in.readObject();
-@@ -240,6 +250,7 @@ public class NotificationContent implements Parcelable, Serializable {
-   }
- 
-   public static class Builder {
-+    private String mChannelId;
-     private String mTitle;
-     private String mText;
-     private String mSubtitle;
-@@ -260,6 +271,11 @@ public class NotificationContent implements Parcelable, Serializable {
-       useDefaultVibrationPattern();
-     }
- 
-+    public Builder setChannelId(String channelId) {
-+      mChannelId = channelId;
-+      return this;
-+    }
-+
-     public Builder setTitle(String title) {
-       mTitle = title;
-       return this;
-@@ -336,6 +352,7 @@ public class NotificationContent implements Parcelable, Serializable {
- 
-     public NotificationContent build() {
-       NotificationContent content = new NotificationContent();
-+      content.mChannelId = mChannelId;
-       content.mTitle = mTitle;
-       content.mSubtitle = mSubtitle;
-       content.mText = mText;
-diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
-index 6bd9928..ee93d70 100644
---- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
-+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.java
-@@ -48,6 +48,10 @@ public class ExpoNotificationBuilder extends ChannelAwareNotificationBuilder {
- 
-     NotificationContent content = getNotificationContent();
- 
-+    if (content.getChannelId() != null) {
-+      builder.setChannelId(content.getChannelId());
-+    }
-+
-     builder.setAutoCancel(content.isAutoDismiss());
-     builder.setOngoing(content.isSticky());
- 
-diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
-index 55b3a8d..1b99d5b 100644
---- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
-+++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
-@@ -12,11 +12,14 @@ import expo.modules.notifications.notifications.model.triggers.FirebaseNotificat
- import expo.modules.notifications.service.NotificationsService
- import expo.modules.notifications.service.interfaces.FirebaseMessagingDelegate
- import expo.modules.notifications.tokens.interfaces.FirebaseTokenListener
-+import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandler
-+import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandlerInterface
-+import expo.modules.backgroundnotificationhandler.ExpoBackgroundNotificationHandlerModule
- import org.json.JSONObject
- import java.lang.ref.WeakReference
- import java.util.*
- 
--open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate {
-+open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate, BackgroundNotificationHandlerInterface {
-   companion object {
-     // Unfortunately we cannot save state between instances of a service other way
-     // than by static properties. Fortunately, using weak references we can
-@@ -89,12 +92,21 @@ open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseM
-   fun getBackgroundTasks() = sBackgroundTaskConsumerReferences.values.mapNotNull { it.get() }
- 
-   override fun onMessageReceived(remoteMessage: RemoteMessage) {
--    NotificationsService.receive(context, createNotification(remoteMessage))
--    getBackgroundTasks().forEach {
--      it.scheduleJob(RemoteMessageSerializer.toBundle(remoteMessage))
-+    if (!ExpoBackgroundNotificationHandlerModule.isForegrounded) {
-+      BackgroundNotificationHandler(context, this).handleMessage(remoteMessage)
-+      return
-+    } else {
-+      showMessage(remoteMessage)
-+      getBackgroundTasks().forEach {
-+        it.scheduleJob(RemoteMessageSerializer.toBundle(remoteMessage))
-+      }
-     }
-   }
- 
-+  override fun showMessage(remoteMessage: RemoteMessage) {
-+    NotificationsService.receive(context, createNotification(remoteMessage))
-+  }
-+
-   protected fun createNotification(remoteMessage: RemoteMessage): Notification {
-     val identifier = getNotificationIdentifier(remoteMessage)
-     val payload = JSONObject(remoteMessage.data as Map<*, *>)
diff --git a/patches/expo-notifications+0.29.10.patch b/patches/expo-notifications+0.29.10.patch
new file mode 100644
index 000000000..a781d5f1a
--- /dev/null
+++ b/patches/expo-notifications+0.29.10.patch
@@ -0,0 +1,169 @@
+diff --git a/node_modules/expo-notifications/android/build.gradle b/node_modules/expo-notifications/android/build.gradle
+index 5dd0c61..bf536dd 100644
+--- a/node_modules/expo-notifications/android/build.gradle
++++ b/node_modules/expo-notifications/android/build.gradle
+@@ -46,6 +46,7 @@ dependencies {
+   implementation 'com.google.firebase:firebase-messaging:24.0.1'
+ 
+   implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
++  implementation project(':expo-background-notification-handler')
+ 
+   if (project.findProject(':expo-modules-test-core')) {
+     testImplementation project(':expo-modules-test-core')
+diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt
+index 7b99e6c..45a450d 100644
+--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt
++++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/interfaces/INotificationContent.kt
+@@ -15,6 +15,7 @@ import org.json.JSONObject
+  * This interface exists to provide a common API for both classes.
+  * */
+ interface INotificationContent : Parcelable {
++  val channelId: String?
+   val title: String?
+   val text: String?
+   val subText: String?
+diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
+index 191b64e..fe8b3c5 100644
+--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
++++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationContent.java
+@@ -35,6 +35,7 @@ import kotlin.coroutines.Continuation;
+  * Refactoring this class may require a migration strategy for the data stored in SharedPreferences.
+  */
+ public class NotificationContent implements Parcelable, Serializable, INotificationContent {
++  private String mChannelId;
+   private String mTitle;
+   private String mText;
+   private String mSubtitle;
+@@ -65,6 +66,11 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
+     }
+   };
+ 
++  @Nullable
++  public String getChannelId() {
++    return mChannelId;
++  }
++
+   @Nullable
+   public String getTitle() {
+     return mTitle;
+@@ -158,6 +164,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
+   }
+ 
+   protected NotificationContent(Parcel in) {
++    mChannelId = in.readString();
+     mTitle = in.readString();
+     mText = in.readString();
+     mSubtitle = in.readString();
+@@ -183,6 +190,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
+ 
+   @Override
+   public void writeToParcel(Parcel dest, int flags) {
++    dest.writeString(mChannelId);
+     dest.writeString(mTitle);
+     dest.writeString(mText);
+     dest.writeString(mSubtitle);
+@@ -203,6 +211,7 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
+   private static final long serialVersionUID = 397666843266836802L;
+ 
+   private void writeObject(java.io.ObjectOutputStream out) throws IOException {
++    out.writeObject(mChannelId);
+     out.writeObject(mTitle);
+     out.writeObject(mText);
+     out.writeObject(mSubtitle);
+@@ -285,6 +294,11 @@ public class NotificationContent implements Parcelable, Serializable, INotificat
+       useDefaultVibrationPattern();
+     }
+ 
++    public Builder setChannelId(String channelId) {
++      content.mChannelId = channelId;
++      return this;
++    }
++
+     public Builder setTitle(String title) {
+       content.mTitle = title;
+       return this;
+diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt
+index 39b5aad..e50797d 100644
+--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt
++++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/NotificationData.kt
+@@ -11,6 +11,9 @@ import org.json.JSONObject
+ * */
+ @JvmInline
+ value class NotificationData(private val data: Map<String, String>) {
++  val channelId: String?
++    get() = data["channelId"]
++
+   val title: String?
+     get() = data["title"]
+ 
+diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt
+index d2cc6cf..6a48ff2 100644
+--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt
++++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/model/RemoteNotificationContent.kt
+@@ -31,6 +31,8 @@ class RemoteNotificationContent(private val remoteMessage: RemoteMessage) : INot
+     return remoteMessage.notification?.imageUrl != null
+   }
+ 
++  override val channelId = remoteMessage.notification?.channelId ?: notificationData.channelId
++
+   override val title = remoteMessage.notification?.title ?: notificationData.title
+ 
+   override val text = remoteMessage.notification?.body ?: notificationData.message
+diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt
+index 8ca6ec5..57c3599 100644
+--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt
++++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/notifications/presentation/builders/ExpoNotificationBuilder.kt
+@@ -101,6 +101,9 @@ open class ExpoNotificationBuilder(
+     builder.setOngoing(content.isSticky)
+ 
+     // see "Notification anatomy" https://developer.android.com/develop/ui/views/notifications#Templates
++    content.channelId?.let {
++      builder.setChannelId(it)
++    }
+     builder.setContentTitle(content.title)
+     builder.setContentText(content.text)
+     builder.setSubText(content.subText)
+diff --git a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
+index 9f22441..5f92f80 100644
+--- a/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
++++ b/node_modules/expo-notifications/android/src/main/java/expo/modules/notifications/service/delegates/FirebaseMessagingDelegate.kt
+@@ -2,6 +2,9 @@ package expo.modules.notifications.service.delegates
+ 
+ import android.content.Context
+ import com.google.firebase.messaging.RemoteMessage
++import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandler
++import expo.modules.backgroundnotificationhandler.BackgroundNotificationHandlerInterface
++import expo.modules.backgroundnotificationhandler.ExpoBackgroundNotificationHandlerModule
+ import expo.modules.interfaces.taskManager.TaskServiceProviderHelper
+ import expo.modules.notifications.notifications.RemoteMessageSerializer
+ import expo.modules.notifications.notifications.background.BackgroundRemoteNotificationTaskConsumer
+@@ -17,7 +20,8 @@ import expo.modules.notifications.tokens.interfaces.FirebaseTokenListener
+ import java.lang.ref.WeakReference
+ import java.util.*
+ 
+-open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate {
++open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseMessagingDelegate,
++  BackgroundNotificationHandlerInterface {
+   companion object {
+     // Unfortunately we cannot save state between instances of a service other way
+     // than by static properties. Fortunately, using weak references we can
+@@ -94,8 +98,17 @@ open class FirebaseMessagingDelegate(protected val context: Context) : FirebaseM
+     DebugLogging.logRemoteMessage("FirebaseMessagingDelegate.onMessageReceived: message", remoteMessage)
+     val notification = createNotification(remoteMessage)
+     DebugLogging.logNotification("FirebaseMessagingDelegate.onMessageReceived: notification", notification)
+-    NotificationsService.receive(context, notification)
+-    runTaskManagerTasks(remoteMessage)
++
++    if (!ExpoBackgroundNotificationHandlerModule.isForegrounded) {
++      BackgroundNotificationHandler(context, this).handleMessage(remoteMessage)
++    } else {
++      showMessage(remoteMessage)
++      runTaskManagerTasks(remoteMessage)
++    }
++  }
++
++  override fun showMessage(remoteMessage: RemoteMessage) {
++    NotificationsService.receive(context, createNotification(remoteMessage))
+   }
+ 
+   private fun runTaskManagerTasks(remoteMessage: RemoteMessage) {
diff --git a/patches/expo-notifications-0.28.7.patch.md b/patches/expo-notifications+0.29.10.patch.md
index 05f841725..05f841725 100644
--- a/patches/expo-notifications-0.28.7.patch.md
+++ b/patches/expo-notifications+0.29.10.patch.md
diff --git a/patches/expo-updates+0.25.14.patch b/patches/expo-updates+0.26.9.patch
index 6fc4fc5fc..6fc4fc5fc 100644
--- a/patches/expo-updates+0.25.14.patch
+++ b/patches/expo-updates+0.26.9.patch
diff --git a/patches/expo-updates+0.25.11.patch.md b/patches/expo-updates+0.26.9.patch.md
index 6d5d7093d..6d5d7093d 100644
--- a/patches/expo-updates+0.25.11.patch.md
+++ b/patches/expo-updates+0.26.9.patch.md
diff --git a/patches/metro+0.80.4.patch b/patches/metro+0.80.4.patch
deleted file mode 100644
index f8ef67c84..000000000
--- a/patches/metro+0.80.4.patch
+++ /dev/null
@@ -1,44 +0,0 @@
-diff --git a/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js b/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
-index 48a1409..ef185c9 100644
---- a/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
-+++ b/node_modules/metro/src/ModuleGraph/worker/JsFileWrapping.js
-@@ -70,14 +70,19 @@ function wrapModule(
-   importDefaultName,
-   importAllName,
-   dependencyMapName,
--  globalPrefix
-+  globalPrefix,
-+  moduleFactoryName
- ) {
-   const params = buildParameters(
-     importDefaultName,
-     importAllName,
-     dependencyMapName
-   );
--  const factory = functionFromProgram(fileAst.program, params);
-+  const factory = functionFromProgram(
-+    fileAst.program,
-+    params,
-+    moduleFactoryName
-+  );
-   const def = t.callExpression(t.identifier(`${globalPrefix}__d`), [factory]);
-   const ast = t.file(t.program([t.expressionStatement(def)]));
-   const requireName = renameRequires(ast);
-@@ -107,7 +112,16 @@ function wrapJson(source, globalPrefix) {
-     "});",
-   ].join("\n");
- }
--function functionFromProgram(program, parameters) {
-+const JS_INVALID_IDENT_RE = /[^a-zA-Z0-9$_]/g;
-+function functionFromProgram(program, parameters, moduleFactoryName) {
-+  let identifier;
-+  if (typeof moduleFactoryName === "string" && moduleFactoryName !== "") {
-+    // Keep the name readable so it shows up in profiler traces.
-+    // Add an unlikely suffix to avoid collisions with the module code.
-+    identifier = t.identifier(
-+      `${moduleFactoryName.replace(JS_INVALID_IDENT_RE, "_")}__module_factory__`
-+    );
-+  }
-   return t.functionExpression(
-     undefined,
-     parameters.map(makeIdentifier),
diff --git a/patches/metro-runtime+0.80.4.patch b/patches/metro-runtime+0.80.4.patch
deleted file mode 100644
index 65303775d..000000000
--- a/patches/metro-runtime+0.80.4.patch
+++ /dev/null
@@ -1,50 +0,0 @@
-diff --git a/node_modules/metro-runtime/src/polyfills/require.js b/node_modules/metro-runtime/src/polyfills/require.js
-index ce67cb4..eeeae84 100644
---- a/node_modules/metro-runtime/src/polyfills/require.js
-+++ b/node_modules/metro-runtime/src/polyfills/require.js
-@@ -22,6 +22,13 @@ global.__c = clear;
- global.__registerSegment = registerSegment;
- var modules = clear();
- 
-+if (__DEV__) {
-+  // Added by Dan for module init logging.
-+  global.__INIT_LOGS__ = []
-+  var initModuleCounter = 0
-+  var initModuleStack = []
-+}
-+
- // Don't use a Symbol here, it would pull in an extra polyfill with all sorts of
- // additional stuff (e.g. Array.from).
- const EMPTY = {};
-@@ -303,7 +310,30 @@ function loadModuleImplementation(moduleId, module) {
-     throw module.error;
-   }
-   if (__DEV__) {
--    var Systrace = requireSystrace();
-+    // Added by Dan for module init logging.
-+    var Systrace = {
-+      beginEvent(label) {
-+        let fullLabel = initModuleCounter++ + ' ' + label
-+        global.__INIT_LOGS__.push(
-+          ' '.repeat(initModuleStack.length) +
-+          ' ENTER ' + fullLabel
-+        )
-+        initModuleStack.push({
-+          fullLabel,
-+          startTime: nativePerformanceNow(),
-+        })
-+      },
-+      endEvent() {
-+        const res = initModuleStack.pop()
-+        const fullLabel = res.fullLabel
-+        const startTime = res.startTime
-+        const timeElapsed = Math.round(nativePerformanceNow() - startTime)
-+        global.__INIT_LOGS__.push(
-+          ' '.repeat(initModuleStack.length) +
-+            ' LEAVE ' + fullLabel + ' [' + timeElapsed + 'ms]',
-+        )
-+      }
-+    };
-     var Refresh = requireRefresh();
-   }
- 
diff --git a/patches/metro-transform-worker+0.80.4.patch b/patches/metro-transform-worker+0.80.4.patch
deleted file mode 100644
index 717b7c2d9..000000000
--- a/patches/metro-transform-worker+0.80.4.patch
+++ /dev/null
@@ -1,41 +0,0 @@
-diff --git a/node_modules/metro-transform-worker/src/index.js b/node_modules/metro-transform-worker/src/index.js
-index 9f2e3d2..5222c8e 100644
---- a/node_modules/metro-transform-worker/src/index.js
-+++ b/node_modules/metro-transform-worker/src/index.js
-@@ -189,6 +189,10 @@ async function transformJS(file, { config, options, projectRoot }) {
-   let dependencyMapName = "";
-   let dependencies;
-   let wrappedAst;
-+  const minify =
-+    options.minify &&
-+    options.unstable_transformProfile !== "hermes-canary" &&
-+    options.unstable_transformProfile !== "hermes-stable";
- 
-   // If the module to transform is a script (meaning that is not part of the
-   // dependency graph and it code will just be prepended to the bundle modules),
-@@ -228,19 +232,20 @@ async function transformJS(file, { config, options, projectRoot }) {
-     if (config.unstable_disableModuleWrapping === true) {
-       wrappedAst = ast;
-     } else {
-+      let moduleFactoryName;
-+      if (options.dev && !minify) {
-+        moduleFactoryName = file.filename;
-+      }
-       ({ ast: wrappedAst } = JsFileWrapping.wrapModule(
-         ast,
-         importDefault,
-         importAll,
-         dependencyMapName,
--        config.globalPrefix
-+        config.globalPrefix,
-+        moduleFactoryName
-       ));
-     }
-   }
--  const minify =
--    options.minify &&
--    options.unstable_transformProfile !== "hermes-canary" &&
--    options.unstable_transformProfile !== "hermes-stable";
-   const reserved = [];
-   if (config.unstable_dependencyMapReservedName != null) {
-     reserved.push(config.unstable_dependencyMapReservedName);
diff --git a/patches/react-native+0.74.1.patch b/patches/react-native+0.74.1.patch
deleted file mode 100644
index d9f55aa30..000000000
--- a/patches/react-native+0.74.1.patch
+++ /dev/null
@@ -1,283 +0,0 @@
-diff --git a/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm b/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm
-index caa5540..c5d4e67 100644
---- a/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm
-+++ b/node_modules/react-native/Libraries/Blob/RCTFileReaderModule.mm
-@@ -73,7 +73,7 @@ @implementation RCTFileReaderModule
-     } else {
-       NSString *type = [RCTConvert NSString:blob[@"type"]];
-       NSString *text = [NSString stringWithFormat:@"data:%@;base64,%@",
--                                                  type != nil && [type length] > 0 ? type : @"application/octet-stream",
-+                                                  ![type isEqual:[NSNull null]] && [type length] > 0 ? type : @"application/octet-stream",
-                                                   [data base64EncodedStringWithOptions:0]];
- 
-       resolve(text);
-diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
-index b0d71dc..41b9a0e 100644
---- a/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
-+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBaseTextInputView.mm
-@@ -377,10 +377,6 @@ - (void)textInputDidBeginEditing
-     self.backedTextInputView.attributedText = [NSAttributedString new];
-   }
- 
--  if (_selectTextOnFocus) {
--    [self.backedTextInputView selectAll:nil];
--  }
--
-   [_eventDispatcher sendTextEventWithType:RCTTextEventTypeFocus
-                                  reactTag:self.reactTag
-                                      text:[self.backedTextInputView.attributedText.string copy]
-@@ -611,6 +607,10 @@ - (UIView *)reactAccessibilityElement
- - (void)reactFocus
- {
-   [self.backedTextInputView reactFocus];
-+
-+  if (_selectTextOnFocus) {
-+    [self.backedTextInputView selectAll:nil];
-+  }
- }
- 
- - (void)reactBlur
-diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
-index e9b330f..ec5f58c 100644
---- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
-+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
-@@ -15,5 +15,8 @@
- @property (nonatomic, copy) NSString *title;
- @property (nonatomic, copy) RCTDirectEventBlock onRefresh;
- @property (nonatomic, weak) UIScrollView *scrollView;
-+@property (nonatomic, copy) UIColor *customTintColor;
-+
-+- (void)forwarderBeginRefreshing;
- 
- @end
-diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
-index b09e653..d2b4e05 100644
---- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
-+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
-@@ -22,6 +22,7 @@ @implementation RCTRefreshControl {
-   NSString *_title;
-   UIColor *_titleColor;
-   CGFloat _progressViewOffset;
-+  UIColor *_customTintColor;
- }
- 
- - (instancetype)init
-@@ -56,6 +57,12 @@ - (void)layoutSubviews
-   _isInitialRender = false;
- }
- 
-+- (void)didMoveToSuperview
-+{
-+  [super didMoveToSuperview];
-+  [self setTintColor:_customTintColor];
-+}
-+
- - (void)beginRefreshingProgrammatically
- {
-   UInt64 beginRefreshingTimestamp = _currentRefreshingStateTimestamp;
-@@ -203,4 +210,58 @@ - (void)refreshControlValueChanged
-   }
- }
- 
-+- (void)setCustomTintColor:(UIColor *)customTintColor
-+{
-+  _customTintColor = customTintColor;
-+  [self setTintColor:customTintColor];
-+}
-+
-+// Fix for https://github.com/facebook/react-native/issues/43388
-+// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor
-+// is set before the refresh control gets added to the scrollview. We'll call this
-+// function whenever the superview changes. We'll also call it if the value of customTintColor
-+// changes.
-+- (void)setTintColor:(UIColor *)tintColor
-+{
-+  if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) {
-+    [super setTintColor:tintColor];
-+  }
-+}
-+
-+/*
-+ This method is used by Bluesky's ExpoScrollForwarder. This allows other React Native
-+ libraries to perform a refresh of a scrollview and access the refresh control's onRefresh
-+ function.
-+ */
-+- (void)forwarderBeginRefreshing
-+{
-+  _refreshingProgrammatically = NO;
-+
-+  [self sizeToFit];
-+
-+  if (!self.scrollView) {
-+    return;
-+  }
-+
-+  UIScrollView *scrollView = (UIScrollView *)self.scrollView;
-+
-+  [UIView animateWithDuration:0.3
-+    delay:0
-+    options:UIViewAnimationOptionBeginFromCurrentState
-+    animations:^(void) {
-+      // Whenever we call this method, the scrollview will always be at a position of
-+      // -130 or less. Scrolling back to -65 simulates the default behavior of RCTRefreshControl
-+      [scrollView setContentOffset:CGPointMake(0, -65)];
-+    }
-+    completion:^(__unused BOOL finished) {
-+      [super beginRefreshing];
-+      [self setCurrentRefreshingState:super.refreshing];
-+
-+      if (self->_onRefresh) {
-+        self->_onRefresh(nil);
-+      }
-+    }
-+  ];
-+}
-+
- @end
-diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
-index 40aaf9c..1c60164 100644
---- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
-+++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
-@@ -22,11 +22,12 @@ - (UIView *)view
- 
- RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
- RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
--RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
- RCT_EXPORT_VIEW_PROPERTY(title, NSString)
- RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor)
- RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat)
- 
-+RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor)
-+
- RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
- {
-   [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
-diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
-index 1aead51..39e6244 100644
---- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
-+++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
-@@ -159,31 +159,6 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view
-   return !shouldDisableScrollInteraction;
- }
- 
--/*
-- * Automatically centers the content such that if the content is smaller than the
-- * ScrollView, we force it to be centered, but when you zoom or the content otherwise
-- * becomes larger than the ScrollView, there is no padding around the content but it
-- * can still fill the whole view.
-- */
--- (void)setContentOffset:(CGPoint)contentOffset
--{
--  UIView *contentView = [self contentView];
--  if (contentView && _centerContent && !CGSizeEqualToSize(contentView.frame.size, CGSizeZero)) {
--    CGSize subviewSize = contentView.frame.size;
--    CGSize scrollViewSize = self.bounds.size;
--    if (subviewSize.width <= scrollViewSize.width) {
--      contentOffset.x = -(scrollViewSize.width - subviewSize.width) / 2.0;
--    }
--    if (subviewSize.height <= scrollViewSize.height) {
--      contentOffset.y = -(scrollViewSize.height - subviewSize.height) / 2.0;
--    }
--  }
--
--  super.contentOffset = CGPointMake(
--      RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
--      RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
--}
--
- - (void)setFrame:(CGRect)frame
- {
-   // Preserving and revalidating `contentOffset`.
-@@ -427,6 +402,11 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews
-   // Does nothing
- }
- 
-+- (void)setFrame:(CGRect)frame {
-+  [super setFrame:frame];
-+  [self centerContentIfNeeded];
-+}
-+
- - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
- {
-   [super insertReactSubview:view atIndex:atIndex];
-@@ -444,6 +424,8 @@ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
-     RCTApplyTransformationAccordingLayoutDirection(_contentView, self.reactLayoutDirection);
-     [_scrollView addSubview:view];
-   }
-+
-+  [self centerContentIfNeeded];
- }
- 
- - (void)removeReactSubview:(UIView *)subview
-@@ -652,9 +634,46 @@ -(void)delegateMethod : (UIScrollView *)scrollView        \
-   }
- 
- RCT_SCROLL_EVENT_HANDLER(scrollViewWillBeginDecelerating, onMomentumScrollBegin)
--RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
- RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
- 
-+-(void)scrollViewDidZoom : (UIScrollView *)scrollView
-+{
-+  [self centerContentIfNeeded];
-+
-+  RCT_SEND_SCROLL_EVENT(onScroll, nil);
-+  RCT_FORWARD_SCROLL_EVENT(scrollViewDidZoom : scrollView);
-+}
-+
-+/*
-+ * Automatically centers the content such that if the content is smaller than the
-+ * ScrollView, we force it to be centered, but when you zoom or the content otherwise
-+ * becomes larger than the ScrollView, there is no padding around the content but it
-+ * can still fill the whole view.
-+ *
-+ * PATCHED: This deviates from the original React Native implementation to fix two issues:
-+ *
-+ * - The scroll view was swallowing any taps immediately after pinching
-+ * - The scroll view was jittering when crossing the full screen threshold while pinching
-+ *
-+ * This implementation is based on https://petersteinberger.com/blog/2013/how-to-center-uiscrollview/.
-+ */
-+-(void)centerContentIfNeeded
-+{
-+  if (_scrollView.centerContent &&
-+      !CGSizeEqualToSize(self.contentSize, CGSizeZero) &&
-+      !CGSizeEqualToSize(self.bounds.size, CGSizeZero)
-+  ) {
-+    CGFloat top = 0, left = 0;
-+    if (self.contentSize.width < self.bounds.size.width) {
-+      left = (self.bounds.size.width - self.contentSize.width) * 0.5f;
-+    }
-+    if (self.contentSize.height < self.bounds.size.height) {
-+      top = (self.bounds.size.height - self.contentSize.height) * 0.5f;
-+    }
-+    _scrollView.contentInset = UIEdgeInsetsMake(top, left, top, left);
-+  }
-+}
-+
- - (void)addScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
- {
-   [_scrollListeners addObject:scrollListener];
-@@ -913,6 +932,7 @@ - (void)updateContentSizeIfNeeded
-   CGSize contentSize = self.contentSize;
-   if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) {
-     _scrollView.contentSize = contentSize;
-+    [self centerContentIfNeeded];
-   }
- }
- 
-diff --git a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
-index 5f5e1ab..aac00b6 100644
---- a/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
-+++ b/node_modules/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/core/JavaTimerManager.java
-@@ -99,8 +99,9 @@ public class JavaTimerManager {
-       }
- 
-       // If the JS thread is busy for multiple frames we cancel any other pending runnable.
--      if (mCurrentIdleCallbackRunnable != null) {
--        mCurrentIdleCallbackRunnable.cancel();
-+      IdleCallbackRunnable currentRunnable = mCurrentIdleCallbackRunnable;
-+      if (currentRunnable != null) {
-+        currentRunnable.cancel();
-       }
- 
-       mCurrentIdleCallbackRunnable = new IdleCallbackRunnable(frameTimeNanos);
diff --git a/patches/react-native+0.76.3.patch b/patches/react-native+0.76.3.patch
new file mode 100644
index 000000000..6f71097a1
--- /dev/null
+++ b/patches/react-native+0.76.3.patch
@@ -0,0 +1,331 @@
+diff --git a/node_modules/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts b/node_modules/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts
+index 62f52a7..ca30165 100644
+--- a/node_modules/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts
++++ b/node_modules/react-native/Libraries/StyleSheet/StyleSheetTypes.d.ts
+@@ -426,9 +426,10 @@ export interface ViewStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle {
+    */
+   pointerEvents?: 'box-none' | 'none' | 'box-only' | 'auto' | undefined;
+   isolation?: 'auto' | 'isolate' | undefined;
+-  cursor?: CursorValue | undefined;
++  cursor?: CursorValue | string | undefined;
+   boxShadow?: ReadonlyArray<BoxShadowValue> | string | undefined;
+   filter?: ReadonlyArray<FilterFunction> | string | undefined;
++  transformOrigin?: string | (string | number)[] | undefined;
+ }
+ 
+ export type FontVariant =
+@@ -536,7 +537,11 @@ export interface TextStyle extends TextStyleIOS, TextStyleAndroid, ViewStyle {
+   textShadowOffset?: {width: number; height: number} | undefined;
+   textShadowRadius?: number | undefined;
+   textTransform?: 'none' | 'capitalize' | 'uppercase' | 'lowercase' | undefined;
+-  userSelect?: 'auto' | 'none' | 'text' | 'contain' | 'all' | undefined;
++  userSelect?: 'auto' | 'none' | 'text' | 'contain' | 'all' | string | undefined;
++  cursor?: CursorValue | string | undefined;
++  boxShadow?: ReadonlyArray<BoxShadowValue> | string | undefined;
++  filter?: ReadonlyArray<FilterFunction> | string | undefined;
++  transformOrigin?: string | (string | number)[] | undefined;
+ }
+ 
+ /**
+@@ -558,5 +563,8 @@ export interface ImageStyle extends FlexStyle, ShadowStyleIOS, TransformsStyle {
+   tintColor?: ColorValue | undefined;
+   opacity?: AnimatableNumericValue | undefined;
+   objectFit?: 'cover' | 'contain' | 'fill' | 'scale-down' | undefined;
+-  cursor?: CursorValue | undefined;
++  cursor?: CursorValue | string | undefined;
++  boxShadow?: ReadonlyArray<BoxShadowValue> | string | undefined;
++  filter?: ReadonlyArray<FilterFunction> | string | undefined;
++  transformOrigin?: string | (string | number)[] | undefined;
+ }
+diff --git a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
+index 93af874..106f8ec 100644
+--- a/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
++++ b/node_modules/react-native/React/Fabric/Mounting/ComponentViews/ScrollView/RCTEnhancedScrollView.mm
+@@ -66,28 +66,51 @@ - (void)preserveContentOffsetWithBlock:(void (^)())block
+  * ScrollView, we force it to be centered, but when you zoom or the content otherwise
+  * becomes larger than the ScrollView, there is no padding around the content but it
+  * can still fill the whole view.
++ * This implementation is based on https://petersteinberger.com/blog/2013/how-to-center-uiscrollview/.
+  */
+-- (void)setContentOffset:(CGPoint)contentOffset
++-(void)centerContentIfNeeded
+ {
+-  if (_isSetContentOffsetDisabled) {
++  if (!_centerContent) {
+     return;
+   }
+ 
+-  if (_centerContent && !CGSizeEqualToSize(self.contentSize, CGSizeZero)) {
+-    CGSize scrollViewSize = self.bounds.size;
+-    if (self.contentSize.width <= scrollViewSize.width) {
+-      contentOffset.x = -(scrollViewSize.width - self.contentSize.width) / 2.0;
+-    }
+-    if (self.contentSize.height <= scrollViewSize.height) {
+-      contentOffset.y = -(scrollViewSize.height - self.contentSize.height) / 2.0;
+-    }
++  CGSize contentSize = self.contentSize;
++  CGSize boundsSize = self.bounds.size;
++  if (CGSizeEqualToSize(contentSize, CGSizeZero) ||
++      CGSizeEqualToSize(boundsSize, CGSizeZero)) {
++    return;
+   }
+ 
++  CGFloat top = 0, left = 0;
++  if (contentSize.width < boundsSize.width) {
++    left = (boundsSize.width - contentSize.width) * 0.5f;
++  }
++  if (contentSize.height < boundsSize.height) {
++    top = (boundsSize.height - contentSize.height) * 0.5f;
++  }
++  self.contentInset = UIEdgeInsetsMake(top, left, top, left);
++}
++
++- (void)setContentOffset:(CGPoint)contentOffset
++{
++  if (_isSetContentOffsetDisabled) {
++    return;
++  }
+   super.contentOffset = CGPointMake(
+       RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
+       RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
+ }
+ 
++- (void)setFrame:(CGRect)frame {
++  [super setFrame:frame];
++  [self centerContentIfNeeded];
++}
++
++- (void)didAddSubview:(UIView *)subview {
++  [super didAddSubview:subview];
++  [self centerContentIfNeeded];
++}
++
+ - (BOOL)touchesShouldCancelInContentView:(UIView *)view
+ {
+   if ([_overridingDelegate respondsToSelector:@selector(touchesShouldCancelInContentView:)]) {
+@@ -257,6 +280,10 @@ - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
+   }
+ }
+ 
++- (void)scrollViewDidZoom:(__unused UIScrollView *)scrollView {
++  [self centerContentIfNeeded];
++}
++
+ #pragma mark -
+ 
+ - (BOOL)isHorizontal:(UIScrollView *)scrollView
+diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
+index e9b330f..ec5f58c 100644
+--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
++++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.h
+@@ -15,5 +15,8 @@
+ @property (nonatomic, copy) NSString *title;
+ @property (nonatomic, copy) RCTDirectEventBlock onRefresh;
+ @property (nonatomic, weak) UIScrollView *scrollView;
++@property (nonatomic, copy) UIColor *customTintColor;
++
++- (void)forwarderBeginRefreshing;
+ 
+ @end
+diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
+index 53bfd04..ff1b1ed 100644
+--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
++++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControl.m
+@@ -23,6 +23,7 @@ @implementation RCTRefreshControl {
+   UIColor *_titleColor;
+   CGFloat _progressViewOffset;
+   BOOL _hasMovedToWindow;
++  UIColor *_customTintColor;
+ }
+ 
+ - (instancetype)init
+@@ -58,6 +59,12 @@ - (void)layoutSubviews
+   _isInitialRender = false;
+ }
+ 
++- (void)didMoveToSuperview
++{
++  [super didMoveToSuperview];
++  [self setTintColor:_customTintColor];
++}
++
+ - (void)didMoveToWindow
+ {
+   [super didMoveToWindow];
+@@ -221,4 +228,50 @@ - (void)refreshControlValueChanged
+   }
+ }
+ 
++// Fix for https://github.com/facebook/react-native/issues/43388
++// A bug in iOS 17.4 causes the haptic to not play when refreshing if the tintColor
++// is set before the refresh control gets added to the scrollview. We'll call this
++// function whenever the superview changes. We'll also call it if the value of customTintColor
++// changes.
++- (void)setTintColor:(UIColor *)tintColor
++{
++  if ([self.superview isKindOfClass:[UIScrollView class]] && self.tintColor != tintColor) {
++    [super setTintColor:tintColor];
++  }
++}
++
++// This method is used by Bluesky's ExpoScrollForwarder. This allows other React Native
++// libraries to perform a refresh of a scrollview and access the refresh control's onRefresh
++// function.
++- (void)forwarderBeginRefreshing
++{
++  _refreshingProgrammatically = NO;
++
++  [self sizeToFit];
++
++  if (!self.scrollView) {
++    return;
++  }
++
++  UIScrollView *scrollView = (UIScrollView *)self.scrollView;
++
++  [UIView animateWithDuration:0.3
++    delay:0
++    options:UIViewAnimationOptionBeginFromCurrentState
++    animations:^(void) {
++      // Whenever we call this method, the scrollview will always be at a position of
++      // -130 or less. Scrolling back to -65 simulates the default behavior of RCTRefreshControl
++      [scrollView setContentOffset:CGPointMake(0, -65)];
++    }
++    completion:^(__unused BOOL finished) {
++      [super beginRefreshing];
++      [self setCurrentRefreshingState:super.refreshing];
++
++      if (self->_onRefresh) {
++        self->_onRefresh(nil);
++      }
++    }
++  ];
++}
++
+ @end
+diff --git a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
+index 40aaf9c..1c60164 100644
+--- a/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
++++ b/node_modules/react-native/React/Views/RefreshControl/RCTRefreshControlManager.m
+@@ -22,11 +22,12 @@ - (UIView *)view
+ 
+ RCT_EXPORT_VIEW_PROPERTY(onRefresh, RCTDirectEventBlock)
+ RCT_EXPORT_VIEW_PROPERTY(refreshing, BOOL)
+-RCT_EXPORT_VIEW_PROPERTY(tintColor, UIColor)
+ RCT_EXPORT_VIEW_PROPERTY(title, NSString)
+ RCT_EXPORT_VIEW_PROPERTY(titleColor, UIColor)
+ RCT_EXPORT_VIEW_PROPERTY(progressViewOffset, CGFloat)
+ 
++RCT_REMAP_VIEW_PROPERTY(tintColor, customTintColor, UIColor)
++
+ RCT_EXPORT_METHOD(setNativeRefreshing : (nonnull NSNumber *)viewTag toRefreshing : (BOOL)refreshing)
+ {
+   [self.bridge.uiManager addUIBlock:^(RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
+diff --git a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
+index e9ce48c..ccd9ad6 100644
+--- a/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
++++ b/node_modules/react-native/React/Views/ScrollView/RCTScrollView.m
+@@ -159,26 +159,8 @@ - (BOOL)touchesShouldCancelInContentView:(__unused UIView *)view
+   return !shouldDisableScrollInteraction;
+ }
+ 
+-/*
+- * Automatically centers the content such that if the content is smaller than the
+- * ScrollView, we force it to be centered, but when you zoom or the content otherwise
+- * becomes larger than the ScrollView, there is no padding around the content but it
+- * can still fill the whole view.
+- */
+ - (void)setContentOffset:(CGPoint)contentOffset
+ {
+-  UIView *contentView = [self contentView];
+-  if (contentView && _centerContent && !CGSizeEqualToSize(contentView.frame.size, CGSizeZero)) {
+-    CGSize subviewSize = contentView.frame.size;
+-    CGSize scrollViewSize = self.bounds.size;
+-    if (subviewSize.width <= scrollViewSize.width) {
+-      contentOffset.x = -(scrollViewSize.width - subviewSize.width) / 2.0;
+-    }
+-    if (subviewSize.height <= scrollViewSize.height) {
+-      contentOffset.y = -(scrollViewSize.height - subviewSize.height) / 2.0;
+-    }
+-  }
+-
+   super.contentOffset = CGPointMake(
+       RCTSanitizeNaNValue(contentOffset.x, @"scrollView.contentOffset.x"),
+       RCTSanitizeNaNValue(contentOffset.y, @"scrollView.contentOffset.y"));
+@@ -427,6 +409,11 @@ - (void)setRemoveClippedSubviews:(__unused BOOL)removeClippedSubviews
+   // Does nothing
+ }
+ 
++- (void)setFrame:(CGRect)frame {
++  [super setFrame:frame];
++  [self centerContentIfNeeded];
++}
++
+ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
+ {
+   [super insertReactSubview:view atIndex:atIndex];
+@@ -443,6 +430,8 @@ - (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
+     _contentView = view;
+     RCTApplyTransformationAccordingLayoutDirection(_contentView, self.reactLayoutDirection);
+     [_scrollView addSubview:view];
++
++    [self centerContentIfNeeded];
+   }
+ }
+ 
+@@ -652,9 +641,46 @@ -(void)delegateMethod : (UIScrollView *)scrollView        \
+   }
+ 
+ RCT_SCROLL_EVENT_HANDLER(scrollViewWillBeginDecelerating, onMomentumScrollBegin)
+-RCT_SCROLL_EVENT_HANDLER(scrollViewDidZoom, onScroll)
+ RCT_SCROLL_EVENT_HANDLER(scrollViewDidScrollToTop, onScrollToTop)
+ 
++-(void)scrollViewDidZoom : (UIScrollView *)scrollView
++{
++  [self centerContentIfNeeded];
++
++  RCT_SEND_SCROLL_EVENT(onScroll, nil);
++  RCT_FORWARD_SCROLL_EVENT(scrollViewDidZoom : scrollView);
++}
++
++/*
++ * Automatically centers the content such that if the content is smaller than the
++ * ScrollView, we force it to be centered, but when you zoom or the content otherwise
++ * becomes larger than the ScrollView, there is no padding around the content but it
++ * can still fill the whole view.
++ * This implementation is based on https://petersteinberger.com/blog/2013/how-to-center-uiscrollview/.
++ */
++-(void)centerContentIfNeeded
++{
++  if (!_scrollView.centerContent) {
++    return;
++  }
++
++  CGSize contentSize = self.contentSize;
++  CGSize boundsSize = self.bounds.size;
++  if (CGSizeEqualToSize(contentSize, CGSizeZero) ||
++      CGSizeEqualToSize(boundsSize, CGSizeZero)) {
++    return;
++  }
++
++  CGFloat top = 0, left = 0;
++  if (contentSize.width < boundsSize.width) {
++    left = (boundsSize.width - contentSize.width) * 0.5f;
++  }
++  if (contentSize.height < boundsSize.height) {
++    top = (boundsSize.height - contentSize.height) * 0.5f;
++  }
++  _scrollView.contentInset = UIEdgeInsetsMake(top, left, top, left);
++}
++
+ - (void)addScrollListener:(NSObject<UIScrollViewDelegate> *)scrollListener
+ {
+   [_scrollListeners addObject:scrollListener];
+@@ -933,6 +959,7 @@ - (void)updateContentSizeIfNeeded
+   CGSize contentSize = self.contentSize;
+   if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize)) {
+     _scrollView.contentSize = contentSize;
++    [self centerContentIfNeeded];
+   }
+ }
+ 
diff --git a/patches/react-native+0.74.1.patch.md b/patches/react-native+0.76.3.patch.md
index 84953df2b..eacb9f267 100644
--- a/patches/react-native+0.74.1.patch.md
+++ b/patches/react-native+0.76.3.patch.md
@@ -12,9 +12,6 @@ Patching `RCTRefreshControl.m` and `RCTRefreshControl.h` to add a new `forwarder
 This method is used by `ExpoScrollForwarder` to initiate a refresh of the underlying `UIScrollView` from inside that
 module.
 
+## ScrollView centerContent fix
 
-## TextInput Patch - `selectTextOnFocus` fix
-
-Patching `RCTBaseTextInputView.m` to fix an issue where `selectTextOnFocus` does not work as expected on iOS 17. This
-patch _only_ fixes the Paper version of `TextInput`. If we migrate to Fabric and the fix has not been made upstream,
-we can apply the same fix. See https://github.com/facebook/react-native/pull/44307.
+Includes https://github.com/facebook/react-native/pull/47591 early. Delete when it's in a release.
diff --git a/patches/react-native-image-crop-picker+0.41.2.patch b/patches/react-native-image-crop-picker+0.41.2.patch
deleted file mode 100644
index 7f1865a5e..000000000
--- a/patches/react-native-image-crop-picker+0.41.2.patch
+++ /dev/null
@@ -1,39 +0,0 @@
-diff --git a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
-index 5de0845..fa477e7 100644
---- a/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
-+++ b/node_modules/react-native-image-crop-picker/android/src/main/java/com/reactnative/ivpusic/imagepicker/PickerModule.java
-@@ -21,6 +21,7 @@ import android.webkit.MimeTypeMap;
- 
- import androidx.core.app.ActivityCompat;
- import androidx.core.content.FileProvider;
-+import androidx.exifinterface.media.ExifInterface;
- 
- import com.facebook.react.bridge.ActivityEventListener;
- import com.facebook.react.bridge.Callback;
-@@ -678,6 +679,15 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
-             throw new Exception("Cannot select remote files");
-         }
-         BitmapFactory.Options original = validateImage(path);
-+        ExifInterface originalExif = new ExifInterface(path);
-+        int orientation = originalExif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1);
-+        boolean invertDimensions = (
-+                orientation == ExifInterface.ORIENTATION_ROTATE_90 ||
-+                        orientation == ExifInterface.ORIENTATION_ROTATE_270 ||
-+                        orientation == ExifInterface.ORIENTATION_TRANSPOSE ||
-+                        orientation == ExifInterface.ORIENTATION_TRANSVERSE
-+        );
-+
- 
-         // if compression options are provided image will be compressed. If none options is provided,
-         // then original image will be returned
-@@ -687,8 +697,8 @@ class PickerModule extends ReactContextBaseJavaModule implements ActivityEventLi
-         long modificationDate = new File(path).lastModified();
- 
-         image.putString("path", "file://" + compressedImagePath);
--        image.putInt("width", options.outWidth);
--        image.putInt("height", options.outHeight);
-+        image.putInt("width", invertDimensions ? options.outHeight : options.outWidth);
-+        image.putInt("height", invertDimensions ? options.outWidth : options.outHeight);
-         image.putString("mime", options.outMimeType);
-         image.putInt("size", (int) new File(compressedImagePath).length());
-         image.putString("modificationDate", String.valueOf(modificationDate));
diff --git a/patches/react-native-svg+15.3.0.patch b/patches/react-native-svg+15.9.0.patch
index 54540023f..54540023f 100644
--- a/patches/react-native-svg+15.3.0.patch
+++ b/patches/react-native-svg+15.9.0.patch