Skip to content

chore(mobile): bump versions for native release 1.5.187 - #14645

Merged
dylanjeffers merged 4 commits into
mainfrom
chore/mobile-native-release-1.5.187
Oct 9, 2026
Merged

dylanjeffers merged 4 commits into
mainfrom
chore/mobile-native-release-1.5.187

Conversation

@dylanjeffers

@dylanjeffers dylanjeffers commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Version bump for native release 1.5.187. Merging it starts the RC and production native builds.

  • packages/mobile/package.json: 1.5.186 → 1.5.187
  • iOS CFBundleShortVersionString: 1.1.199 → 1.1.200
  • Android versionName: 1.1.535 → 1.1.536

1.5.187 contains:

Merge order

  1. Wait until 1.5.186 is live in both stores. Until then, any native change on main would go into a 1.5.186 rebuild.
  2. Dispatch a production OTA from main before merging anything below, for both store versions, so users on the current binaries get the latest JS:
    gh workflow run mobile.yml --repo AudiusProject/apps --ref main -f ota_channel=production
    gh workflow run mobile.yml --repo AudiusProject/apps --ref main -f ota_channel=production -f binary_version=1.5.185
    
  3. Land everything on main in one push. fix(mobile): replace fs and gradient native libs #14627 and feat(mobile): move push notifications to notifee + firebase messaging #14640 are based on this branch: merge them into it first, then merge this PR into main. Any push to main that leaves the version unchanged publishes an RC OTA to the current binary's history. Merging fix(mobile): replace fs and gradient native libs #14627 straight to main would OTA its JS onto 1.5.186 RC binaries, and it calls TurboModuleRegistry.getEnforcing('ReactNativeFs') at import, so they would crash. With one push that also changes the version, the version check skips the OTA and starts the native builds instead. After that, OTAs route to 1.5.187 histories.
  4. Device-test the RC builds before submitting:

🤖 Generated with Claude Code

- package.json 1.5.186 -> 1.5.187 (triggers the RC and production native builds)
- iOS CFBundleShortVersionString 1.1.199 -> 1.1.200
- Android versionName 1.1.535 -> 1.1.536

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 082ff2a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@socket-security

socket-security Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Dependency limit exceeded — report not shown.

This pull request scan exceeded the 10,000-dependency limit applied to this scan, so the results are incomplete and may be inaccurate. To avoid reporting false positives, Socket has not posted a report.

Upgrade your plan to raise the dependency limit and get complete reports, or view the partial scan in the dashboard.

Socket is always free for open source. If this is a non-commercial open source project, contact us to request a free Team account.

)

