From 2a56a442fd8e19272f5bc9d1a2e3e07b023b52d8 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Wed, 7 Oct 2026 22:48:09 -0700 Subject: [PATCH 1/4] chore(mobile): bump versions for native release 1.5.187 - 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 --- packages/mobile/android/app/build.gradle | 2 +- packages/mobile/ios/AudiusReactNative/Info.plist | 2 +- packages/mobile/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index bbac1531a87..5daa9707320 100755 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -134,7 +134,7 @@ android { // versionCode is automatically incremented in CI versionCode 1 // Make sure this is above the currently released Android version in the play store if your changes touch native code: - versionName "1.1.535" + versionName "1.1.536" resValue "string", "build_config_package", "co.audius.app" resConfigs "en" } diff --git a/packages/mobile/ios/AudiusReactNative/Info.plist b/packages/mobile/ios/AudiusReactNative/Info.plist index 60fe39b4f65..244cdfe018c 100644 --- a/packages/mobile/ios/AudiusReactNative/Info.plist +++ b/packages/mobile/ios/AudiusReactNative/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1.199 + 1.1.200 CFBundleSignature ???? CFBundleURLTypes diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 9fd700ca66a..91d7b8ce369 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@audius/mobile", - "version": "1.5.186", + "version": "1.5.187", "private": true, "scripts": { "android:dev": "ENVFILE=.env.dev turbo run android -- --mode=prodDebug", From 37ec8b2bdec6df9fa5d87f400b3af63e97bbf43b Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Thu, 8 Oct 2026 17:10:53 -0700 Subject: [PATCH 2/4] fix(mobile): patch SwiftAudioEx insert index for one-item queues (#14650) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- packages/mobile/ios/Podfile | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/mobile/ios/Podfile b/packages/mobile/ios/Podfile index c5d685c23e7..fa8f6f35214 100644 --- a/packages/mobile/ios/Podfile +++ b/packages/mobile/ios/Podfile @@ -140,6 +140,24 @@ target 'AudiusReactNative' do end end + # SwiftAudioEx 1.x only shifts the current index for an insert in front + # of the current item when the queue already has more than one item. With + # a single item, inserting at 0 leaves the index one slot early, so the + # player replays the current track instead of advancing. Fixed upstream + # on main but not released. Fail if the line changes so an update can't + # silently drop the patch. + queue_manager = File.join(__dir__, 'Pods/SwiftAudioEx/Sources/SwiftAudioEx/QueueManager.swift') + buggy_line = 'if (self.items.count > 1 && currentIndex >= index) {' + fixed_line = 'if (self.items.count > 0 && currentIndex >= index) {' + text = File.read(queue_manager) + if text.include?(buggy_line) + File.chmod(0644, queue_manager) + File.write(queue_manager, text.sub(buggy_line, fixed_line)) + Pod::UI.puts "Patched SwiftAudioEx QueueManager.swift: shift current index when inserting into a one-item queue".green + elsif !text.include?(fixed_line) + raise "SwiftAudioEx QueueManager.swift no longer contains '#{buggy_line}'. Check whether the insert index fix is still needed and update the Podfile patch." + end + end end From c0e82d4dc04060c786f971fc215a471526e87722 Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Thu, 8 Oct 2026 17:11:04 -0700 Subject: [PATCH 3/4] fix(mobile): replace fs and gradient native libs (#14627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- package-lock.json | 72 +++----- packages/mobile/android/build.gradle | 13 +- packages/mobile/android/settings.gradle | 4 - packages/mobile/ios/Podfile | 1 - packages/mobile/ios/Podfile.lock | 48 +++-- packages/mobile/ios/PrivacyInfo.xcprivacy | 10 +- packages/mobile/metro.config.js | 8 +- packages/mobile/package.json | 4 +- .../BottomTabBarButton.tsx | 3 +- packages/mobile/src/components/card/Card.tsx | 3 +- .../ReferralLinkCopyButton.tsx | 5 +- .../copy-text-tile/CopyTextTile.tsx | 3 +- .../src/components/core/GradientIcon.tsx | 2 +- .../src/components/core/GradientText.tsx | 4 +- .../src/components/core/LockedStatusBadge.tsx | 8 +- .../lineup-tile/LineupTileAccessStatus.tsx | 2 +- .../now-playing-drawer/TrackingBar.tsx | 3 +- .../components/progress-bar/ProgressBar.tsx | 4 +- .../mobile/src/components/scrubber/Slider.tsx | 3 +- .../share-drawer/useShareToStory.tsx | 2 +- .../src/components/skeleton/Skeleton.tsx | 3 +- .../SuggestedArtistsList.tsx | 2 +- .../Button/BaseButton/BaseButton.tsx | 2 +- .../components/CoverPhoto/CoverPhoto.tsx | 2 +- .../LinearGradient/LinearGradient.tsx | 172 ++++++++++++++++++ .../RadialGradient/RadialGradient.tsx | 111 +++++++---- .../src/harmony-native/components/index.ts | 1 + .../src/screens/audio-screen/AudioScreen.tsx | 2 +- .../EditCoinDetailsScreen.tsx | 2 +- .../components/CoinInfoCard.tsx | 2 +- .../components/GraduationProgressBar.tsx | 4 +- .../explore-screen/components/ColorTile.tsx | 3 +- .../screens/rewards-screen/RewardsScreen.tsx | 3 +- .../src/screens/rewards-screen/TiersTile.tsx | 2 +- .../src/shims/react-native-linear-gradient.ts | 6 + .../sagas/migrateOfflineDataPathSaga.ts | 2 +- 36 files changed, 362 insertions(+), 159 deletions(-) create mode 100644 packages/mobile/src/harmony-native/components/LinearGradient/LinearGradient.tsx create mode 100644 packages/mobile/src/shims/react-native-linear-gradient.ts diff --git a/package-lock.json b/package-lock.json index 382d61ba269..eabd08bc935 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7076,6 +7076,27 @@ "react": ">=16.8.0" } }, + "node_modules/@dr.pogodin/react-native-fs": { + "version": "2.32.1", + "resolved": "https://registry.npmjs.org/@dr.pogodin/react-native-fs/-/react-native-fs-2.32.1.tgz", + "integrity": "sha512-vNPgyPpw4OGaooIay8RUx0qUaFExas0ce6kmud1oTKjqeohEqwzuFZBa7gJ1bmF2cEkplp8sUzw2BVv4NDPrVA==", + "license": "MIT", + "workspaces": [ + "example" + ], + "dependencies": { + "buffer": "^6.0.3", + "http-status-codes": "^2.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/birdofpreyru" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@dual-bundle/import-meta-resolve": { "version": "4.1.0", "dev": true, @@ -24982,10 +25003,6 @@ "semver": "bin/semver.js" } }, - "node_modules/@react-native/normalize-color": { - "version": "2.1.0", - "license": "MIT" - }, "node_modules/@react-native/normalize-colors": { "version": "0.74.85", "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.85.tgz", @@ -54385,15 +54402,6 @@ "node": ">= 0.6" } }, - "node_modules/deprecated-react-native-prop-types": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "@react-native/normalize-color": "*", - "invariant": "*", - "prop-types": "*" - } - }, "node_modules/dequal": { "version": "2.0.3", "license": "MIT", @@ -85576,23 +85584,6 @@ "prop-types": "^15.5.10" } }, - "node_modules/react-native-fs": { - "version": "2.20.0", - "license": "MIT", - "dependencies": { - "base-64": "^0.1.0", - "utf8": "^3.0.0" - }, - "peerDependencies": { - "react-native": "*", - "react-native-windows": "*" - }, - "peerDependenciesMeta": { - "react-native-windows": { - "optional": true - } - } - }, "node_modules/react-native-get-random-values": { "version": "1.10.0", "license": "MIT", @@ -85729,13 +85720,6 @@ } } }, - "node_modules/react-native-radial-gradient": { - "version": "1.1.4", - "license": "MIT", - "dependencies": { - "deprecated-react-native-prop-types": "^2.3.0" - } - }, "node_modules/react-native-randombytes": { "version": "3.6.1", "license": "ISC", @@ -118920,7 +118904,7 @@ }, "packages/mobile": { "name": "@audius/mobile", - "version": "1.5.185", + "version": "1.5.186", "dependencies": { "@amplitude/analytics-react-native": "1.4.11", "@audius/common": "*", @@ -118930,6 +118914,7 @@ "@audius/sdk": "*", "@bravemobile/react-native-code-push": "^12.3.2", "@coinflowlabs/react-native": "4.5.2", + "@dr.pogodin/react-native-fs": "2.32.1", "@ebay/nice-modal-react": "^1.2.13", "@emotion/native": "^11.11.0", "@emotion/react": "11.14.0", @@ -118998,7 +118983,6 @@ "react-native-draggable-flatlist": "4.0.1", "react-native-dynamic": "1.0.0", "react-native-fast-crypto": "3.0.0", - "react-native-fs": "^2.18.0", "react-native-gesture-handler": "2.25.0", "react-native-get-random-values": "1.10.0", "react-native-gifted-charts": "1.4.46", @@ -119011,7 +118995,6 @@ "react-native-intersection-observer": "0.2.1", "react-native-keyboard-aware-scroll-view": "0.9.5", "react-native-keyboard-controller": "1.19.0", - "react-native-linear-gradient": "2.8.3", "react-native-markdown-display": "7.0.2", "react-native-modal": "^13.0.0", "react-native-modal-datetime-picker": "18.0.0", @@ -119019,7 +119002,6 @@ "react-native-pager-view": "6.7.1", "react-native-permissions": "5.4.4", "react-native-qrcode-svg": "6.2.0", - "react-native-radial-gradient": "1.1.4", "react-native-randombytes": "3.6.1", "react-native-reanimated": "~3.19.5", "react-native-restart": "0.0.27", @@ -121233,14 +121215,6 @@ "react-native": "*" } }, - "packages/mobile/node_modules/react-native-linear-gradient": { - "version": "2.8.3", - "license": "MIT", - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "packages/mobile/node_modules/react-native-markdown-display": { "version": "7.0.2", "license": "MIT", diff --git a/packages/mobile/android/build.gradle b/packages/mobile/android/build.gradle index b81370371c6..f60dd2eb5a3 100644 --- a/packages/mobile/android/build.gradle +++ b/packages/mobile/android/build.gradle @@ -25,6 +25,17 @@ buildscript { } } +// @dr.pogodin/react-native-fs ships its generated module spec but only adds it +// to the build on the new architecture. The module also runs on the bridge, so +// compile the spec on the old architecture too. +subprojects { subproject -> + if (subproject.name == 'dr.pogodin_react-native-fs' && findProperty('newArchEnabled') != 'true') { + subproject.plugins.withId('com.android.library') { + subproject.android.sourceSets.main.java.srcDirs += ["${subproject.projectDir}/generated/java"] + } + } +} + apply plugin: "com.facebook.react.rootproject" allprojects { @@ -39,4 +50,4 @@ allprojects { dirs "$rootDir/libs" } } -} \ No newline at end of file +} diff --git a/packages/mobile/android/settings.gradle b/packages/mobile/android/settings.gradle index 7b69b74c854..977048fe7c9 100755 --- a/packages/mobile/android/settings.gradle +++ b/packages/mobile/android/settings.gradle @@ -4,10 +4,6 @@ extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autoli rootProject.name = 'AudiusReactNative' include ':react-native-randombytes' project(':react-native-randombytes').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-randombytes/android') -include ':react-native-fs' -project(':react-native-fs').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-fs/android') -include ':react-native-radial-gradient' -project(':react-native-radial-gradient').projectDir = new File(rootProject.projectDir, '../../../node_modules/react-native-radial-gradient/android') include ':react-native-config' project(':react-native-config').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-config/android') include ':react-native-google-cast' diff --git a/packages/mobile/ios/Podfile b/packages/mobile/ios/Podfile index fa8f6f35214..c4c4b3407b6 100644 --- a/packages/mobile/ios/Podfile +++ b/packages/mobile/ios/Podfile @@ -31,7 +31,6 @@ project 'AudiusReactNative', 'Prod.Release' => :release target 'AudiusReactNative' do - pod 'SRSRadialGradient', :path => '../../../node_modules/react-native-radial-gradient/ios' pod 'ffmpeg-kit-ios-full-gpl', :podspec => './ffmpeg-kit-ios-full-gpl.podspec' pod 'ffmpeg-kit-react-native', :subspecs => ['full-gpl'], :podspec => '../../../node_modules/ffmpeg-kit-react-native/ffmpeg-kit-react-native.podspec' diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index f9c1ce8ba5f..3b80412ba02 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -3,14 +3,36 @@ PODS: - React-Core - Base64 (1.1.2) - boost (1.84.0) - - BVLinearGradient (2.8.3): - - React-Core - CodePush (12.3.2): - Base64 (~> 1.1) - JWT (~> 3.0.0-beta.12) - React-Core - SSZipArchive (~> 2.5.5) - DoubleConversion (1.1.6) + - dr-pogodin-react-native-fs (2.32.1): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga - fast_float (6.1.4) - FBLazyVector (0.79.5) - ffmpeg-kit-ios-full-gpl (6.0) @@ -2011,8 +2033,6 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - RNFS (2.20.0): - - React-Core - RNGestureHandler (2.25.0): - DoubleConversion - glog @@ -2264,8 +2284,6 @@ PODS: - SnapSDK/SCSDKLoginKit (1.15.0): - SnapSDK/SCSDKCoreKit - SocketRocket (0.7.1) - - SRSRadialGradient (1.1.4): - - React - SSZipArchive (2.5.5) - SwiftAudioEx (1.0.0) - tiktok-opensdk-react-native (0.10.7): @@ -2308,9 +2326,9 @@ PODS: DEPENDENCIES: - "amplitude-react-native (from `../../../node_modules/@amplitude/analytics-react-native`)" - boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`) - - BVLinearGradient (from `../node_modules/react-native-linear-gradient`) - "CodePush (from `../../../node_modules/@bravemobile/react-native-code-push`)" - DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - "dr-pogodin-react-native-fs (from `../../../node_modules/@dr.pogodin/react-native-fs`)" - fast_float (from `../../../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`) - ffmpeg-kit-ios-full-gpl (from `./ffmpeg-kit-ios-full-gpl.podspec`) @@ -2412,7 +2430,6 @@ DEPENDENCIES: - "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)" - "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)" - "RNFlashList (from `../../../node_modules/@shopify/flash-list`)" - - RNFS (from `../../../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) - RNPermissions (from `../../../node_modules/react-native-permissions`) @@ -2423,7 +2440,6 @@ DEPENDENCIES: - RNSVG (from `../node_modules/react-native-svg`) - RNZipArchive (from `../../../node_modules/react-native-zip-archive`) - "snap-kit-react-native (from `../../../node_modules/@snapchat/snap-kit-react-native`)" - - SRSRadialGradient (from `../../../node_modules/react-native-radial-gradient/ios`) - tiktok-opensdk-react-native (from `../../../node_modules/tiktok-opensdk-react-native`) - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`) @@ -2448,12 +2464,12 @@ EXTERNAL SOURCES: :path: "../../../node_modules/@amplitude/analytics-react-native" boost: :podspec: "../../../node_modules/react-native/third-party-podspecs/boost.podspec" - BVLinearGradient: - :path: "../node_modules/react-native-linear-gradient" CodePush: :path: "../../../node_modules/@bravemobile/react-native-code-push" DoubleConversion: :podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + dr-pogodin-react-native-fs: + :path: "../../../node_modules/@dr.pogodin/react-native-fs" fast_float: :podspec: "../../../node_modules/react-native/third-party-podspecs/fast_float.podspec" FBLazyVector: @@ -2651,8 +2667,6 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-community/datetimepicker" RNFlashList: :path: "../../../node_modules/@shopify/flash-list" - RNFS: - :path: "../../../node_modules/react-native-fs" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNImageCropPicker: @@ -2673,8 +2687,6 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native-zip-archive" snap-kit-react-native: :path: "../../../node_modules/@snapchat/snap-kit-react-native" - SRSRadialGradient: - :path: "../../../node_modules/react-native-radial-gradient/ios" tiktok-opensdk-react-native: :path: "../../../node_modules/tiktok-opensdk-react-native" Yoga: @@ -2684,9 +2696,9 @@ SPEC CHECKSUMS: amplitude-react-native: 9d57e1bcc4175039e36283390aa3daeaea9441a5 Base64: cecfb41a004124895a7bcee567a89bae5a89d49b boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 - BVLinearGradient: 880f91a7854faff2df62518f0281afb1c60d49a3 CodePush: f52aac93456bc76d8219ca3673cc7ce81470429d DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb + dr-pogodin-react-native-fs: 69314b5d4e9f561657febeae7a250c2bb8561874 fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52 ffmpeg-kit-ios-full-gpl: 7d416729f7b3604b64cee2a752c42608a8d228e0 @@ -2790,7 +2802,6 @@ SPEC CHECKSUMS: RNCMaskedView: dc63cbc7b2f614b8b4526e11ed677b36e92cd9fb RNDateTimePicker: a793ed8822283f576dd0a205a0916c5098c2611f RNFlashList: c68de6a14cce0a262b0a4fabdea690d9f3361063 - RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 1070e4e691b23028309d796cc30652d9875138c7 RNImageCropPicker: 64b6bec9ec185ee59b71a93d7058816bd0e1916e RNPermissions: 0a18cdcfdd1773b969f817ce20d47c2b74724af8 @@ -2803,7 +2814,6 @@ SPEC CHECKSUMS: snap-kit-react-native: 751006199818fccb38c8a01196495167bc25e9fb SnapSDK: 1e68aad748e080f49e3802559b3b76026666ef62 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - SRSRadialGradient: 8363f6c3ce63636f5ffee74a7016f5c84a6df0a1 SSZipArchive: c69881e8ac5521f0e622291387add5f60f30f3c4 SwiftAudioEx: 6f511018b7a0fdfd14ed1bb4081f953588245cc0 tiktok-opensdk-react-native: f6b1a34bb4c1889c0dae5838effe484c673ea0c0 @@ -2812,6 +2822,6 @@ SPEC CHECKSUMS: TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 Yoga: adb397651e1c00672c12e9495babca70777e411e -PODFILE CHECKSUM: 7750e6bb179cd027ed6cf7522c2da1f14bef882b +PODFILE CHECKSUM: e90397933b1403467b612b76e102f32a3bc9ae1a COCOAPODS: 1.15.2 diff --git a/packages/mobile/ios/PrivacyInfo.xcprivacy b/packages/mobile/ios/PrivacyInfo.xcprivacy index 906f4fa6116..fa469f312af 100644 --- a/packages/mobile/ios/PrivacyInfo.xcprivacy +++ b/packages/mobile/ios/PrivacyInfo.xcprivacy @@ -11,6 +11,7 @@ C617.1 3B52.1 + 0A2A.1 @@ -23,18 +24,19 @@ NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPICategoryDiskSpace NSPrivacyAccessedAPITypeReasons - 35F9.1 + 85F4.1 + E174.1 NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryDiskSpace + NSPrivacyAccessedAPICategorySystemBootTime NSPrivacyAccessedAPITypeReasons - E174.1 + 35F9.1 diff --git a/packages/mobile/metro.config.js b/packages/mobile/metro.config.js index 025e82786d1..f31d86dfd69 100644 --- a/packages/mobile/metro.config.js +++ b/packages/mobile/metro.config.js @@ -149,9 +149,15 @@ const config = { // Aliases for '@audius/web' to allow for absolute paths ...getClientAliases(), + // react-native-gifted-charts requires this at module load + 'react-native-linear-gradient': path.resolve( + __dirname, + 'src/shims/react-native-linear-gradient.ts' + ), + // Various polyfills to enable @audius/sdk to run in react-native child_process: emptyPolyfill, - fs: resolveModule('react-native-fs'), + fs: resolveModule('@dr.pogodin/react-native-fs'), net: emptyPolyfill, tls: resolveModule('tls-browserify') }, diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 91d7b8ce369..55722f285bf 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -50,6 +50,7 @@ "@audius/sdk": "*", "@bravemobile/react-native-code-push": "^12.3.2", "@coinflowlabs/react-native": "4.5.2", + "@dr.pogodin/react-native-fs": "2.32.1", "@ebay/nice-modal-react": "^1.2.13", "@emotion/native": "^11.11.0", "@emotion/react": "11.14.0", @@ -118,7 +119,6 @@ "react-native-draggable-flatlist": "4.0.1", "react-native-dynamic": "1.0.0", "react-native-fast-crypto": "3.0.0", - "react-native-fs": "^2.18.0", "react-native-gesture-handler": "2.25.0", "react-native-get-random-values": "1.10.0", "react-native-gifted-charts": "1.4.46", @@ -131,7 +131,6 @@ "react-native-intersection-observer": "0.2.1", "react-native-keyboard-aware-scroll-view": "0.9.5", "react-native-keyboard-controller": "1.19.0", - "react-native-linear-gradient": "2.8.3", "react-native-markdown-display": "7.0.2", "react-native-modal": "^13.0.0", "react-native-modal-datetime-picker": "18.0.0", @@ -139,7 +138,6 @@ "react-native-pager-view": "6.7.1", "react-native-permissions": "5.4.4", "react-native-qrcode-svg": "6.2.0", - "react-native-radial-gradient": "1.1.4", "react-native-randombytes": "3.6.1", "react-native-reanimated": "~3.19.5", "react-native-restart": "0.0.27", diff --git a/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/BottomTabBarButton.tsx b/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/BottomTabBarButton.tsx index 2eca59fcabb..00b5fd84458 100644 --- a/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/BottomTabBarButton.tsx +++ b/packages/mobile/src/components/bottom-tab-bar/bottom-tab-bar-buttons/BottomTabBarButton.tsx @@ -5,9 +5,10 @@ import { useTheme } from '@emotion/react' import type { LottieViewProps } from 'lottie-react-native' import LottieView from 'lottie-react-native' import { Pressable, StyleSheet } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import { usePrevious } from 'react-use' +import { LinearGradient } from '@audius/harmony-native' + import { BOTTOM_BAR_BUTTON_HEIGHT, BOTTOM_BAR_TOP_PADDING } from '../constants' export type BaseBottomTabBarButtonProps = { diff --git a/packages/mobile/src/components/card/Card.tsx b/packages/mobile/src/components/card/Card.tsx index cf8cab3819f..8f5df1c0111 100644 --- a/packages/mobile/src/components/card/Card.tsx +++ b/packages/mobile/src/components/card/Card.tsx @@ -3,9 +3,8 @@ import type { ComponentType, ReactNode } from 'react' import type { User } from '@audius/common/models' import type { StyleProp, TextStyle, ViewStyle } from 'react-native' import { Text, View } from 'react-native' -import type { LinearGradientProps } from 'react-native-linear-gradient' -import type { ImageProps } from '@audius/harmony-native' +import type { LinearGradientProps, ImageProps } from '@audius/harmony-native' import type { TileProps } from 'app/components/core' import { Tile } from 'app/components/core' import { UserBadges } from 'app/components/user-badges' diff --git a/packages/mobile/src/components/challenge-rewards-drawer/ReferralLinkCopyButton.tsx b/packages/mobile/src/components/challenge-rewards-drawer/ReferralLinkCopyButton.tsx index 230199cadce..8c05a3b5693 100644 --- a/packages/mobile/src/components/challenge-rewards-drawer/ReferralLinkCopyButton.tsx +++ b/packages/mobile/src/components/challenge-rewards-drawer/ReferralLinkCopyButton.tsx @@ -2,9 +2,8 @@ import { useCallback } from 'react' import Clipboard from '@react-native-clipboard/clipboard' import { Animated, View, TouchableHighlight } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' -import { IconCopy } from '@audius/harmony-native' +import { LinearGradient, IconCopy } from '@audius/harmony-native' import Text from 'app/components/text' import { usePressScaleAnimation } from 'app/hooks/usePressScaleAnimation' import { useToast } from 'app/hooks/useToast' @@ -70,9 +69,7 @@ export const ReferralLinkCopyButton = ({ > diff --git a/packages/mobile/src/components/copy-text-tile/CopyTextTile.tsx b/packages/mobile/src/components/copy-text-tile/CopyTextTile.tsx index 584a39c8888..443044791a0 100644 --- a/packages/mobile/src/components/copy-text-tile/CopyTextTile.tsx +++ b/packages/mobile/src/components/copy-text-tile/CopyTextTile.tsx @@ -2,9 +2,8 @@ import { useCallback } from 'react' import Clipboard from '@react-native-clipboard/clipboard' import { View } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' -import { IconCopy } from '@audius/harmony-native' +import { LinearGradient, IconCopy } from '@audius/harmony-native' import { Tile, Text } from 'app/components/core' import { useToast } from 'app/hooks/useToast' import { makeStyles } from 'app/styles' diff --git a/packages/mobile/src/components/core/GradientIcon.tsx b/packages/mobile/src/components/core/GradientIcon.tsx index bfa2dc14cdc..5c93856ecf1 100644 --- a/packages/mobile/src/components/core/GradientIcon.tsx +++ b/packages/mobile/src/components/core/GradientIcon.tsx @@ -1,9 +1,9 @@ import type { ComponentType } from 'react' import MaskedView from '@react-native-masked-view/masked-view' -import LinearGradient from 'react-native-linear-gradient' import type { SvgProps } from 'react-native-svg' +import { LinearGradient } from '@audius/harmony-native' import { useThemeColors } from 'app/utils/theme' type GradientIconProps = SvgProps & { diff --git a/packages/mobile/src/components/core/GradientText.tsx b/packages/mobile/src/components/core/GradientText.tsx index 51aa9d3b85e..e6b13b8e995 100644 --- a/packages/mobile/src/components/core/GradientText.tsx +++ b/packages/mobile/src/components/core/GradientText.tsx @@ -1,8 +1,8 @@ import MaskedView from '@react-native-masked-view/masked-view' import type { TextProps } from 'react-native' -import type { LinearGradientProps } from 'react-native-linear-gradient' -import LinearGradient from 'react-native-linear-gradient' +import type { LinearGradientProps } from '@audius/harmony-native' +import { LinearGradient } from '@audius/harmony-native' import Text from 'app/components/text' import { useThemeColors } from 'app/utils/theme' diff --git a/packages/mobile/src/components/core/LockedStatusBadge.tsx b/packages/mobile/src/components/core/LockedStatusBadge.tsx index f5a959bbc5e..ed91330ff7c 100644 --- a/packages/mobile/src/components/core/LockedStatusBadge.tsx +++ b/packages/mobile/src/components/core/LockedStatusBadge.tsx @@ -1,7 +1,11 @@ import { View } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' -import { IconLock, IconLockUnlocked, useTheme } from '@audius/harmony-native' +import { + LinearGradient, + IconLock, + IconLockUnlocked, + useTheme +} from '@audius/harmony-native' import { makeStyles } from 'app/styles' import { spacing } from 'app/styles/spacing' import { useColor } from 'app/utils/theme' diff --git a/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx b/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx index f6e2ea32ae9..ae2cebf6c93 100644 --- a/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx +++ b/packages/mobile/src/components/lineup-tile/LineupTileAccessStatus.tsx @@ -14,11 +14,11 @@ import { } from '@audius/common/store' import { USDC } from '@audius/fixed-decimal' import { TouchableOpacity } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import { useDispatch, useSelector } from 'react-redux' import type { FlexProps } from '@audius/harmony-native' import { + LinearGradient, Button, Flex, IconLock, diff --git a/packages/mobile/src/components/now-playing-drawer/TrackingBar.tsx b/packages/mobile/src/components/now-playing-drawer/TrackingBar.tsx index f9a018ea10a..c7da89c78fd 100644 --- a/packages/mobile/src/components/now-playing-drawer/TrackingBar.tsx +++ b/packages/mobile/src/components/now-playing-drawer/TrackingBar.tsx @@ -4,11 +4,11 @@ import { useCurrentTrack } from '@audius/common/hooks' import { playbackSelectors, playbackRateValueMap } from '@audius/common/store' import { Genre } from '@audius/common/utils' import { Animated, Dimensions, Easing } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import TrackPlayer, { useIsPlaying } from 'react-native-track-player' import { useSelector } from 'react-redux' import { useAsync } from 'react-use' +import { LinearGradient } from '@audius/harmony-native' import { makeStyles } from 'app/styles' import { useThemeColors } from 'app/utils/theme' @@ -143,7 +143,6 @@ export const TrackingBar = (props: TrackingBarProps) => { { style={[styles.skeleton, { transform: [{ translateX: shimmerPos }] }]} > & { + /** Two or more colors, evenly spaced unless `locations` is provided. */ + colors: readonly string[] + /** Gradient start as a fraction of the box. Defaults to the top edge. */ + start?: LinearGradientPoint + /** Gradient end as a fraction of the box. Defaults to the bottom edge. */ + end?: LinearGradientPoint + /** + * Bearing in degrees (0 points up, increasing clockwise). Overrides `start` + * and `end`, and measures the box to place the gradient line like a CSS + * angle gradient. + */ + angle?: number + /** Per-color stop positions in [0, 1]; should match `colors` length. */ + locations?: readonly number[] + style?: StyleProp + children?: ReactNode +} + +// Top to bottom by default. +const DEFAULT_START: LinearGradientPoint = { x: 0.5, y: 0 } +const DEFAULT_END: LinearGradientPoint = { x: 0.5, y: 1 } + +const BORDER_RADIUS_KEYS = [ + 'borderRadius', + 'borderTopLeftRadius', + 'borderTopRightRadius', + 'borderBottomLeftRadius', + 'borderBottomRightRadius', + 'borderTopStartRadius', + 'borderTopEndRadius', + 'borderBottomStartRadius', + 'borderBottomEndRadius', + 'borderCurve' +] as const + +// The gradient line passes through the center along the bearing and is long +// enough that the perpendiculars through opposite corners meet its ends. +export const getAngleStartEnd = ( + angle: number, + width: number, + height: number +): { start: LinearGradientPoint; end: LinearGradientPoint } => { + const w = width > 0 ? width : 1 + const h = height > 0 ? height : 1 + const radians = (angle * Math.PI) / 180 + const sin = Math.sin(radians) + const cos = Math.cos(radians) + const halfLength = (w * Math.abs(sin) + h * Math.abs(cos)) / 2 + const dx = (sin * halfLength) / w + const dy = (cos * halfLength) / h + return { + start: { x: 0.5 - dx, y: 0.5 + dy }, + end: { x: 0.5 + dx, y: 0.5 - dy } + } +} + +const styles = StyleSheet.create({ + clip: { overflow: 'hidden' } +}) + +/** + * Linear gradient drawn with react-native-svg. `start` and `end` are fractions + * of the box (objectBoundingBox units), so no layout measurement is needed + * unless `angle` is set. The gradient renders behind any children and is + * clipped to the container's border radii. + */ +export const LinearGradient = (props: LinearGradientProps) => { + const { + colors, + locations, + start: startProp = DEFAULT_START, + end: endProp = DEFAULT_END, + angle, + style, + children, + onLayout, + ...other + } = props + + const [size, setSize] = useState({ width: 0, height: 0 }) + const handleLayout = useCallback( + (e: LayoutChangeEvent) => { + if (angle !== undefined) { + const { width, height } = e.nativeEvent.layout + setSize((prev) => + prev.width === width && prev.height === height + ? prev + : { width, height } + ) + } + onLayout?.(e) + }, + [angle, onLayout] + ) + + const { start, end } = + angle === undefined + ? { start: startProp, end: endProp } + : getAngleStartEnd(angle, size.width, size.height) + + // useId output is not a valid SVG id, so strip it to safe characters. + const gradientId = `linear-gradient-${useId().replace(/[^\w-]/g, '')}` + + // Clip the gradient (not the children) to the container's corner radii. + const clipStyle = useMemo(() => { + const flat = StyleSheet.flatten(style) ?? {} + const radii = Object.fromEntries( + BORDER_RADIUS_KEYS.filter((key) => flat[key] !== undefined).map((key) => [ + key, + flat[key] + ]) + ) as ViewStyle + return [StyleSheet.absoluteFill, radii, styles.clip] + }, [style]) + + return ( + + + + + + {colors.map((color, index) => ( + 1 ? index / (colors.length - 1) : 0) + } + stopColor={color} + /> + ))} + + + + + + {children} + + ) +} diff --git a/packages/mobile/src/harmony-native/components/RadialGradient/RadialGradient.tsx b/packages/mobile/src/harmony-native/components/RadialGradient/RadialGradient.tsx index 1fdf297915c..5c4f04a60c6 100644 --- a/packages/mobile/src/harmony-native/components/RadialGradient/RadialGradient.tsx +++ b/packages/mobile/src/harmony-native/components/RadialGradient/RadialGradient.tsx @@ -1,57 +1,94 @@ -import { useState } from 'react' +import { useId, useState } from 'react' import { css } from '@emotion/native' -import { Platform, View } from 'react-native' -import type { RadialGradientProps } from 'react-native-radial-gradient' -import RNRadialGradient from 'react-native-radial-gradient' +import type { ViewProps } from 'react-native' +import { StyleSheet, View } from 'react-native' +import Svg, { + Defs, + RadialGradient as SvgRadialGradient, + Rect, + Stop +} from 'react-native-svg' const fullSize = css({ height: '100%', width: '100%' }) -// RadialGradient that uses percentages instead of pixels for center and radius -// This makes RadialGradient much more useful and dynamic +export type RadialGradientProps = ViewProps & { + colors: string[] + center?: number[] + stops?: number[] + radius?: number +} + +// RadialGradient that takes its center and radius as percentages of the +// laid-out size instead of pixels. Drawn with react-native-svg behind any +// children. export const RadialGradient = (props: RadialGradientProps) => { const { - center: centerProp = [50, 50], - radius: radiusProp, + colors, + stops, + center = [50, 50], + radius = 50, style, + children, + onLayout, ...other } = props - const [{ height, width }, setDimensions] = useState({ - height: 0, - width: 0 - }) - - let center = centerProp - ? [(centerProp[0] * width) / 100, (centerProp[1] * height) / 100] - : undefined + const [{ height, width }, setDimensions] = useState({ height: 0, width: 0 }) - // We update the center prop to use x,y coordinates on iOS due to - // bug where center prop cannot be updated on iOS - if (Platform.OS === 'ios') { - center = center - ? ({ - x: center[0], - y: center[1] - } as unknown as RadialGradientProps['center']) - : undefined - } + // useId output is not a valid SVG id, so strip it to safe characters. + const gradientId = `radial-gradient-${useId().replace(/[^\w-]/g, '')}` - // Since we cant have ellipse gradients, we use the average of height and width - const radius = radiusProp - ? (radiusProp * ((height + width) / 2)) / 100 - : undefined + const cx = (center[0] * width) / 100 + const cy = (center[1] * height) / 100 + // Use the average of width and height so the gradient stays circular. + const r = (radius * ((height + width) / 2)) / 100 return ( setDimensions(e.nativeEvent.layout)} + onLayout={(e) => { + setDimensions(e.nativeEvent.layout) + onLayout?.(e) + }} > - + {width > 0 && height > 0 ? ( + + + + {colors.map((color, index) => ( + 1 ? index / (colors.length - 1) : 0) + } + stopColor={color} + /> + ))} + + + + + ) : null} + {children} ) } diff --git a/packages/mobile/src/harmony-native/components/index.ts b/packages/mobile/src/harmony-native/components/index.ts index 5e8f0475d4d..f82a532997b 100644 --- a/packages/mobile/src/harmony-native/components/index.ts +++ b/packages/mobile/src/harmony-native/components/index.ts @@ -25,6 +25,7 @@ export type { SocialButtonProps } from './Button/SocialButton/types' export * from './Button/PlainButton/PlainButton' export * from './Button/PlainButton/types' export * from './CompletionCheck/CompletionCheck' +export * from './LinearGradient/LinearGradient' export * from './RadialGradient/RadialGradient' export * from './Image/Image' export * from './Radio' diff --git a/packages/mobile/src/screens/audio-screen/AudioScreen.tsx b/packages/mobile/src/screens/audio-screen/AudioScreen.tsx index 7a8b6cbdc95..95171a811e6 100644 --- a/packages/mobile/src/screens/audio-screen/AudioScreen.tsx +++ b/packages/mobile/src/screens/audio-screen/AudioScreen.tsx @@ -11,10 +11,10 @@ import { AUDIO } from '@audius/fixed-decimal' import { useFocusEffect } from '@react-navigation/native' import { Image, Linking } from 'react-native' import { TouchableOpacity } from 'react-native-gesture-handler' -import LinearGradient from 'react-native-linear-gradient' import { useDispatch, useSelector } from 'react-redux' import { + LinearGradient, Button, Flex, IconCrown, diff --git a/packages/mobile/src/screens/coin-details-screen/EditCoinDetailsScreen.tsx b/packages/mobile/src/screens/coin-details-screen/EditCoinDetailsScreen.tsx index 77ef64dd304..d8d56866260 100644 --- a/packages/mobile/src/screens/coin-details-screen/EditCoinDetailsScreen.tsx +++ b/packages/mobile/src/screens/coin-details-screen/EditCoinDetailsScreen.tsx @@ -19,9 +19,9 @@ import { useRoute, useNavigation } from '@react-navigation/native' import { useFormikContext, Formik } from 'formik' import { Image as RNImage, View, StyleSheet } from 'react-native' import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view' -import LinearGradient from 'react-native-linear-gradient' import { + LinearGradient, IconInstagram, IconLink, IconPlus, diff --git a/packages/mobile/src/screens/coin-details-screen/components/CoinInfoCard.tsx b/packages/mobile/src/screens/coin-details-screen/components/CoinInfoCard.tsx index 23c1aa84e46..f487b53c9ec 100644 --- a/packages/mobile/src/screens/coin-details-screen/components/CoinInfoCard.tsx +++ b/packages/mobile/src/screens/coin-details-screen/components/CoinInfoCard.tsx @@ -7,9 +7,9 @@ import { WidthSizes } from '@audius/common/models' import { shortenSPLAddress } from '@audius/common/utils' import Clipboard from '@react-native-clipboard/clipboard' import { Image, Linking, StyleSheet, View } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import { + LinearGradient, Divider, Flex, IconCopy, diff --git a/packages/mobile/src/screens/coin-details-screen/components/GraduationProgressBar.tsx b/packages/mobile/src/screens/coin-details-screen/components/GraduationProgressBar.tsx index 83d055a63d0..074dfe5de07 100644 --- a/packages/mobile/src/screens/coin-details-screen/components/GraduationProgressBar.tsx +++ b/packages/mobile/src/screens/coin-details-screen/components/GraduationProgressBar.tsx @@ -1,6 +1,4 @@ -import LinearGradient from 'react-native-linear-gradient' - -import { Box, spacing, useTheme } from '@audius/harmony-native' +import { LinearGradient, Box, spacing, useTheme } from '@audius/harmony-native' interface GraduationProgressBarProps { value: number diff --git a/packages/mobile/src/screens/explore-screen/components/ColorTile.tsx b/packages/mobile/src/screens/explore-screen/components/ColorTile.tsx index 7a62aa68c1f..681309bdd88 100644 --- a/packages/mobile/src/screens/explore-screen/components/ColorTile.tsx +++ b/packages/mobile/src/screens/explore-screen/components/ColorTile.tsx @@ -7,9 +7,9 @@ import type { ViewStyle } from 'react-native' import { Animated, Image, View } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import type { SvgProps } from 'react-native-svg' +import { LinearGradient } from '@audius/harmony-native' import IconAudioRewardsPill from 'app/assets/images/iconAudioRewardsPill.svg' import { Pressable } from 'app/components/core' import Text from 'app/components/text' @@ -142,7 +142,6 @@ export const ColorTile = ({ > diff --git a/packages/mobile/src/screens/rewards-screen/RewardsScreen.tsx b/packages/mobile/src/screens/rewards-screen/RewardsScreen.tsx index 9b8140b9f77..3a0c01eec13 100644 --- a/packages/mobile/src/screens/rewards-screen/RewardsScreen.tsx +++ b/packages/mobile/src/screens/rewards-screen/RewardsScreen.tsx @@ -5,10 +5,9 @@ import { useRemoteVar } from '@audius/common/hooks' import { StringKeys } from '@audius/common/services' import { tokenDashboardPageActions } from '@audius/common/store' import { useFocusEffect } from '@react-navigation/native' -import LinearGradient from 'react-native-linear-gradient' import { useDispatch } from 'react-redux' -import { IconCrown, Flex, Text } from '@audius/harmony-native' +import { LinearGradient, IconCrown, Flex, Text } from '@audius/harmony-native' import { ScrollView, Screen, Tile, ScreenContent } from 'app/components/core' import { useToast } from 'app/hooks/useToast' import { makeStyles } from 'app/styles' diff --git a/packages/mobile/src/screens/rewards-screen/TiersTile.tsx b/packages/mobile/src/screens/rewards-screen/TiersTile.tsx index ebca2560060..627a3de67f5 100644 --- a/packages/mobile/src/screens/rewards-screen/TiersTile.tsx +++ b/packages/mobile/src/screens/rewards-screen/TiersTile.tsx @@ -13,9 +13,9 @@ import { badgeTiers, getTierAndNumberForBalance } from '@audius/common/store' import type { Nullable } from '@audius/common/utils' import { formatNumberCommas } from '@audius/common/utils' import { Linking } from 'react-native' -import LinearGradient from 'react-native-linear-gradient' import { + LinearGradient, Button, Flex, IconDiscord, diff --git a/packages/mobile/src/shims/react-native-linear-gradient.ts b/packages/mobile/src/shims/react-native-linear-gradient.ts new file mode 100644 index 00000000000..6511eae8bf7 --- /dev/null +++ b/packages/mobile/src/shims/react-native-linear-gradient.ts @@ -0,0 +1,6 @@ +// Metro resolves `react-native-linear-gradient` here so dependencies that +// still import it (react-native-gifted-charts) render our svg gradient. +import { LinearGradient } from 'app/harmony-native/components/LinearGradient/LinearGradient' + +export { LinearGradient } +export default LinearGradient diff --git a/packages/mobile/src/store/offline-downloads/sagas/migrateOfflineDataPathSaga.ts b/packages/mobile/src/store/offline-downloads/sagas/migrateOfflineDataPathSaga.ts index fb72c845b97..ecbbfcee866 100644 --- a/packages/mobile/src/store/offline-downloads/sagas/migrateOfflineDataPathSaga.ts +++ b/packages/mobile/src/store/offline-downloads/sagas/migrateOfflineDataPathSaga.ts @@ -1,8 +1,8 @@ import path from 'path' +import * as RNFS from '@dr.pogodin/react-native-fs' import { difference } from 'lodash' import ReactNativeBlobUtil from 'react-native-blob-util' -import RNFS from 'react-native-fs' import { call, put, select } from 'typed-redux-saga' import { downloadsRoot } from 'app/services/offline-downloader' From 082ff2a4ff45211c75144a27c09674f9b81bac7f Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Thu, 8 Oct 2026 17:13:50 -0700 Subject: [PATCH 4/4] feat(mobile): move push notifications to notifee + firebase messaging (#14640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- package-lock.json | 850 +++++++++++++++++- packages/mobile/android/app/build.gradle | 1 - .../android/app/src/main/AndroidManifest.xml | 5 + .../java/co/audius/app/MainApplication.kt | 1 - packages/mobile/android/build.gradle | 6 + packages/mobile/android/settings.gradle | 2 - packages/mobile/index.js | 8 + .../project.pbxproj | 14 + .../ios/AudiusReactNative/AppDelegate.mm | 34 +- packages/mobile/ios/Podfile | 5 + packages/mobile/ios/Podfile.lock | 186 +++- packages/mobile/ios/PrivacyInfo.xcprivacy | 14 +- packages/mobile/package.json | 4 +- packages/mobile/src/notifications.test.ts | 211 +++++ packages/mobile/src/notifications.ts | 161 +++- 15 files changed, 1396 insertions(+), 106 deletions(-) create mode 100644 packages/mobile/src/notifications.test.ts diff --git a/package-lock.json b/package-lock.json index eabd08bc935..42065d2330f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5746,6 +5746,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -12702,6 +12703,635 @@ "node": ">=14" } }, + "node_modules/@firebase/ai": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.13.0.tgz", + "integrity": "sha512-nJJDQKqjAcbkZdZGT/5WTVLrGZ+pYhWbwKC90nNzmvtoRTtnOJaNS34fhKSHQeB9SALgD2kxuWT5I4AkytdZ/Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.4", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.22", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.22.tgz", + "integrity": "sha512-8BSaq/QRGU1+xyi8L2PTLTJU7MH9aMA72RQdIxrbhWFauOZY9OXo8f2YDN/972xA8d588tlnNVEQ2Mo69pT9Ow==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.28", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.28.tgz", + "integrity": "sha512-lIAlqUUbBu93FJMlQfslryQtBwwzdzvp23ePC6FNgymXk6Ook5v4Uvc0vdutvoIeqmyA3LfP0ZeRFK8+11kOOQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.22", + "@firebase/analytics-types": "0.8.4", + "@firebase/component": "0.7.3", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.4.tgz", + "integrity": "sha512-zQ+XTgkwH6CY/eUSHJRP7e4LxM30RCxlCmob5sy2axs25GE3Ny0XdgpDscMTHHQIGqWkxPXad4w2Mw9sCgT8zQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.14.13", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.14.13.tgz", + "integrity": "sha512-H89Jeyp31+EZk9GPu6vaeL9mEmoXgM3nASB7UPBYYS/lqAks21mO1BU1dF8NbsVTL6tgGZkGUtiGJgxtDiwHkw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.11.4.tgz", + "integrity": "sha512-G8EsbVJV9gSfoibx0dNoNOUrvr+PkL7J//+W/BST/oUassimkZeq9bjj3bKkB0pn4og5GMQ9qs7FefwP00kkgg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.4.tgz", + "integrity": "sha512-9iP0MvmaVagulNXmrca96U3tqNAI3j98wsC1z7rj62nnOTajlrHM//jjB9VoHqRw6/islMskp6RsKnM7vhLDqA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.11.4", + "@firebase/app-check-types": "0.5.4", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.4.tgz", + "integrity": "sha512-zz3i6e13B8BfWiLy8MABtTh8aGIACgKbf9UVnyHcWs+yQzJXgQcl8A46b0zfaiJHdQ+niF0ouAfcpuf+3LMPQg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.4.tgz", + "integrity": "sha512-xV7JsIyzVr15aA7f3Pi0rB9gdBuVubs89FGA8VkRYA4g0l78poADgdfrScgf7NndSg9mm7cR7PJyY0+t22KaGw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.13.tgz", + "integrity": "sha512-pn3FvXwUR34kWPccDQfCKsNZcM2wD1OS+J1jeEgzM1ZNXoxR2NaF6e5DjDuRrnTwR6LN2XQQt0IqE6yKmgpCQg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.14.13", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.5.tgz", + "integrity": "sha512-YevqTjvo7Iujsa9Dwowmd6dSoElhzmD63ZSrq6bzjvQ6POjYgNjOFHLmNIgJs48eNO093NCERibuFnxbfOvU7A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/logger": "0.5.1" + } + }, + "node_modules/@firebase/app/node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/@firebase/auth": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.13.2.tgz", + "integrity": "sha512-B4w0iS7MxRg28oIh2fJFTE6cM0lYdBrW19eHpc42jqEcloUjlYyVrpPqZvqA4+v9KFEVSKEs2SfWyta7hbzkJQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.7.tgz", + "integrity": "sha512-XgKnOgY1Siq7gylAmLkYtHAlRxNeWEAspH+nO3gJZJnfHqoTHbr9UjJ3nHNFALYXV5CfpQlyPROyB2ztySBHBQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.13.2", + "@firebase/auth-types": "0.13.1", + "@firebase/component": "0.7.3", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.5.tgz", + "integrity": "sha512-1Li/YuBDBAXcKv7BzY4U28gontUmAaw53sYiqbaVOMCFb2lFKK/c3CGMUWqtwe7+TXrl3poWnTCL5umYBg85Eg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.1.tgz", + "integrity": "sha512-0c1Mnid0uMDfGJHeUS4zfvBa4/CedJXotGy/n/NZJnBjwiJawt0ZYU+wH2VAVLiRCEfG2ncCkAX3yd1/2nrB7g==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.3.tgz", + "integrity": "sha512-wFofIaa2879ogD/WvkjYXJxRmfnL0scen6ORgaC3na1FNOR9ASIUANQdhqQcmWu/h77/pVHY7ch5flewa5Bcew==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.7.1.tgz", + "integrity": "sha512-2LbUU8mmSA63HknxQMmWHjpzuNLBKflvVwQc2tpoVKg0biWleNEJX031ELks0vzFs+dDjOUkCJR72RP6mQHFOg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.5", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.3.tgz", + "integrity": "sha512-XwWCa+E4TvNGpGwXrycLRNfdogADwFcvuhyow6wDWma9W54roaQIhe+4PM0KiLsIftBdSCGI7OKCXrdSRHbIhw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.4", + "@firebase/auth-interop-types": "0.2.5", + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.4.tgz", + "integrity": "sha512-3pK35F1MAgmqFJQlf2nhQl44vtAXQO1uaCaQOEUI9kCRtLFqi7N+QRKR7lFZPg+xIZIyubgxQaxY69YgfZRZWg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/database": "1.1.3", + "@firebase/database-types": "1.0.20", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.20.tgz", + "integrity": "sha512-kegbOk/w8iU64pr0q6k2ItyNGjnQBMHFhwS7ohdWI4W+pc0/zhhdGXTdFj6X1oxItRjPoYOsSQmERgBkn/ihxw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.5", + "@firebase/util": "1.15.1" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.15.0.tgz", + "integrity": "sha512-Fj9osqYkz2Rqr7kW3/A8BRd8CyJ7yA5K8YjhihRdyJWbL+FsELVcR6DpoCplrp1IyU+xeGgTubo1UOySXpY+EA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "@firebase/webchannel-wrapper": "1.0.6", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.10", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.10.tgz", + "integrity": "sha512-yMP3FADDjikdrQv4YmvL4EkIny6Hw+N+a2O5T40rlHiniyMpRPxgYkKiFOvMZnsqKLqBVnKqCAElC0pa/IZtdw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/firestore": "4.15.0", + "@firebase/firestore-types": "3.0.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.4.tgz", + "integrity": "sha512-jGn+JSS4X9zZsrfu7Yw66v5YRdOLD1oyQh4USR0xWl4CUqV/DA6bNIXRPpxH/cUl3iVTNiP6MN7g+EL42A4qfA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.13.5.tgz", + "integrity": "sha512-bWCx713f4kE/uFV7gdFOLBS7lDoiZj48MRkbAqe35gkXcCeWF4QjRNO07Jhmve7EJIoQOBczL29y2r8VRuN1kw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.4", + "@firebase/auth-interop-types": "0.2.5", + "@firebase/component": "0.7.3", + "@firebase/messaging-interop-types": "0.2.5", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.4.5.tgz", + "integrity": "sha512-10qlUXGY25G5/1g9UihqksPp2po+ZqSE7LEizsrdUP7vrTmkysXxGSZCDyojSEp6mQe/ecRDdDDI+z4XRdb4wQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/functions": "0.13.5", + "@firebase/functions-types": "0.6.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.4.tgz", + "integrity": "sha512-zV6kgqtduR4rUAdC/ilS7kmb93XD7bEZoJDlVBZqlOw2uGGGCNBQBuleww2rr0Ulr3L9o2TDjumEt68/l1f9DQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.22", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.22.tgz", + "integrity": "sha512-ef6nn3GGQTdReCfotRMG77PJZu8CqEbiK5pEoBnM0gTu/Z9v0i/az2p3HABsa/1beQmmyh1OsOjf7P5+pgwdZw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/util": "1.15.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.22.tgz", + "integrity": "sha512-C/zpAuTP5S9OgKSPvXRupw3hoY/JZSlA1wFjD/Sb7LIQE0FNbcMdO8Y4KXVEkjVzma/DDDDIAzxEXqKMAzc88w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/installations-types": "0.5.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.4.tgz", + "integrity": "sha512-U2eFapdHwjb43Vx9o+Pmj4dFfvcHEK1IirEFLqMtWrTHvmdrS3gBpBD1kmJk/9HjsOtoHZxJ2Paoe79e+L1ZPg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/installations/node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/@firebase/logger": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.1.tgz", + "integrity": "sha512-vZKLsqE1ABOy8OjQiE7cUTFn4gvaqlk88yp8N94Pk/sDpq61YqZGqmVFZTvOyflTwuYFcWirBdYGoJgbDaXKYQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.13.0.tgz", + "integrity": "sha512-GZoo0uGRvEbszo83xcgbjJp4FpkmBEr4l8Z4hi8gl+P1Spn/MTK3HapanMzSX4yUHuTEiF5hasWRxOaz+o5sxQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/messaging-interop-types": "0.2.5", + "@firebase/util": "1.15.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.27.tgz", + "integrity": "sha512-JNOiu1PPgdHzEPEtoFiNxQuu0x9bm4bfETSQCpGfcTlgWkhlSK7uh7nlsjC10TQLUNgYetLmuutaYTh8aeYLVA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/messaging": "0.13.0", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.5.tgz", + "integrity": "sha512-tUEKnaAP2Y/MNIqgnriPpV6e5l13Vs/+p2yrd6NGlncPJT9O3a8muYZtdnWe+IJ4fgKLHJVC79n/asxk/N5Msw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/messaging/node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/@firebase/performance": { + "version": "0.7.12", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.12.tgz", + "integrity": "sha512-fe7nV8teUU3OBHlMUZ9Lw4gLhCW2k4m5Uc3pfWGV+fl8uwJQBGp9Q3lqsJ+HSrFu3Q2pJyLAgrClPGSKyDeYgQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.25.tgz", + "integrity": "sha512-q6NjTXpIPoFuUmCmMN/maCdTgzT6aExs9xZo+PxfVLj6uLVGvpyAD6XWjmcrb7jChsFBYbq7E5dyNDF7Zhy9kA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/performance": "0.7.12", + "@firebase/performance-types": "0.2.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.4.tgz", + "integrity": "sha512-kJSEk7b0uhpcPRyL4SQ/GPujLqk52XNKcXlnsKDbWGAb9vugcLvOU3u6zfEdwd+d8hWJb5S5ZizV1JFFI0nkKg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.8.4.tgz", + "integrity": "sha512-lslywR5lGvHWTu4z/MPoYs3UwS3CKdeY+ELXY87087VsOpBpkD+9Orra23tA9GW683arPTDOM3CM6eKmtiOO3g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/installations": "0.6.22", + "@firebase/logger": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.25.tgz", + "integrity": "sha512-FnA5S4IxFJAAFrCnYzWlO0FCaizlYdqhe42ygFMA+wE/mUP+w36iXzHyKj1OO1A+2gyMFjeRHyg8HhkJ6c5vRA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/logger": "0.5.1", + "@firebase/remote-config": "0.8.4", + "@firebase/remote-config-types": "0.5.1", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.1.tgz", + "integrity": "sha512-cX/1LT6KQwkXzck2eSzeKnuvXZCyr8qaPpDcikoJs7jmI+oBOXixpDLeDtWj1U6GNMkIoXrEDNoyT2Ypcyp5/A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.3.tgz", + "integrity": "sha512-YX4/YL6P6/fufSSeGnVhjWddcIXbFq2cWIhMKFTZo1E/Rtcl2mJj/BYUQTwJfcE1Tl8un1FOya4L05jcSLN/Eg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.3.tgz", + "integrity": "sha512-gruVqjtUGX8tEoeNbaWXZm0Zfcfcb7fvmDmBxV8yPAbWvExRnZYLO2+qw9idxNE7BvPXt5csyjSYHy//dAizxw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.3", + "@firebase/storage": "0.14.3", + "@firebase/storage-types": "0.8.4", + "@firebase/util": "1.15.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.4.tgz", + "integrity": "sha512-BT7cwxJOx8SWwlQfrlC+bD/Sk3Cw+1odCi8UZNFNWTVZoPsBnA5W+mqtZzVnvsdJpXCFGSGQ7R7vOR6dtM/BRA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.1.tgz", + "integrity": "sha512-LUdM4Wg7YM9Pq/49nGYySJA0CSQEKnGffFzWV8+6gXN7mGxn+FL1IqvFbuZUtAQcfZgHYDwCE1wwlK7rB7gl2g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.6.tgz", + "integrity": "sha512-Vr/Mqu79dMwGRAyGbJ4uN4+BtXB3/mRTdzetD1daWNeG8QaWuzhhbG77GltO5c0yYmYls8i250iX73624GJd7Q==", + "license": "Apache-2.0" + }, "node_modules/@floating-ui/core": { "version": "1.7.3", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", @@ -12905,6 +13535,66 @@ "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.16.tgz", + "integrity": "sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/proto-loader/node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/@hapi/hoek": { "version": "9.3.0", "dev": true, @@ -17532,6 +18222,15 @@ "node": ">= 8" } }, + "node_modules/@notifee/react-native": { + "version": "9.1.8", + "resolved": "https://registry.npmjs.org/@notifee/react-native/-/react-native-9.1.8.tgz", + "integrity": "sha512-Az/dueoPerJsbbjRxu8a558wKY+gONUrfoy3Hs++5OqbeMsR0dYe6P+4oN6twrLFyzAhEA1tEoZRvQTFDRmvQg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native": "*" + } + }, "node_modules/@npmcli/config": { "version": "6.4.0", "dev": true, @@ -19521,19 +20220,24 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { - "version": "2.0.4", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { - "version": "1.1.0", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", "license": "BSD-3-Clause", "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" + "@protobufjs/aspromise": "^1.1.1" } }, "node_modules/@protobufjs/float": { @@ -19553,7 +20257,9 @@ "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { - "version": "1.1.0", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", "license": "BSD-3-Clause" }, "node_modules/@radix-ui/colors": { @@ -24235,6 +24941,40 @@ "react-native": "*" } }, + "node_modules/@react-native-firebase/app": { + "version": "24.1.1", + "resolved": "https://registry.npmjs.org/@react-native-firebase/app/-/app-24.1.1.tgz", + "integrity": "sha512-NKCUgBeilQvchHQeKwB6RuE3Ne2AZMlClXjJBVySkCAF2FNO73ML2brqrAyegRkuVswtqHsB6wAgypmP82WnnA==", + "license": "Apache-2.0", + "dependencies": { + "firebase": "12.14.0" + }, + "peerDependencies": { + "expo": ">=47.0.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/@react-native-firebase/messaging": { + "version": "24.1.1", + "resolved": "https://registry.npmjs.org/@react-native-firebase/messaging/-/messaging-24.1.1.tgz", + "integrity": "sha512-tMb75+4ehbQshlH+PifbxKbP+pyDgK/dfBIvlFJeDVEAbdaM2nVpMkVyV6s5WJ3WRPUt6v+hyU1P3krQPYhkig==", + "license": "Apache-2.0", + "peerDependencies": { + "@react-native-firebase/app": "24.1.1", + "expo": ">=47.0.0" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, "node_modules/@react-native/assets-registry": { "version": "0.79.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.5.tgz", @@ -60330,6 +61070,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "license": "Apache-2.0", @@ -60794,6 +61546,42 @@ "node": ">=8" } }, + "node_modules/firebase": { + "version": "12.14.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.14.0.tgz", + "integrity": "sha512-aEZ/lniDR1hOCYpx/x/V8Nrrqq9pepKDNkqP/4WGZFC69gTv6F59Z4/54W/SUP4L/hFlrRNmWj35aweQq+IHow==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.13.0", + "@firebase/analytics": "0.10.22", + "@firebase/analytics-compat": "0.2.28", + "@firebase/app": "0.14.13", + "@firebase/app-check": "0.11.4", + "@firebase/app-check-compat": "0.4.4", + "@firebase/app-compat": "0.5.13", + "@firebase/app-types": "0.9.5", + "@firebase/auth": "1.13.2", + "@firebase/auth-compat": "0.6.7", + "@firebase/data-connect": "0.7.1", + "@firebase/database": "1.1.3", + "@firebase/database-compat": "2.1.4", + "@firebase/firestore": "4.15.0", + "@firebase/firestore-compat": "0.4.10", + "@firebase/functions": "0.13.5", + "@firebase/functions-compat": "0.4.5", + "@firebase/installations": "0.6.22", + "@firebase/installations-compat": "0.2.22", + "@firebase/messaging": "0.13.0", + "@firebase/messaging-compat": "0.2.27", + "@firebase/performance": "0.7.12", + "@firebase/performance-compat": "0.2.25", + "@firebase/remote-config": "0.8.4", + "@firebase/remote-config-compat": "0.2.25", + "@firebase/storage": "0.14.3", + "@firebase/storage-compat": "0.4.3", + "@firebase/util": "1.15.1" + } + }, "node_modules/fix-hmr": { "version": "1.0.2", "license": "MIT", @@ -65340,6 +66128,12 @@ "version": "1.0.0", "license": "ISC" }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "dev": true, @@ -70665,7 +71459,6 @@ }, "node_modules/lodash.camelcase": { "version": "4.3.0", - "dev": true, "license": "MIT" }, "node_modules/lodash.clonedeep": { @@ -103236,6 +104029,12 @@ "node": ">= 8" } }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, "node_modules/web-worker": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", @@ -105948,6 +106747,29 @@ "node": ">=4.0.0" } }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/websocket/node_modules/debug": { "version": "2.6.9", "license": "MIT", @@ -118904,7 +119726,7 @@ }, "packages/mobile": { "name": "@audius/mobile", - "version": "1.5.186", + "version": "1.5.187", "dependencies": { "@amplitude/analytics-react-native": "1.4.11", "@audius/common": "*", @@ -118923,6 +119745,7 @@ "@hcaptcha/react-native-hcaptcha": "1.6.0", "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.0", + "@notifee/react-native": "^9.1.8", "@optimizely/optimizely-sdk": "4.9.2", "@peculiar/webcrypto": "1.4.3", "@react-native-async-storage/async-storage": "^2.0.0", @@ -118933,6 +119756,8 @@ "@react-native-community/netinfo": "^11.3.3", "@react-native-community/slider": "4.5.0", "@react-native-documents/picker": "^10.1.2", + "@react-native-firebase/app": "^24.1.1", + "@react-native-firebase/messaging": "^24.1.1", "@react-native-masked-view/masked-view": "^0.3.2", "@react-navigation/bottom-tabs": "7.19.2", "@react-navigation/core": "7.22.1", @@ -118998,7 +119823,6 @@ "react-native-markdown-display": "7.0.2", "react-native-modal": "^13.0.0", "react-native-modal-datetime-picker": "18.0.0", - "react-native-notifications": "5.1.0", "react-native-pager-view": "6.7.1", "react-native-permissions": "5.4.4", "react-native-qrcode-svg": "6.2.0", @@ -121241,14 +122065,6 @@ "react-native": ">=0.65.0" } }, - "packages/mobile/node_modules/react-native-notifications": { - "version": "5.1.0", - "license": "MIT", - "peerDependencies": { - "react": "*", - "react-native": "*" - } - }, "packages/mobile/node_modules/react-native-qrcode-svg": { "version": "6.2.0", "license": "MIT", diff --git a/packages/mobile/android/app/build.gradle b/packages/mobile/android/app/build.gradle index 5daa9707320..4aa27585396 100755 --- a/packages/mobile/android/app/build.gradle +++ b/packages/mobile/android/app/build.gradle @@ -194,7 +194,6 @@ dependencies { implementation 'androidx.core:core-splashscreen:1.0.0' implementation "androidx.appcompat:appcompat:1.0.0" - implementation project(':react-native-notifications') implementation platform('com.google.firebase:firebase-bom:31.1.1') implementation 'com.google.firebase:firebase-messaging' implementation 'com.google.firebase:firebase-analytics' diff --git a/packages/mobile/android/app/src/main/AndroidManifest.xml b/packages/mobile/android/app/src/main/AndroidManifest.xml index 8ca6c1c1ab5..a769bac310a 100755 --- a/packages/mobile/android/app/src/main/AndroidManifest.xml +++ b/packages/mobile/android/app/src/main/AndroidManifest.xml @@ -92,6 +92,11 @@ + + App) TrackPlayer.registerPlaybackService(() => require('./audio-service')) +// On Android, firebase starts a headless JS task for each push that arrives +// while the app is in the background. The OS displays the push itself, so +// there is nothing to do here. +setBackgroundMessageHandler(getMessaging(), async () => {}) diff --git a/packages/mobile/ios/AudiusReactNative.xcodeproj/project.pbxproj b/packages/mobile/ios/AudiusReactNative.xcodeproj/project.pbxproj index 5eca50b1fcb..b4fbde5fe32 100644 --- a/packages/mobile/ios/AudiusReactNative.xcodeproj/project.pbxproj +++ b/packages/mobile/ios/AudiusReactNative.xcodeproj/project.pbxproj @@ -261,6 +261,7 @@ CBD3B687421F14165031EAF0 /* [CP] Embed Pods Frameworks */, B73C21C532E1201FEFBAECDA /* [CP] Copy Pods Resources */, 23A43AB82E37E73000CDCDF2 /* Embed Foundation Extensions */, + CB6D10CC025B9D671A7354C5 /* [CP-User] [RNFB] Core Configuration */, ); buildRules = ( ); @@ -434,6 +435,19 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AudiusReactNative/Pods-AudiusReactNative-resources.sh\"\n"; showEnvVarsInLog = 0; }; + CB6D10CC025B9D671A7354C5 /* [CP-User] [RNFB] Core Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "[CP-User] [RNFB] Core Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##########################################################################\n##########################################################################\n#\n# NOTE THAT IF YOU CHANGE THIS FILE YOU MUST RUN pod install AFTERWARDS\n#\n# This file is installed as an Xcode build script in the project file\n# by cocoapods, and you will not see your changes until you pod install\n#\n##########################################################################\n##########################################################################\n\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"note: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"note: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"note: -> RNFB build script started\"\necho \"note: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"note: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"note: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n if ! _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\"); then\n echo \"error: Failed to parse firebase.json, check for syntax errors.\"\n exit 1\n fi\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"error: python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_analytics_storage\n _ANALYTICS_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_analytics_storage\")\n if [[ $_ANALYTICS_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_storage\n _ANALYTICS_AD_STORAGE=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_storage\")\n if [[ $_ANALYTICS_AD_STORAGE ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_STORAGE\")\")\n fi\n\n # config.analytics_default_allow_ad_user_data\n _ANALYTICS_AD_USER_DATA=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_user_data\")\n if [[ $_ANALYTICS_AD_USER_DATA ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AD_USER_DATA\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"note: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"note: <- RNFB build script finished\"\n"; + }; CBD3B687421F14165031EAF0 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/packages/mobile/ios/AudiusReactNative/AppDelegate.mm b/packages/mobile/ios/AudiusReactNative/AppDelegate.mm index c82bdf3a6ff..d985f2a5acb 100644 --- a/packages/mobile/ios/AudiusReactNative/AppDelegate.mm +++ b/packages/mobile/ios/AudiusReactNative/AppDelegate.mm @@ -8,7 +8,8 @@ #import #import #import -#import "RNNotifications.h" +#import +#import "NotifeeCore+UNUserNotificationCenter.h" @implementation AppDelegate { NSDictionary *_launchOptions; @@ -17,7 +18,12 @@ @implementation AppDelegate { - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [RNNotifications startMonitorNotifications]; + [FIRApp configure]; + // Our iOS pushes come straight from APNs (SNS), not FCM. Firebase messaging + // only reports taps on FCM pushes and passes the rest to the delegate it + // replaced, so notifee has to be that delegate to report them to JS. + // Both libraries install themselves after launch, so make notifee go first. + [[NotifeeCoreUNUserNotificationCenter instance] observe]; NSString *receiverAppID = @"222B31C8"; GCKDiscoveryCriteria *criteria = [[GCKDiscoveryCriteria alloc] initWithApplicationID:receiverAppID]; GCKCastOptions* options = [[GCKCastOptions alloc] initWithDiscoveryCriteria:criteria]; @@ -72,9 +78,10 @@ - (UIWindow *)loadReactNativeWindowInScene:(UIWindowScene *)scene return window; } -// Scene apps get the launch URL, user activity and notification through the -// connection options rather than launchOptions. Linking.getInitialURL and -// Notifications.getInitialNotification read them from the bridge's launchOptions. +// Scene apps get the launch URL and user activity through the connection +// options rather than launchOptions. Linking.getInitialURL reads them from the +// bridge's launchOptions. A tapped notification needs no copy: the notification +// center delegate still receives it at launch. - (NSDictionary *)launchOptionsWithConnectionOptions:(UISceneConnectionOptions *)connectionOptions { NSMutableDictionary *launchOptions = [NSMutableDictionary dictionaryWithDictionary:_launchOptions ?: @{}]; @@ -92,11 +99,6 @@ - (NSDictionary *)launchOptionsWithConnectionOptions:(UISceneConnectionOptions * }; } - NSDictionary *notification = connectionOptions.notificationResponse.notification.request.content.userInfo; - if (notification != nil && launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] == nil) { - launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey] = notification; - } - return launchOptions; } @@ -120,16 +122,4 @@ - (NSURL *)bundleURL #endif } -- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { - [RNNotifications didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; -} - -- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { - [RNNotifications didFailToRegisterForRemoteNotificationsWithError:error]; -} - -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler { - [RNNotifications didReceiveBackgroundNotification:userInfo withCompletionHandler:completionHandler]; -} - @end diff --git a/packages/mobile/ios/Podfile b/packages/mobile/ios/Podfile index c4c4b3407b6..2c519c4ca74 100644 --- a/packages/mobile/ios/Podfile +++ b/packages/mobile/ios/Podfile @@ -36,6 +36,11 @@ target 'AudiusReactNative' do pod 'nSure' + # Firebase's Swift pods import these, and pods link statically here, so they + # need module maps. + pod 'GoogleUtilities', :modular_headers => true + pod 'nanopb', :modular_headers => true + config = use_native_modules! use_react_native!( diff --git a/packages/mobile/ios/Podfile.lock b/packages/mobile/ios/Podfile.lock index 3b80412ba02..7d3f60ecc4c 100644 --- a/packages/mobile/ios/Podfile.lock +++ b/packages/mobile/ios/Podfile.lock @@ -39,9 +39,79 @@ PODS: - ffmpeg-kit-react-native/full-gpl (6.0.2): - ffmpeg-kit-ios-full-gpl (= 6.0) - React-Core + - Firebase/CoreOnly (12.10.0): + - FirebaseCore (~> 12.10.0) + - Firebase/Messaging (12.10.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 12.10.0) + - FirebaseCore (12.10.0): + - FirebaseCoreInternal (~> 12.10.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (12.10.0): + - FirebaseCore (~> 12.10.0) + - FirebaseCoreInternal (12.10.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseInstallations (12.10.0): + - FirebaseCore (~> 12.10.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - PromisesObjC (~> 2.4) + - FirebaseMessaging (12.10.0): + - FirebaseCore (~> 12.10.0) + - FirebaseInstallations (~> 12.10.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Reachability (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - nanopb (~> 3.30910.0) - fmt (11.0.2) - glog (0.3.5) - google-cast-sdk-dynamic-xcframework-no-bluetooth (4.7.1) + - GoogleDataTransport (10.1.1): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleUtilities (8.1.2): + - GoogleUtilities/AppDelegateSwizzler (= 8.1.2) + - GoogleUtilities/Environment (= 8.1.2) + - GoogleUtilities/Logger (= 8.1.2) + - GoogleUtilities/MethodSwizzler (= 8.1.2) + - GoogleUtilities/Network (= 8.1.2) + - "GoogleUtilities/NSData+zlib (= 8.1.2)" + - GoogleUtilities/Privacy (= 8.1.2) + - GoogleUtilities/Reachability (= 8.1.2) + - GoogleUtilities/SwizzlerTestHelpers (= 8.1.2) + - GoogleUtilities/UserDefaults (= 8.1.2) + - GoogleUtilities/AppDelegateSwizzler (8.1.2): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.2): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.2): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (8.1.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.2): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.2)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.2) + - GoogleUtilities/Reachability (8.1.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/SwizzlerTestHelpers (8.1.2): + - GoogleUtilities/MethodSwizzler + - GoogleUtilities/UserDefaults (8.1.2): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy - hermes-engine (0.79.5): - hermes-engine/Pre-built (= 0.79.5) - hermes-engine/Pre-built (0.79.5) @@ -73,6 +143,11 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) - nSure (1.3.13) - PromisesObjC (2.4.0) - RCT-Folly (2024.11.18.00): @@ -1519,8 +1594,6 @@ PODS: - Yoga - react-native-netinfo (11.4.1): - React-Core - - react-native-notifications (5.1.0): - - React-Core - react-native-pager-view (6.7.1): - DoubleConversion - glog @@ -1575,9 +1648,9 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga - - react-native-track-player (4.0.1): + - react-native-track-player (4.1.2): - React-Core - - SwiftAudioEx (= 1.0.0) + - SwiftAudioEx (= 1.1.0) - react-native-version-number (0.3.6): - React - react-native-video (6.18.0): @@ -2009,6 +2082,58 @@ PODS: - Yoga - RNDateTimePicker (8.3.0): - React-Core + - RNFBApp (24.1.1): + - DoubleConversion + - Firebase/CoreOnly (= 12.10.0) + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - Yoga + - RNFBMessaging (24.1.1): + - DoubleConversion + - Firebase/Messaging (= 12.10.0) + - FirebaseCoreExtension + - glog + - hermes-engine + - RCT-Folly (= 2024.11.18.00) + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-hermes + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - RNFBApp + - Yoga - RNFlashList (1.8.3): - DoubleConversion - glog @@ -2066,6 +2191,11 @@ PODS: - React-Core - React-RCTImage - TOCropViewController (~> 2.7.4) + - RNNotifee (9.1.8): + - React-Core + - RNNotifee/NotifeeCore (= 9.1.8) + - RNNotifee/NotifeeCore (9.1.8): + - React-Core - RNPermissions (5.4.4): - React-Core - RNReactNativeHapticFeedback (2.3.3): @@ -2285,7 +2415,7 @@ PODS: - SnapSDK/SCSDKCoreKit - SocketRocket (0.7.1) - SSZipArchive (2.5.5) - - SwiftAudioEx (1.0.0) + - SwiftAudioEx (1.1.0) - tiktok-opensdk-react-native (0.10.7): - DoubleConversion - glog @@ -2335,8 +2465,10 @@ DEPENDENCIES: - ffmpeg-kit-react-native/full-gpl (from `../../../node_modules/ffmpeg-kit-react-native/ffmpeg-kit-react-native.podspec`) - fmt (from `../../../node_modules/react-native/third-party-podspecs/fmt.podspec`) - glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`) + - GoogleUtilities - hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) - lottie-react-native (from `../node_modules/lottie-react-native`) + - nanopb - nSure - RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) - RCT-Folly/Fabric (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) @@ -2382,13 +2514,12 @@ DEPENDENCIES: - react-native-in-app-review (from `../node_modules/react-native-in-app-review`) - react-native-keyboard-controller (from `../../../node_modules/react-native-keyboard-controller`) - "react-native-netinfo (from `../../../node_modules/@react-native-community/netinfo`)" - - react-native-notifications (from `../node_modules/react-native-notifications`) - react-native-pager-view (from `../../../node_modules/react-native-pager-view`) - react-native-randombytes (from `../../../node_modules/react-native-randombytes`) - react-native-restart (from `../node_modules/react-native-restart`) - react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`) - "react-native-slider (from `../node_modules/@react-native-community/slider`)" - - react-native-track-player (from `../node_modules/react-native-track-player`) + - react-native-track-player (from `../../../node_modules/react-native-track-player`) - react-native-version-number (from `../../../node_modules/react-native-version-number`) - react-native-video (from `../../../node_modules/react-native-video`) - react-native-view-shot (from `../node_modules/react-native-view-shot`) @@ -2429,9 +2560,12 @@ DEPENDENCIES: - "RNCClipboard (from `../../../node_modules/@react-native-clipboard/clipboard`)" - "RNCMaskedView (from `../node_modules/@react-native-masked-view/masked-view`)" - "RNDateTimePicker (from `../node_modules/@react-native-community/datetimepicker`)" + - "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)" + - "RNFBMessaging (from `../../../node_modules/@react-native-firebase/messaging`)" - "RNFlashList (from `../../../node_modules/@shopify/flash-list`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) + - "RNNotifee (from `../../../node_modules/@notifee/react-native`)" - RNPermissions (from `../../../node_modules/react-native-permissions`) - RNReactNativeHapticFeedback (from `../../../node_modules/react-native-haptic-feedback`) - RNReanimated (from `../node_modules/react-native-reanimated`) @@ -2446,9 +2580,18 @@ DEPENDENCIES: SPEC REPOS: trunk: - Base64 + - Firebase + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseInstallations + - FirebaseMessaging - google-cast-sdk-dynamic-xcframework-no-bluetooth + - GoogleDataTransport + - GoogleUtilities - JWT - lottie-ios + - nanopb - nSure - PromisesObjC - SnapSDK @@ -2571,8 +2714,6 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native-keyboard-controller" react-native-netinfo: :path: "../../../node_modules/@react-native-community/netinfo" - react-native-notifications: - :path: "../node_modules/react-native-notifications" react-native-pager-view: :path: "../../../node_modules/react-native-pager-view" react-native-randombytes: @@ -2584,7 +2725,7 @@ EXTERNAL SOURCES: react-native-slider: :path: "../node_modules/@react-native-community/slider" react-native-track-player: - :path: "../node_modules/react-native-track-player" + :path: "../../../node_modules/react-native-track-player" react-native-version-number: :path: "../../../node_modules/react-native-version-number" react-native-video: @@ -2665,12 +2806,18 @@ EXTERNAL SOURCES: :path: "../node_modules/@react-native-masked-view/masked-view" RNDateTimePicker: :path: "../node_modules/@react-native-community/datetimepicker" + RNFBApp: + :path: "../../../node_modules/@react-native-firebase/app" + RNFBMessaging: + :path: "../../../node_modules/@react-native-firebase/messaging" RNFlashList: :path: "../../../node_modules/@shopify/flash-list" RNGestureHandler: :path: "../node_modules/react-native-gesture-handler" RNImageCropPicker: :path: "../node_modules/react-native-image-crop-picker" + RNNotifee: + :path: "../../../node_modules/@notifee/react-native" RNPermissions: :path: "../../../node_modules/react-native-permissions" RNReactNativeHapticFeedback: @@ -2703,13 +2850,22 @@ SPEC CHECKSUMS: FBLazyVector: d2a9cd223302b6c9aa4aa34c1a775e9db609eb52 ffmpeg-kit-ios-full-gpl: 7d416729f7b3604b64cee2a752c42608a8d228e0 ffmpeg-kit-react-native: 3cea88c9c5cfad62e1465279ea7d800dfbba3b00 + Firebase: 99f203d3a114c6ba591f3b32263a9626e450af65 + FirebaseCore: f4428e22415ea3b3eca652c2098413dabf2a23a9 + FirebaseCoreExtension: 3473a6ec16a91aee29fb75994a86c0220d2cddf3 + FirebaseCoreInternal: e7bbaeb00ab73011298f35ed223aa7371e212948 + FirebaseInstallations: 047343aa91fd6a1ebfa3eb374ddecf36a8aaddfd + FirebaseMessaging: ed18fb50634e6e85b5d3e77e628c21e2c928cc53 fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 google-cast-sdk-dynamic-xcframework-no-bluetooth: 1fa9e267df3fd6f8a1c6e3345142ca5286297968 + GoogleDataTransport: a24e58982ab3ba2f64d79613e027fe7f57e88539 + GoogleUtilities: 766ace00c6b10d8148408f329d10c4f051931850 hermes-engine: f03b0e06d3882d71e67e45b073bb827da1a21aae JWT: ef71dfb03e1f842081e64dc42eef0e164f35d251 lottie-ios: a881093fab623c467d3bce374367755c272bdd59 lottie-react-native: 173db75996ba0ebc176424e04cfc0cc0175b307d + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nSure: 2fc3fc973c44aa0be9a3446f84cb514adc475205 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809 @@ -2754,13 +2910,12 @@ SPEC CHECKSUMS: react-native-in-app-review: db8bb167a5f238e7ceca5c242d6b36ce8c4404a4 react-native-keyboard-controller: c8c8a6ce30420ca67fae84dd1a9f9a861a988e2a react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac - react-native-notifications: 4601a5a8db4ced6ae7cfc43b44d35fe437ac50c4 react-native-pager-view: e4cf30bc28e3ccf864f6cfb90590d8cea13b7393 react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162 react-native-safe-area-context: e48706fb09fde89f82e12bfb49b140cfdcb761f7 react-native-slider: f7d96d39d20d367a20d7af5bdae138aed3b2e75f - react-native-track-player: 97d76dbbd35f27cc709e5f04540615e54264b3f9 + react-native-track-player: 24ecd55d64da8a6471ba96484b67bc9cee6fc629 react-native-version-number: b415bbec6a13f2df62bf978e85bc0d699462f37f react-native-video: 5efc3c2185906e2c49c81e7e2c763193f5c5e419 react-native-view-shot: 6b7ed61d77d88580fed10954d45fad0eb2d47688 @@ -2801,9 +2956,12 @@ SPEC CHECKSUMS: RNCClipboard: dfeb43751adff21e588657b5b6c888c72f3aa68e RNCMaskedView: dc63cbc7b2f614b8b4526e11ed677b36e92cd9fb RNDateTimePicker: a793ed8822283f576dd0a205a0916c5098c2611f + RNFBApp: ef18c25022685d22dfc058983c2f287b1838e848 + RNFBMessaging: fae02a2b3354255cf16e7c598d282a6d9531e5c7 RNFlashList: c68de6a14cce0a262b0a4fabdea690d9f3361063 RNGestureHandler: 1070e4e691b23028309d796cc30652d9875138c7 RNImageCropPicker: 64b6bec9ec185ee59b71a93d7058816bd0e1916e + RNNotifee: 4a6ee5c7deaf00e005050052d73ee6315dff7ec9 RNPermissions: 0a18cdcfdd1773b969f817ce20d47c2b74724af8 RNReactNativeHapticFeedback: b837dd570ef9c316ed9f93274a2cdb152a8d78ab RNReanimated: b61d01db005bc4114b04e7a88c3f32cbd3865dac @@ -2815,13 +2973,13 @@ SPEC CHECKSUMS: SnapSDK: 1e68aad748e080f49e3802559b3b76026666ef62 SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 SSZipArchive: c69881e8ac5521f0e622291387add5f60f30f3c4 - SwiftAudioEx: 6f511018b7a0fdfd14ed1bb4081f953588245cc0 + SwiftAudioEx: f6aa653770f3a0d3851edaf8d834a30aee4a7646 tiktok-opensdk-react-native: f6b1a34bb4c1889c0dae5838effe484c673ea0c0 TikTokOpenSDKCore: e6f34e48bd6e85e4d94f9c04782c13d5defafb55 TikTokOpenShareSDK: a7da017bc66c28d0aefea9342c0cfcc7e52ea2b7 TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 Yoga: adb397651e1c00672c12e9495babca70777e411e -PODFILE CHECKSUM: e90397933b1403467b612b76e102f32a3bc9ae1a +PODFILE CHECKSUM: 2f4f4e3fbbf9bb9553299f2691874304461629c9 COCOAPODS: 1.15.2 diff --git a/packages/mobile/ios/PrivacyInfo.xcprivacy b/packages/mobile/ios/PrivacyInfo.xcprivacy index fa469f312af..657971cb2ec 100644 --- a/packages/mobile/ios/PrivacyInfo.xcprivacy +++ b/packages/mobile/ios/PrivacyInfo.xcprivacy @@ -6,20 +6,22 @@ NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPICategoryUserDefaults NSPrivacyAccessedAPITypeReasons - C617.1 - 3B52.1 - 0A2A.1 + CA92.1 + 1C8F.1 + C56D.1 NSPrivacyAccessedAPIType - NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPICategoryFileTimestamp NSPrivacyAccessedAPITypeReasons - CA92.1 + C617.1 + 3B52.1 + 0A2A.1 diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 55722f285bf..0a7579eeb93 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -59,6 +59,7 @@ "@hcaptcha/react-native-hcaptcha": "1.6.0", "@noble/hashes": "1.1.5", "@noble/secp256k1": "1.7.0", + "@notifee/react-native": "^9.1.8", "@optimizely/optimizely-sdk": "4.9.2", "@peculiar/webcrypto": "1.4.3", "@react-native-async-storage/async-storage": "^2.0.0", @@ -69,6 +70,8 @@ "@react-native-community/netinfo": "^11.3.3", "@react-native-community/slider": "4.5.0", "@react-native-documents/picker": "^10.1.2", + "@react-native-firebase/app": "^24.1.1", + "@react-native-firebase/messaging": "^24.1.1", "@react-native-masked-view/masked-view": "^0.3.2", "@react-navigation/bottom-tabs": "7.19.2", "@react-navigation/core": "7.22.1", @@ -134,7 +137,6 @@ "react-native-markdown-display": "7.0.2", "react-native-modal": "^13.0.0", "react-native-modal-datetime-picker": "18.0.0", - "react-native-notifications": "5.1.0", "react-native-pager-view": "6.7.1", "react-native-permissions": "5.4.4", "react-native-qrcode-svg": "6.2.0", diff --git a/packages/mobile/src/notifications.test.ts b/packages/mobile/src/notifications.test.ts new file mode 100644 index 00000000000..b99ed304e84 --- /dev/null +++ b/packages/mobile/src/notifications.test.ts @@ -0,0 +1,211 @@ +const mockMessaging = { + getAPNSToken: jest.fn(), + getInitialNotification: jest.fn(), + getToken: jest.fn(), + isDeviceRegisteredForRemoteMessages: jest.fn(), + onNotificationOpenedApp: jest.fn(), + onTokenRefresh: jest.fn(), + registerDeviceForRemoteMessages: jest.fn(), + requestPermission: jest.fn() +} +const mockNotifee = { + onForegroundEvent: jest.fn(), + onBackgroundEvent: jest.fn(), + setBadgeCount: jest.fn() +} +const mockStorage: Record = {} + +jest.mock('@react-native-firebase/messaging', () => ({ + getMessaging: () => 'messaging', + getAPNSToken: (...args: any[]) => mockMessaging.getAPNSToken(...args), + getInitialNotification: (...args: any[]) => + mockMessaging.getInitialNotification(...args), + getToken: (...args: any[]) => mockMessaging.getToken(...args), + isDeviceRegisteredForRemoteMessages: (...args: any[]) => + mockMessaging.isDeviceRegisteredForRemoteMessages(...args), + onNotificationOpenedApp: (...args: any[]) => + mockMessaging.onNotificationOpenedApp(...args), + onTokenRefresh: (...args: any[]) => mockMessaging.onTokenRefresh(...args), + registerDeviceForRemoteMessages: (...args: any[]) => + mockMessaging.registerDeviceForRemoteMessages(...args), + requestPermission: (...args: any[]) => + mockMessaging.requestPermission(...args) +})) +jest.mock('@notifee/react-native', () => ({ + __esModule: true, + default: mockNotifee, + EventType: { PRESS: 1, DISMISSED: 0 } +})) +jest.mock('@react-native-async-storage/async-storage', () => ({ + getItem: async (key: string) => mockStorage[key] ?? null, + setItem: async (key: string, value: string) => { + mockStorage[key] = value + }, + removeItem: async (key: string) => { + delete mockStorage[key] + } +})) +jest.mock('react-native-permissions', () => ({ + requestNotifications: jest.fn() +})) +jest.mock('@audius/common/api', () => ({ + getCurrentAccountQueryKey: () => ['account'] +})) +jest.mock('app/services/analytics', () => ({ + track: jest.fn(), + make: (e: any) => e +})) +jest.mock('app/services/audius-backend-instance', () => ({ + audiusBackendInstance: {} +})) +jest.mock('app/services/query-client', () => ({ + queryClient: { getQueryData: () => undefined } +})) +jest.mock('app/services/sdk/audius-sdk', () => ({ audiusSdk: jest.fn() })) +jest.mock('app/types/analytics', () => ({ + EventNames: { NOTIFICATIONS_OPEN_PUSH_NOTIFICATION: 'open' } +})) + +const loadNotifications = (os: 'ios' | 'android') => { + let mod: any + jest.isolateModules(() => { + jest.doMock('react-native', () => ({ Platform: { OS: os } })) + mod = require('./notifications').default + }) + return mod +} + +const iosPress = (data: Record) => ({ + type: 1, + detail: { notification: { title: 't', body: 'b', data } } +}) + +describe('PushNotifications', () => { + beforeEach(() => { + jest.clearAllMocks() + for (const key of Object.keys(mockStorage)) delete mockStorage[key] + }) + + describe('ios', () => { + it('registers the lowercase APNs token, not an FCM token', async () => { + mockMessaging.isDeviceRegisteredForRemoteMessages.mockReturnValue(false) + mockMessaging.getAPNSToken.mockResolvedValue('ABCDEF0123') + const notifications = loadNotifications('ios') + + const token = await notifications.getToken() + + expect(mockMessaging.registerDeviceForRemoteMessages).toHaveBeenCalled() + expect(mockMessaging.getToken).not.toHaveBeenCalled() + expect(token).toEqual({ token: 'abcdef0123', os: 'ios' }) + expect(JSON.parse(mockStorage['@device-token'])).toEqual(token) + }) + + it('falls back to the stored token when APNs has none', async () => { + mockMessaging.isDeviceRegisteredForRemoteMessages.mockReturnValue(true) + mockMessaging.getAPNSToken.mockResolvedValue(null) + mockStorage['@device-token'] = JSON.stringify({ + token: 'stored', + os: 'ios' + }) + const notifications = loadNotifications('ios') + + expect(await notifications.getToken()).toEqual({ + token: 'stored', + os: 'ios' + }) + }) + + it('holds a cold-start tap until navigation is ready, then navigates once', async () => { + const notifications = loadNotifications('ios') + const navigate = jest.fn() + const onForeground = mockNotifee.onForegroundEvent.mock.calls[0][0] + + onForeground( + iosPress({ data: { type: 'Follow', id: 1 }, 'media-url': 'x' }) + ) + notifications.setNavigation({ navigate }) + expect(navigate).not.toHaveBeenCalled() + + await notifications.openInitialNotification() + expect(navigate).toHaveBeenCalledTimes(1) + expect(navigate).toHaveBeenCalledWith({ type: 'Follow', id: 1 }) + expect(mockMessaging.getInitialNotification).not.toHaveBeenCalled() + + // A later remount must not replay it + await notifications.openInitialNotification() + expect(navigate).toHaveBeenCalledTimes(1) + }) + + it('navigates immediately on a tap once ready, ignoring other events', async () => { + const notifications = loadNotifications('ios') + const navigate = jest.fn() + notifications.setNavigation({ navigate }) + await notifications.openInitialNotification() + const onBackground = mockNotifee.onBackgroundEvent.mock.calls[0][0] + + await onBackground({ type: 0, detail: { notification: { data: {} } } }) + expect(navigate).not.toHaveBeenCalled() + + await onBackground(iosPress({ data: { type: 'Repost', id: 2 } })) + expect(navigate).toHaveBeenCalledWith({ type: 'Repost', id: 2 }) + }) + + it('requests permission through firebase and sets the badge', async () => { + const notifications = loadNotifications('ios') + await notifications.requestPermission() + expect(mockMessaging.requestPermission).toHaveBeenCalled() + notifications.setBadgeCount(0) + expect(mockNotifee.setBadgeCount).toHaveBeenCalledWith(0) + }) + }) + + describe('android', () => { + it('registers the FCM token', async () => { + mockMessaging.getToken.mockResolvedValue('fcm:Token') + const notifications = loadNotifications('android') + + expect(await notifications.getToken()).toEqual({ + token: 'fcm:Token', + os: 'android' + }) + expect(mockMessaging.getAPNSToken).not.toHaveBeenCalled() + }) + + it('opens the launch notification with parsed data', async () => { + mockMessaging.getInitialNotification.mockResolvedValue({ + notification: { title: 't', body: 'b' }, + data: { type: 'Follow', id: '12', userIds: '[1,2]' } + }) + const notifications = loadNotifications('android') + const navigate = jest.fn() + notifications.setNavigation({ navigate }) + + await notifications.openInitialNotification() + expect(navigate).toHaveBeenCalledTimes(1) + expect(navigate).toHaveBeenCalledWith({ + type: 'Follow', + id: 12, + userIds: [1, 2] + }) + expect(mockNotifee.onForegroundEvent).not.toHaveBeenCalled() + }) + + it('opens a tap from the background', async () => { + mockMessaging.getInitialNotification.mockResolvedValue(null) + const notifications = loadNotifications('android') + const navigate = jest.fn() + notifications.setNavigation({ navigate }) + await notifications.openInitialNotification() + + const onOpened = mockMessaging.onNotificationOpenedApp.mock.calls[0][1] + onOpened({ data: { type: 'Repost', id: '3' } }) + expect(navigate).toHaveBeenCalledWith({ type: 'Repost', id: 3 }) + }) + + it('does not touch the iOS badge', () => { + const notifications = loadNotifications('android') + notifications.setBadgeCount(0) + expect(mockNotifee.setBadgeCount).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/mobile/src/notifications.ts b/packages/mobile/src/notifications.ts index 036638238f6..1ca54ee4de2 100644 --- a/packages/mobile/src/notifications.ts +++ b/packages/mobile/src/notifications.ts @@ -1,10 +1,22 @@ import { getCurrentAccountQueryKey } from '@audius/common/api' import { MobileOS } from '@audius/common/models' import type { AccountState } from '@audius/common/store' +import notifee, { EventType } from '@notifee/react-native' +import type { Event as NotifeeEvent } from '@notifee/react-native' import AsyncStorage from '@react-native-async-storage/async-storage' +import { + getAPNSToken, + getInitialNotification, + getMessaging, + getToken as getFcmToken, + isDeviceRegisteredForRemoteMessages, + onNotificationOpenedApp, + onTokenRefresh, + registerDeviceForRemoteMessages, + requestPermission +} from '@react-native-firebase/messaging' +import type { FirebaseMessagingTypes } from '@react-native-firebase/messaging' import { Platform } from 'react-native' -import { Notifications } from 'react-native-notifications' -import type { Registered, Notification } from 'react-native-notifications' import { requestNotifications } from 'react-native-permissions' import { track, make } from 'app/services/analytics' @@ -52,7 +64,7 @@ function parseAndroidNotificationData(data: Record): any { } function extractNotificationCampaignIdFromPayload( - payload: Notification['payload'] + payload: Record | undefined ): string | undefined { const target = payload?.data?.data ?? payload?.data ?? payload ?? undefined if (!target || typeof target !== 'object') return undefined @@ -87,29 +99,68 @@ async function reportNotificationCampaignPushOpen( }) } -// Set to true while the push notification service is registering with the os -let isRegistering = false +// A tapped push, normalized across platforms. `payload` matches what +// react-native-notifications used to hand us: the APNs userInfo minus `aps` on +// iOS, the FCM data map on Android. +type OpenedNotification = { + title?: string + body?: string + payload: Record +} + +const fromRemoteMessage = ( + message: FirebaseMessagingTypes.RemoteMessage +): OpenedNotification => ({ + title: message.notification?.title, + body: message.notification?.body, + payload: message.data ?? {} +}) + +// Our iOS pushes go straight to APNs through SNS, not through FCM, so +// @react-native-firebase/messaging ignores taps on them. Notifee reports them +// as PRESS events instead (AppDelegate makes sure it sees them). +const fromNotifeeEvent = ({ + type, + detail +}: NotifeeEvent): OpenedNotification | null => { + if (type !== EventType.PRESS || !detail.notification) return null + return { + title: detail.notification.title, + body: detail.notification.body, + payload: (detail.notification.data ?? {}) as Record + } +} // Singleton class class PushNotifications { - lastId: number token: Token | null navigation: NotificationNavigation | null + // A tap that arrived before navigation was ready (cold start) + private pendingOpen: OpenedNotification | null + private isNavigationReady: boolean - // onNotification is a function passed in that is to be called when a - // notification is to be emitted. constructor() { - this.configure() - this.lastId = 0 this.token = null this.navigation = null + this.pendingOpen = null + this.isNavigationReady = false + this.configure() } setNavigation = (navigation: NotificationNavigation) => { this.navigation = navigation } - onNotification = (notification: Notification) => { + private handleOpened = (notification: OpenedNotification | null) => { + if (!notification) return + if (!this.isNavigationReady) { + this.pendingOpen = notification + return + } + this.onNotification(notification) + } + + onNotification = (notification: OpenedNotification) => { console.info(`Received notification ${JSON.stringify(notification)}`) const { title, body, payload } = notification const notificationCampaignId = @@ -138,19 +189,43 @@ class PushNotifications { this.navigation?.navigate(data) } - // Method used to open the push notification that the user pressed while the app was closed + // Called once navigation is ready. Opens the push the user tapped to launch + // the app, if any. openInitialNotification = async () => { - const notification = await Notifications.getInitialNotification() + this.isNavigationReady = true + if (Platform.OS === MobileOS.ANDROID) { + const message = await getInitialNotification(getMessaging()) + if (message) { + this.pendingOpen = fromRemoteMessage(message) + } + } + const notification = this.pendingOpen + this.pendingOpen = null if (notification) { this.onNotification(notification) } } - async onRegister(event: Registered) { - const token = { token: event.deviceToken, os: Platform.OS } + private async persistToken(deviceToken: string) { + const token = { token: deviceToken, os: Platform.OS } this.token = token await AsyncStorage.setItem(DEVICE_TOKEN, JSON.stringify(token)) - isRegistering = false + return token + } + + // identity-service registers iOS tokens with an SNS APNs platform app, so iOS + // must send the raw APNs token, not an FCM token. Lowercase hex matches what + // react-native-notifications sent, so existing rows and endpoints are reused. + private async fetchDeviceToken() { + if (Platform.OS === MobileOS.IOS) { + const messaging = getMessaging() + if (!isDeviceRegisteredForRemoteMessages(messaging)) { + await registerDeviceForRemoteMessages(messaging) + } + const apnsToken = await getAPNSToken(messaging) + return apnsToken ? apnsToken.toLowerCase() : null + } + return await getFcmToken(getMessaging()) } deregister() { @@ -158,10 +233,22 @@ class PushNotifications { } async configure() { - Notifications.events().registerRemoteNotificationsRegistered( - this.onRegister - ) - Notifications.events().registerNotificationOpened(this.onNotification) + if (Platform.OS === MobileOS.IOS) { + notifee.onForegroundEvent((event) => + this.handleOpened(fromNotifeeEvent(event)) + ) + notifee.onBackgroundEvent(async (event) => + this.handleOpened(fromNotifeeEvent(event)) + ) + } else { + const messaging = getMessaging() + onNotificationOpenedApp(messaging, (message) => + this.handleOpened(fromRemoteMessage(message)) + ) + onTokenRefresh(messaging, (token) => { + this.persistToken(token).catch(() => {}) + }) + } try { const token = await AsyncStorage.getItem(DEVICE_TOKEN) @@ -175,40 +262,30 @@ class PushNotifications { } } - async hasPermission(): Promise { - return await Notifications.isRegisteredForRemoteNotifications() - } - async requestPermission() { - isRegistering = true - if (Platform.OS === MobileOS.ANDROID) { // Android 13+ needs POST_NOTIFICATIONS. Use requestNotifications — PERMISSIONS.ANDROID // does not expose POST_NOTIFICATIONS in react-native-permissions v5, so request(undefined) crashed native code. await requestNotifications() + } else { + await requestPermission(getMessaging()) } - - Notifications.registerRemoteNotifications() - } - - cancelNotif() { - Notifications.cancelLocalNotification(this.lastId) - } - - cancelAll() { - Notifications.ios.cancelAllLocalNotifications() } setBadgeCount(count: number) { - Notifications.ios.setBadgeCount(count) + if (Platform.OS === MobileOS.IOS) { + notifee.setBadgeCount(count) + } } async getToken() { - // Wait until the device token and OS are persisted to async storage - // isRegistering modified as global - // eslint-disable-next-line no-unmodified-loop-condition - while (isRegistering) { - await new Promise((resolve) => setTimeout(resolve, 100)) + try { + const deviceToken = await this.fetchDeviceToken() + if (deviceToken) { + return await this.persistToken(deviceToken) + } + } catch (e) { + console.error('Failed to fetch push token', e) } const token = await AsyncStorage.getItem(DEVICE_TOKEN) if (token) {