Ships in native release **1.5.187**. It targets
`chore/mobile-native-release-1.5.187` (#14645).

## What
A `post_install` step in the Podfile patches one line of the
SwiftAudioEx pod source, `Sources/SwiftAudioEx/QueueManager.swift`:

```diff
-            if (self.items.count > 1 && currentIndex >= index) {
+            if (self.items.count > 0 && currentIndex >= index) {
```

SwiftAudioEx 1.0.0 (our Podfile.lock) and 1.1.0 (RNTP 4.1.2's podspec)
only shift the current index for an insert in front of the current item
when the queue already has more than one item. With one item, inserting
at 0 leaves the index one slot early, and when the track ends the player
replays it instead of advancing. Upstream `main` has the `> 0` fix, but
it isn't released yet.

The step replaces only that exact line. If the file is already patched
it does nothing. If the line is missing (for example after a
SwiftAudioEx update), it raises and `pod install` fails, so the patch
can't be skipped silently.

## Relation to #14649
#14649 is the JS fix, which ships OTA. It avoids the bad insert by
appending the later tracks first. When the tapped track is the last one
in the list, it uses a temporary placeholder instead. This PR fixes the
root cause natively, which covers the last-item case without the
placeholder. It also covers any other single-item insert.

## Testing
- `RCT_NEW_ARCH_ENABLED=0 bundle exec pod install` prints the patch
message, and
`Pods/SwiftAudioEx/Sources/SwiftAudioEx/QueueManager.swift:148` now
reads `items.count > 0`.
- I checked the patch logic on three versions of the file: unpatched
gets patched, already patched is left alone, and a changed line raises.
- Confirmed the same line is in SwiftAudioEx tag 1.0.0 and in 1.1.0.
- Podfile.lock is not changed. The local install showed the known RNTP
4.0.1→4.1.2 / SwiftAudioEx 1.0.0→1.1.0 drift, which I left out.
- I haven't rebuilt the native app with this patch. The simulator check
of the last-item case was done with the JS fix (#14649).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@pull-request-size pull-request-size Bot added size/S and removed size/XS labels Oct 9, 2026
## Summary

Replaces abandoned native libraries ahead of the React Native upgrade.
Targets native release **1.5.187**. Stays on RN 0.79 with the old
architecture. Rebased on current `main`.

**1. `react-native-fs` → `@dr.pogodin/react-native-fs@2.32.1`**
- Pinned because 2.33+ uses a codegen EventEmitter spec that the RN 0.79
codegen rejects.
- Call sites (`useShareToStory`, `migrateOfflineDataPathSaga`) use a
namespace import, since the fork has no default export. The Metro `fs`
polyfill alias points at the fork.
- **Android:** the fork only compiles its generated module spec with the
new architecture on. An `android/build.gradle` hook adds the generated
sources when `newArchEnabled` is false, and the module still registers
on the bridge. The manual `react-native-fs` include is gone from
`settings.gradle`.
- **iOS:** `PrivacyInfo.xcprivacy` declares the required-reason APIs the
new pod uses (file timestamp `0A2A.1`, disk space `85F4.1`).

**2. `react-native-linear-gradient` / `react-native-radial-gradient` →
harmony-native `LinearGradient` / `RadialGradient` on
`react-native-svg`**
- **Rounded corners:** the SVG is clipped to the container's corner
radii.
- **Angles:** the `angle` prop uses react-native-linear-gradient's own
formula, so diagonal gradients on wide boxes (scrubber, tracking bar)
keep their colors.
- **RadialGradient** renders its children.
- **Wallet chart crash:** `react-native-gifted-charts` loads
`react-native-linear-gradient` at startup. A Metro shim maps it to our
component.
- The `SRSRadialGradient` pod and the Android `settings.gradle` include
are removed.

No code added to `main` since the original commits imports any of the
removed libraries or uses `useAngle`, so no new call sites needed
converting.

## Scrypt swap dropped

The earlier version also replaced `react-native-fast-crypto` with
`@noble/hashes` scrypt. That's removed. Under Hermes it took about
**11.7 s per derivation** (N=32768, r=8, p=1), against about 90 ms under
V8, because Hermes has no JIT. Sign-in runs two derivations, so it would
have added more than 20 s to sign-in, and more on phones.

`main` already fixes the original problem in #14638: Android uses the
native `AudiusScrypt` module, and iOS keeps `react-native-fast-crypto`.
This PR doesn't touch `createPrivateKey.ts`, `react-native.config.js` or
the `react-native-fast-crypto` dependency.

## Verification

- **Typecheck and lint:** `tsc --noEmit -p packages/mobile` passes, and
eslint passes on every touched file.
- **Pods:** installed with `RCT_NEW_ARCH_ENABLED=0`. No
`Pods-AudiusReactNative/*.release.xcconfig` sets
`RCT_NEW_ARCH_ENABLED=1`.
- **iOS:** a Release build with `.env.prod` succeeds. It runs on an iOS
26.5 simulator, launches to the sign-up screen and logs no JS errors.
- **iOS offline-path migration:** I created a file under
`Documents/downloads/...` before first launch. On launch,
`migrateOfflineDataPathSaga` moved it to `Library/Caches/downloads/...`
using the fork's `readDir`, `exists`, `mkdir` and `moveFile`.
- **iOS gradients:** I used a temporary, uncommitted panel on the
sign-up screen to render:
  - a 135° rounded `LinearGradient` like the scrubber
  - a start/end `LinearGradient` with a child, like the tiers header
  - a `RadialGradient` with a child
  - a `Skeleton`
  - `GradientText`
  - a primary `Button` with a `gradient`
  - a gifted-charts area chart through the shim
  
  All rendered correctly.
- **Android:** `assembleProdRelease` (arm64-v8a, signed with the debug
keystore) builds. On an API 36 emulator after `pm clear`, logcat shows
`Loading JS bundle from "assets://index.android.bundle"`. The app
reaches the sign-up and sign-in screens with no fatal or JS errors. The
fork's module therefore resolves on the bridge, since `getEnforcing`
would throw at import otherwise.

**Not verified:**
- **Android gradient rendering:** the emulator was starved for CPU
during that pass, and every system process hit an ANR.
- **Screens that need a signed-in account:** the real scrubber/now
playing, rewards tiers and referral copy button weren't checked. I
relied on the component-level panel above instead.
- **Share-to-story:** not exercised at runtime.

## ⚠️ Must ship with a native version bump

`@dr.pogodin/react-native-fs` looks up its module with
`TurboModuleRegistry.getEnforcing('ReactNativeFs')` at import, and
`migrateOfflineDataPathSaga` imports it at startup. If this JS is
published as an OTA onto a binary without the module (any current store
build), the app crashes at launch. Merge only together with the
`packages/mobile/package.json` version bump for the native release that
includes it (1.5.187), so that OTAs move to the new binary's history.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@pull-request-size pull-request-size Bot added size/L and removed size/S labels Oct 9, 2026
…#14640)

Replaces the unmaintained `react-native-notifications` (Wix) with
`@notifee/react-native` 9.1.8 and `@react-native-firebase/messaging`
24.1.1. Rebased from #14506 onto current main (RN 0.79, React Navigation
7, UIScene, target SDK 36). Targets native release **1.5.187**.

> [!IMPORTANT]
> **Native release only. Do not ship this as an OTA.** It adds and
removes native modules. A JS bundle built from this branch would crash
on any binary that still has the Wix module, and the reverse is also
true. Store builds that predate this change must not receive a CodePush
bundle built from it.

## Token compatibility (read this first)

**Verdict: compatible, with no server changes needed.** iOS still
registers the raw APNs token. The FCM token is used only on Android,
which is what Android sent before too.

How push works today:
- The app POSTs `{deviceToken, deviceType}` to identity `POST
/push_notifications/device_token`
([push_notifications.js](https://github.com/AudiusProject/apps/blob/main/packages/identity-service/src/routes/push_notifications.js)).
Identity calls SNS `createPlatformEndpoint` against `awsSNSiOSARN` or
`awsSNSAndroidARN` and upserts `NotificationDeviceTokens` (the primary
key is `deviceToken`).
- The SNS platform apps in us-west-1 are
`app/APNS/audius_production_ios` and
`app/GCM/audius_production_android`, checked read-only with `aws sns
list-platform-applications`. **The iOS app is an APNs app, not FCM.**
- pedalboard `apps/notifications/src/sns.ts` publishes to the stored
`awsARN`, with an `APNS` payload on iOS and a `GCM` payload on Android.

What the old draft got wrong: #14506 registered `messaging().getToken()`
on iOS, which is an FCM token. SNS rejects that for an APNs platform
app, so identity would 500 and new iOS installs would get no pushes. It
also used `onNotificationOpenedApp` and `getInitialNotification` for iOS
taps. Firebase only reports pushes that carry `gcm.message_id`, and ours
come straight from APNs, so iOS taps would never have navigated.

What this PR does:
- **iOS:** `registerDeviceForRemoteMessages()`, then `getAPNSToken()`,
then `.toLowerCase()`. Firebase returns uppercase hex and Wix sent
lowercase hex. Without the lowercase step, every existing iOS user would
get a second `NotificationDeviceTokens` row and a second SNS endpoint
for the same device, and so a duplicate copy of every push.
- **Android:** `getToken()`. This is the same FCM token for the same
Firebase project (`google-services.json` is unchanged), so existing rows
are reused.
- `onTokenRefresh` (FCM) is Android-only, so it never overwrites the
stored iOS APNs token.

Other server-side notes:
- **General inbox (pedalboard#93):** the server drops those pushes and
never sends them, so there is no silent or data-only push for the client
to handle. The push listener fix (pedalboard#103) does not depend on the
client.
- **Firebase config:** `GoogleService-Info.plist` and the
`google-services.json` files are already committed and nothing new was
added. `GoogleService-Info.plist` was already in Copy Bundle Resources;
this is the first time iOS actually initializes Firebase (`[FIRApp
configure]`). No Firebase Analytics pod is added on iOS.

## UIScene interaction

Main copied `connectionOptions.notificationResponse` into
`launchOptions` so Wix's `getInitialNotification` could read it. Nothing
reads it now, so that copy is removed (URL and user-activity forwarding
stay).

Taps now reach JS like this:
- Firebase messaging and notifee both install a
`UNUserNotificationCenter` delegate after launch. Whichever installs
last forwards anything it does not own to the other one.
- Firebase passes non-FCM pushes to the previous delegate, and notifee
turns a remote push it receives into a `PRESS` event only when it has no
previous delegate.
- So `AppDelegate` calls `[[NotifeeCoreUNUserNotificationCenter
instance] observe]` first. The chain becomes Firebase → notifee →
`PRESS` event.

On a cold launch, iOS still delivers the tap to the delegate when the
app uses scenes. Notifee queues the event until JS subscribes.
`PushNotifications` keeps it in `pendingOpen` until
`openInitialNotification()` runs, which happens when navigation is
ready, and then navigates once. On iOS, `getInitialNotification` (from
Firebase or notifee) is never called, so a tap cannot be handled twice.
On Android, cold start uses `getInitialNotification` and warm taps use
`onNotificationOpenedApp`. Notifee is not involved on Android.

## What changed

- `notifications.ts`: rewritten on top of main's version. It keeps
main's `type`/`id` analytics props, the `notification_campaign_id` open
report, the Android string-to-type data parsing, and the same
`payload.data.data ?? payload.data ?? payload` unwrapping as before. The
iOS payload is the APNs userInfo minus `aps`, which is the shape Wix
produced.
- Removed unused `cancelNotif`, `cancelAll`, `hasPermission` and
`lastId`.
- `setBadgeCount` (used by `useResetNotificationBadgeCount`) calls
`notifee.setBadgeCount` on iOS only, as before.
- `requestPermission`:
- Android: still `requestNotifications()` from react-native-permissions
(`POST_NOTIFICATIONS`).
- iOS: `requestPermission()` from Firebase messaging (alert, badge and
sound, as Wix did).
- `index.js`: a no-op `setBackgroundMessageHandler`. On Android,
Firebase starts a headless JS task for every push received in the
background, and without a handler it logs a warning.
- iOS:
- `AppDelegate.mm`: drop Wix, call `[FIRApp configure]`, and set up
notifee first.
- Podfile: `GoogleUtilities`/`nanopb` modular headers so the Firebase
Swift pods link statically.
- `pod install` adds Firebase 12.10, RNFBApp/RNFBMessaging 24.1.1 and
RNNotifee 9.1.8. `[RNFB] Core Configuration` is added as a build phase,
and the privacy manifest picks up the Firebase reasons.
- Android:
  - Remove Wix from `settings.gradle` and `app/build.gradle`.
- **Pin the Firebase BoM to 33.16.0** in `android/build.gradle`. RNFB 24
defaults to BoM 34, whose `play-services-measurement` 23.x needs Kotlin
2.2, and the app is on 2.0.21. That pin moves `firebase-messaging` from
23.x to 24.1.2 and `firebase-analytics` to 22.5.0.
- Remove RNFB's `default_notification_color` meta-data
(`tools:node="remove"`). RNFB defaults the accent to white, and this
keeps the system default we had.
- `RichPushExtension`: untouched. It reads `media-url` from the APNs
payload and is independent of the JS library.
- **Notification categories:** none are registered and none are sent,
before or after.
- **Android channels:** the app never created a channel. Wix didn't
create one either, so background pushes landed in FCM's
`fcm_fallback_notification_channel`, and they still do (verified below).
Users keep their channel settings.
- **Foreground pushes:** still not shown on either platform, which
matches current behavior. Wix never completed `willPresent` on iOS and
never posted in the foreground on Android.
- Cherry-pick conflicts: `package-lock.json`, `Podfile.lock` and
`PrivacyInfo.xcprivacy` were taken from main and regenerated.
`AppDelegate.mm` and `notifications.ts` were redone by hand on main's
versions.

## Verification

- `npx tsc --noEmit -p packages/mobile`: clean.
- eslint and prettier on `notifications.ts`, `notifications.test.ts` and
`index.js`: clean, apart from the existing unused `React` import in
`index.js`.
- New `src/notifications.test.ts`: 9 pass. It covers the iOS lowercase
APNs token, the stored-token fallback, cold-start tap held until
navigation is ready and handled once, warm taps, Android FCM token,
launch notification parsing and the badge being iOS-only.
- The mobile jest preset maps `react-native` to
`packages/mobile/node_modules`. With the current hoisted install, that
file isn't there, so every mobile test (for example
`artistNames.test.ts`) fails at setup on main too. I ran this suite with
the mapper pointed at the root `node_modules`.
- **iOS Release build** (`ENVFILE=.env.prod`, generic iOS Simulator,
arm64, `RCT_NEW_ARCH_ENABLED=0`, worktree-local DerivedData): BUILD
SUCCEEDED. The release xcconfig has no `RCT_NEW_ARCH_ENABLED=1`.
- **Android** `assembleProdRelease -PreactNativeArchitectures=arm64-v8a`
with the debug keystore: BUILD SUCCESSFUL. Before the BoM pin it failed
with the Kotlin 2.2 metadata error.

Device checks were run with a temporary logging build that was not
committed.

**iOS (new iOS 26.5 simulator, ad-hoc signed so `aps-environment`
applies, bundled JS confirmed):**

| Check | Result |
|---|---|
| Permission prompt | Shown, allowed |
| APNs token | 160 lowercase hex characters, `os=ios`. No FCM token is
used. |
| Foreground push (`simctl push`, Follow payload) | Not shown (same as
today) |
| Background push (Repost, with `media-url`) | Banner shown, badge set
to 4 |
| Warm tap | One `PRESS`. Payload
`{data:{type:'Repost',...},'media-url':...}` matches the Wix shape. |
| Cold tap (app killed, CodePush update removed) | Bundled JS loaded.
Exactly one open event, delivered after launch through the scene. |

**Android (fresh clone of the API 36 AVD, `pm clear`, logcat shows
`[CodePush] Loading JS bundle from "assets://index.android.bundle"`):**

| Check | Result |
|---|---|
| FCM token | Fetched (142 characters) |
| Push delivery | Simulated as a root `c2dm RECEIVE` broadcast with an
FCM notification payload |
| Background push | Displayed by FCM on
`fcm_fallback_notification_channel`, `color=0x00000000`,
`ic_notification` icon |
| Foreground push | Not displayed (same as today) |
| Cold tap (process killed) | `getInitialNotification` gives one
navigate with parsed data (`userIds:[1]`) |
| Warm tap | `onNotificationOpenedApp` gives one navigate |

No real account was signed in or out, so routing past `navigate(data)`
was not exercised. That code path is unchanged.

## Device test checklist (physical devices, before release)

The test needs:
- A TestFlight or App Store-signed iOS build (production
`aps-environment`; the SNS APNs app is production, so dev-signed builds
won't receive).
- A Play-signed or internal-track Android build.
- A test account you can sign in with.
- Identity and SNS read access to check rows.

1. **Upgrade in place**, on both platforms. Install the current store
build, sign in and enable push. Note the `NotificationDeviceTokens` row
for your user. Then install the 1.5.187 build over it and launch.
- Expect the same `deviceToken` (iOS lowercase hex, Android the same FCM
token).
   - Expect no new row and no new SNS endpoint.
- Expect only one push per event afterwards (a duplicate means the token
changed).
2. **Fresh install**: sign in, enable push, and confirm a new row with
an SNS endpoint is created. On iOS the token is 64 hex characters on
device.
3. Trigger a follow, repost, favorite, DM and comment from another
account. For each, check:
- App killed: the push shows. Tapping it opens the app and lands on the
right screen, once.
   - App in background: same as above.
   - App in foreground: no banner (current behavior).
- iOS repost/favorite with an image: the rich image still attaches
(`RichPushExtension`).
4. **Badge**: let a few pushes accumulate, then open the app. The icon
badge clears and `clearNotificationBadges` resets the server count, so
the next push shows 1.
5. **Android 13+ permission**: on a fresh install, the
`POST_NOTIFICATIONS` prompt appears when enabling push, and denying it
is respected.
6. **Android channel**: in App info → Notifications, the channel is
still the system "Miscellaneous"/default one, and a setting changed
there before the upgrade survives it.
7. **Disable push** in settings: the deregister call succeeds and the
row and endpoint are removed.
8. **General inbox**: a DM in a chat filed as General produces no push,
and Priority still does.
9. Campaign push (with `notification_campaign_id`): opening it records a
push open.

## Open risks

- **Android headless JS per background push.** Firebase's receiver
starts a headless JS task for every push that arrives while the app is
backgrounded. Wix did not. This boots JS in the background more often.
Watch battery and ANR reports. On the emulator, one delivery while JS
was booting took about 70s in `Service took too long to process intent`,
which I attribute to the slow emulator, but please watch it on device.
- **Delegate order depends on notifee internals**
(`NotifeeCoreUNUserNotificationCenter observe`). It is a compile-time
import, so a rename fails the build rather than silently breaking taps.
Re-check it on notifee upgrades.
- The **Firebase BoM pin to 33.16** should be removed when the app moves
to Kotlin 2.2 (RN 0.81+ ladder).
- RNFB auto-registers for remote notifications at launch on iOS. That
doesn't show a prompt.

Supersedes #14506.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@dylanjeffers
dylanjeffers marked this pull request as ready for review October 9, 2026 00:25
@dylanjeffers
dylanjeffers merged commit 5c4ee7e into main Oct 9, 2026
13 checks passed
@dylanjeffers
dylanjeffers deleted the chore/mobile-native-release-1.5.187 branch October 9, 2026 00:26
dylanjeffers added a commit that referenced this pull request Oct 9, 2026
Phase 1 of the React Native upgrade: 0.79.5 → 0.81.6 with the old
architecture still on.

**Base: `chore/mobile-native-release-1.5.188` (#14654).** Rebased onto
it after 1.5.187 (#14645, which squashed #14627, #14640 and #14650)
merged to main; the two lib-swap commits are gone from this branch.
Merge #14647 and #14651 into this branch first, then this into #14654,
then #14654 into main as one push (see #14654 for the full order).

## Rebase onto 1.5.187 (2026-10-08)
- `patches/react-native+0.79.5.patch` (the ExceptionsManager Hermes fix
from #14643) is renamed to `react-native+0.81.6.patch`. The anchors are
unchanged in 0.81.6, and `scripts/patch-package.sh` applies it with
`--error-on-fail` during `npm install` (checked: `getErrorStackSafe` is
in the installed `ExceptionsManager.js`).
- notifee and Firebase from #14640 are kept as they are:
`@notifee/react-native` 9.1.8 is the latest release and
`@react-native-firebase/app`/`messaging` 24.1.1 build on 0.81.6 on the
old arch (both declare `react-native: *`). No bump needed.
- The Firebase Android BoM stays pinned to 33.16.0. This branch moves
Kotlin to 2.1.20, and BoM 34 needs 2.2, so the pin still applies; the
comment now says so.
- The UIScene and notifee lines in `AppDelegate.mm` (#14626, #14640) are
untouched by this branch.
- `package-lock.json` and `Podfile.lock` were regenerated (`npm
install`, then `RCT_NEW_ARCH_ENABLED=0 bundle exec pod install`); the
release xcconfig has no `RCT_NEW_ARCH_ENABLED=1`. The SwiftAudioEx
post_install patch (#14650) still applies.

**Needs a native release (1.5.188).** Don't OTA this JS onto older
binaries. Old architecture stays on: Android `newArchEnabled=false`, iOS
`RCTNewArchEnabled=false`, pods installed with `RCT_NEW_ARCH_ENABLED=0`.

Two commits, one per step. Each step builds on iOS and Android:
1. 0.79.5 → 0.80.3
2. 0.80.3 → 0.81.6 (0.81.6 is the latest 0.81 patch on npm)

## Version bumps

| Package | From | To | Why |
|---|---|---|---|
| react-native | 0.79.5 | 0.81.6 | the upgrade |
| `@react-native/*` (babel-preset, eslint-config, metro-config,
metro-babel-transformer, typescript-config) | 0.79.5 | 0.81.6 | must
match RN |
| `@react-native-community/cli`, `cli-platform-android`,
`cli-platform-ios` | 18.0.0 | 20.1.3 | CLI line for 0.81; the platform
packages were left at 18 on the old ladder branch |
| react, react-dom, react-test-renderer (whole monorepo, root
`overrides` too) | 19.0.0 | **19.1.4** | RN's bundled renderer throws at
startup unless `react` is **exactly** its version: 19.1.0 for 0.80.3,
19.1.4 for 0.81.6. The old ladder branch used 19.1.8, which would throw
"Incompatible React versions" on launch. |
| @types/react / @types/react-dom | 19.0.0 | 19.1.17 / 19.1.11 | match
React 19.1 |
| react-native-gesture-handler | 2.25.0 | 2.28.0 | 2.28 is the first 2.x
with official 0.81 support (2.25's table stops at 0.79). Stays on 2.x;
v3 drops the old architecture. |
| @amplitude/analytics-react-native | 1.4.11 | 1.4.14 | Kotlin 2.1 (RN
0.80+) turns the deprecated `toUpperCase(Locale)` in 1.4.11 into a
compile error |
| tiktok-opensdk-react-native | ^0.10.7 | 0.10.7 (pinned) | it now
carries a patch. 0.10.9 fixes the Android signature, but drops the
`handleOpenURL`/`handleUserActivity` declarations that SceneDelegate
imports |

Patches added (`patches/`):
- `react-native-track-player+4.1.2.patch`: Kotlin 2.1 rejects
`Arguments.fromBundle(Bundle?)`. Now resolves `null` when there is no
item. 4.1.2 is the last 4.x and has no fixed release.
- `tiktok-opensdk-react-native+0.10.7.patch`: `onNewIntent(intent:
Intent)` (RN 0.80 made `ActivityEventListener` non-null) and
`reactApplicationContext.currentActivity`.

Checked against 0.81 on the old architecture and left alone: Reanimated
3.19.5 (the official table lists 3.19.x for 0.78–0.81 on Paper;
Reanimated 4 is out of scope), track-player 4.1.2 (patched above),
collapsible-tab-view 8.0.1 (JS only), screens 4.18.0 (0.81 support since
4.14; `react-native-screens+4.18.0.patch` still applies; 4.25+ drops the
old arch), pager-view 6.7.1 (7+ drops the old arch), video 6.18.0, svg
15.15.0 (0.81 support since 15.12.1), google-cast 4.6.2 (5.x is new-arch
only), notifications 5.1.0, code-push 12.3.2 (README lists 0.77–0.86;
13.x needs a new OTA history), bootsplash 6.3.11, safe-area-context
5.6.2, keyboard-controller 1.19.0, flash-list 1.8.3, datetimepicker
8.3.0 (builds against 0.81.6).

## Template changes (rn-diff-purge 0.79.5 → 0.81.6)
- Android: Kotlin 2.0.21 → 2.1.20, Gradle 8.13 → 8.14.3, new
`gradlew`/`gradlew.bat`/wrapper jar, `MainApplication` uses
`loadReactNative(this)` in place of `SoLoader.init` plus `load()`, and
`edgeToEdgeEnabled=false` is added as in the template. The app already
draws edge-to-edge through react-native-bars and the SDK 36 target, so
this flag changes nothing.
- AGP comes from RN: 8.8 → 8.11.0, which supports compileSdk 36, so
`android.suppressUnsupportedCompileSdk=36` is removed.
- `settings.gradle` resolves `@react-native/gradle-plugin` through
`require.resolve` from `react-native`, not a hard-coded hoisted path.
With this lockfile npm nests it under
`node_modules/react-native/node_modules`.
- Kept: `newArchEnabled=false`, the `AudiusScrypt` module
(`ScryptPackage` is still registered), the debug manifest (it adds
`SYSTEM_ALERT_WINDOW`), and
`android:enableOnBackInvokedCallback="false"`.
- **Back handling:** since 0.80, `ReactActivity` registers an
`OnBackPressedCallback` when targetSdk ≥ 36, which calls
`onBackPressed()` → JS `BackHandler`. Our `invokeDefaultOnBackPressed`
override (`moveTaskToBack`) still runs. The manifest opt-out is kept as
asked and can probably go in a later release.
- iOS: no template change between 0.79.5 and 0.81.6. `Info.plist` and
`RichPushExtension/Info.plist` now set `RCTNewArchEnabled=false`, which
RN 0.80+ reads **at runtime**; a missing key means new arch. `pod
install` writes the key and re-sorts the plist each time, so the sorted
version is committed.

## AppDelegate / UIScene
No code change needed. `RCTAppDelegate` is marked deprecated in 0.81 in
favour of `RCTReactNativeFactory`, so there is a
`-Wdeprecated-declarations` warning, but every API we use is unchanged:
`automaticallyLoadReactNativeWindow`, `rootViewFactory`,
`createRootViewController`, `setRootView:toRootViewController:`,
`customizeRootView:`, `bundleURL`, and `dependencyProvider`.
`didFinishLaunching` still creates the factory and skips the window.
Moving to `RCTReactNativeFactory` can wait for the 0.82+ phase.

Verified on a Release build: cold launch, a deep link while killed, and
a deep link while backgrounded, on iOS 26.5 and iOS 27. iOS 27 needs
UIScene and showed no launch crash.

## Web / React 19.1
- Typecheck: web, common and harmony are clean, and so is mobile.
- `vite build` (prod env): succeeds.
- web vitest: 178 passed, 9 skipped. `TrackTile › Public Premium
(non-owner)` timed out once while the machine was under heavy load, then
passed 3 out of 3 when run alone.

## Verification matrix (before the rebase; see the bottom for the
rebased stack)
| | iOS 26.5 sim (Release) | iOS 27 sim (Release) | Android API 36 emu
(prodRelease, arm64) |
|---|---|---|---|
| Build | ✅ Xcode 27, `generic/platform=iOS Simulator` | same binary | ✅
debug keystore |
| Old arch at runtime | ✅ JS thread `com.facebook.react.JavaScript` | –
| ✅ `ReactRootView` + "Legacy Architecture" warning |
| Launch past splash | ✅ | ✅ | ✅ `assets://index.android.bundle` |
| Sign-up / sign-in screens | (signed in) | ✅ | ✅ including Create
Password |
| Signed-in feed, scroll | ✅ | – | – |
| Playback + now playing drawer (open, drag closed) | ✅ | – | – |
| Background → foreground while playing | ✅ audio kept going (0:09 →
0:41) | – | – |
| Search | ✅ | – | – |
| Profile: collapsible header + tab swipe | ✅ | – | – |
| Deep link, warm | ✅ `audius://audius` | ✅ link delivered | – |
| Deep link, cold | ✅ `audius://deadmau5` | ✅ (signed out → sign-on) | –
|
| Hardware Back | – | – | ✅ pops Create Password; at root backgrounds
the app without killing it |
| Sign-on footer above nav bar | – | – | ✅ |
| Red JS errors | none, apart from a recurring `JSON Parse error:
Unexpected character: <` (not yet traced) | none | none |

The CodePush folder was moved aside or cleared before every cold launch,
so all results above are from the bundled JS.

## Still needs a real device
- background audio with the screen locked for more than 2 minutes
- lock-screen and notification controls
- Chromecast
- push notifications

## Notes
- A Release build that downloads a production OTA built for 1.5.186 or
1.5.187 runs a 0.79 bundle on a 0.81 binary. 1.5.188 ships with its own
package.json version (#14654) so it never receives those OTAs.
- The Hermes `ExceptionsManager` fix now applies through patch-package
(#14643), renamed for 0.81.6 above.

## Verification of the rebased stack (2026-10-08)
Built from a local, unpushed integration branch: #14652's tip (which
contains #14647) merged with #14651, on top of #14654.

| Check | Result |
|---|---|
| `npx tsc --noEmit -p packages/mobile` | pass |
| eslint on the touched files / `turbo run verify` lint | pass (1
existing warning in `HostRemixContestDrawer.tsx`) |
| `cd packages/mobile && npm test` | 9 suites, 34 tests pass |
| iOS Release, `generic/platform=iOS Simulator`, arm64,
`ENVFILE=.env.prod` | builds; Info.plist 1.1.201,
`RCTNewArchEnabled=false`; bundle carries 1.5.188 |
| Android `assembleProdRelease`, arm64-v8a, debug keystore | builds;
versionName 1.1.537 |
| iOS 26.5 sim, installed over the signed-in app, CodePush moved aside |
launch to feed; JS thread `com.facebook.react.JavaScript` (old arch) |
| Playback from the 2nd feed track | plays; play bar tracker moves |
| Auto-advance | seek to 2:43/2:45 on track 2, advanced to track 3 |
| Now playing drawer | opens, scrubber advances, tap-to-seek works |
| Profile tabs | swipe Tracks → Albums, tap Reposts |
| notifee/RNFB APNs token | a temporary console log (not committed,
rebuilt without it afterwards) printed an 80-byte lowercase APNs token
at startup |
| Android 16 emulator, `pm clear` | `Loading JS bundle from
"assets://index.android.bundle"`, "Legacy Architecture" warning, sign-up
and sign-in screens render |
| Android hardware Back | **not verified**: the emulator's system_server
was killed by its watchdog three times under host load, so Back never
got a clean run |
| JS errors on iOS | only `Could not cache profile images` (content node
timeout) |

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant