From cf3cc37ec94da3744847d7ba583e0975f9109955 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Tue, 2 Dec 2025 20:23:25 +0100 Subject: [PATCH 01/27] feat: callkit/telecom integration --- package.json | 1 + packages/react-native-callingx/.editorconfig | 15 + packages/react-native-callingx/.gitattributes | 3 + packages/react-native-callingx/.nvmrc | 1 + .../react-native-callingx/.watchmanconfig | 1 + packages/react-native-callingx/.yarnrc.yml | 4 + .../react-native-callingx/CODE_OF_CONDUCT.md | 132 + .../react-native-callingx/CONTRIBUTING.md | 129 + .../react-native-callingx/Callingx.podspec | 21 + packages/react-native-callingx/LICENSE | 20 + packages/react-native-callingx/README.md | 412 + .../android/build.gradle | 81 + .../android/gradle.properties | 5 + .../android/src/main/AndroidManifest.xml | 16 + .../src/main/java/com/callingx/Call.kt | 74 + .../src/main/java/com/callingx/CallAction.kt | 52 + .../com/callingx/CallNotificationManager.kt | 303 + .../main/java/com/callingx/CallRepository.kt | 482 ++ .../src/main/java/com/callingx/CallService.kt | 338 + .../main/java/com/callingx/CallingxModule.kt | 576 ++ .../main/java/com/callingx/CallingxPackage.kt | 16 + .../java/com/callingx/HeadlessTaskManager.kt | 165 + .../main/java/com/callingx/ResourceUtils.kt | 53 + .../src/main/java/com/callingx/Utils.kt | 61 + .../main/res/drawable/ic_phone_paused_24.xml | 11 + .../main/res/drawable/ic_round_call_24.xml | 11 + .../android/src/main/res/drawable/ic_user.xml | 27 + .../react-native-callingx/babel.config.js | 12 + .../react-native-callingx/eslint.config.mjs | 29 + .../example/.bundle/config | 2 + .../example/.watchmanconfig | 1 + .../react-native-callingx/example/Gemfile | 16 + .../react-native-callingx/example/README.md | 97 + .../example/android/app/build.gradle | 119 + .../example/android/app/debug.keystore | Bin 0 -> 2257 bytes .../example/android/app/proguard-rules.pro | 10 + .../android/app/src/debug/AndroidManifest.xml | 9 + .../android/app/src/main/AndroidManifest.xml | 26 + .../java/callingx/example/MainActivity.kt | 22 + .../java/callingx/example/MainApplication.kt | 38 + .../res/drawable/rn_edit_text_material.xml | 37 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3056 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 5024 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2096 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4569 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 7098 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6464 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10676 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9250 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15523 bytes .../app/src/main/res/values/strings.xml | 3 + .../app/src/main/res/values/styles.xml | 9 + .../example/android/build.gradle | 21 + .../example/android/gradle.properties | 44 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + .../example/android/gradlew | 251 + .../example/android/gradlew.bat | 99 + .../example/android/settings.gradle | 6 + .../react-native-callingx/example/app.json | 4 + .../example/babel.config.js | 12 + .../react-native-callingx/example/index.js | 5 + .../example/ios/.xcode.env | 11 + .../CallingxExample.xcodeproj/project.pbxproj | 471 ++ .../xcschemes/CallingxExample.xcscheme | 88 + .../ios/CallingxExample/AppDelegate.swift | 48 + .../AppIcon.appiconset/Contents.json | 53 + .../Images.xcassets/Contents.json | 6 + .../example/ios/CallingxExample/Info.plist | 52 + .../CallingxExample/LaunchScreen.storyboard | 47 + .../ios/CallingxExample/PrivacyInfo.xcprivacy | 37 + .../react-native-callingx/example/ios/Podfile | 37 + .../example/jest.config.js | 3 + .../example/metro.config.js | 16 + .../example/package-lock.json | 7016 +++++++++++++++++ .../example/package.json | 34 + .../example/react-native.config.js | 21 + .../react-native-callingx/example/src/App.tsx | 20 + packages/react-native-callingx/ios/Callingx.h | 50 + .../react-native-callingx/ios/Callingx.mm | 1080 +++ .../ios/CallingxPublic.h | 45 + .../react-native-callingx/ios/UUIDStorage.h | 16 + .../react-native-callingx/ios/UUIDStorage.mm | 96 + packages/react-native-callingx/lefthook.yml | 16 + packages/react-native-callingx/package.json | 170 + .../react-native.config.js | 8 + .../src/CallingxModule.ts | 220 + .../react-native-callingx/src/EventManager.ts | 62 + .../src/__tests__/index.test.tsx | 1 + packages/react-native-callingx/src/index.tsx | 2 + .../src/spec/NativeCallingx.ts | 102 + packages/react-native-callingx/src/types.ts | 146 + .../src/utils/constants.ts | 53 + .../src/utils/headlessTask.ts | 52 + .../src/utils/permissions.ts | 109 + .../react-native-callingx/tsconfig.build.json | 4 + packages/react-native-callingx/tsconfig.json | 30 + packages/react-native-callingx/turbo.json | 42 + .../ios/StreamVideoReactNative.h | 3 +- .../ios/StreamVideoReactNative.m | 123 +- packages/react-native-sdk/package.json | 6 +- .../useCallingExpWithCallingStateEffect.ts | 180 + .../useIosCallkeepWithCallingStateEffect.ts | 235 - .../src/providers/StreamCall/index.tsx | 12 +- .../src/utils/StreamVideoRN/index.ts | 18 +- .../src/utils/push/android.ts | 507 +- .../src/utils/push/internal/ios.ts | 63 +- .../src/utils/push/internal/rxSubjects.ts | 24 +- .../src/utils/push/internal/utils.ts | 25 + .../src/utils/push/libs/callingx.ts | 18 + .../src/utils/push/libs/callkeep.ts | 16 - .../src/utils/push/libs/index.ts | 2 +- .../src/utils/push/setupCallingExpEvents.ts | 100 + .../src/utils/push/setupIosCallKeepEvents.ts | 203 - .../src/utils/push/setupIosVoipPushEvents.ts | 9 + .../dogfood/android/app/build.gradle | 2 - .../getstream/rnvideosample/MainActivity.kt | 8 + .../dogfood/ios/AppDelegate.swift | 34 +- .../react-native/dogfood/ios/Podfile.lock | 670 +- .../react-native/dogfood/metro.config.js | 1 + sample-apps/react-native/dogfood/package.json | 2 +- .../dogfood/src/utils/setPushConfig.ts | 19 + yarn.lock | 3175 +++++++- 124 files changed, 18267 insertions(+), 1341 deletions(-) create mode 100644 packages/react-native-callingx/.editorconfig create mode 100644 packages/react-native-callingx/.gitattributes create mode 100644 packages/react-native-callingx/.nvmrc create mode 100644 packages/react-native-callingx/.watchmanconfig create mode 100644 packages/react-native-callingx/.yarnrc.yml create mode 100644 packages/react-native-callingx/CODE_OF_CONDUCT.md create mode 100644 packages/react-native-callingx/CONTRIBUTING.md create mode 100644 packages/react-native-callingx/Callingx.podspec create mode 100644 packages/react-native-callingx/LICENSE create mode 100644 packages/react-native-callingx/README.md create mode 100644 packages/react-native-callingx/android/build.gradle create mode 100644 packages/react-native-callingx/android/gradle.properties create mode 100644 packages/react-native-callingx/android/src/main/AndroidManifest.xml create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/Call.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/CallAction.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/CallNotificationManager.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/CallRepository.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/CallingxPackage.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/HeadlessTaskManager.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/ResourceUtils.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/Utils.kt create mode 100644 packages/react-native-callingx/android/src/main/res/drawable/ic_phone_paused_24.xml create mode 100644 packages/react-native-callingx/android/src/main/res/drawable/ic_round_call_24.xml create mode 100644 packages/react-native-callingx/android/src/main/res/drawable/ic_user.xml create mode 100644 packages/react-native-callingx/babel.config.js create mode 100644 packages/react-native-callingx/eslint.config.mjs create mode 100644 packages/react-native-callingx/example/.bundle/config create mode 100644 packages/react-native-callingx/example/.watchmanconfig create mode 100644 packages/react-native-callingx/example/Gemfile create mode 100644 packages/react-native-callingx/example/README.md create mode 100644 packages/react-native-callingx/example/android/app/build.gradle create mode 100644 packages/react-native-callingx/example/android/app/debug.keystore create mode 100644 packages/react-native-callingx/example/android/app/proguard-rules.pro create mode 100644 packages/react-native-callingx/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packages/react-native-callingx/example/android/app/src/main/AndroidManifest.xml create mode 100644 packages/react-native-callingx/example/android/app/src/main/java/callingx/example/MainActivity.kt create mode 100644 packages/react-native-callingx/example/android/app/src/main/java/callingx/example/MainApplication.kt create mode 100644 packages/react-native-callingx/example/android/app/src/main/res/drawable/rn_edit_text_material.xml create mode 100644 packages/react-native-callingx/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 packages/react-native-callingx/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 packages/react-native-callingx/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 packages/react-native-callingx/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 packages/react-native-callingx/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 packages/react-native-callingx/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 packages/react-native-callingx/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 packages/react-native-callingx/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 packages/react-native-callingx/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 packages/react-native-callingx/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 packages/react-native-callingx/example/android/app/src/main/res/values/strings.xml create mode 100644 packages/react-native-callingx/example/android/app/src/main/res/values/styles.xml create mode 100644 packages/react-native-callingx/example/android/build.gradle create mode 100644 packages/react-native-callingx/example/android/gradle.properties create mode 100644 packages/react-native-callingx/example/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 packages/react-native-callingx/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 packages/react-native-callingx/example/android/gradlew create mode 100644 packages/react-native-callingx/example/android/gradlew.bat create mode 100644 packages/react-native-callingx/example/android/settings.gradle create mode 100644 packages/react-native-callingx/example/app.json create mode 100644 packages/react-native-callingx/example/babel.config.js create mode 100644 packages/react-native-callingx/example/index.js create mode 100644 packages/react-native-callingx/example/ios/.xcode.env create mode 100644 packages/react-native-callingx/example/ios/CallingxExample.xcodeproj/project.pbxproj create mode 100644 packages/react-native-callingx/example/ios/CallingxExample.xcodeproj/xcshareddata/xcschemes/CallingxExample.xcscheme create mode 100644 packages/react-native-callingx/example/ios/CallingxExample/AppDelegate.swift create mode 100644 packages/react-native-callingx/example/ios/CallingxExample/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packages/react-native-callingx/example/ios/CallingxExample/Images.xcassets/Contents.json create mode 100644 packages/react-native-callingx/example/ios/CallingxExample/Info.plist create mode 100644 packages/react-native-callingx/example/ios/CallingxExample/LaunchScreen.storyboard create mode 100644 packages/react-native-callingx/example/ios/CallingxExample/PrivacyInfo.xcprivacy create mode 100644 packages/react-native-callingx/example/ios/Podfile create mode 100644 packages/react-native-callingx/example/jest.config.js create mode 100644 packages/react-native-callingx/example/metro.config.js create mode 100644 packages/react-native-callingx/example/package-lock.json create mode 100644 packages/react-native-callingx/example/package.json create mode 100644 packages/react-native-callingx/example/react-native.config.js create mode 100644 packages/react-native-callingx/example/src/App.tsx create mode 100644 packages/react-native-callingx/ios/Callingx.h create mode 100644 packages/react-native-callingx/ios/Callingx.mm create mode 100644 packages/react-native-callingx/ios/CallingxPublic.h create mode 100644 packages/react-native-callingx/ios/UUIDStorage.h create mode 100644 packages/react-native-callingx/ios/UUIDStorage.mm create mode 100644 packages/react-native-callingx/lefthook.yml create mode 100644 packages/react-native-callingx/package.json create mode 100644 packages/react-native-callingx/react-native.config.js create mode 100644 packages/react-native-callingx/src/CallingxModule.ts create mode 100644 packages/react-native-callingx/src/EventManager.ts create mode 100644 packages/react-native-callingx/src/__tests__/index.test.tsx create mode 100644 packages/react-native-callingx/src/index.tsx create mode 100644 packages/react-native-callingx/src/spec/NativeCallingx.ts create mode 100644 packages/react-native-callingx/src/types.ts create mode 100644 packages/react-native-callingx/src/utils/constants.ts create mode 100644 packages/react-native-callingx/src/utils/headlessTask.ts create mode 100644 packages/react-native-callingx/src/utils/permissions.ts create mode 100644 packages/react-native-callingx/tsconfig.build.json create mode 100644 packages/react-native-callingx/tsconfig.json create mode 100644 packages/react-native-callingx/turbo.json create mode 100644 packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts delete mode 100644 packages/react-native-sdk/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts create mode 100644 packages/react-native-sdk/src/utils/push/libs/callingx.ts delete mode 100644 packages/react-native-sdk/src/utils/push/libs/callkeep.ts create mode 100644 packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts delete mode 100644 packages/react-native-sdk/src/utils/push/setupIosCallKeepEvents.ts diff --git a/package.json b/package.json index abfd03be8b..68f39506dd 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "start:react-native:ios:dogfood": "yarn workspace @stream-io/video-react-native-dogfood run ios", "start:react-native:android:dogfood": "yarn workspace @stream-io/video-react-native-dogfood run android", "build:styling": "yarn workspace @stream-io/video-styling run build", + "build:callingx": "yarn workspace react-native-callingx run build", "start:styling": "yarn workspace @stream-io/video-styling run start", "build:client": "yarn workspace @stream-io/video-client run build", "start:client": "yarn workspace @stream-io/video-client run start", diff --git a/packages/react-native-callingx/.editorconfig b/packages/react-native-callingx/.editorconfig new file mode 100644 index 0000000000..65365be68e --- /dev/null +++ b/packages/react-native-callingx/.editorconfig @@ -0,0 +1,15 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +indent_style = space +indent_size = 2 + +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/packages/react-native-callingx/.gitattributes b/packages/react-native-callingx/.gitattributes new file mode 100644 index 0000000000..e27f70fa49 --- /dev/null +++ b/packages/react-native-callingx/.gitattributes @@ -0,0 +1,3 @@ +*.pbxproj -text +# specific for windows script files +*.bat text eol=crlf diff --git a/packages/react-native-callingx/.nvmrc b/packages/react-native-callingx/.nvmrc new file mode 100644 index 0000000000..c004e356d6 --- /dev/null +++ b/packages/react-native-callingx/.nvmrc @@ -0,0 +1 @@ +v22.20.0 diff --git a/packages/react-native-callingx/.watchmanconfig b/packages/react-native-callingx/.watchmanconfig new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/packages/react-native-callingx/.watchmanconfig @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/packages/react-native-callingx/.yarnrc.yml b/packages/react-native-callingx/.yarnrc.yml new file mode 100644 index 0000000000..a95225543b --- /dev/null +++ b/packages/react-native-callingx/.yarnrc.yml @@ -0,0 +1,4 @@ +nodeLinker: node-modules +nmHoistingLimits: workspaces + +yarnPath: .yarn/releases/yarn-4.11.0.cjs diff --git a/packages/react-native-callingx/CODE_OF_CONDUCT.md b/packages/react-native-callingx/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..8b4fcfd341 --- /dev/null +++ b/packages/react-native-callingx/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/packages/react-native-callingx/CONTRIBUTING.md b/packages/react-native-callingx/CONTRIBUTING.md new file mode 100644 index 0000000000..fe8c655768 --- /dev/null +++ b/packages/react-native-callingx/CONTRIBUTING.md @@ -0,0 +1,129 @@ +# Contributing + +Contributions are always welcome, no matter how large or small! + +We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md). + +## Development workflow + +This project is a monorepo managed using [Yarn workspaces](https://yarnpkg.com/features/workspaces). It contains the following packages: + +- The library package in the root directory. +- An example app in the `example/` directory. + +To get started with the project, make sure you have the correct version of [Node.js](https://nodejs.org/) installed. See the [`.nvmrc`](./.nvmrc) file for the version used in this project. + +Run `yarn` in the root directory to install the required dependencies for each package: + +```sh +yarn +``` + +> Since the project relies on Yarn workspaces, you cannot use [`npm`](https://github.com/npm/cli) for development without manually migrating. + +The [example app](/example/) demonstrates usage of the library. You need to run it to test any changes you make. + +It is configured to use the local version of the library, so any changes you make to the library's source code will be reflected in the example app. Changes to the library's JavaScript code will be reflected in the example app without a rebuild, but native code changes will require a rebuild of the example app. + +If you want to use Android Studio or Xcode to edit the native code, you can open the `example/android` or `example/ios` directories respectively in those editors. To edit the Objective-C or Swift files, open `example/ios/CallingxExample.xcworkspace` in Xcode and find the source files at `Pods > Development Pods > react-native-callingx`. + +To edit the Java or Kotlin files, open `example/android` in Android studio and find the source files at `react-native-callingx` under `Android`. + +You can use various commands from the root directory to work with the project. + +To start the packager: + +```sh +yarn example start +``` + +To run the example app on Android: + +```sh +yarn example android +``` + +To run the example app on iOS: + +```sh +yarn example ios +``` + +To confirm that the app is running with the new architecture, you can check the Metro logs for a message like this: + +```sh +Running "CallingxExample" with {"fabric":true,"initialProps":{"concurrentRoot":true},"rootTag":1} +``` + +Note the `"fabric":true` and `"concurrentRoot":true` properties. + +Make sure your code passes TypeScript: + +```sh +yarn typecheck +``` + +To check for linting errors, run the following: + +```sh +yarn lint +``` + +To fix formatting errors, run the following: + +```sh +yarn lint --fix +``` + +Remember to add tests for your change if possible. Run the unit tests by: + +```sh +yarn test +``` + +### Commit message convention + +We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages: + +- `fix`: bug fixes, e.g. fix crash due to deprecated method. +- `feat`: new features, e.g. add new method to the module. +- `refactor`: code refactor, e.g. migrate from class components to hooks. +- `docs`: changes into documentation, e.g. add usage example for the module. +- `test`: adding or updating tests, e.g. add integration tests using detox. +- `chore`: tooling changes, e.g. change CI config. + +Our pre-commit hooks verify that your commit message matches this format when committing. + +### Publishing to npm + +We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc. + +To publish new versions, run the following: + +```sh +yarn release +``` + +### Scripts + +The `package.json` file contains various scripts for common tasks: + +- `yarn`: setup project by installing dependencies. +- `yarn typecheck`: type-check files with TypeScript. +- `yarn lint`: lint files with [ESLint](https://eslint.org/). +- `yarn test`: run unit tests with [Jest](https://jestjs.io/). +- `yarn example start`: start the Metro server for the example app. +- `yarn example android`: run the example app on Android. +- `yarn example ios`: run the example app on iOS. + +### Sending a pull request + +> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github). + +When you're sending a pull request: + +- Prefer small pull requests focused on one change. +- Verify that linters and tests are passing. +- Review the documentation to make sure it looks good. +- Follow the pull request template when opening a pull request. +- For pull requests that change the API or implementation, discuss with maintainers first by opening an issue. diff --git a/packages/react-native-callingx/Callingx.podspec b/packages/react-native-callingx/Callingx.podspec new file mode 100644 index 0000000000..de77381a5d --- /dev/null +++ b/packages/react-native-callingx/Callingx.podspec @@ -0,0 +1,21 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "Callingx" + s.version = package["version"] + s.summary = package["description"] + s.homepage = package["homepage"] + s.license = package["license"] + s.authors = package["author"] + + s.platforms = { :ios => min_ios_version_supported } + s.source = { :git => "https://github.com/greenfrvr/react-native-callingx.git", :tag => "#{s.version}" } + + s.source_files = "ios/**/*.{h,m,mm,swift,cpp}" + # s.private_header_files = "ios/**/*.h" + s.public_header_files = "ios/CallingxPublic.h" #this is needed to make static methods visible + + install_modules_dependencies(s) +end diff --git a/packages/react-native-callingx/LICENSE b/packages/react-native-callingx/LICENSE new file mode 100644 index 0000000000..e64a3f45af --- /dev/null +++ b/packages/react-native-callingx/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2025 Artsiom Grintsevich +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/react-native-callingx/README.md b/packages/react-native-callingx/README.md new file mode 100644 index 0000000000..d9bbff63e5 --- /dev/null +++ b/packages/react-native-callingx/README.md @@ -0,0 +1,412 @@ +# react-native-callingx + +A React Native Turbo Module for seamless native calling integration. This library provides a unified API to integrate with **CallKit** on iOS and the **Telecom/ConnectionService** API on Android, enabling your app to display system-level calling UI and interact with native call controls. + +## Features + +- 📞 **Incoming call UI** — Display native incoming call screens (even when the app is killed) +- 📲 **Outgoing call registration** — Register outgoing calls with the system +- 🎛️ **Call controls** — Mute, hold, end calls with native system integration +- 🔔 **Custom notifications** — Configurable Android notification channels +- ⚡ **Turbo Module** — Built with the New Architecture for optimal performance +- 📱 **Background support** — Handle calls when the app is backgrounded or killed + +## Requirements + +- React Native 0.73+ (New Architecture / Turbo Modules) +- iOS 13.0+ +- Android API 26+ (Android 8.0 Oreo) + +## Installation + +```sh +npm install react-native-callingx +# or +yarn add react-native-callingx +``` + +### iOS Setup + +1. Add the required background modes to your `Info.plist`: + +```xml +UIBackgroundModes + + voip + audio + +``` + +2. Run pod install: + +```sh +cd ios && pod install +``` + +3. For VoIP push notifications, configure your `AppDelegate` to report incoming calls: + +```objc +#import + +- (void)pushRegistry:(PKPushRegistry *)registry +didReceiveIncomingPushWithPayload:(PKPushPayload *)payload + forType:(PKPushType)type +withCompletionHandler:(void (^)(void))completion { + + // Extract call information from payload + NSString *callId = payload.dictionaryPayload[@"call_id"]; + NSString *callerName = payload.dictionaryPayload[@"caller_name"]; + NSString *handle = payload.dictionaryPayload[@"handle"]; + BOOL hasVideo = [payload.dictionaryPayload[@"has_video"] boolValue]; + + [Callingx reportNewIncomingCall:callId + handle:handle + handleType:@"generic" + hasVideo:hasVideo + localizedCallerName:callerName + supportsHolding:YES + supportsDTMF:NO + supportsGrouping:NO + supportsUngrouping:NO + fromPushKit:YES + payload:payload.dictionaryPayload + withCompletionHandler:completion]; +} +``` + +### Android Setup + +Handle incoming call intents in your `MainActivity`: + +```kotlin +import android.content.Intent +import android.os.Bundle +import com.callingx.CallingxModule + +class MainActivity : ReactActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + CallingxModule.handleCallingIntent(this, intent) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + CallingxModule.handleCallingIntent(this, intent) + } +} +``` + +## Usage + +### Setup + +Initialize the module with platform-specific configuration: + +```typescript +import { CallingxModule } from 'react-native-callingx'; + +// Setup must be called before any other method +CallingxModule.setup({ + ios: { + appName: 'My App', + supportsVideo: true, + maximumCallsPerCallGroup: 1, + maximumCallGroups: 1, + handleType: 'generic', // 'generic' | 'number' | 'phone' | 'email' + }, + android: { + incomingChannel: { + id: 'incoming_calls', + name: 'Incoming Calls', + sound: 'ringtone', // optional custom sound + vibration: true, + }, + outgoingChannel: { + id: 'ongoing_calls', + name: 'Ongoing Calls', + }, + // Optional: transform display text + titleTransformer: (name) => `Call from ${name}`, + subtitleTransformer: (phoneNumber) => phoneNumber, + }, +}); +``` + +### Request Permissions + +Before displaying calls, request the required permissions: + +```typescript +const permissions = await CallingxModule.requestPermissions(); +console.log('Audio permission:', permissions.recordAudio); +console.log('Notification permission:', permissions.postNotifications); +``` + +### Display Incoming Call + +Show the native incoming call UI: + +```typescript +await CallingxModule.displayIncomingCall( + 'unique-call-id', + '+1234567890', // phone number / handle + 'John Doe', // caller name + true // has video +); +``` + +### Start Outgoing Call + +Register an outgoing call with the system: + +```typescript +await CallingxModule.startCall( + 'unique-call-id', + '+1234567890', + 'John Doe', + false // audio only +); +``` + +### Answer Call + +Answer an incoming call programmatically: + +```typescript +await CallingxModule.answerIncomingCall('unique-call-id'); +``` + +### Activate Call + +Mark a call as active (connected): + +```typescript +await CallingxModule.setCurrentCallActive('unique-call-id'); +``` + +### End Call + +End a call with a specific reason: + +```typescript +import type { EndCallReason } from 'react-native-callingx'; + +// Available reasons: 'local' | 'remote' | 'rejected' | 'busy' | +// 'answeredElsewhere' | 'missed' | 'error' +await CallingxModule.endCallWithReason('unique-call-id', 'remote'); +``` + +### Mute/Unmute + +Toggle call mute state: + +```typescript +await CallingxModule.setMutedCall('unique-call-id', true); // mute +await CallingxModule.setMutedCall('unique-call-id', false); // unmute +``` + +### Hold/Unhold + +Toggle call hold state: + +```typescript +await CallingxModule.setOnHoldCall('unique-call-id', true); // hold +await CallingxModule.setOnHoldCall('unique-call-id', false); // unhold +``` + +### Update Display + +Update the caller information during a call: + +```typescript +await CallingxModule.updateDisplay( + 'unique-call-id', + '+1234567890', + 'Updated Name' +); +``` + +### Event Listeners + +Subscribe to call events: + +```typescript +import { CallingxModule } from 'react-native-callingx'; +import type { EventName } from 'react-native-callingx'; + +// Answer event - user answered from system UI +const answerSubscription = CallingxModule.addEventListener( + 'answerCall', + (params) => { + console.log('Call answered:', params.callId); + } +); + +// End event - call ended +const endSubscription = CallingxModule.addEventListener('endCall', (params) => { + console.log('Call ended:', params.callId, 'Cause:', params.cause); +}); + +// Hold toggle event +const holdSubscription = CallingxModule.addEventListener( + 'didToggleHoldCallAction', + (params) => { + console.log('Hold toggled:', params.callId, 'On hold:', params.hold); + } +); + +// Mute toggle event +const muteSubscription = CallingxModule.addEventListener( + 'didPerformSetMutedCallAction', + (params) => { + console.log('Mute toggled:', params.callId, 'Muted:', params.muted); + } +); + +// Start call action (outgoing call initiated from system) +const startSubscription = CallingxModule.addEventListener( + 'didReceiveStartCallAction', + (params) => { + console.log('Start call action:', params.callId); + } +); + +// Clean up when done +answerSubscription.remove(); +endSubscription.remove(); +// ... remove other subscriptions +``` + +### Handle Initial Events + +When the app is launched from a killed state by a call action, retrieve queued events: + +```typescript +// Get events that occurred before the module was initialized +const initialEvents = CallingxModule.getInitialEvents(); +initialEvents.forEach((event) => { + console.log('Initial event:', event.eventName, event.params); +}); + +// Clear initial events after processing +await CallingxModule.clearInitialEvents(); +``` + +### Background Tasks (Android) + +Run background tasks for call-related operations: + +```typescript +// Start a managed background task +await CallingxModule.startBackgroundTask(async (taskData, stopTask) => { + try { + // Perform background work (e.g., connect to call server) + await connectToCallServer(); + } finally { + stopTask(); // Always call when done + } +}); + +// Or stop manually +await CallingxModule.stopBackgroundTask(); +``` + +## API Reference + +### CallingxModule + +| Method | Description | +| ---------------------------------------------------------------- | ---------------------------------------------------- | +| `setup(options)` | Initialize the module with platform-specific options | +| `requestPermissions()` | Request required permissions (audio, notifications) | +| `checkPermissions()` | Check current permission status | +| `displayIncomingCall(callId, phoneNumber, callerName, hasVideo)` | Display incoming call UI | +| `answerIncomingCall(callId)` | Answer an incoming call | +| `startCall(callId, phoneNumber, callerName, hasVideo)` | Register an outgoing call | +| `setCurrentCallActive(callId)` | Mark call as active/connected | +| `updateDisplay(callId, phoneNumber, callerName)` | Update caller display info | +| `endCallWithReason(callId, reason)` | End call with specified reason | +| `setMutedCall(callId, isMuted)` | Toggle call mute state | +| `setOnHoldCall(callId, isOnHold)` | Toggle call hold state | +| `addEventListener(eventName, callback)` | Subscribe to call events | +| `getInitialEvents()` | Get queued events from app launch | +| `clearInitialEvents()` | Clear queued initial events | +| `startBackgroundTask(taskProvider)` | Start Android background task | +| `stopBackgroundTask()` | Stop Android background task | +| `log(message, level)` | Log message to native console | + +### Events + +| Event | Parameters | Description | +| ------------------------------ | ------------------- | --------------------------------- | +| `answerCall` | `{ callId }` | User answered call from system UI | +| `endCall` | `{ callId, cause }` | Call ended | +| `didToggleHoldCallAction` | `{ callId, hold }` | Hold state changed | +| `didPerformSetMutedCallAction` | `{ callId, muted }` | Mute state changed | +| `didReceiveStartCallAction` | `{ callId }` | Outgoing call action received | + +### Types + +```typescript +type EndCallReason = + | 'local' // Call ended by local user + | 'remote' // Call ended by remote party + | 'rejected' // Call was rejected + | 'busy' // Remote party is busy + | 'answeredElsewhere' // Answered on another device + | 'missed' // Call was missed + | 'error'; // Call failed due to error + +type CallingExpiOSOptions = { + appName: string; + supportsVideo?: boolean; + maximumCallsPerCallGroup?: number; + maximumCallGroups?: number; + handleType?: 'generic' | 'number' | 'phone' | 'email'; +}; + +type CallingExpAndroidOptions = { + incomingChannel?: { + id: string; + name: string; + sound?: string; + vibration?: boolean; + }; + outgoingChannel?: { + id: string; + name: string; + sound?: string; + vibration?: boolean; + }; +}; + +type PermissionsResult = { + recordAudio: boolean; + postNotifications: boolean; +}; +``` + +## Troubleshooting + +### iOS + +- **Incoming call not showing**: Ensure `voip` background mode is enabled and VoIP push certificate is configured +- **CallKit errors**: Check that `appName` is set in setup options +- **Audio issues**: The module automatically configures the audio session, but ensure no conflicts with other audio libraries + +### Android + +- **Notifications not showing**: Check POST_NOTIFICATIONS permission on Android 13+ +- **Call not answered on tap**: Ensure `handleCallingIntent` is called in both `onCreate` and `onNewIntent` in your MainActivity + +## Contributing + +See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. + +## License + +MIT + +--- + +Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) diff --git a/packages/react-native-callingx/android/build.gradle b/packages/react-native-callingx/android/build.gradle new file mode 100644 index 0000000000..dd344a9b40 --- /dev/null +++ b/packages/react-native-callingx/android/build.gradle @@ -0,0 +1,81 @@ +buildscript { + ext.getExtOrDefault = {name -> + return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['Callingx_' + name] + } + + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:8.7.2" + // noinspection DifferentKotlinGradleVersion + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}" + } +} + + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" +apply plugin: "kotlin-parcelize" + +apply plugin: "com.facebook.react" + +def getExtOrIntegerDefault(name) { + return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["Callingx_" + name]).toInteger() +} + +android { + namespace "com.callingx" + + compileSdkVersion getExtOrIntegerDefault("compileSdkVersion") + + defaultConfig { + minSdkVersion getExtOrIntegerDefault("minSdkVersion") + targetSdkVersion getExtOrIntegerDefault("targetSdkVersion") + } + + buildFeatures { + buildConfig true + } + + buildTypes { + release { + minifyEnabled false + } + } + + lintOptions { + disable "GradleCompatible" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + sourceSets { + main { + java.srcDirs += [ + "generated/java", + "generated/jni", + "build/generated/source/codegen/java", + "build/generated/source/codegen/jni" + ] + } + } +} + +repositories { + mavenCentral() + google() +} + +def kotlin_version = getExtOrDefault("kotlinVersion") + +dependencies { + implementation "com.facebook.react:react-android" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation "androidx.core:core-telecom:1.0.1" +} diff --git a/packages/react-native-callingx/android/gradle.properties b/packages/react-native-callingx/android/gradle.properties new file mode 100644 index 0000000000..6567d6ce3d --- /dev/null +++ b/packages/react-native-callingx/android/gradle.properties @@ -0,0 +1,5 @@ +Callingx_kotlinVersion=2.0.21 +Callingx_minSdkVersion=24 +Callingx_targetSdkVersion=34 +Callingx_compileSdkVersion=35 +Callingx_ndkVersion=27.1.12297006 diff --git a/packages/react-native-callingx/android/src/main/AndroidManifest.xml b/packages/react-native-callingx/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..1944cbb8c2 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/AndroidManifest.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/Call.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/Call.kt new file mode 100644 index 0000000000..9f21740df8 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/Call.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.callingx + +import android.os.ParcelUuid +import android.os.Bundle +import android.telecom.DisconnectCause +import androidx.core.telecom.CallAttributesCompat +import androidx.core.telecom.CallEndpointCompat +import kotlinx.coroutines.channels.Channel + +/** + * Custom representation of a call state. + */ +sealed class Call { + + /** + * There is no current or past calls in the stack + */ + object None : Call() + + /** + * Represents a registered call with the telecom stack with the values provided by the + * Telecom SDK + */ + data class Registered( + val id: String, + val callAttributes: CallAttributesCompat, + val displayOptions: Bundle?, + val isActive: Boolean, + val isOnHold: Boolean, + val isMuted: Boolean, + val errorCode: Int?, + val currentCallEndpoint: CallEndpointCompat?, + val availableCallEndpoints: List, + internal val actionSource: Channel, + ) : Call() { + + /** + * @return true if it's an incoming registered call, false otherwise + */ + fun isIncoming() = callAttributes.direction == CallAttributesCompat.DIRECTION_INCOMING + + /** + * Sends an action to the call session. It will be processed if it's still registered. + * + * @return true if the action was sent, false otherwise + */ + fun processAction(action: CallAction) = actionSource.trySend(action).isSuccess + } + + /** + * Represent a previously registered call that was disconnected + */ + data class Unregistered( + val id: String, + val callAttributes: CallAttributesCompat, + val disconnectCause: DisconnectCause, + ) : Call() +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallAction.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallAction.kt new file mode 100644 index 0000000000..8360cb2dd7 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallAction.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.callingx + +import android.os.ParcelUuid +import android.os.Parcelable +import android.telecom.DisconnectCause +import kotlinx.parcelize.Parcelize + +/** + * Simple interface to represent related call actions to communicate with the registered call scope + * in the [TelecomCallRepository.registerCall] + * + * Note: we are using [Parcelize] to make the actions parcelable so they can be directly used in the + * call notification. + */ +sealed interface CallAction : Parcelable { + @Parcelize + data class Answer(val isAudioCall: Boolean) : CallAction + + @Parcelize + data class Disconnect(val cause: DisconnectCause) : CallAction + + @Parcelize + object Hold : CallAction + + @Parcelize + object Activate : CallAction + + @Parcelize + data class ToggleMute(val isMute: Boolean) : CallAction + + @Parcelize + data class SwitchAudioEndpoint(val endpointId: ParcelUuid) : CallAction + + @Parcelize + data class TransferCall(val endpointId: ParcelUuid) : CallAction +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallNotificationManager.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallNotificationManager.kt new file mode 100644 index 0000000000..ec346b954a --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallNotificationManager.kt @@ -0,0 +1,303 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.callingx + +import android.Manifest +import android.app.Notification +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.telecom.DisconnectCause +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.Person +import androidx.core.content.PermissionChecker +import androidx.core.graphics.drawable.IconCompat + +/** + * Handles call status changes and updates the notification accordingly. For more guidance around + * notifications check https://developer.android.com/develop/ui/views/notifications + * + * @see updateCallNotification + */ +@RequiresApi(Build.VERSION_CODES.O) +class CallNotificationManager( + private val context: Context, + private val notificationManager: NotificationManagerCompat = + NotificationManagerCompat.from(context) +) { + + internal companion object { + private const val TAG = "TelecomCallNotificationManager" + + const val NOTIFICATION_ID = 200 + const val NOTIFICATION_ACTION = "notification_action" + const val DEFAULT_INCOMING_CHANNEL_ID = "incoming_channel" + const val DEFAULT_INCOMING_CHANNEL_NAME = "Incoming calls" + const val DEFAULT_ONGOING_CHANNEL_ID = "ongoing_channel" + const val DEFAULT_ONGOING_CHANNEL_NAME = "Ongoing calls" + + private const val PREFS_NAME = "CallingxPrefs" + private const val PREFIX_IN = "incoming_" + private const val PREFIX_OUT = "outgoing_" + private const val KEY_ID = "id" + private const val KEY_NAME = "name" + private const val KEY_SOUND = "sound" + private const val KEY_VIBRATION = "vibration" + + fun saveNotificationsConfig(context: Context, config: NotificationsConfig) { + Log.d(TAG, "saveNotificationsConfig: Saving notifications config: $config") + + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + prefs.edit() + .apply { + // Incoming channel + putString(PREFIX_IN + KEY_ID, config.incomingChannel.id) + putString(PREFIX_IN + KEY_NAME, config.incomingChannel.name) + putString(PREFIX_IN + KEY_SOUND, config.incomingChannel.sound) + putBoolean(PREFIX_IN + KEY_VIBRATION, config.incomingChannel.vibration) + + // Outgoing channel + putString(PREFIX_OUT + KEY_ID, config.outgoingChannel.id) + putString(PREFIX_OUT + KEY_NAME, config.outgoingChannel.name) + putString(PREFIX_OUT + KEY_SOUND, config.outgoingChannel.sound) + putBoolean(PREFIX_OUT + KEY_VIBRATION, config.outgoingChannel.vibration) + } + .apply() + } + + fun loadNotificationsConfig(context: Context): NotificationsConfig { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + Log.d( + TAG, + "loadNotificationsConfig: Loading notifications config ${prefs.getString(PREFIX_IN + KEY_ID, "")}" + ) + return NotificationsConfig( + incomingChannel = + ChannelConfig( + id = prefs.getString(PREFIX_IN + KEY_ID, "") + ?: DEFAULT_INCOMING_CHANNEL_ID, + name = prefs.getString(PREFIX_IN + KEY_NAME, "") + ?: DEFAULT_INCOMING_CHANNEL_NAME, + sound = prefs.getString(PREFIX_IN + KEY_SOUND, "") ?: "", + vibration = prefs.getBoolean(PREFIX_IN + KEY_VIBRATION, false), + importance = NotificationManagerCompat.IMPORTANCE_MAX + ), + outgoingChannel = + ChannelConfig( + id = prefs.getString(PREFIX_OUT + KEY_ID, "") + ?: DEFAULT_ONGOING_CHANNEL_ID, + name = prefs.getString(PREFIX_OUT + KEY_NAME, "") + ?: DEFAULT_ONGOING_CHANNEL_NAME, + sound = prefs.getString(PREFIX_OUT + KEY_SOUND, "") ?: "", + vibration = prefs.getBoolean(PREFIX_OUT + KEY_VIBRATION, false), + importance = NotificationManagerCompat.IMPORTANCE_DEFAULT + ) + ) + } + } + + private var notificationsConfig: NotificationsConfig + + init { + notificationsConfig = CallNotificationManager.loadNotificationsConfig(context) + createNotificationChannels(notificationsConfig) + Log.d(TAG, "CallNotificationManager: Notifications config: $notificationsConfig") + } + + fun createNotification(call: Call.Registered): Notification { + Log.d(TAG, "createNotification: Creating notification for call ID: ${call.id}") + + val contentIntent = getActivityIntent(call.id) + val callStyle = createCallStyle(call) + val channelId = + if (call.isIncoming() && !call.isActive) { + notificationsConfig.incomingChannel.id + } else { + notificationsConfig.outgoingChannel.id + } + + val builder = + NotificationCompat.Builder(context, channelId) + .setContentIntent(contentIntent) + .setFullScreenIntent(contentIntent, true) + .setStyle(callStyle) + .setSmallIcon(R.drawable.ic_round_call_24) + .setCategory(NotificationCompat.CATEGORY_CALL) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setOngoing(true) + + call.displayOptions?.let { + if (it.containsKey(CallService.EXTRA_DISPLAY_SUBTITLE)) { + builder.setContentText(it.getString(CallService.EXTRA_DISPLAY_SUBTITLE)) + } + } + + // if (call.isOnHold) { + // val activateAction = TelecomCallAction.Activate + // builder.addAction( + // R.drawable.ic_phone_paused_24, + // "Resume", + // getPendingIntent(activateAction) + // ) + // } + + return builder.build() + } + + /** Updates, creates or dismisses a CallStyle notification based on the given [TelecomCall] */ + fun updateCallNotification(call: Call) { + if (!canPostNotifications()) { + Log.w(TAG, "updateCallNotification: Notifications are not granted, skipping update") + return + } + + when (call) { + Call.None, is Call.Unregistered -> { + Log.d(TAG, "Dismissing notification (call is None or Unregistered)") + notificationManager.cancel(NOTIFICATION_ID) + } + is Call.Registered -> { + val notification = createNotification(call) + notificationManager.notify(NOTIFICATION_ID, notification) + Log.d(TAG, "updateCallNotification: Notification posted successfully") + } + } + } + + fun cancelNotifications() { + notificationManager.cancel(NOTIFICATION_ID) + } + + fun createNotificationChannels(notificationsConfig: NotificationsConfig) { + val incomingChannel = createNotificationChannel(notificationsConfig.incomingChannel) + val ongoingChannel = createNotificationChannel(notificationsConfig.outgoingChannel) + + notificationManager.createNotificationChannelsCompat( + listOf( + incomingChannel, + ongoingChannel, + ), + ) + Log.d(TAG, "createNotificationChannels: Notification channels registered") + } + + private fun createNotificationChannel(config: ChannelConfig): NotificationChannelCompat { + return NotificationChannelCompat.Builder(config.id, config.importance) + .apply { + setName(config.name) + setVibrationEnabled(config.vibration) + ResourceUtils.getSoundUri(context, config.sound)?.let { setSound(it, null) } + } + .build() + } + + private fun createCallStyle(call: Call.Registered): NotificationCompat.CallStyle { + val caller = createPerson(call) + + if (call.isIncoming() && !call.isActive) { + return NotificationCompat.CallStyle.forIncomingCall( + caller, + getBroadcastIntent(call.id, CallingxModule.CALL_END_ACTION) { + putExtra( + CallingxModule.EXTRA_DISCONNECT_CAUSE, + getDisconnectCauseString(DisconnectCause(DisconnectCause.REJECTED)) + ) + }, + getActivityIntent(call.id, CallingxModule.CALL_ANSWERED_ACTION) + ) + } + + return NotificationCompat.CallStyle.forOngoingCall( + caller, + getBroadcastIntent(call.id, CallingxModule.CALL_END_ACTION) { + putExtra( + CallingxModule.EXTRA_DISCONNECT_CAUSE, + getDisconnectCauseString(DisconnectCause(DisconnectCause.LOCAL)) + ) + } + ) + } + + // this intent will send action directly to the module + private fun getBroadcastIntent( + callId: String, + action: String, + addExtras: Intent.() -> Unit = {} + ): PendingIntent { + val callIntent = + Intent(action).apply { + setPackage(context.packageName) + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + addExtras(this) + } + + return PendingIntent.getBroadcast( + context, + callIntent.hashCode(), + callIntent, + PendingIntent.FLAG_IMMUTABLE, + ) + } + + // this intent will send action to the launch activity, as for asnwering call case we need to + // bring the app to foreground + private fun getActivityIntent( + callId: String, + action: String? = null, + ): PendingIntent { + val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + val callIntent = + Intent(launchIntent).apply { + action?.let { this.action = it } + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + + return PendingIntent.getActivity( + context, + callIntent.hashCode(), + callIntent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + } + + private fun createPerson(call: Call.Registered): Person { + val displayCallerName = call.displayOptions?.getString(CallService.EXTRA_DISPLAY_TITLE) + val address = call.callAttributes.address.toString() + + return Person.Builder() + .setName(displayCallerName ?: call.callAttributes.displayName) + .setUri(address) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_user)) + .setImportant(true) + .build() + } + + private fun canPostNotifications(): Boolean { + val permission = + PermissionChecker.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + permission == PermissionChecker.PERMISSION_GRANTED + } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallRepository.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallRepository.kt new file mode 100644 index 0000000000..7cc325f72c --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallRepository.kt @@ -0,0 +1,482 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.callingx + +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.telecom.DisconnectCause +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.telecom.CallAttributesCompat +import androidx.core.telecom.CallControlResult +import androidx.core.telecom.CallControlScope +import androidx.core.telecom.CallsManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.scan +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +/** + * The central repository that keeps track of the current call and allows to register new calls. + * + * This class contains the main logic to integrate with Telecom SDK. + * + * @see registerCall + */ +@RequiresApi(Build.VERSION_CODES.O) +class CallRepository(context: Context) { + + interface Listener { + fun onCallStateChanged(call: Call) + fun onIsCallAnswered(callId: String) + fun onIsCallDisconnected(cause: DisconnectCause) + fun onIsCallInactive(callId: String) + fun onIsCallActive(callId: String) + fun onCallRegistered(callId: String) + fun onMuteCallChanged(callId: String, isMuted: Boolean) + fun onCallEndpointChanged(callId: String, endpoint: String) + } + + companion object { + private const val TAG = "TelecomCallRepository" + } + + private var listener: Listener? = null + private var observeCallStateJob: Job? = null + + private val callsManager: CallsManager + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + // Keeps track of the current TelecomCall state + private val _currentCall: MutableStateFlow = MutableStateFlow(Call.None) + val currentCall = _currentCall.asStateFlow() + + init { + val capabilities = + CallsManager.CAPABILITY_SUPPORTS_CALL_STREAMING and + CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING + callsManager = + CallsManager(context.applicationContext).apply { + registerAppWithTelecom(capabilities) + } + Log.d(TAG, "[repository] init: CallsManager created and registered") + } + + fun setListener(listener: Listener) { + this.listener = listener + + observeCallStateJob?.cancel() + observeCallStateJob = observeCallState() + } + + fun release() { + val currentCall = currentCall.value + if (currentCall is Call.Registered) { + currentCall.processAction( + CallAction.Disconnect(DisconnectCause(DisconnectCause.LOCAL)) + ) + } + _currentCall.value = Call.None + + observeCallStateJob?.cancel() + observeCallStateJob = null + listener = null + + scope.cancel() + } + + /** + * Register a new call with the provided attributes. Use the [currentCall] StateFlow to receive + * status updates and process call related actions. + */ + suspend fun registerCall( + callId: String, + displayName: String, + address: Uri, + isIncoming: Boolean, + isVideo: Boolean, + displayOptions: Bundle?, + ) { + Log.d( + TAG, + "[repository] registerCall: Starting registration - Name: $displayName, Address: $address, Incoming: $isIncoming" + ) + + // For simplicity we don't support multiple calls + check(_currentCall.value !is Call.Registered) { + "There cannot be more than one call at the same time." + } + Log.d( + TAG, + "[repository] registerCall: No existing call found, proceeding with registration" + ) + + // Create the call attributes + val attributes = createCallAttributes(displayName, address, isIncoming, isVideo) + // Creates a channel to send actions to the call scope. + val actionSource = Channel() + + // Register the call and handle actions in the scope + try { + callsManager.addCall( + attributes, + onIsCallAnswered, // Watch needs to know if it can answer the call + onIsCallDisconnected, + onIsCallActive, + onIsCallInactive + ) { + Log.d(TAG, "[repository] registerCall: Inside call scope, setting up call handlers") + + // Consume the actions to interact with the call inside the scope + launch { processCallActions(actionSource.consumeAsFlow()) } + + // Update the state to registered with default values while waiting for Telecom + // updates + Log.d( + TAG, + "[repository] registerCall: Creating Registered call state with ID: $callId" + ) + _currentCall.value = + Call.Registered( + id = callId, + isActive = false, // can we just register the call as active? + isOnHold = false, + callAttributes = attributes, + displayOptions = displayOptions, + isMuted = false, + errorCode = null, + currentCallEndpoint = null, + availableCallEndpoints = emptyList(), + actionSource = actionSource, + ) + Log.d(TAG, "[repository] registerCall: Call state updated to Registered") + + launch { + currentCallEndpoint.collect { + updateCurrentCall { copy(currentCallEndpoint = it) } + } + } + launch { + availableEndpoints.collect { + updateCurrentCall { copy(availableCallEndpoints = it) } + } + } + launch { isMuted.collect { updateCurrentCall { copy(isMuted = it) } } } + } + Log.d(TAG, "[repository] registerCall: Call successfully registered with Telecom SDK") + } catch (e: Exception) { + Log.e(TAG, "[repository] registerCall: Error registering call", e) + throw e + } finally { + Log.d(TAG, "[repository] registerCall: Call scope ended, setting state to None") + _currentCall.value = Call.None + } + } + + private fun observeCallState(): Job { + return currentCall + .scan(Pair(null, currentCall.value)) { (_, prev), next -> + Pair(prev, next) + } + .onEach { (previous, current) -> + when { + previous is Call.None && current is Call.Registered -> { + listener?.onCallRegistered(current.id) + } + previous is Call.Registered && current is Call.Registered -> { + if (previous.isMuted != current.isMuted) { + Log.d(TAG, "[repository] observeCallState: Mute changed: ${current.isMuted}") + listener?.onMuteCallChanged(current.id, current.isMuted) + } + if (previous.currentCallEndpoint != current.currentCallEndpoint) { + current.currentCallEndpoint?.let { + listener?.onCallEndpointChanged(current.id, it.name.toString()) + } + } + } + } + listener?.onCallStateChanged(current) + } + .launchIn(scope) + } + + /** Collect the action source to handle client actions inside the call scope */ + private suspend fun CallControlScope.processCallActions(actionSource: Flow) { + actionSource.collect { action -> + Log.d(TAG, "[repository] processCallActions: action: ${action::class.simpleName}") + when (action) { + is CallAction.Answer -> { + doAnswer(action.isAudioCall) + } + is CallAction.Disconnect -> { + doDisconnect(action) + } + is CallAction.SwitchAudioEndpoint -> { + doSwitchEndpoint(action) + } + is CallAction.TransferCall -> { + Log.d( + TAG, + "[repository] processCallActions: Transfer to endpoint: ${action.endpointId}" + ) + val call = _currentCall.value as? Call.Registered + val endpoints = + call?.availableCallEndpoints?.firstOrNull { + it.identifier == action.endpointId + } + if (endpoints != null) { + requestEndpointChange( + endpoint = endpoints, + ) + } else { + Log.w( + TAG, + "[repository] processCallActions: Endpoint not found for transfer, ignoring" + ) + } + } + CallAction.Hold -> { + when (val result = setInactive()) { + is CallControlResult.Success -> { + onIsCallInactive() + } + is CallControlResult.Error -> { + Log.e( + TAG, + "[repository] processCallActions: Hold action failed with error code: ${result.errorCode}" + ) + updateCurrentCall { copy(errorCode = result.errorCode) } + } + } + } + CallAction.Activate -> { + when (val result = setActive()) { + is CallControlResult.Success -> { + onIsCallActive() + } + is CallControlResult.Error -> { + Log.e( + TAG, + "[repository] processCallActions: Activate action failed with error code: ${result.errorCode}" + ) + updateCurrentCall { copy(errorCode = result.errorCode) } + } + } + } + is CallAction.ToggleMute -> { + // We cannot programmatically mute the telecom stack. Instead we just update + // the state of the call and this will start/stop audio capturing. + Log.d(TAG, "[repository] processCallActions: Toggling mute: ${action.isMute}") + updateCurrentCall { + val newMutedState = action.isMute + copy(isMuted = newMutedState) + } + } + } + } + Log.d(TAG, "[repository] processCallActions: Action collection ended") + } + + /** + * Update the current state of our call applying the transform lambda only if the call is + * registered. Otherwise keep the current state + */ + private fun updateCurrentCall(transform: Call.Registered.() -> Call) { + val currentState = _currentCall.value + Log.d( + TAG, + "[repository] updateCurrentCall: Current call state: ${currentState::class.simpleName}" + ) + _currentCall.update { call -> + if (call is Call.Registered) { + val updated = call.transform() + Log.d( + TAG, + "[repository] updateCurrentCall: Call state updated to: ${updated::class.simpleName}" + ) + updated + } else { + Log.w( + TAG, + "[repository] updateCurrentCall: Call is not Registered, skipping update" + ) + call + } + } + } + + private suspend fun CallControlScope.doSwitchEndpoint( + action: CallAction.SwitchAudioEndpoint + ) { + Log.d(TAG, "[repository] doSwitchEndpoint: Switching to endpoint: ${action.endpointId}") + // TODO once availableCallEndpoints is a state flow we can just get the value + val endpoints = (_currentCall.value as Call.Registered).availableCallEndpoints + + // Switch to the given endpoint or fallback to the best possible one. + val newEndpoint = endpoints.firstOrNull { it.identifier == action.endpointId } + + if (newEndpoint != null) { + Log.d( + TAG, + "[repository] doSwitchEndpoint: Found endpoint: ${newEndpoint.name}, requesting change" + ) + requestEndpointChange(newEndpoint).also { + Log.d(TAG, "[repository] doSwitchEndpoint: Endpoint change result: $it") + } + } else { + Log.w(TAG, "[repository] doSwitchEndpoint: Endpoint not found in available endpoints") + } + } + + private suspend fun CallControlScope.doDisconnect(action: CallAction.Disconnect) { + Log.d(TAG, "[repository] doDisconnect: Disconnecting call with cause: ${action.cause}") + disconnect(action.cause) + Log.d(TAG, "[repository] doDisconnect: Disconnect called, triggering onIsCallDisconnected") + onIsCallDisconnected(action.cause) + } + + private suspend fun CallControlScope.doAnswer(isAudioCall: Boolean) { + val callType = + if (isAudioCall) CallAttributesCompat.CALL_TYPE_AUDIO_CALL + else CallAttributesCompat.CALL_TYPE_VIDEO_CALL + + when (val result = answer(callType)) { + is CallControlResult.Success -> { + onIsCallAnswered(callType) + } + is CallControlResult.Error -> { + Log.e( + TAG, + "[repository] doAnswer: Answer failed with error code: ${result.errorCode}" + ) + updateCurrentCall { + Call.Unregistered( + id = id, + callAttributes = callAttributes, + disconnectCause = DisconnectCause(DisconnectCause.BUSY), + ) + } + } + } + } + + /** + * Can the call be successfully answered?? TIP: We would check the connection/call state to see + * if we can answer a call Example you may need to wait for another call to hold. + */ + val onIsCallAnswered: suspend (type: Int) -> Unit = { + Log.d(TAG, "[repository] onIsCallAnswered: Call answered, type: $it") + updateCurrentCall { copy(isActive = true, isOnHold = false) } + + val call = _currentCall.value + if (call is Call.Registered) { + listener?.onIsCallAnswered(call.id) + } + Log.d(TAG, "[repository] onIsCallAnswered: Call state updated to active") + } + + /** Can the call perform a disconnect */ + val onIsCallDisconnected: suspend (cause: DisconnectCause) -> Unit = { + Log.d( + TAG, + "[repository] onIsCallDisconnected: Call disconnected, cause: ${it.reason}, description: ${it.description}" + ) + updateCurrentCall { Call.Unregistered(id, callAttributes, it) } + + listener?.onIsCallDisconnected(it) + Log.d(TAG, "[repository] onIsCallDisconnected: Call state updated to Unregistered") + } + + /** + * Check is see if we can make the call active. Other calls and state might stop us from + * activating the call + */ + val onIsCallActive: suspend () -> Unit = { + Log.d(TAG, "[repository] onIsCallActive: Call became active") + updateCurrentCall { + copy( + errorCode = null, + isActive = true, + isOnHold = false, + ) + } + + val call = _currentCall.value + if (call is Call.Registered) { + listener?.onIsCallActive(call.id) + } + Log.d(TAG, "[repository] onIsCallActive: Call state updated") + } + + /** Check to see if we can make the call inactivate */ + val onIsCallInactive: suspend () -> Unit = { + Log.d(TAG, "[repository] onIsCallInactive: Call became inactive (on hold)") + updateCurrentCall { copy(errorCode = null, isOnHold = true) } + + val call = _currentCall.value + if (call is Call.Registered) { + listener?.onIsCallInactive(call.id) + } + Log.d(TAG, "[repository] onIsCallInactive: Call state updated to on hold") + } + + private fun createCallAttributes( + displayName: String, + address: Uri, + isIncoming: Boolean, + isVideo: Boolean + ): CallAttributesCompat { + Log.d( + TAG, + "createCallAttributes: Creating CallAttributes - Direction: ${if (isIncoming) "Incoming" else "Outgoing"}, Type: ${if (isVideo) "Video" else "Audio"}" + ) + return CallAttributesCompat( + displayName = displayName, + address = address, + direction = + if (isIncoming) { + CallAttributesCompat.DIRECTION_INCOMING + } else { + CallAttributesCompat.DIRECTION_OUTGOING + }, + callType = + if (isVideo) { + CallAttributesCompat.CALL_TYPE_VIDEO_CALL + } else { + CallAttributesCompat.CALL_TYPE_AUDIO_CALL + }, + callCapabilities = + CallAttributesCompat.SUPPORTS_SET_INACTIVE or + CallAttributesCompat.SUPPORTS_STREAM or + CallAttributesCompat.SUPPORTS_TRANSFER, + ) + } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt new file mode 100644 index 0000000000..46f851c4d2 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt @@ -0,0 +1,338 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.callingx + +import android.app.Service +import android.content.Intent +import android.net.Uri +import android.os.Binder +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import android.telecom.DisconnectCause +import android.util.Log +import androidx.annotation.RequiresApi +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch + +/** + * This service handles the app call logic (show notification, record mic, display audio, etc..). It + * can get started by the user or by an upcoming push notification to start a call. + * + * It holds the call scope used to register a call with the Telecom SDK in our + * TelecomCallRepository. + * + * When registering a call with the Telecom SDK and displaying a CallStyle notification, the SDK + * will grant you foreground service delegation so there is no need to make this a FGS. + * + * Note: you could potentially make this service run in a different process since audio or video + * calls can consume significant memory, although that would require more complex setup to make it + * work across multiple process. + */ +@RequiresApi(Build.VERSION_CODES.O) +class CallService : Service(), CallRepository.Listener { + + companion object { + private const val TAG = "TelecomCallService" + + internal const val EXTRA_CALL_ID = "extra_call_id" + internal const val EXTRA_NAME = "extra_name" + internal const val EXTRA_URI = "extra_uri" + internal const val EXTRA_IS_VIDEO = "extra_is_video" + internal const val EXTRA_DISPLAY_TITLE = "displayTitle" + internal const val EXTRA_DISPLAY_SUBTITLE = "displaySubtitle" + internal const val EXTRA_DISPLAY_OPTIONS = "display_options" + // Background task extras + internal const val EXTRA_TASK_NAME = "task_name" + internal const val EXTRA_TASK_DATA = "task_data" + internal const val EXTRA_TASK_TIMEOUT = "task_timeout" + + internal const val ACTION_INCOMING_CALL = "incoming_call" + internal const val ACTION_OUTGOING_CALL = "outgoing_call" + internal const val ACTION_UPDATE_CALL = "update_call" + internal const val ACTION_START_BACKGROUND_TASK = "start_background_task" + internal const val ACTION_STOP_BACKGROUND_TASK = "stop_background_task" + internal const val ACTION_STOP_SERVICE = "stop_service" + } + + inner class CallServiceBinder : Binder() { + fun getService(): CallService = this@CallService + } + + private lateinit var headlessJSManager: HeadlessTaskManager + private lateinit var notificationManager: CallNotificationManager + private lateinit var telecomRepository: CallRepository + + private val binder = CallServiceBinder() + private val scope: CoroutineScope = CoroutineScope(SupervisorJob()) + + private var isInForeground = false + + override fun onCreate() { + super.onCreate() + Log.d(TAG, "[service] onCreate: TelecomCallService created") + + notificationManager = CallNotificationManager(applicationContext) + headlessJSManager = HeadlessTaskManager(applicationContext) + telecomRepository = CallRepository(applicationContext) + telecomRepository.setListener(this) + + sendBroadcastEvent(CallingxModule.SERVICE_READY_ACTION) + } + + override fun onDestroy() { + super.onDestroy() + Log.d(TAG, "[service] onDestroy: TelecomCallService destroyed") + + notificationManager.cancelNotifications() + telecomRepository.release() + headlessJSManager.release() + + if (isInForeground) { + stopForeground(STOP_FOREGROUND_REMOVE) + isInForeground = false + } + + scope.cancel() + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + Log.d(TAG, "[service] onTaskRemoved: Task removed") + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Log.d(TAG, "[service] onStartCommand: Received intent with action: ${intent?.action}") + + if (intent == null || intent.action == null) { + Log.w(TAG, "[service] onStartCommand: Intent is null, returning START_NOT_STICKY") + return START_NOT_STICKY + } + + when (intent.action) { + ACTION_INCOMING_CALL -> { + registerCall(intent, true) + } + ACTION_OUTGOING_CALL -> { + registerCall(intent, false) + } + ACTION_START_BACKGROUND_TASK -> { + val taskName = intent.getStringExtra(EXTRA_TASK_NAME)!! + val taskData = intent.getBundleExtra(EXTRA_TASK_DATA)!! + val taskTimeout = intent.getLongExtra(EXTRA_TASK_TIMEOUT, 0) + startBackgroundTask(taskName, taskData, taskTimeout) + } + ACTION_STOP_BACKGROUND_TASK -> { + stopBackgroundTask() + } + ACTION_UPDATE_CALL -> { + // TODO: update the call details + // updateServiceState(telecomRepository.currentCall.value) // not used for now + } + else -> { + Log.e(TAG, "[service] onStartCommand: Unknown action: ${intent.action}") + throw IllegalArgumentException("Unknown action") + } + } + + return START_STICKY + } + + override fun onBind(intent: Intent): IBinder? = binder + + override fun onUnbind(intent: Intent): Boolean { + Log.d(TAG, "[service] onUnbind: Service unbound") + return super.onUnbind(intent) + } + + override fun onCallStateChanged(call: Call) { + Log.d(TAG, "[service] onCallStateChanged: Call state changed: ${call::class.simpleName}") + Log.d(TAG, "[service] updateServiceState: Updating service state for call type: ${call}") + when (call) { + is Call.Registered -> { + Log.d( + TAG, + "[service] updateServiceState: Call registered - Active: ${call.isActive}, OnHold: ${call.isOnHold}, Muted: ${call.isMuted}" + ) + // Update the call state. + if (isInForeground) { + Log.d( + TAG, + "[service] updateServiceState: Updating notification for call: ${call.id}" + ) + notificationManager.updateCallNotification(call) + } else { + Log.d( + TAG, + "[service] updateServiceState: Starting foreground for call: ${call.id}" + ) + val notification = notificationManager.createNotification(call) + startForeground(CallNotificationManager.NOTIFICATION_ID, notification) + isInForeground = true + } + } + is Call.Unregistered -> { + Log.d(TAG, "[service] updateServiceState: Call unregistered, stopping service") + notificationManager.updateCallNotification(call) + + if (isInForeground) { + stopForeground(STOP_FOREGROUND_REMOVE) + isInForeground = false + } + + stopSelf() + } + is Call.None -> { + Log.d(TAG, "[service] updateServiceState: No active call, stopping audio loop") + notificationManager.updateCallNotification(call) + + if (isInForeground) { + stopForeground(STOP_FOREGROUND_REMOVE) + isInForeground = false + } + } + } + } + + override fun onIsCallAnswered(callId: String) { + sendBroadcastEvent(CallingxModule.CALL_ANSWERED_ACTION) { + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + } + } + + override fun onIsCallDisconnected(cause: DisconnectCause) { + // we're not passing the callId here to prevent infinite loops + // callEnd event with callId will sent only when after interaction with notification buttons + sendBroadcastEvent(CallingxModule.CALL_END_ACTION) { + putExtra(CallingxModule.EXTRA_DISCONNECT_CAUSE, getDisconnectCauseString(cause)) + } + } + + override fun onIsCallInactive(callId: String) { + sendBroadcastEvent(CallingxModule.CALL_INACTIVE_ACTION) { + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + } + } + + override fun onIsCallActive(callId: String) { + sendBroadcastEvent(CallingxModule.CALL_ACTIVE_ACTION) { + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + } + } + + override fun onCallRegistered(callId: String) { + Log.d(TAG, "[service] onCallRegistered: Call registered: $callId") + sendBroadcastEvent(CallingxModule.CALL_REGISTERED_ACTION) { + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + } + } + + override fun onMuteCallChanged(callId: String, isMuted: Boolean) { + Log.d(TAG, "[service] onMuteCallChanged: Call muted: $callId, $isMuted") + sendBroadcastEvent(CallingxModule.CALL_MUTED_ACTION) { + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + putExtra(CallingxModule.EXTRA_MUTED, isMuted) + } + } + + override fun onCallEndpointChanged(callId: String, endpoint: String) { + Log.d(TAG, "[service] onCallEndpointChanged: Call endpoint changed: $callId, $endpoint") + sendBroadcastEvent(CallingxModule.CALL_ENDPOINT_CHANGED_ACTION) { + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + putExtra(CallingxModule.EXTRA_AUDIO_ENDPOINT, endpoint) + } + } + + public fun processAction(action: CallAction) { + Log.d(TAG, "[service] processAction: Processing action: ${action::class.simpleName}") + val currentCall = telecomRepository.currentCall.value + if (currentCall is Call.Registered) { + currentCall.processAction(action) + } else { + Log.e(TAG, "[service] processAction: Call not registered, ignoring action") + } + } + + public fun startBackgroundTask(taskName: String, data: Bundle, timeout: Long) { + Log.d( + TAG, + "[service] startBackgroundTask: Starting background task: $taskName, $data, $timeout" + ) + headlessJSManager.startHeadlessTask(taskName, data, timeout) + } + + public fun stopBackgroundTask() { + Log.d(TAG, "[service] stopBackgroundTask: Stopping background task") + headlessJSManager.stopHeadlessTask() + } + + private fun registerCall(intent: Intent, incoming: Boolean) { + Log.d(TAG, "[service] registerCall: ${if (incoming) "in" else "out"} call") + + // If we have an ongoing call ignore command + if (telecomRepository.currentCall.value is Call.Registered) { + Log.w(TAG, "[service] registerCall: Call already registered, ignoring new call request") + return + } + + val callId = intent.getStringExtra(EXTRA_CALL_ID)!! + val name = intent.getStringExtra(EXTRA_NAME)!! + val isVideo = intent.getBooleanExtra(EXTRA_IS_VIDEO, false) + val uri = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(EXTRA_URI, Uri::class.java)!! + } else { + @Suppress("DEPRECATION") intent.getParcelableExtra(EXTRA_URI)!! + } + val displayOptions = intent.getBundleExtra(EXTRA_DISPLAY_OPTIONS) + + Log.d(TAG, "[service] registerCall: Call details - Name: $name, URI: $uri") + + scope.launch { + telecomRepository.registerCall( + callId, + name, + uri, + incoming, + isVideo, + displayOptions, + ) + } + } + + private fun sendBroadcastEvent(action: String, applyParams: Intent.() -> Unit = {}) { + val intent = + Intent(action).apply { + setPackage(packageName) + applyParams(this) + } + Log.d(TAG, "[service] sendBroadcastEvent: Sending broadcast event: ${intent.action}") + sendBroadcast(intent) + } + + // private fun hasMicPermission(): Boolean { + // val hasPermission = + // PermissionChecker.checkSelfPermission( + // this, + // Manifest.permission.RECORD_AUDIO, + // ) == PermissionChecker.PERMISSION_GRANTED + // Log.d(TAG, "[service] hasMicPermission: Mic permission granted = $hasPermission") + // return hasPermission + // } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt new file mode 100644 index 0000000000..499877dc26 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt @@ -0,0 +1,576 @@ +package com.callingx + +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.ServiceConnection +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.IBinder +import android.telecom.DisconnectCause +import android.util.Log +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.LifecycleEventListener +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableArray +import com.facebook.react.bridge.WritableMap +import com.facebook.react.bridge.WritableNativeArray +import com.facebook.react.module.annotations.ReactModule +import com.facebook.react.modules.core.DeviceEventManagerModule + +@ReactModule(name = CallingxModule.NAME) +class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec(reactContext) { + + companion object { + const val TAG = "TelecomCallingx" + const val NAME = "Callingx" + + const val EXTRA_CALL_ID = "call_id" + const val EXTRA_MUTED = "is_muted" + const val EXTRA_ON_HOLD = "hold" + const val EXTRA_DISCONNECT_CAUSE = "disconnect_cause" + const val EXTRA_AUDIO_ENDPOINT = "audio_endpoint" + + const val CALL_REGISTERED_ACTION = "call_registered" + const val CALL_ANSWERED_ACTION = "call_answered" + // const val CALL_DISCONNECTED_ACTION = "call_disconnected" + const val CALL_INACTIVE_ACTION = "call_inactive" + const val CALL_ACTIVE_ACTION = "call_active" + const val CALL_MUTED_ACTION = "call_muted" + const val CALL_ENDPOINT_CHANGED_ACTION = "call_endpoint_changed" + const val CALL_END_ACTION = "call_end" + + const val SERVICE_READY_ACTION = "service_ready" + + public fun handleCallingIntent(context: Context, intent: Intent) { + if (intent?.action == CALL_ANSWERED_ACTION) { + val callId = intent.getStringExtra(EXTRA_CALL_ID) + // Send broadcast or directly call your module to handle the answer + val broadcastIntent = + Intent(CALL_ANSWERED_ACTION).apply { + setPackage(context.packageName) + putExtra(EXTRA_CALL_ID, callId) + } + context.sendBroadcast(broadcastIntent) + } + } + } + + // Track binding state carefully + private enum class BindingState { + UNBOUND, + BINDING, + BOUND + } + + private var callService: CallService? = null + private var bindingState = BindingState.UNBOUND + + // private lateinit var notificationsConfig: NotificationsConfig + + private var delayedEvents = WritableNativeArray() + private var isModuleInitialized = false + + private val callEventBroadcastReceiver = CallEventBroadcastReceiver() + private val appStateListener = + object : LifecycleEventListener { + override fun onHostResume() { + // App resumed - ensure service is bound if needed + Log.d(TAG, "[module] onHostResume: App resumed") + // if (!isBound && shouldServiceBeRunning()) { + // bindToService() + // } + } + + override fun onHostPause() { + // App paused - start unbind timer + Log.d(TAG, "[module] onHostPause: App paused") + // startUnbindTimer() + } + + override fun onHostDestroy() { + // App destroyed - force unbind + // forceUnbindService() + Log.d(TAG, "[module] onHostDestroy: App destroyed") + unbindServiceSafely() + } + } + + init { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + reactContext.registerReceiver( + callEventBroadcastReceiver, + getReceiverFilter(), + Context.RECEIVER_NOT_EXPORTED + ) + } else { + @Suppress("UnspecifiedRegisterReceiverFlag") + reactContext.registerReceiver(callEventBroadcastReceiver, getReceiverFilter()) + } + } + + override fun getName(): String = NAME + + override fun initialize() { + super.initialize() + reactApplicationContext.addLifecycleEventListener(appStateListener) + + tryToBindIfNeeded() + + Log.d(TAG, "[module] initialize: Initializing module") + } + + override fun invalidate() { + super.invalidate() + Log.d(TAG, "[module] invalidate: Invalidating module") + + unbindServiceSafely() + + reactApplicationContext.removeLifecycleEventListener(appStateListener) + reactApplicationContext.unregisterReceiver(callEventBroadcastReceiver) + isModuleInitialized = false + } + + override fun setupiOS(options: ReadableMap) { + // leave empty + } + + override fun setupAndroid(options: ReadableMap) { + Log.d(TAG, "[module] setupAndroid: Setting up Android: $options") + val notificationsConfig = extractNotificationsConfig(options) + CallNotificationManager.saveNotificationsConfig( + reactApplicationContext, + notificationsConfig + ) + + isModuleInitialized = true + Log.d(TAG, "[module] setupAndroid: Notifications config: $notificationsConfig") + } + + override fun getInitialEvents(): WritableArray { + // NOTE: writabel native array can be consumed only once, think of getting rid from clear + // event and clear eat immidiate after getting initial events + val events = delayedEvents + delayedEvents = WritableNativeArray() + return events + } + + override fun clearInitialEvents(promise: Promise) { + delayedEvents = WritableNativeArray() + promise.resolve(true) + } + + override fun setCurrentCallActive(callId: String, promise: Promise) { + Log.d(TAG, "[module] activateCall: Activating call: $callId") + executeServiceAction(CallAction.Activate, promise) + } + + override fun displayIncomingCall( + callId: String, + phoneNumber: String, + callerName: String, + hasVideo: Boolean, + displayOptions: ReadableMap?, + promise: Promise + ) { + Log.d( + TAG, + "[module] displayIncomingCall: Displaying incoming call: $callId, $phoneNumber, $callerName, $hasVideo" + ) + startCallService( + CallService.ACTION_INCOMING_CALL, + callId, + callerName, + phoneNumber, + hasVideo, + displayOptions + ) + promise.resolve(true) + } + + override fun answerIncomingCall(callId: String, promise: Promise) { + Log.d(TAG, "[module] answerIncomingCall: Answering call: $callId") + // TODO: get the call type from the call attributes + val isAudioCall = true // TODO: get the call type from the call attributes + // registeredCall.callAttributes.callType == + // CallAttributesCompat.CALL_TYPE_AUDIO_CALL + // currentCall?.processAction(TelecomCallAction.Answer(isAudioCall)) + executeServiceAction(CallAction.Answer(isAudioCall), promise) + } + + override fun startCall( + callId: String, + phoneNumber: String, + callerName: String, + hasVideo: Boolean, + displayOptions: ReadableMap?, + promise: Promise + ) { + Log.d( + TAG, + "[module] startCall: Starting outgoing call: $callId, $phoneNumber, $callerName, $hasVideo, $displayOptions" + ) + startCallService( + CallService.ACTION_OUTGOING_CALL, + callId, + callerName, + phoneNumber, + hasVideo, + displayOptions + ) + promise.resolve(true) + } + + override fun updateDisplay( + callId: String, + phoneNumber: String, + callerName: String, + displayOptions: ReadableMap?, + promise: Promise + ) { + Log.d(TAG, "[module] updateDisplay: Updating display: $callId, $phoneNumber, $callerName") + startCallService( + CallService.ACTION_UPDATE_CALL, + callId, + callerName, + phoneNumber, + false, + displayOptions, + ) + promise.resolve(true) + } + + override fun endCallWithReason(callId: String, reason: Double, promise: Promise) { + Log.d(TAG, "[module] endCallWithReason: Ending call: $callId, $reason") + val action = CallAction.Disconnect(DisconnectCause(reason.toInt())) + executeServiceAction(action, promise) + } + + override fun endCall(callId: String, promise: Promise) { + Log.d(TAG, "[module] endCall: Ending call: $callId") + val action = CallAction.Disconnect(DisconnectCause(DisconnectCause.LOCAL)) + executeServiceAction(action, promise) + } + + override fun setMutedCall(callId: String, isMuted: Boolean, promise: Promise) { + Log.d(TAG, "[module] setMutedCall: Setting muted call: $callId, $isMuted") + val action = CallAction.ToggleMute(isMuted) + executeServiceAction(action, promise) + } + + override fun setOnHoldCall(callId: String, isOnHold: Boolean, promise: Promise) { + Log.d(TAG, "[module] setOnHoldCall: Setting on hold call: $callId, $isOnHold") + val action = if (isOnHold) CallAction.Hold else CallAction.Activate + executeServiceAction(action, promise) + } + + override fun startBackgroundTask(taskName: String, timeout: Double, promise: Promise) { + Log.d(TAG, "[module] startBackgroundTask: Starting background task: $taskName, $timeout") + + Intent(reactApplicationContext, CallService::class.java) + .apply { + this.action = CallService.ACTION_START_BACKGROUND_TASK + putExtra(CallService.EXTRA_TASK_NAME, taskName) + putExtra(CallService.EXTRA_TASK_DATA, Bundle()) + putExtra(CallService.EXTRA_TASK_TIMEOUT, timeout.toLong()) + } + .also { reactApplicationContext.startService(it) } + + promise.resolve(true) + } + + override fun stopBackgroundTask(taskName: String, promise: Promise) { + Log.d(TAG, "[module] stopBackgroundTask: Stopping background task: $taskName") + + Intent(reactApplicationContext, CallService::class.java) + .apply { + this.action = CallService.ACTION_STOP_BACKGROUND_TASK + putExtra(CallService.EXTRA_TASK_NAME, taskName) + } + .also { reactApplicationContext.startService(it) } + + promise.resolve(true) + } + + override fun log(message: String, level: String) { + Log.d(TAG, "[module] log: $message, $level") + when (level) { + "debug" -> Log.d(TAG, "[module] log: $message") + "info" -> Log.i(TAG, "[module] log: $message") + "warn" -> Log.w(TAG, "[module] log: $message") + "error" -> Log.e(TAG, "[module] log: $message") + } + } + + private fun startCallService( + action: String, + callId: String, + callerName: String, + phoneNumber: String, + hasVideo: Boolean, + displayOptions: ReadableMap? + ) { + Intent(reactApplicationContext, CallService::class.java) + .apply { + this.action = action + putExtra(CallService.EXTRA_CALL_ID, callId) + putExtra(CallService.EXTRA_NAME, callerName) + putExtra(CallService.EXTRA_URI, Uri.parse(phoneNumber)) + putExtra(CallService.EXTRA_IS_VIDEO, hasVideo) + putExtra(CallService.EXTRA_DISPLAY_OPTIONS, Arguments.toBundle(displayOptions)) + } + .also { reactApplicationContext.startForegroundService(it) } + } + + private fun executeServiceAction(action: CallAction, promise: Promise) { + Log.d(TAG, "[module] executeServiceAction: Executing service action: $action") + when (bindingState) { + BindingState.BOUND -> { + if (callService != null) { + callService?.processAction(action) + promise.resolve(true) + } else { + promise.reject("ERROR", "Service reference lost") + } + } + BindingState.BINDING -> { + Log.d(TAG, "executeServiceAction: Service binding, queueing action") + promise.reject( + "SERVICE_BINDING", + "Service is connecting, please try again in a moment" + ) + } + BindingState.UNBOUND -> { + promise.reject( + "SERVICE_NOT_CONNECTED", + "Service not connected. Call may not be active." + ) + } + } + } + + private fun sendJSEvent(eventName: String, params: WritableMap? = null) { + if (isModuleInitialized && reactApplicationContext.hasActiveReactInstance()) { + val paramsMap = + Arguments.createMap().apply { + params?.let { + it.toHashMap().forEach { key, value -> + if (value is Boolean) { + putBoolean(key, value) + } else { + putString(key, value.toString()) + } + } + } + } + reactApplicationContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(eventName, params) + + val value = + Arguments.createMap().apply { + putString("eventName", eventName) + putMap("params", paramsMap) + } + emitOnNewEvent(value) + } else { + Arguments.createMap() + .apply { + putString("name", eventName) + putMap("params", params) + } + .also { delayedEvents.pushMap(it) } + } + } + + private fun getReceiverFilter(): IntentFilter = + IntentFilter().apply { + addAction(CALL_REGISTERED_ACTION) + addAction(CALL_ANSWERED_ACTION) + addAction(CALL_ACTIVE_ACTION) + addAction(CALL_INACTIVE_ACTION) + addAction(CALL_MUTED_ACTION) + addAction(CALL_ENDPOINT_CHANGED_ACTION) + addAction(CALL_END_ACTION) + addAction(SERVICE_READY_ACTION) + } + + private fun bindToServiceIfNeeded() { + when (bindingState) { + BindingState.BOUND -> { + Log.d(TAG, "[module] bindToServiceIfNeeded: Already bound") + return + } + BindingState.BINDING -> { + Log.d(TAG, "[module] bindToServiceIfNeeded: Already binding") + return + } + BindingState.UNBOUND -> { + Log.d(TAG, "[module] bindToServiceIfNeeded: Attempting to bind") + val intent = Intent(reactApplicationContext, CallService::class.java) + try { + val success = + reactApplicationContext.bindService( + intent, + serviceConnection, + Context.BIND_AUTO_CREATE or Context.BIND_IMPORTANT + ) + if (success) { + bindingState = BindingState.BINDING + Log.d(TAG, "[module] bindToServiceIfNeeded: Bind request successful") + } else { + Log.e(TAG, "[module] bindToServiceIfNeeded: Bind request failed") + bindingState = BindingState.UNBOUND + } + } catch (e: Exception) { + Log.e(TAG, "[module] bindToServiceIfNeeded: Exception during bind", e) + bindingState = BindingState.UNBOUND + } + } + } + } + + private fun unbindServiceSafely() { + Log.d(TAG, "[module] unbindServiceSafely: Unbinding service") + if (bindingState == BindingState.BOUND || bindingState == BindingState.BINDING) { + try { + reactApplicationContext.unbindService(serviceConnection) + Log.d(TAG, "[module] unbindServiceSafely: Successfully unbound") + } catch (e: IllegalArgumentException) { + Log.w( + TAG, + "[module] unbindServiceSafely: Service not registered or already unbound" + ) + } catch (e: Exception) { + Log.e(TAG, "[module] unbindServiceSafely: Error unbinding service", e) + } finally { + bindingState = BindingState.UNBOUND + callService = null + } + } + } + + private fun tryToBindIfNeeded() { + val intent = Intent(reactApplicationContext, CallService::class.java) + try { + val success = + reactApplicationContext.bindService( + intent, + serviceConnection, + 0 // No flags - only bind if service exists + ) + if (success) { + bindingState = BindingState.BINDING + Log.d(TAG, "[module] checkForExistingService: Service exists, binding") + } else { + Log.d(TAG, "[module] checkForExistingService: No existing service") + } + } catch (e: Exception) { + Log.e(TAG, "[module] checkForExistingService: Error checking for service", e) + } + } + + private inner class CallEventBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val action = intent.action + val callId = intent.getStringExtra(EXTRA_CALL_ID) + + if (action == SERVICE_READY_ACTION) { + Log.d(TAG, "[module] onReceive: Service is ready, initiating binding") + bindToServiceIfNeeded() + } + + if (action == null) { + Log.e(TAG, "[module] onReceive: Received intent with null action or callId") + return + } + + Log.d( + TAG, + "[module] onReceive: Received intent: $action callId: $callId callService: ${callService != null}" + ) + val params = Arguments.createMap() + if (callId != null) { + params.putString("callId", callId) + } + + when (action) { + CALL_REGISTERED_ACTION -> { + sendJSEvent("didReceiveStartCallAction", params) + } + CALL_ANSWERED_ACTION -> { + sendJSEvent("answerCall", params) + } + CALL_END_ACTION -> { + params.putString("cause", intent.getStringExtra(EXTRA_DISCONNECT_CAUSE)) + sendJSEvent("endCall", params) + + if (callId == null) { + // callId null means the call was disconnected, service should be unbound + unbindServiceSafely() + } + } + CALL_INACTIVE_ACTION -> { + params.putBoolean("hold", true) + sendJSEvent("didToggleHoldCallAction", params) + } + CALL_ACTIVE_ACTION -> { + params.putBoolean("hold", false) + sendJSEvent("didToggleHoldCallAction", params) + } + CALL_MUTED_ACTION -> { + if (intent.hasExtra(EXTRA_MUTED)) { + params.putBoolean("muted", intent.getBooleanExtra(EXTRA_MUTED, false)) + } + sendJSEvent("didPerformSetMutedCallAction", params) + } + CALL_ENDPOINT_CHANGED_ACTION -> { + if (intent.hasExtra(EXTRA_AUDIO_ENDPOINT)) { + params.putString("output", intent.getStringExtra(EXTRA_AUDIO_ENDPOINT)) + } + sendJSEvent("didChangeAudioRoute", params) + } + } + } + } + + private val serviceConnection = + object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + Log.d(TAG, "[module] onServiceConnected: Service connected") + val binder = service as? CallService.CallServiceBinder + callService = binder?.getService() + bindingState = BindingState.BOUND + } + + override fun onServiceDisconnected(name: ComponentName?) { + Log.d(TAG, "onServiceDisconnected: Service disconnected unexpectedly") + callService = null + bindingState = BindingState.UNBOUND + } + + override fun onBindingDied(name: ComponentName?) { + Log.e(TAG, "[module] onBindingDied: Service binding died") + callService = null + bindingState = BindingState.UNBOUND + + // Must unbind to clean up the dead binding + try { + reactApplicationContext.unbindService(this) + } catch (e: Exception) { + Log.w(TAG, "[module] onBindingDied: Error unbinding dead connection", e) + } + } + + override fun onNullBinding(name: ComponentName?) { + Log.e(TAG, "[module] onNullBinding: Service returned null binding") + bindingState = BindingState.UNBOUND + callService = null + } + } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxPackage.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxPackage.kt new file mode 100644 index 0000000000..f7ca68c6b7 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxPackage.kt @@ -0,0 +1,16 @@ +package com.callingx + +import com.facebook.react.BaseReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.module.model.ReactModuleInfo +import com.facebook.react.module.model.ReactModuleInfoProvider + +class CallingPackage : BaseReactPackage() { + override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? = + if (name == CallingxModule.NAME) CallingxModule(reactContext) else null + + override fun getReactModuleInfoProvider(): ReactModuleInfoProvider = ReactModuleInfoProvider { + mapOf(CallingxModule.NAME to ReactModuleInfo(CallingxModule.NAME, CallingxModule.NAME, false, false, false, true)) + } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/HeadlessTaskManager.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/HeadlessTaskManager.kt new file mode 100644 index 0000000000..a3ecb1c4d4 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/HeadlessTaskManager.kt @@ -0,0 +1,165 @@ +package com.callingx + +import android.content.Context +import android.os.Bundle +import android.util.Log +import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost +import com.facebook.react.ReactInstanceEventListener +import com.facebook.react.ReactNativeHost +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.UiThreadUtil +import com.facebook.react.internal.featureflags.ReactNativeNewArchitectureFeatureFlags +import com.facebook.react.jstasks.HeadlessJsTaskConfig +import com.facebook.react.jstasks.HeadlessJsTaskContext +import com.facebook.react.jstasks.HeadlessJsTaskEventListener + +class HeadlessTaskManager(private val context: Context) : HeadlessJsTaskEventListener { + + private var activeTaskId: Int? = null + + companion object { + private const val TAG = "TelecomHeadlessJSHelper" + } + + public fun startHeadlessTask(taskName: String, data: Bundle, timeout: Long) { + if (activeTaskId != null) { + Log.w(TAG, "startHeadlessTask: Task already starting or active, ignoring new task request") + return + } + + if (UiThreadUtil.isOnUiThread()) { + startTask(HeadlessJsTaskConfig(taskName, Arguments.fromBundle(data), timeout, true)) + } else { + UiThreadUtil.runOnUiThread( + Runnable { + startTask(HeadlessJsTaskConfig(taskName, Arguments.fromBundle(data), timeout, true)) + } + ) + } + } + + public fun stopHeadlessTask() { + Log.d(TAG, "stopHeadlessTask: Stopping headless task") + activeTaskId?.let { taskId -> + if (UiThreadUtil.isOnUiThread()) { + stopTask(taskId) + } else { + UiThreadUtil.runOnUiThread(Runnable { stopTask(taskId) }) + } + } + } + + protected fun startTask(taskConfig: HeadlessJsTaskConfig) { + UiThreadUtil.assertOnUiThread() + // acquireWakeLockNow(this) + + val context = reactContext + if (context == null) { + createReactContextAndScheduleTask(taskConfig) + } else { + invokeStartTask(context, taskConfig) + } + } + + private fun invokeStartTask(reactContext: ReactContext, taskConfig: HeadlessJsTaskConfig) { + Log.d(TAG, "invokeStartTask: Invoking start task") + val headlessJsTaskContext = HeadlessJsTaskContext.getInstance(reactContext) + headlessJsTaskContext.addTaskEventListener(this) + + UiThreadUtil.runOnUiThread { + val taskId = headlessJsTaskContext.startTask(taskConfig) + activeTaskId = taskId + } + } + + private fun stopTask(taskId: Int) { + reactContext?.let { context -> + val headlessJsTaskContext = HeadlessJsTaskContext.getInstance(context) + if (headlessJsTaskContext.isTaskRunning(taskId)) { + headlessJsTaskContext.finishTask(taskId) + Log.d(TAG, "Stopped task: $taskId") + } + } + } + + fun release() { + stopHeadlessTask() + + reactContext?.let { context -> + val headlessJsTaskContext = HeadlessJsTaskContext.getInstance(context) + headlessJsTaskContext.removeTaskEventListener(this) + } + // wakeLock?.release() + } + + override fun onHeadlessJsTaskStart(taskId: Int) { + Log.d(TAG, "onHeadlessJsTaskStart: Task started: $taskId") + } + + override fun onHeadlessJsTaskFinish(taskId: Int) { + Log.d(TAG, "onHeadlessJsTaskFinish: Task finished: $taskId") + activeTaskId = null + } + + /** + * Get the [ReactNativeHost] used by this app. By default, assumes [getApplication] is an instance + * of [ReactApplication] and calls [ReactApplication.reactNativeHost]. + * + * Override this method if your application class does not implement `ReactApplication` or you + * simply have a different mechanism for storing a `ReactNativeHost`, e.g. as a static field + * somewhere. + */ + @Suppress("DEPRECATION") + protected open val reactNativeHost: ReactNativeHost + get() = (context.applicationContext as ReactApplication).reactNativeHost + + /** + * Get the [ReactHost] used by this app. By default, assumes [getApplication] is an instance of + * [ReactApplication] and calls [ReactApplication.reactHost]. This method assumes it is called in + * new architecture and returns null if not. + */ + protected open val reactHost: ReactHost? + get() = (context.applicationContext as ReactApplication).reactHost + + protected val reactContext: ReactContext? + get() { + if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) { + val reactHost = + checkNotNull(reactHost) { "ReactHost is not initialized in New Architecture" } + return reactHost.currentReactContext + } else { + val reactInstanceManager = reactNativeHost.reactInstanceManager + return reactInstanceManager.currentReactContext + } + } + + private fun createReactContextAndScheduleTask(taskConfig: HeadlessJsTaskConfig) { + if (ReactNativeNewArchitectureFeatureFlags.enableBridgelessArchitecture()) { + val reactHost = checkNotNull(reactHost) + reactHost.addReactInstanceEventListener( + object : ReactInstanceEventListener { + override fun onReactContextInitialized(context: ReactContext) { + Log.d(TAG, "createReactContextAndScheduleTask: React context initialized") + invokeStartTask(context, taskConfig) + reactHost.removeReactInstanceEventListener(this) + } + } + ) + reactHost.start() + } else { + val reactInstanceManager = reactNativeHost.reactInstanceManager + reactInstanceManager.addReactInstanceEventListener( + object : ReactInstanceEventListener { + override fun onReactContextInitialized(context: ReactContext) { + Log.d(TAG, "createReactContextAndScheduleTask: React context initialized") + invokeStartTask(context, taskConfig) + reactInstanceManager.removeReactInstanceEventListener(this) + } + } + ) + reactInstanceManager.createReactContextInBackground() + } + } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/ResourceUtils.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/ResourceUtils.kt new file mode 100644 index 0000000000..75a41b5a69 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/ResourceUtils.kt @@ -0,0 +1,53 @@ +import android.content.Context +import android.media.RingtoneManager +import android.net.Uri + +class ResourceUtils { + companion object { + private val resourceIdCache = mutableMapOf() + + private fun getResourceIdByName(context: Context, name: String?, type: String): Int { + if (name.isNullOrEmpty()) { + return 0 + } + + val normalizedName = name.lowercase().replace("-", "_") + val key = "${normalizedName}_$type" + + synchronized(resourceIdCache) { + resourceIdCache[key]?.let { + return it + } + + val packageName = context.packageName + + val id = context.resources.getIdentifier(normalizedName, type, packageName) + resourceIdCache[key] = id + return id + } + } + + fun getSoundUri(context: Context, sound: String?): Uri? { + return when { + sound == null -> null + sound.contains("://") -> Uri.parse(sound) + sound.equals("default", ignoreCase = true) -> { + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + } + else -> { + // The API user is attempting to set a sound by file name, verify it exists + var soundResourceId = getResourceIdByName(context, sound, "raw") + if (soundResourceId == 0 && sound.contains(".")) { + soundResourceId = getResourceIdByName(context, sound.substringBeforeLast('.'), "raw") + } + if (soundResourceId == 0) { + null + } else { + // Use the actual sound name vs the resource ID, to obtain a stable URI, Issue #341 + Uri.parse("android.resource://${context.packageName}/raw/$sound") + } + } + } + } + } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/Utils.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/Utils.kt new file mode 100644 index 0000000000..191b829d98 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/Utils.kt @@ -0,0 +1,61 @@ +package com.callingx + +import android.content.Context +import android.media.RingtoneManager +import android.net.Uri +import android.telecom.DisconnectCause +import com.facebook.react.bridge.ReadableMap + +data class ChannelConfig( + val id: String, + val name: String, + val sound: String, + val vibration: Boolean, + val importance: Int, +) + +data class NotificationsConfig( + val incomingChannel: ChannelConfig, + val outgoingChannel: ChannelConfig, +) + +fun extractNotificationsConfig(config: ReadableMap): NotificationsConfig { + return NotificationsConfig( + incomingChannel = extractChannelConfig(config.getMap("incomingChannel")), + outgoingChannel = extractChannelConfig(config.getMap("outgoingChannel")), + ) +} + +fun extractChannelConfig(channel: ReadableMap?): ChannelConfig { + if (channel == null) { + //return + return ChannelConfig( + id = "", + name = "", + sound = "", + vibration = false, + importance = 0 + ) + } + + return ChannelConfig( + id = channel.getString("id") ?: "", + name = channel.getString("name") ?: "", + sound = channel.getString("sound") ?: "", + vibration = channel.getBoolean("vibration"), + importance = 0 + ) +} + +fun getDisconnectCauseString(cause: DisconnectCause): String { + return when (cause.code) { + DisconnectCause.LOCAL -> "local" + DisconnectCause.REMOTE -> "remote" + DisconnectCause.REJECTED -> "rejected" + DisconnectCause.BUSY -> "busy" + DisconnectCause.ANSWERED_ELSEWHERE -> "answeredElsewhere" + DisconnectCause.MISSED -> "missed" + DisconnectCause.ERROR -> "error" + else -> cause.toString() + } +} diff --git a/packages/react-native-callingx/android/src/main/res/drawable/ic_phone_paused_24.xml b/packages/react-native-callingx/android/src/main/res/drawable/ic_phone_paused_24.xml new file mode 100644 index 0000000000..7773fecd3e --- /dev/null +++ b/packages/react-native-callingx/android/src/main/res/drawable/ic_phone_paused_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/packages/react-native-callingx/android/src/main/res/drawable/ic_round_call_24.xml b/packages/react-native-callingx/android/src/main/res/drawable/ic_round_call_24.xml new file mode 100644 index 0000000000..3daf420f62 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/res/drawable/ic_round_call_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/packages/react-native-callingx/android/src/main/res/drawable/ic_user.xml b/packages/react-native-callingx/android/src/main/res/drawable/ic_user.xml new file mode 100644 index 0000000000..eaaa2941f8 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/res/drawable/ic_user.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/packages/react-native-callingx/babel.config.js b/packages/react-native-callingx/babel.config.js new file mode 100644 index 0000000000..0c05fd6963 --- /dev/null +++ b/packages/react-native-callingx/babel.config.js @@ -0,0 +1,12 @@ +module.exports = { + overrides: [ + { + exclude: /\/node_modules\//, + presets: ['module:react-native-builder-bob/babel-preset'], + }, + { + include: /\/node_modules\//, + presets: ['module:@react-native/babel-preset'], + }, + ], +}; diff --git a/packages/react-native-callingx/eslint.config.mjs b/packages/react-native-callingx/eslint.config.mjs new file mode 100644 index 0000000000..16b00bbcd9 --- /dev/null +++ b/packages/react-native-callingx/eslint.config.mjs @@ -0,0 +1,29 @@ +import { fixupConfigRules } from '@eslint/compat'; +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import prettier from 'eslint-plugin-prettier'; +import { defineConfig } from 'eslint/config'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default defineConfig([ + { + extends: fixupConfigRules(compat.extends('@react-native', 'prettier')), + plugins: { prettier }, + rules: { + 'react/react-in-jsx-scope': 'off', + 'prettier/prettier': 'error', + }, + }, + { + ignores: ['node_modules/', 'lib/'], + }, +]); diff --git a/packages/react-native-callingx/example/.bundle/config b/packages/react-native-callingx/example/.bundle/config new file mode 100644 index 0000000000..848943bb52 --- /dev/null +++ b/packages/react-native-callingx/example/.bundle/config @@ -0,0 +1,2 @@ +BUNDLE_PATH: "vendor/bundle" +BUNDLE_FORCE_RUBY_PLATFORM: 1 diff --git a/packages/react-native-callingx/example/.watchmanconfig b/packages/react-native-callingx/example/.watchmanconfig new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/packages/react-native-callingx/example/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/packages/react-native-callingx/example/Gemfile b/packages/react-native-callingx/example/Gemfile new file mode 100644 index 0000000000..6a4c5f1718 --- /dev/null +++ b/packages/react-native-callingx/example/Gemfile @@ -0,0 +1,16 @@ +source 'https://rubygems.org' + +# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version +ruby ">= 2.6.10" + +# Exclude problematic versions of cocoapods and activesupport that causes build failures. +gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' +gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' +gem 'xcodeproj', '< 1.26.0' +gem 'concurrent-ruby', '< 1.3.4' + +# Ruby 3.4.0 has removed some libraries from the standard library. +gem 'bigdecimal' +gem 'logger' +gem 'benchmark' +gem 'mutex_m' diff --git a/packages/react-native-callingx/example/README.md b/packages/react-native-callingx/example/README.md new file mode 100644 index 0000000000..3e2c3f8505 --- /dev/null +++ b/packages/react-native-callingx/example/README.md @@ -0,0 +1,97 @@ +This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). + +# Getting Started + +> **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding. + +## Step 1: Start Metro + +First, you will need to run **Metro**, the JavaScript build tool for React Native. + +To start the Metro dev server, run the following command from the root of your React Native project: + +```sh +# Using npm +npm start + +# OR using Yarn +yarn start +``` + +## Step 2: Build and run your app + +With Metro running, open a new terminal window/pane from the root of your React Native project, and use one of the following commands to build and run your Android or iOS app: + +### Android + +```sh +# Using npm +npm run android + +# OR using Yarn +yarn android +``` + +### iOS + +For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps). + +The first time you create a new project, run the Ruby bundler to install CocoaPods itself: + +```sh +bundle install +``` + +Then, and every time you update your native dependencies, run: + +```sh +bundle exec pod install +``` + +For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html). + +```sh +# Using npm +npm run ios + +# OR using Yarn +yarn ios +``` + +If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device. + +This is one way to run your app — you can also build it directly from Android Studio or Xcode. + +## Step 3: Modify your app + +Now that you have successfully run the app, let's make changes! + +Open `App.tsx` in your text editor of choice and make some changes. When you save, your app will automatically update and reflect these changes — this is powered by [Fast Refresh](https://reactnative.dev/docs/fast-refresh). + +When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload: + +- **Android**: Press the R key twice or select **"Reload"** from the **Dev Menu**, accessed via Ctrl + M (Windows/Linux) or Cmd ⌘ + M (macOS). +- **iOS**: Press R in iOS Simulator. + +## Congratulations! :tada: + +You've successfully run and modified your React Native App. :partying_face: + +### Now what? + +- If you want to add this new React Native code to an existing application, check out the [Integration guide](https://reactnative.dev/docs/integration-with-existing-apps). +- If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started). + +# Troubleshooting + +If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. + +# Learn More + +To learn more about React Native, take a look at the following resources: + +- [React Native Website](https://reactnative.dev) - learn more about React Native. +- [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment. +- [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**. +- [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts. +- [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native. diff --git a/packages/react-native-callingx/example/android/app/build.gradle b/packages/react-native-callingx/example/android/app/build.gradle new file mode 100644 index 0000000000..48d74066cf --- /dev/null +++ b/packages/react-native-callingx/example/android/app/build.gradle @@ -0,0 +1,119 @@ +apply plugin: "com.android.application" +apply plugin: "org.jetbrains.kotlin.android" +apply plugin: "com.facebook.react" + +/** + * This is the configuration block to customize your React Native Android app. + * By default you don't need to apply any configuration, just uncomment the lines you need. + */ +react { + /* Folders */ + // The root of your project, i.e. where "package.json" lives. Default is '../..' + // root = file("../../") + // The folder where the react-native NPM package is. Default is ../../node_modules/react-native + // reactNativeDir = file("../../node_modules/react-native") + // The folder where the react-native Codegen package is. Default is ../../node_modules/@react-native/codegen + // codegenDir = file("../../node_modules/@react-native/codegen") + // The cli.js file which is the React Native CLI entrypoint. Default is ../../node_modules/react-native/cli.js + // cliFile = file("../../node_modules/react-native/cli.js") + + /* Variants */ + // The list of variants to that are debuggable. For those we're going to + // skip the bundling of the JS bundle and the assets. By default is just 'debug'. + // If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants. + // debuggableVariants = ["liteDebug", "prodDebug"] + + /* Bundling */ + // A list containing the node command and its flags. Default is just 'node'. + // nodeExecutableAndArgs = ["node"] + // + // The command to run when bundling. By default is 'bundle' + // bundleCommand = "ram-bundle" + // + // The path to the CLI configuration file. Default is empty. + // bundleConfig = file(../rn-cli.config.js) + // + // The name of the generated asset file containing your JS bundle + // bundleAssetName = "MyApplication.android.bundle" + // + // The entry file for bundle generation. Default is 'index.android.js' or 'index.js' + // entryFile = file("../js/MyApplication.android.js") + // + // A list of extra flags to pass to the 'bundle' commands. + // See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle + // extraPackagerArgs = [] + + /* Hermes Commands */ + // The hermes compiler command to run. By default it is 'hermesc' + // hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc" + // + // The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map" + // hermesFlags = ["-O", "-output-source-map"] + + /* Autolinking */ + autolinkLibrariesWithApp() +} + +/** + * Set this to true to Run Proguard on Release builds to minify the Java bytecode. + */ +def enableProguardInReleaseBuilds = false + +/** + * The preferred build flavor of JavaScriptCore (JSC) + * + * For example, to use the international variant, you can use: + * `def jscFlavor = io.github.react-native-community:jsc-android-intl:2026004.+` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+' + +android { + ndkVersion rootProject.ext.ndkVersion + buildToolsVersion rootProject.ext.buildToolsVersion + compileSdk rootProject.ext.compileSdkVersion + + namespace "callingx.example" + defaultConfig { + applicationId "callingx.example" + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + } + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + // Caution! In production, you need to generate your own keystore file. + // see https://reactnative.dev/docs/signed-apk-android. + signingConfig signingConfigs.debug + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } +} + +dependencies { + // The version of react-native is set by the React Native Gradle Plugin + implementation("com.facebook.react:react-android") + + if (hermesEnabled.toBoolean()) { + implementation("com.facebook.react:hermes-android") + } else { + implementation jscFlavor + } +} diff --git a/packages/react-native-callingx/example/android/app/debug.keystore b/packages/react-native-callingx/example/android/app/debug.keystore new file mode 100644 index 0000000000000000000000000000000000000000..364e105ed39fbfd62001429a68140672b06ec0de GIT binary patch literal 2257 zcmchYXEfYt8;7T1^dLH$VOTZ%2NOdOH5j5LYLtZ0q7x-V8_6gU5)#7dkq{HTmsfNq zB3ZqcAxeY^G10@?efK?Q&)M(qInVv!xjx+IKEL}p*K@LYvIzo#AZG>st5|P)KF1_Z;y){W{<7K{nl!CPuE z_^(!C(Ol0n8 zK13*rzAtW>(wULKPRYLd7G18F8#1P`V*9`(Poj26eOXYyBVZPno~Cvvhx7vPjAuZo zF?VD!zB~QG(!zbw#qsxT8%BSpqMZ4f70ZPn-3y$L8{EVbbN9$H`B&Z1quk9tgp5FM zuxp3pJ0b8u|3+#5bkJ4SRnCF2l7#DyLYXYY8*?OuAwK4E6J{0N=O3QNVzQ$L#FKkR zi-c@&!nDvezOV$i$Lr}iF$XEcwnybQ6WZrMKuw8gCL^U#D;q3t&HpTbqyD%vG=TeDlzCT~MXUPC|Leb-Uk+ z=vnMd(|>ld?Fh>V8poP;q;;nc@en$|rnP0ytzD&fFkCeUE^kG9Kx4wUh!!rpjwKDP zyw_e|a^x_w3E zP}}@$g>*LLJ4i0`Gx)qltL}@;mDv}D*xR^oeWcWdPkW@Uu)B^X&4W1$p6}ze!zudJ zyiLg@uggoMIArBr*27EZV7djDg@W1MaL+rcZ-lrANJQ%%>u8)ZMWU@R2qtnmG(acP z0d_^!t>}5W zpT`*2NR+0+SpTHb+6Js4b;%LJB;B_-ChhnU5py}iJtku*hm5F0!iql8Hrpcy1aYbT z1*dKC5ua6pMX@@iONI?Hpr%h;&YaXp9n!ND7-=a%BD7v&g zOO41M6EbE24mJ#S$Ui0-brR5ML%@|ndz^)YLMMV1atna{Fw<;TF@>d&F|!Z>8eg>>hkFrV)W+uv=`^F9^e zzzM2*oOjT9%gLoub%(R57p-`TXFe#oh1_{&N-YN z<}artH|m=d8TQuKSWE)Z%puU|g|^^NFwC#N=@dPhasyYjoy(fdEVfKR@cXKHZV-`06HsP`|Ftx;8(YD$fFXumLWbGnu$GMqRncXYY9mwz9$ap zQtfZB^_BeNYITh^hA7+(XNFox5WMeG_LtJ%*Q}$8VKDI_p8^pqX)}NMb`0e|wgF7D zuQACY_Ua<1ri{;Jwt@_1sW9zzdgnyh_O#8y+C;LcZq6=4e^cs6KvmK@$vVpKFGbQ= z$)Eux5C|Fx;Gtmv9^#Y-g@7Rt7*eLp5n!gJmn7&B_L$G?NCN`AP>cXQEz}%F%K;vUs{+l4Q{}eWW;ATe2 zqvXzxoIDy(u;F2q1JH7Sf;{jy_j})F+cKlIOmNfjBGHoG^CN zM|Ho&&X|L-36f}Q-obEACz`sI%2f&k>z5c$2TyTSj~vmO)BW~+N^kt`Jt@R|s!){H ze1_eCrlNaPkJQhL$WG&iRvF*YG=gXd1IyYQ9ew|iYn7r~g!wOnw;@n42>enAxBv*A zEmV*N#sxdicyNM=A4|yaOC5MByts}s_Hpfj|y<6G=o=!3S@eIFKDdpR7|FY>L&Wat&oW&cm&X~ z5Bt>Fcq(fgnvlvLSYg&o6>&fY`ODg4`V^lWWD=%oJ#Kbad2u~! zLECFS*??>|vDsNR&pH=Ze0Eo`sC_G`OjoEKVHY|wmwlX&(XBE<@sx3Hd^gtd-fNwUHsylg06p`U2y_={u}Bc + + + + diff --git a/packages/react-native-callingx/example/android/app/src/main/AndroidManifest.xml b/packages/react-native-callingx/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..e1892528b8 --- /dev/null +++ b/packages/react-native-callingx/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/packages/react-native-callingx/example/android/app/src/main/java/callingx/example/MainActivity.kt b/packages/react-native-callingx/example/android/app/src/main/java/callingx/example/MainActivity.kt new file mode 100644 index 0000000000..bfa630a644 --- /dev/null +++ b/packages/react-native-callingx/example/android/app/src/main/java/callingx/example/MainActivity.kt @@ -0,0 +1,22 @@ +package callingx.example + +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate + +class MainActivity : ReactActivity() { + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String = "CallingxExample" + + /** + * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] + * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] + */ + override fun createReactActivityDelegate(): ReactActivityDelegate = + DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) +} diff --git a/packages/react-native-callingx/example/android/app/src/main/java/callingx/example/MainApplication.kt b/packages/react-native-callingx/example/android/app/src/main/java/callingx/example/MainApplication.kt new file mode 100644 index 0000000000..c2529473dc --- /dev/null +++ b/packages/react-native-callingx/example/android/app/src/main/java/callingx/example/MainApplication.kt @@ -0,0 +1,38 @@ +package callingx.example + +import android.app.Application +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactHost +import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost +import com.facebook.react.defaults.DefaultReactNativeHost + +class MainApplication : Application(), ReactApplication { + + override val reactNativeHost: ReactNativeHost = + object : DefaultReactNativeHost(this) { + override fun getPackages(): List = + PackageList(this).packages.apply { + // Packages that cannot be autolinked yet can be added manually here, for example: + // add(MyReactNativePackage()) + } + + override fun getJSMainModuleName(): String = "index" + + override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG + + override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED + } + + override val reactHost: ReactHost + get() = getDefaultReactHost(applicationContext, reactNativeHost) + + override fun onCreate() { + super.onCreate() + loadReactNative(this) + } +} diff --git a/packages/react-native-callingx/example/android/app/src/main/res/drawable/rn_edit_text_material.xml b/packages/react-native-callingx/example/android/app/src/main/res/drawable/rn_edit_text_material.xml new file mode 100644 index 0000000000..5c25e728ea --- /dev/null +++ b/packages/react-native-callingx/example/android/app/src/main/res/drawable/rn_edit_text_material.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + diff --git a/packages/react-native-callingx/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/react-native-callingx/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a2f5908281d070150700378b64a84c7db1f97aa1 GIT binary patch literal 3056 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 axPAxe%7rDusX+w*00006jj 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⁣ zwiG+x#%j}{0gU~J8;<|!B1@-XaB@{KORFwrYg_8rOv({b0EO#DbeQRm;B6_9=mXGf z-x|VL{zd`)#@yN}HkCSJbjbNlE|zL3Wm9Q8HY`sV)}3%pgN>cL^67{Z;PPL(*wT8N zUjXU{@|*hvm}({wsAC=x0^ok0%UAz0;sogW{B!nDqk|JJ5x~4NfTDgP49^zeu`csl?5mY@JdQdISc zFs!E{^grmkLnUk9 zny~m)1vws@5BFI<-0Tuo2JWX(0v`W|t(wg;s--L47WTvTMz-8l#TL^=OJNRS2?_Qj z3AKT+gvbyBi#H*-tJ%tWD|>EV3wy|8qxfzS!5RW;Jpl5*zo&^UBU=fG#2}UvRyNkK zA06Dy9;K1ca@r2T>yThYgI!ont$(G{6q#2QT+00r_x0(b)gsE`lBB?2gr55gq^D3Fi&p%E(p9>U%bv zkg1Jco(RbyTX7FDHOnl7-O@ zI$AaIl?9NJKPm(WiBP`1-#CB1QzU>&hKm)fpa5DKE{2$X0hGz-0uZ?cyTk(YC!Y&| zL=1VrNERSA5NA2jq7FACfX4JfPyj5XXl1yv0>~s;eF7L2$>&oMqeTFT2m$y7FlkON z_yurD1yIOvA;5C6016pyxBznGUt0kJ&k5r#;&>Jow`r)sp9R~PmK~lz$3xH%LT*1U zJdOyABZ3!FvNoR*vN$5ykHS8f`jA4zV+|L}i1C4`B2c{R0;UdYxaU|H)2avz@ z=mEYc|2S<+(B2Tj+FkX+2D+yFI!k9lWMA61DJ{)e;lum$(;O87?vGJJe!KtK04+N_ zI*P~t@dUb>9Xh{dbyl{-ZQ(UMgz7$|QfL5XSPkskt^NgctYC#;4WcZB1@%@wy@2t3 z2z0DI7&%b$*Aw~abe?GxE`ez@+6hOh-6*8fHRV{1os$EL@}uUZeG4h1&Be`98q*7j z=3-v+lhIjfWVo12!<>%V^a6lTgW3+_#W6n|p*~==zOH7z$0{LSZk(Tpd7EaD04hnA zL;#fxS0aD{`5^&D`}>0Uq?byDD-l2=!wm_bLcUl4gc(% za1p|itVANvFF>hghAS07Im1;IK;|b*W)}VDyI;BIp2=K*yu2a)j?B|f<44NI$NbmJ z#dE0>jI$fMr&@>4kN8MLFb4&2O9fEKaQg%(QO$4_1rVQywG^CmBLh#}_7gKW3vd?| z2?1^&KWq8}8I^_S0|)MowU_pw$q@nl@Nkn$z>BQq_KA^9yaR`(R3u{{Ig;cwt z@AJ^{ODQCm^neroM9nKNUAXi9RCK`OsP_LuR0PUR(YZCCX5dNF6VzcoK&=b^r`W?ltt|*F zpkoae%ZT{C1h~EcFui~b7fF`vb<<~j_VquuUA$}QqIKYELPp#;{u?q8Dz}WAG-(3; zjrm$i%7UbyZMM(Y{>!uJ#vNB?R~B{6Htp=>e*<{fQQ5W7V(1coCWlOON!MzZxhum| ztZBQpGR z;~#ur^&PockKdV{Q6R>o`Pl{0x!DEbpZ7y9Y;*ZvE!*gU`V1W3znva{f=?WO5I&>B z&hw6}tjECtaghm5z|C#%M;Yf_*pI^};h}Vl=^r9EN=tVDj86D;C$jIJ?K7VP+00000NkvXXu0mjf D5i!M* literal 0 HcmV?d00001 diff --git a/packages/react-native-callingx/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/packages/react-native-callingx/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..459ca609d3ae0d3943ab44cdc27feef9256dc6d7 GIT binary patch literal 7098 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 literal 0 HcmV?d00001 diff --git a/packages/react-native-callingx/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/react-native-callingx/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..8ca12fe024be86e868d14e91120a6902f8e88ac6 GIT binary patch literal 6464 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 B0*L?s literal 0 HcmV?d00001 diff --git a/packages/react-native-callingx/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/packages/react-native-callingx/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..8e19b410a1b15ff180f3dacac19395fe3046cdec GIT binary patch literal 10676 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 literal 0 HcmV?d00001 diff --git a/packages/react-native-callingx/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/packages/react-native-callingx/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..4c19a13c239cb67b8a2134ddd5f325db1d2d5bee GIT binary patch literal 15523 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 BIP(Ai literal 0 HcmV?d00001 diff --git a/packages/react-native-callingx/example/android/app/src/main/res/values/strings.xml b/packages/react-native-callingx/example/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..79a2a58ede --- /dev/null +++ b/packages/react-native-callingx/example/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + CallingxExample + diff --git a/packages/react-native-callingx/example/android/app/src/main/res/values/styles.xml b/packages/react-native-callingx/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..7ba83a2ad5 --- /dev/null +++ b/packages/react-native-callingx/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/packages/react-native-callingx/example/android/build.gradle b/packages/react-native-callingx/example/android/build.gradle new file mode 100644 index 0000000000..dad99b022a --- /dev/null +++ b/packages/react-native-callingx/example/android/build.gradle @@ -0,0 +1,21 @@ +buildscript { + ext { + buildToolsVersion = "36.0.0" + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 + ndkVersion = "27.1.12297006" + kotlinVersion = "2.1.20" + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle") + classpath("com.facebook.react:react-native-gradle-plugin") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") + } +} + +apply plugin: "com.facebook.react.rootproject" diff --git a/packages/react-native-callingx/example/android/gradle.properties b/packages/react-native-callingx/example/android/gradle.properties new file mode 100644 index 0000000000..9afe61598f --- /dev/null +++ b/packages/react-native-callingx/example/android/gradle.properties @@ -0,0 +1,44 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m +org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true + +# Use this property to specify which architecture you want to build. +# You can also override it from the CLI using +# ./gradlew -PreactNativeArchitectures=x86_64 +reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 + +# Use this property to enable support to the new architecture. +# This will allow you to use TurboModules and the Fabric render in +# your application. You should enable this flag either if you want +# to write custom TurboModules/Fabric components OR use libraries that +# are providing them. +newArchEnabled=true + +# Use this property to enable or disable the Hermes JS engine. +# If set to false, you will be using JSC instead. +hermesEnabled=true + +# Use this property to enable edge-to-edge display support. +# This allows your app to draw behind system bars for an immersive UI. +# Note: Only works with ReactActivity and should not be used with custom Activity. +edgeToEdgeEnabled=false diff --git a/packages/react-native-callingx/example/android/gradle/wrapper/gradle-wrapper.jar b/packages/react-native-callingx/example/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/packages/react-native-callingx/example/android/gradlew.bat b/packages/react-native-callingx/example/android/gradlew.bat new file mode 100644 index 0000000000..dd2b8eedbd --- /dev/null +++ b/packages/react-native-callingx/example/android/gradlew.bat @@ -0,0 +1,99 @@ +@REM Copyright (c) Meta Platforms, Inc. and affiliates. +@REM +@REM This source code is licensed under the MIT license found in the +@REM LICENSE file in the root directory of this source tree. + +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/packages/react-native-callingx/example/android/settings.gradle b/packages/react-native-callingx/example/android/settings.gradle new file mode 100644 index 0000000000..ae60f4c242 --- /dev/null +++ b/packages/react-native-callingx/example/android/settings.gradle @@ -0,0 +1,6 @@ +pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } +plugins { id("com.facebook.react.settings") } +extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } +rootProject.name = 'callingx.example' +include ':app' +includeBuild('../node_modules/@react-native/gradle-plugin') diff --git a/packages/react-native-callingx/example/app.json b/packages/react-native-callingx/example/app.json new file mode 100644 index 0000000000..cbb977effe --- /dev/null +++ b/packages/react-native-callingx/example/app.json @@ -0,0 +1,4 @@ +{ + "name": "CallingxExample", + "displayName": "CallingxExample" +} diff --git a/packages/react-native-callingx/example/babel.config.js b/packages/react-native-callingx/example/babel.config.js new file mode 100644 index 0000000000..486a09304d --- /dev/null +++ b/packages/react-native-callingx/example/babel.config.js @@ -0,0 +1,12 @@ +const path = require('path'); +const { getConfig } = require('react-native-builder-bob/babel-config'); +const pkg = require('../package.json'); + +const root = path.resolve(__dirname, '..'); + +module.exports = getConfig( + { + presets: ['module:@react-native/babel-preset'], + }, + { root, pkg } +); diff --git a/packages/react-native-callingx/example/index.js b/packages/react-native-callingx/example/index.js new file mode 100644 index 0000000000..117ddcae40 --- /dev/null +++ b/packages/react-native-callingx/example/index.js @@ -0,0 +1,5 @@ +import { AppRegistry } from 'react-native'; +import App from './src/App'; +import { name as appName } from './app.json'; + +AppRegistry.registerComponent(appName, () => App); diff --git a/packages/react-native-callingx/example/ios/.xcode.env b/packages/react-native-callingx/example/ios/.xcode.env new file mode 100644 index 0000000000..3d5782c715 --- /dev/null +++ b/packages/react-native-callingx/example/ios/.xcode.env @@ -0,0 +1,11 @@ +# This `.xcode.env` file is versioned and is used to source the environment +# used when running script phases inside Xcode. +# To customize your local environment, you can create an `.xcode.env.local` +# file that is not versioned. + +# NODE_BINARY variable contains the PATH to the node executable. +# +# Customize the NODE_BINARY variable here. +# For example, to use nvm with brew, add the following line +# . "$(brew --prefix nvm)/nvm.sh" --no-use +export NODE_BINARY=$(command -v node) diff --git a/packages/react-native-callingx/example/ios/CallingxExample.xcodeproj/project.pbxproj b/packages/react-native-callingx/example/ios/CallingxExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..1246c566ee --- /dev/null +++ b/packages/react-native-callingx/example/ios/CallingxExample.xcodeproj/project.pbxproj @@ -0,0 +1,471 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 0C80B921A6F3F58F76C31292 /* libPods-CallingxExample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-CallingxExample.a */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; }; + 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 13B07F961A680F5B00A75B9A /* CallingxExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CallingxExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = CallingxExample/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = CallingxExample/Info.plist; sourceTree = ""; }; + 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = CallingxExample/PrivacyInfo.xcprivacy; sourceTree = ""; }; + 3B4392A12AC88292D35C810B /* Pods-CallingxExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CallingxExample.debug.xcconfig"; path = "Target Support Files/Pods-CallingxExample/Pods-CallingxExample.debug.xcconfig"; sourceTree = ""; }; + 5709B34CF0A7D63546082F79 /* Pods-CallingxExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CallingxExample.release.xcconfig"; path = "Target Support Files/Pods-CallingxExample/Pods-CallingxExample.release.xcconfig"; sourceTree = ""; }; + 5DCACB8F33CDC322A6C60F78 /* libPods-CallingxExample.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-CallingxExample.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 761780EC2CA45674006654EE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = CallingxExample/AppDelegate.swift; sourceTree = ""; }; + 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = CallingxExample/LaunchScreen.storyboard; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0C80B921A6F3F58F76C31292 /* libPods-CallingxExample.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 13B07FAE1A68108700A75B9A /* CallingxExample */ = { + isa = PBXGroup; + children = ( + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 761780EC2CA45674006654EE /* AppDelegate.swift */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, + 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */, + ); + name = CallingxExample; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + 5DCACB8F33CDC322A6C60F78 /* libPods-CallingxExample.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 13B07FAE1A68108700A75B9A /* CallingxExample */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + BBD78D7AC51CEA395F1C20DB /* Pods */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* CallingxExample.app */, + ); + name = Products; + sourceTree = ""; + }; + BBD78D7AC51CEA395F1C20DB /* Pods */ = { + isa = PBXGroup; + children = ( + 3B4392A12AC88292D35C810B /* Pods-CallingxExample.debug.xcconfig */, + 5709B34CF0A7D63546082F79 /* Pods-CallingxExample.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 13B07F861A680F5B00A75B9A /* CallingxExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "CallingxExample" */; + buildPhases = ( + C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */, + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */, + E235C05ADACE081382539298 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = CallingxExample; + productName = CallingxExample; + productReference = 13B07F961A680F5B00A75B9A /* CallingxExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1210; + TargetAttributes = { + 13B07F861A680F5B00A75B9A = { + LastSwiftMigration = 1120; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "CallingxExample" */; + compatibilityVersion = "Xcode 12.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* CallingxExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(SRCROOT)/.xcode.env.local", + "$(SRCROOT)/.xcode.env", + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "set -e\n\nWITH_ENVIRONMENT=\"$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"$REACT_NATIVE_PATH/scripts/react-native-xcode.sh\"\n\n/bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n"; + }; + 00EEFC60759A1932668264C0 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-CallingxExample/Pods-CallingxExample-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-CallingxExample/Pods-CallingxExample-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-CallingxExample/Pods-CallingxExample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + C38B50BA6285516D6DCD4F65 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-CallingxExample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E235C05ADACE081382539298 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-CallingxExample/Pods-CallingxExample-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-CallingxExample/Pods-CallingxExample-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-CallingxExample/Pods-CallingxExample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3B4392A12AC88292D35C810B /* Pods-CallingxExample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = CallingxExample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "callingx.example"; + PRODUCT_NAME = CallingxExample; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5709B34CF0A7D63546082F79 /* Pods-CallingxExample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = CallingxExample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + PRODUCT_BUNDLE_IDENTIFIER = "callingx.example"; + PRODUCT_NAME = CallingxExample; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "\"$(SDKROOT)/usr/lib/swift\"", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + "-DFOLLY_HAVE_CLOCK_GETTIME=1", + ); + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++20"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.1; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "\"$(SDKROOT)/usr/lib/swift\"", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-DFOLLY_NO_CONFIG", + "-DFOLLY_MOBILE=1", + "-DFOLLY_USE_LIBCPP=1", + "-DFOLLY_CFG_NO_COROUTINES=1", + "-DFOLLY_HAVE_CLOCK_GETTIME=1", + ); + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "CallingxExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "CallingxExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/packages/react-native-callingx/example/ios/CallingxExample.xcodeproj/xcshareddata/xcschemes/CallingxExample.xcscheme b/packages/react-native-callingx/example/ios/CallingxExample.xcodeproj/xcshareddata/xcschemes/CallingxExample.xcscheme new file mode 100644 index 0000000000..c2dd5830ea --- /dev/null +++ b/packages/react-native-callingx/example/ios/CallingxExample.xcodeproj/xcshareddata/xcschemes/CallingxExample.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/react-native-callingx/example/ios/CallingxExample/AppDelegate.swift b/packages/react-native-callingx/example/ios/CallingxExample/AppDelegate.swift new file mode 100644 index 0000000000..bd6ee494c1 --- /dev/null +++ b/packages/react-native-callingx/example/ios/CallingxExample/AppDelegate.swift @@ -0,0 +1,48 @@ +import UIKit +import React +import React_RCTAppDelegate +import ReactAppDependencyProvider + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + var reactNativeDelegate: ReactNativeDelegate? + var reactNativeFactory: RCTReactNativeFactory? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + let delegate = ReactNativeDelegate() + let factory = RCTReactNativeFactory(delegate: delegate) + delegate.dependencyProvider = RCTAppDependencyProvider() + + reactNativeDelegate = delegate + reactNativeFactory = factory + + window = UIWindow(frame: UIScreen.main.bounds) + + factory.startReactNative( + withModuleName: "CallingxExample", + in: window, + launchOptions: launchOptions + ) + + return true + } +} + +class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { + override func sourceURL(for bridge: RCTBridge) -> URL? { + self.bundleURL() + } + + override func bundleURL() -> URL? { +#if DEBUG + RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") +#else + Bundle.main.url(forResource: "main", withExtension: "jsbundle") +#endif + } +} diff --git a/packages/react-native-callingx/example/ios/CallingxExample/Images.xcassets/AppIcon.appiconset/Contents.json b/packages/react-native-callingx/example/ios/CallingxExample/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..ddd7fca89e --- /dev/null +++ b/packages/react-native-callingx/example/ios/CallingxExample/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,53 @@ +{ + "images": [ + { + "idiom": "iphone", + "scale": "2x", + "size": "20x20" + }, + { + "idiom": "iphone", + "scale": "3x", + "size": "20x20" + }, + { + "idiom": "iphone", + "scale": "2x", + "size": "29x29" + }, + { + "idiom": "iphone", + "scale": "3x", + "size": "29x29" + }, + { + "idiom": "iphone", + "scale": "2x", + "size": "40x40" + }, + { + "idiom": "iphone", + "scale": "3x", + "size": "40x40" + }, + { + "idiom": "iphone", + "scale": "2x", + "size": "60x60" + }, + { + "idiom": "iphone", + "scale": "3x", + "size": "60x60" + }, + { + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/packages/react-native-callingx/example/ios/CallingxExample/Images.xcassets/Contents.json b/packages/react-native-callingx/example/ios/CallingxExample/Images.xcassets/Contents.json new file mode 100644 index 0000000000..97a8662ebd --- /dev/null +++ b/packages/react-native-callingx/example/ios/CallingxExample/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "version": 1, + "author": "xcode" + } +} diff --git a/packages/react-native-callingx/example/ios/CallingxExample/Info.plist b/packages/react-native-callingx/example/ios/CallingxExample/Info.plist new file mode 100644 index 0000000000..f23ea7e4eb --- /dev/null +++ b/packages/react-native-callingx/example/ios/CallingxExample/Info.plist @@ -0,0 +1,52 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + CallingxExample + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + NSAppTransportSecurity + + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + + NSLocationWhenInUseUsageDescription + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + arm64 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/packages/react-native-callingx/example/ios/CallingxExample/LaunchScreen.storyboard b/packages/react-native-callingx/example/ios/CallingxExample/LaunchScreen.storyboard new file mode 100644 index 0000000000..80913b79e6 --- /dev/null +++ b/packages/react-native-callingx/example/ios/CallingxExample/LaunchScreen.storyboard @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/react-native-callingx/example/ios/CallingxExample/PrivacyInfo.xcprivacy b/packages/react-native-callingx/example/ios/CallingxExample/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..41b8317f06 --- /dev/null +++ b/packages/react-native-callingx/example/ios/CallingxExample/PrivacyInfo.xcprivacy @@ -0,0 +1,37 @@ + + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryFileTimestamp + NSPrivacyAccessedAPITypeReasons + + C617.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategorySystemBootTime + NSPrivacyAccessedAPITypeReasons + + 35F9.1 + + + + NSPrivacyCollectedDataTypes + + NSPrivacyTracking + + + diff --git a/packages/react-native-callingx/example/ios/Podfile b/packages/react-native-callingx/example/ios/Podfile new file mode 100644 index 0000000000..e1ac14a934 --- /dev/null +++ b/packages/react-native-callingx/example/ios/Podfile @@ -0,0 +1,37 @@ +ENV['RCT_NEW_ARCH_ENABLED'] = '1' + +# Resolve react_native_pods.rb with node to allow for hoisting +require Pod::Executable.execute_command('node', ['-p', + 'require.resolve( + "react-native/scripts/react_native_pods.rb", + {paths: [process.argv[1]]}, + )', __dir__]).strip + +platform :ios, min_ios_version_supported +prepare_react_native_project! + +linkage = ENV['USE_FRAMEWORKS'] +if linkage != nil + Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green + use_frameworks! :linkage => linkage.to_sym +end + +target 'CallingxExample' do + config = use_native_modules! + + use_react_native!( + :path => config[:reactNativePath], + # An absolute path to your application root. + :app_path => "#{Pod::Config.instance.installation_root}/.." + ) + + post_install do |installer| + # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 + react_native_post_install( + installer, + config[:reactNativePath], + :mac_catalyst_enabled => false, + # :ccache_enabled => true + ) + end +end diff --git a/packages/react-native-callingx/example/jest.config.js b/packages/react-native-callingx/example/jest.config.js new file mode 100644 index 0000000000..8eb675e9bc --- /dev/null +++ b/packages/react-native-callingx/example/jest.config.js @@ -0,0 +1,3 @@ +module.exports = { + preset: 'react-native', +}; diff --git a/packages/react-native-callingx/example/metro.config.js b/packages/react-native-callingx/example/metro.config.js new file mode 100644 index 0000000000..2da198e82c --- /dev/null +++ b/packages/react-native-callingx/example/metro.config.js @@ -0,0 +1,16 @@ +const path = require('path'); +const { getDefaultConfig } = require('@react-native/metro-config'); +const { withMetroConfig } = require('react-native-monorepo-config'); + +const root = path.resolve(__dirname, '..'); + +/** + * Metro configuration + * https://facebook.github.io/metro/docs/configuration + * + * @type {import('metro-config').MetroConfig} + */ +module.exports = withMetroConfig(getDefaultConfig(__dirname), { + root, + dirname: __dirname, +}); diff --git a/packages/react-native-callingx/example/package-lock.json b/packages/react-native-callingx/example/package-lock.json new file mode 100644 index 0000000000..a43dc4e62a --- /dev/null +++ b/packages/react-native-callingx/example/package-lock.json @@ -0,0 +1,7016 @@ +{ + "name": "react-native-callingx-example", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "react-native-callingx-example", + "version": "0.0.1", + "dependencies": { + "react": "19.1.0", + "react-native": "0.81.1", + "react-native-callingx": "file:.." + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", + "@react-native-community/cli": "20.0.0", + "@react-native-community/cli-platform-android": "20.0.0", + "@react-native-community/cli-platform-ios": "20.0.0", + "@react-native/babel-preset": "0.81.1", + "@react-native/metro-config": "0.81.1", + "@react-native/typescript-config": "0.81.1", + "@types/react": "^19.1.0", + "react-native-builder-bob": "^0.40.15", + "react-native-monorepo-config": "^0.1.9" + }, + "engines": { + "node": ">=20" + } + }, + "..": { + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@commitlint/config-conventional": "^19.8.1", + "@eslint/compat": "^1.3.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.35.0", + "@react-native-community/cli": "20.0.1", + "@react-native/babel-preset": "0.81.1", + "@react-native/eslint-config": "^0.81.1", + "@release-it/conventional-changelog": "^10.0.1", + "@types/jest": "^29.5.14", + "@types/react": "^19.1.0", + "commitlint": "^19.8.1", + "del-cli": "^6.0.0", + "eslint": "^9.35.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "jest": "^29.7.0", + "lefthook": "^2.0.3", + "prettier": "^3.4.2", + "react": "19.1.0", + "react-native": "0.81.1", + "react-native-builder-bob": "^0.40.15", + "release-it": "^19.0.4", + "turbo": "^2.5.6", + "typescript": "^5.9.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/@ark/schema": { + "version": "0.55.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@ark/util": "0.55.0" + } + }, + "node_modules/@ark/util": { + "version": "0.55.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.3", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-flow": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.4", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.28.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-strict-mode": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.5", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.4", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.28.5", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.5", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.28.5", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.4", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.4", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse--for-generate-function-map": { + "name": "@babel/traverse", + "version": "7.28.5", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "devOptional": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@react-native-community/cli": { + "version": "20.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-clean": "20.0.0", + "@react-native-community/cli-config": "20.0.0", + "@react-native-community/cli-doctor": "20.0.0", + "@react-native-community/cli-server-api": "20.0.0", + "@react-native-community/cli-tools": "20.0.0", + "@react-native-community/cli-types": "20.0.0", + "chalk": "^4.1.2", + "commander": "^9.4.1", + "deepmerge": "^4.3.0", + "execa": "^5.0.0", + "find-up": "^5.0.0", + "fs-extra": "^8.1.0", + "graceful-fs": "^4.1.3", + "prompts": "^2.4.2", + "semver": "^7.5.2" + }, + "bin": { + "rnc-cli": "build/bin.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native-community/cli-clean": { + "version": "20.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "20.0.0", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2" + } + }, + "node_modules/@react-native-community/cli-config": { + "version": "20.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "20.0.0", + "chalk": "^4.1.2", + "cosmiconfig": "^9.0.0", + "deepmerge": "^4.3.0", + "fast-glob": "^3.3.2", + "joi": "^17.2.1" + } + }, + "node_modules/@react-native-community/cli-config-android": { + "version": "20.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "20.0.0", + "chalk": "^4.1.2", + "fast-glob": "^3.3.2", + "fast-xml-parser": "^4.4.1" + } + }, + "node_modules/@react-native-community/cli-config-apple": { + "version": "20.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "20.0.0", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-glob": "^3.3.2" + } + }, + "node_modules/@react-native-community/cli-doctor": { + "version": "20.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-config": "20.0.0", + "@react-native-community/cli-platform-android": "20.0.0", + "@react-native-community/cli-platform-apple": "20.0.0", + "@react-native-community/cli-platform-ios": "20.0.0", + "@react-native-community/cli-tools": "20.0.0", + "chalk": "^4.1.2", + "command-exists": "^1.2.8", + "deepmerge": "^4.3.0", + "envinfo": "^7.13.0", + "execa": "^5.0.0", + "node-stream-zip": "^1.9.1", + "ora": "^5.4.1", + "semver": "^7.5.2", + "wcwidth": "^1.0.1", + "yaml": "^2.2.1" + } + }, + "node_modules/@react-native-community/cli-doctor/node_modules/semver": { + "version": "7.7.3", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-native-community/cli-platform-android": { + "version": "20.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-config-android": "20.0.0", + "@react-native-community/cli-tools": "20.0.0", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "logkitty": "^0.7.1" + } + }, + "node_modules/@react-native-community/cli-platform-apple": { + "version": "20.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-config-apple": "20.0.0", + "@react-native-community/cli-tools": "20.0.0", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "fast-xml-parser": "^4.4.1" + } + }, + "node_modules/@react-native-community/cli-platform-ios": { + "version": "20.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-platform-apple": "20.0.0" + } + }, + "node_modules/@react-native-community/cli-server-api": { + "version": "20.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native-community/cli-tools": "20.0.0", + "body-parser": "^1.20.3", + "compression": "^1.7.1", + "connect": "^3.6.5", + "errorhandler": "^1.5.1", + "nocache": "^3.0.1", + "open": "^6.2.0", + "pretty-format": "^29.7.0", + "serve-static": "^1.13.1", + "ws": "^6.2.3" + } + }, + "node_modules/@react-native-community/cli-tools": { + "version": "20.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@vscode/sudo-prompt": "^9.0.0", + "appdirsjs": "^1.2.4", + "chalk": "^4.1.2", + "execa": "^5.0.0", + "find-up": "^5.0.0", + "launch-editor": "^2.9.1", + "mime": "^2.4.1", + "ora": "^5.4.1", + "prompts": "^2.4.2", + "semver": "^7.5.2" + } + }, + "node_modules/@react-native-community/cli-tools/node_modules/semver": { + "version": "7.7.3", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-native-community/cli-types": { + "version": "20.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "joi": "^17.2.1" + } + }, + "node_modules/@react-native-community/cli/node_modules/semver": { + "version": "7.7.3", + "devOptional": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-native/assets-registry": { + "version": "0.81.1", + "license": "MIT", + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.81.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@react-native/codegen": "0.81.1" + }, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/babel-preset": { + "version": "0.81.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.81.1", + "babel-plugin-syntax-hermes-parser": "0.29.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.81.1", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.29.1", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/community-cli-plugin": { + "version": "0.81.1", + "license": "MIT", + "dependencies": { + "@react-native/dev-middleware": "0.81.1", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "metro": "^0.83.1", + "metro-config": "^0.83.1", + "metro-core": "^0.83.1", + "semver": "^7.1.3" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@react-native-community/cli": "*", + "@react-native/metro-config": "*" + }, + "peerDependenciesMeta": { + "@react-native-community/cli": { + "optional": true + }, + "@react-native/metro-config": { + "optional": true + } + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/semver": { + "version": "7.7.3", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-native/debugger-frontend": { + "version": "0.81.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.81.1", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.81.1", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^6.2.3" + }, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/open": { + "version": "7.4.2", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.81.1", + "license": "MIT", + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.81.1", + "license": "MIT", + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/metro-babel-transformer": { + "version": "0.81.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@react-native/babel-preset": "0.81.1", + "hermes-parser": "0.29.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/metro-config": { + "version": "0.81.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@react-native/js-polyfills": "0.81.1", + "@react-native/metro-babel-transformer": "0.81.1", + "metro-config": "^0.83.1", + "metro-runtime": "^0.83.1" + }, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.81.1", + "license": "MIT" + }, + "node_modules/@react-native/typescript-config": { + "version": "0.81.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.81.1", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@types/react": "^19.1.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "devOptional": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "devOptional": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "24.10.1", + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.7", + "devOptional": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "license": "MIT" + }, + "node_modules/@vscode/sudo-prompt": { + "version": "9.3.1", + "devOptional": true, + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "license": "MIT" + }, + "node_modules/ansi-fragments": { + "version": "0.2.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "colorette": "^1.0.7", + "slice-ansi": "^2.0.0", + "strip-ansi": "^5.0.0" + } + }, + "node_modules/ansi-fragments/node_modules/ansi-regex": { + "version": "4.1.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-fragments/node_modules/strip-ansi": { + "version": "5.2.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/appdirsjs": { + "version": "1.2.7", + "devOptional": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "devOptional": true, + "license": "Python-2.0" + }, + "node_modules/arkregex": { + "version": "0.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@ark/util": "0.55.0" + } + }, + "node_modules/arktype": { + "version": "2.1.27", + "dev": true, + "license": "MIT", + "dependencies": { + "@ark/schema": "0.55.0", + "@ark/util": "0.55.0", + "arkregex": "0.0.3" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "license": "MIT" + }, + "node_modules/astral-regex": { + "version": "1.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-generator-function": { + "version": "1.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.29.1", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.29.1" + } + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.31", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "devOptional": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "devOptional": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "devOptional": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-edge-launcher": { + "version": "0.2.0", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "devOptional": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "devOptional": true, + "license": "MIT" + }, + "node_modules/command-exists": { + "version": "1.2.9", + "devOptional": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "9.5.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "devOptional": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compressible/node_modules/mime-db": { + "version": "1.54.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "devOptional": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/content-type": { + "version": "1.0.5", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.47.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "devOptional": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "devOptional": true, + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.19", + "devOptional": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dedent": { + "version": "0.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "devOptional": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/del": { + "version": "6.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.259", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.20.0", + "devOptional": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/errorhandler": { + "version": "1.5.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.7", + "escape-html": "~1.0.3" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "4.5.3", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^1.1.1" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "devOptional": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "1.5.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flow-enums-runtime": { + "version": "0.0.6", + "license": "MIT" + }, + "node_modules/fresh": { + "version": "0.5.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "0.1.2", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "devOptional": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "async-generator-function": "^1.0.0", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "devOptional": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "devOptional": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.29.1", + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.29.1", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.29.1" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "1.2.1", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/invariant": { + "version": "2.2.4", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "devOptional": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-git-dirty": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^4.0.3", + "is-git-repository": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-git-dirty/node_modules/execa": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/is-git-dirty/node_modules/get-stream": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-git-dirty/node_modules/human-signals": { + "version": "1.1.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/is-git-repository": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^4.0.3", + "is-absolute": "^1.0.0" + } + }, + "node_modules/is-git-repository/node_modules/execa": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/is-git-repository/node_modules/get-stream": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-git-repository/node_modules/human-signals": { + "version": "1.1.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "devOptional": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ci-info": { + "version": "3.9.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi": { + "version": "17.13.3", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsc-safe-url": { + "version": "0.2.4", + "license": "0BSD" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "devOptional": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "devOptional": true, + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/launch-editor": { + "version": "2.12.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.3" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "devOptional": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "devOptional": true, + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/logkitty": { + "version": "0.7.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-fragments": "^0.2.1", + "dayjs": "^1.8.15", + "yargs": "^15.1.0" + }, + "bin": { + "logkitty": "bin/logkitty.js" + } + }, + "node_modules/logkitty/node_modules/find-up": { + "version": "4.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/locate-path": { + "version": "5.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/p-limit": { + "version": "2.3.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/logkitty/node_modules/p-locate": { + "version": "4.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logkitty/node_modules/yargs": { + "version": "15.4.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/metro": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "@babel/types": "^7.25.2", + "accepts": "^1.3.7", + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "error-stack-parser": "^2.0.6", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "hermes-parser": "0.32.0", + "image-size": "^1.0.2", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "jsc-safe-url": "^0.2.2", + "lodash.throttle": "^4.1.1", + "metro-babel-transformer": "0.83.3", + "metro-cache": "0.83.3", + "metro-cache-key": "0.83.3", + "metro-config": "0.83.3", + "metro-core": "0.83.3", + "metro-file-map": "0.83.3", + "metro-resolver": "0.83.3", + "metro-runtime": "0.83.3", + "metro-source-map": "0.83.3", + "metro-symbolicate": "0.83.3", + "metro-transform-plugins": "0.83.3", + "metro-transform-worker": "0.83.3", + "mime-types": "^2.1.27", + "nullthrows": "^1.1.1", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "throat": "^5.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-babel-transformer": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "hermes-parser": "0.32.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.32.0", + "license": "MIT" + }, + "node_modules/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.32.0", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.32.0" + } + }, + "node_modules/metro-cache": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "exponential-backoff": "^3.1.1", + "flow-enums-runtime": "^0.0.6", + "https-proxy-agent": "^7.0.5", + "metro-core": "0.83.3" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-cache-key": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-config": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "connect": "^3.6.5", + "flow-enums-runtime": "^0.0.6", + "jest-validate": "^29.7.0", + "metro": "0.83.3", + "metro-cache": "0.83.3", + "metro-core": "0.83.3", + "metro-runtime": "0.83.3", + "yaml": "^2.6.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-core": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.83.3" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-file-map": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "fb-watchman": "^2.0.0", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-minify-terser": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "terser": "^5.15.0" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-resolver": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-runtime": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-source-map": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-symbolicate": "0.83.3", + "nullthrows": "^1.1.1", + "ob1": "0.83.3", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-symbolicate": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-source-map": "0.83.3", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-transform-plugins": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "flow-enums-runtime": "^0.0.6", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-transform-worker": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "metro": "0.83.3", + "metro-babel-transformer": "0.83.3", + "metro-cache": "0.83.3", + "metro-cache-key": "0.83.3", + "metro-minify-terser": "0.83.3", + "metro-source-map": "0.83.3", + "metro-transform-plugins": "0.83.3", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro/node_modules/hermes-estree": { + "version": "0.32.0", + "license": "MIT" + }, + "node_modules/metro/node_modules/hermes-parser": { + "version": "0.32.0", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.32.0" + } + }, + "node_modules/metro/node_modules/ws": { + "version": "7.5.10", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "devOptional": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nocache": { + "version": "3.0.4", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "license": "MIT" + }, + "node_modules/node-stream-zip": { + "version": "1.15.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/antelle" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/ob1": { + "version": "0.83.3", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "devOptional": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "6.4.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/open/node_modules/is-wsl": { + "version": "1.1.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "devOptional": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/promise": { + "version": "8.3.0", + "license": "MIT", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "devOptional": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "devOptional": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue": { + "version": "6.0.2", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "devOptional": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "19.1.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-devtools-core": { + "version": "6.1.5", + "license": "MIT", + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.10", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "license": "MIT" + }, + "node_modules/react-native": { + "version": "0.81.1", + "license": "MIT", + "dependencies": { + "@jest/create-cache-key-function": "^29.7.0", + "@react-native/assets-registry": "0.81.1", + "@react-native/codegen": "0.81.1", + "@react-native/community-cli-plugin": "0.81.1", + "@react-native/gradle-plugin": "0.81.1", + "@react-native/js-polyfills": "0.81.1", + "@react-native/normalize-colors": "0.81.1", + "@react-native/virtualized-lists": "0.81.1", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "babel-jest": "^29.7.0", + "babel-plugin-syntax-hermes-parser": "0.29.1", + "base64-js": "^1.5.1", + "commander": "^12.0.0", + "flow-enums-runtime": "^0.0.6", + "glob": "^7.1.1", + "invariant": "^2.2.4", + "jest-environment-node": "^29.7.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.83.1", + "metro-source-map": "^0.83.1", + "nullthrows": "^1.1.1", + "pretty-format": "^29.7.0", + "promise": "^8.3.0", + "react-devtools-core": "^6.1.5", + "react-refresh": "^0.14.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.26.0", + "semver": "^7.1.3", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.3", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@types/react": "^19.1.0", + "react": "^19.1.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-native-builder-bob": { + "version": "0.40.15", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-transform-flow-strip-types": "^7.26.5", + "@babel/plugin-transform-strict-mode": "^7.24.7", + "@babel/preset-env": "^7.25.2", + "@babel/preset-react": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "arktype": "^2.1.15", + "babel-plugin-syntax-hermes-parser": "^0.28.0", + "browserslist": "^4.20.4", + "cross-spawn": "^7.0.3", + "dedent": "^0.7.0", + "del": "^6.1.1", + "escape-string-regexp": "^4.0.0", + "fs-extra": "^10.1.0", + "glob": "^8.0.3", + "is-git-dirty": "^2.0.1", + "json5": "^2.2.1", + "kleur": "^4.1.4", + "prompts": "^2.4.2", + "react-native-monorepo-config": "^0.1.8", + "which": "^2.0.2", + "yargs": "^17.5.1" + }, + "bin": { + "bob": "bin/bob" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >= 23.4.0" + } + }, + "node_modules/react-native-builder-bob/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.28.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-parser": "0.28.1" + } + }, + "node_modules/react-native-builder-bob/node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/react-native-builder-bob/node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/react-native-builder-bob/node_modules/glob": { + "version": "8.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-native-builder-bob/node_modules/hermes-estree": { + "version": "0.28.1", + "dev": true, + "license": "MIT" + }, + "node_modules/react-native-builder-bob/node_modules/hermes-parser": { + "version": "0.28.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.28.1" + } + }, + "node_modules/react-native-builder-bob/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/react-native-builder-bob/node_modules/kleur": { + "version": "4.1.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react-native-builder-bob/node_modules/minimatch": { + "version": "5.1.6", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native-callingx": { + "resolved": "..", + "link": true + }, + "node_modules/react-native-monorepo-config": { + "version": "0.1.10", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^5.0.0", + "fast-glob": "^3.3.3" + } + }, + "node_modules/react-native-monorepo-config/node_modules/escape-string-regexp": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-native/node_modules/commander": { + "version": "12.1.0", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/react-native/node_modules/semver": { + "version": "7.7.3", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "devOptional": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "devOptional": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "devOptional": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "devOptional": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "devOptional": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "devOptional": true, + "license": "ISC" + }, + "node_modules/resolve": { + "version": "1.22.11", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "devOptional": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "devOptional": true, + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.26.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/serialize-error": { + "version": "2.1.0", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "devOptional": true, + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "devOptional": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "devOptional": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "2.1.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "3.2.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "1.9.3", + "devOptional": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.3", + "devOptional": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.5.7", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "license": "MIT" + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strnum": { + "version": "1.1.2", + "devOptional": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/terser": { + "version": "5.44.1", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/throat": { + "version": "5.0.0", + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.7.1", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "devOptional": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "devOptional": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vlq": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/walker": { + "version": "1.0.8", + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "devOptional": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "devOptional": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "devOptional": true, + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "devOptional": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "6.2.3", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "devOptional": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.1", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "devOptional": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yargs/node_modules/cliui": { + "version": "8.0.1", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/wrap-ansi": { + "version": "7.0.0", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yargs/node_modules/y18n": { + "version": "5.0.8", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/react-native-callingx/example/package.json b/packages/react-native-callingx/example/package.json new file mode 100644 index 0000000000..b8d5ee8f1d --- /dev/null +++ b/packages/react-native-callingx/example/package.json @@ -0,0 +1,34 @@ +{ + "name": "react-native-callingx-example", + "version": "0.0.1", + "private": true, + "scripts": { + "android": "react-native run-android", + "ios": "react-native run-ios", + "start": "react-native start", + "build:android": "react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"", + "build:ios": "react-native build-ios --mode Debug" + }, + "dependencies": { + "react": "19.1.0", + "react-native": "0.81.1", + "react-native-callingx": "file:.." + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", + "@babel/runtime": "^7.25.0", + "@react-native-community/cli": "20.0.0", + "@react-native-community/cli-platform-android": "20.0.0", + "@react-native-community/cli-platform-ios": "20.0.0", + "@react-native/babel-preset": "0.81.1", + "@react-native/metro-config": "0.81.1", + "@react-native/typescript-config": "0.81.1", + "@types/react": "^19.1.0", + "react-native-builder-bob": "^0.40.15", + "react-native-monorepo-config": "^0.1.9" + }, + "engines": { + "node": ">=20" + } +} diff --git a/packages/react-native-callingx/example/react-native.config.js b/packages/react-native-callingx/example/react-native.config.js new file mode 100644 index 0000000000..59d969820e --- /dev/null +++ b/packages/react-native-callingx/example/react-native.config.js @@ -0,0 +1,21 @@ +const path = require('path'); +const pkg = require('../package.json'); + +module.exports = { + project: { + ios: { + automaticPodsInstallation: true, + }, + }, + dependencies: { + [pkg.name]: { + root: path.join(__dirname, '..'), + platforms: { + // Codegen script incorrectly fails without this + // So we explicitly specify the platforms with empty object + ios: {}, + android: {}, + }, + }, + }, +}; diff --git a/packages/react-native-callingx/example/src/App.tsx b/packages/react-native-callingx/example/src/App.tsx new file mode 100644 index 0000000000..84e1040e26 --- /dev/null +++ b/packages/react-native-callingx/example/src/App.tsx @@ -0,0 +1,20 @@ +import { Text, View, StyleSheet } from 'react-native'; +import { multiply } from 'react-native-callingx'; + +const result = multiply(3, 7); + +export default function App() { + return ( + + Result: {result} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, +}); diff --git a/packages/react-native-callingx/ios/Callingx.h b/packages/react-native-callingx/ios/Callingx.h new file mode 100644 index 0000000000..90b635a903 --- /dev/null +++ b/packages/react-native-callingx/ios/Callingx.h @@ -0,0 +1,50 @@ +// +// CallingModule.h +// POCCallingX +// +// Created by Artem Grintsevich on 17/11/2025. +// + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface Callingx : NativeCallingxSpecBase + +@property (nonatomic, strong) CXCallController *callKeepCallController; +@property (nonatomic, strong) CXProvider *callKeepProvider; + ++ (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options NS_AVAILABLE_IOS(9_0); + ++ (BOOL)application:(UIApplication *)application +continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler; + ++ (void)reportNewIncomingCall:(NSString *)uuidString + handle:(NSString *)handle + handleType:(NSString *)handleType + hasVideo:(BOOL)hasVideo + localizedCallerName:(NSString * _Nullable)localizedCallerName + supportsHolding:(BOOL)supportsHolding + supportsDTMF:(BOOL)supportsDTMF + supportsGrouping:(BOOL)supportsGrouping + supportsUngrouping:(BOOL)supportsUngrouping + fromPushKit:(BOOL)fromPushKit + payload:(NSDictionary * _Nullable)payload + withCompletionHandler:(void (^_Nullable)(void))completion; + ++ (void)endCallWithUUID:(NSString *)uuidString + reason:(int)reason; + ++ (BOOL)isCallActive:(NSString *)uuidString; + ++ (void)setup:(NSDictionary *)options; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-callingx/ios/Callingx.mm b/packages/react-native-callingx/ios/Callingx.mm new file mode 100644 index 0000000000..69c396490e --- /dev/null +++ b/packages/react-native-callingx/ios/Callingx.mm @@ -0,0 +1,1080 @@ +// +// Callingx.m +// POCCallingX +// +// Created by Artem Grintsevich on 17/11/2025. +// + +#import "Callingx.h" +#import +#import + +#import +#import +#import "UUIDStorage.h" + +#ifdef DEBUG +static int const OUTGOING_CALL_WAKEUP_DELAY = 10; +#else +static int const OUTGOING_CALL_WAKEUP_DELAY = 5; +#endif + +static NSString *const CallingxDidReceiveStartCallAction = @"didReceiveStartCallAction"; +static NSString *const CallingxPerformAnswerCallAction = @"answerCall"; +static NSString *const CallingxPerformEndCallAction = @"endCall"; +static NSString *const CallingxDidToggleHoldAction = @"didToggleHoldCallAction"; +static NSString *const CallingxDidPerformSetMutedCallAction = @"didPerformSetMutedCallAction"; +static NSString *const CallingxDidChangeAudioRoute = @"didChangeAudioRoute"; +static NSString *const CallingxDidLoadWithEvents = @"didLoadWithEvents"; +static NSString *const CallingxDidDisplayIncomingCall = @"didDisplayIncomingCall"; + +static NSString *const RNCallKeepHandleStartCallNotification = @"RNCallKeepHandleStartCallNotification"; +static NSString *const RNCallKeepDidActivateAudioSession = @"RNCallKeepDidActivateAudioSession"; +static NSString *const RNCallKeepDidDeactivateAudioSession = @"RNCallKeepDidDeactivateAudioSession"; +static NSString *const RNCallKeepPerformPlayDTMFCallAction = @"RNCallKeepDidPerformDTMFAction"; +static NSString *const RNCallKeepProviderReset = @"RNCallKeepProviderReset"; +static NSString *const RNCallKeepCheckReachability = @"RNCallKeepCheckReachability"; + +@implementation Callingx { + NSOperatingSystemVersion _version; + bool _isReachable; + bool _isInitialized; + NSMutableArray *_delayedEvents; +} + +static bool isSetupNatively; +static CXProvider *sharedProvider; +static UUIDStorage *uuidStorage; + +#pragma mark - Class Methods + ++ (id)allocWithZone:(NSZone *)zone { + static Callingx *sharedInstance = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedInstance = [super allocWithZone:zone]; + }); + return sharedInstance; +} + ++ (NSDictionary *)getSettings { + return [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"CallingxSettings"]; +} + ++ (void)initUUIDStorage { + if (uuidStorage == nil) { + uuidStorage = [[UUIDStorage alloc] init]; + NSLog(@"[Callingx] initUUIDStorage"); + } +} + ++ (void)initCallKitProvider { + if (sharedProvider == nil) { + NSDictionary *settings = [self getSettings]; + if (settings != nil) { + sharedProvider = [[CXProvider alloc] initWithConfiguration:[Callingx getProviderConfiguration:settings]]; + NSLog(@"[Callingx] initCallKitProvider"); + } + } +} + ++ (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary *)settings { +#ifdef DEBUG + NSLog(@"[Callingx][getProviderConfiguration]"); +#endif + CXProviderConfiguration *providerConfiguration = [[CXProviderConfiguration alloc] init]; + providerConfiguration.supportsVideo = YES; + providerConfiguration.maximumCallGroups = 3; + providerConfiguration.maximumCallsPerCallGroup = 1; + providerConfiguration.supportedHandleTypes = [Callingx getSupportedHandleTypes:settings[@"handleType"]]; + + if (settings[@"supportsVideo"]) { + providerConfiguration.supportsVideo = [settings[@"supportsVideo"] boolValue]; + } + if (settings[@"maximumCallGroups"]) { + providerConfiguration.maximumCallGroups = [settings[@"maximumCallGroups"] integerValue]; + } + if (settings[@"maximumCallsPerCallGroup"]) { + providerConfiguration.maximumCallsPerCallGroup = [settings[@"maximumCallsPerCallGroup"] integerValue]; + } + if (settings[@"imageName"]) { + providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:settings[@"imageName"]]); + } + if (settings[@"ringtoneSound"]) { + providerConfiguration.ringtoneSound = settings[@"ringtoneSound"]; + } + if (@available(iOS 11.0, *)) { + if (settings[@"includesCallsInRecents"]) { + providerConfiguration.includesCallsInRecents = [settings[@"includesCallsInRecents"] boolValue]; + } + } + return providerConfiguration; +} + ++ (void)reportNewIncomingCall:(NSString *)callId + handle:(NSString *)handle + handleType:(NSString *)handleType + hasVideo:(BOOL)hasVideo + localizedCallerName:(NSString *_Nullable)localizedCallerName + supportsHolding:(BOOL)supportsHolding + supportsDTMF:(BOOL)supportsDTMF + supportsGrouping:(BOOL)supportsGrouping + supportsUngrouping:(BOOL)supportsUngrouping + fromPushKit:(BOOL)fromPushKit + payload:(NSDictionary *_Nullable)payload + withCompletionHandler:(void (^_Nullable)(void))completion { +#ifdef DEBUG + NSLog(@"[Callingx][reportNewIncomingCall] callId = %@", callId); +#endif + + [Callingx initUUIDStorage]; + [Callingx initCallKitProvider]; + + if ([uuidStorage containsCid:callId]) { + NSLog(@"[Callingx][reportNewIncomingCall] callId already exists"); + return; + } + + CXHandleType _handleType = [Callingx getHandleType:handleType]; + NSUUID *uuid = [uuidStorage getOrCreateUUIDForCid:callId]; + CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + callUpdate.remoteHandle = [[CXHandle alloc] initWithType:_handleType value:handle]; + callUpdate.supportsHolding = supportsHolding; + callUpdate.supportsDTMF = supportsDTMF; + callUpdate.supportsGrouping = supportsGrouping; + callUpdate.supportsUngrouping = supportsUngrouping; + callUpdate.hasVideo = hasVideo; + callUpdate.localizedCallerName = localizedCallerName; + + [sharedProvider + reportNewIncomingCallWithUUID:uuid + update:callUpdate + completion:^(NSError *_Nullable error) { + NSLog(@"[Callingx][reportNewIncomingCall] callId = %@, error = %@", callId, error); + Callingx *callKeep = [Callingx allocWithZone:nil]; + [callKeep + sendEventWithNameWrapper: + CallingxDidDisplayIncomingCall + body:@{ + @"error" : error && error.localizedDescription + ? error.localizedDescription + : @"", + @"errorCode" : error + ? [callKeep getIncomingCallErrorCode: error] + : @"", + @"callId" : callId, + @"handle" : handle, + @"localizedCallerName" : localizedCallerName + ? localizedCallerName + : @"", + @"hasVideo" : hasVideo + ? @"1" + : @"0", + @"supportsHolding" : supportsHolding + ? @"1" + : @"0", + @"supportsDTMF" : supportsDTMF + ? @"1" + : @"0", + @"supportsGrouping" : supportsGrouping + ? @"1" + : @"0", + @"supportsUngrouping" : supportsUngrouping + ? @"1" + : @"0", + @"fromPushKit" : fromPushKit + ? @"1" + : @"0", + @"payload" : payload + ? payload + : @"", + }]; + if (error == nil) { + NSLog(@"[Callingx][reportNewIncomingCall] success callId = %@", callId); + } + if (completion != nil) { + NSLog(@"[Callingx][reportNewIncomingCall] completion"); + completion(); + } + }]; +} + +//+ (BOOL)application:(UIApplication *)application +//continueUserActivity:(NSUserActivity *)userActivity +// restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler +//{ +//#ifdef DEBUG +// NSLog(@"[Callingx][application:continueUserActivity]"); +//#endif +// INInteraction *interaction = userActivity.interaction; +// INPerson *contact; +// NSString *handle; +// BOOL isAudioCall; +// BOOL isVideoCall; +// +// // HACK TO AVOID XCODE 10 COMPILE CRASH +// // REMOVE ON NEXT MAJOR RELEASE OF RNCALLKIT +//#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 +// //XCode 11 +// // iOS 13 returns an INStartCallIntent userActivity type +// if (@available(iOS 13, *)) { +// INStartCallIntent *intent = (INStartCallIntent*)interaction.intent; +// // callCapability is not available on iOS > 13.2, but it is in 13.1 weirdly... +// if ([intent respondsToSelector:@selector(callCapability)]) { +// isAudioCall = intent.callCapability == INCallCapabilityAudioCall; +// isVideoCall = intent.callCapability == INCallCapabilityVideoCall; +// } else { +// isAudioCall = [userActivity.activityType isEqualToString:INStartAudioCallIntentIdentifier]; +// isVideoCall = [userActivity.activityType isEqualToString:INStartVideoCallIntentIdentifier]; +// } +// } else { +//#endif +// // XCode 10 and below +// isAudioCall = [userActivity.activityType isEqualToString:INStartAudioCallIntentIdentifier]; +// isVideoCall = [userActivity.activityType isEqualToString:INStartVideoCallIntentIdentifier]; +// // HACK TO AVOID XCODE 10 COMPILE CRASH +// // REMOVE ON NEXT MAJOR RELEASE OF RNCALLKIT +//#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000 +// } +//#endif +// +// if (isAudioCall) { +// INStartAudioCallIntent *startAudioCallIntent = (INStartAudioCallIntent *)interaction.intent; +// contact = [startAudioCallIntent.contacts firstObject]; +// } else if (isVideoCall) { +// INStartVideoCallIntent *startVideoCallIntent = (INStartVideoCallIntent *)interaction.intent; +// contact = [startVideoCallIntent.contacts firstObject]; +// } +// +// if (contact != nil) { +// handle = contact.personHandle.value; +// } +// +// if (handle != nil && handle.length > 0 ){ +// NSDictionary *userInfo = @{ +// @"handle": handle, +// @"video": @(isVideoCall) +// }; +// +// RNCallKeep *callKeep = [RNCallKeep allocWithZone: nil]; +// [callKeep sendEventWithNameWrapper:CallingxDidReceiveStartCallAction body:userInfo]; +// return YES; +// } +// return NO; +//} + ++ (NSSet *)getSupportedHandleTypes:(id)handleType { + if (handleType) { + if ([handleType isKindOfClass:[NSArray class]]) { + NSSet *types = [NSSet set]; + + for (NSString *type in handleType) { + types = [types setByAddingObject: [NSNumber numberWithInteger:[Callingx getHandleType:type]]]; + } + + return types; + } else { + CXHandleType _handleType = [Callingx getHandleType:handleType]; + + return [NSSet setWithObjects:[NSNumber numberWithInteger:_handleType], nil]; + } + } else { + return [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypePhoneNumber], nil]; + } +} + ++ (CXHandleType)getHandleType:(NSString *)handleType { + if ([handleType isEqualToString:@"generic"]) { + return CXHandleTypeGeneric; + } else if ([handleType isEqualToString:@"number"]) { + return CXHandleTypePhoneNumber; + } else if ([handleType isEqualToString:@"phone"]) { + return CXHandleTypePhoneNumber; + } else if ([handleType isEqualToString:@"email"]) { + return CXHandleTypeEmailAddress; + } else { + return CXHandleTypeGeneric; + } +} + ++ (NSString *)getAudioOutput { + @try { + NSArray *outputs = + [AVAudioSession sharedInstance].currentRoute.outputs; + if (outputs != nil && outputs.count > 0) { + return outputs[0].portType; + } + } @catch (NSException *error) { + NSLog(@"getAudioOutput error :%@", [error description]); + } + + return nil; +} + ++ (void)setup:(NSDictionary *)options { + Callingx *callKeep = [Callingx allocWithZone: nil]; + [callKeep setup:options]; + isSetupNatively = YES; +} + ++ (void)endCall:(NSString *)callId reason:(int)reason { +#ifdef DEBUG + NSLog(@"[Callingx][endCall] callId = %@ reason = %d", callId, reason); +#endif + NSUUID *uuid = [uuidStorage getUUIDForCid:callId]; + if (uuid == nil) { + return; + } + + switch (reason) { + case 1: + [sharedProvider reportCallWithUUID:uuid + endedAtDate:[NSDate date] + reason:CXCallEndedReasonFailed]; + break; + case 2: + [sharedProvider reportCallWithUUID:uuid + endedAtDate:[NSDate date] + reason:CXCallEndedReasonRemoteEnded]; + break; + case 3: + [sharedProvider reportCallWithUUID:uuid + endedAtDate:[NSDate date] + reason:CXCallEndedReasonUnanswered]; + break; + case 4: + [sharedProvider reportCallWithUUID:uuid + endedAtDate:[NSDate date] + reason:CXCallEndedReasonAnsweredElsewhere]; + break; + case 5: + [sharedProvider reportCallWithUUID:uuid + endedAtDate:[NSDate date] + reason:CXCallEndedReasonDeclinedElsewhere]; + break; + default: + break; + } +} + ++ (BOOL)requiresMainQueueSetup { + return YES; +} + ++ (NSString *)moduleName { + return @"Callingx"; +} + +#pragma mark - Instance methods + +- (instancetype)init { +#ifdef DEBUG + NSLog(@"[Callingx][init]"); +#endif + if (self = [super init]) { + _isReachable = NO; + _isInitialized = NO; + + if (_delayedEvents == nil) + _delayedEvents = [NSMutableArray array]; + + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(onAudioRouteChange:) + name:AVAudioSessionRouteChangeNotification + object:nil]; + + // Init provider directly, in case of an app killed and when we've already + // stored our settings + [Callingx initCallKitProvider]; + [Callingx initUUIDStorage]; + + self.callKeepProvider = sharedProvider; + [self.callKeepProvider setDelegate:nil queue:nil]; + [self.callKeepProvider setDelegate:self queue:nil]; + } + return self; +} + +- (void)dealloc { +#ifdef DEBUG + NSLog(@"[Callingx][dealloc]"); +#endif + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + if (self.callKeepProvider != nil) { + [self.callKeepProvider setDelegate:nil queue:nil]; + [self.callKeepProvider invalidate]; + } + sharedProvider = nil; + _isInitialized = NO; + _isReachable = NO; +} + +// Override method of RCTEventEmitter +- (NSArray *)supportedEvents { + return @[ + CallingxDidReceiveStartCallAction, + CallingxPerformAnswerCallAction, + CallingxPerformEndCallAction, + CallingxDidPerformSetMutedCallAction, + CallingxDidToggleHoldAction, + CallingxDidLoadWithEvents, + CallingxDidChangeAudioRoute, + CallingxDidDisplayIncomingCall, + RNCallKeepDidActivateAudioSession, + RNCallKeepDidDeactivateAudioSession, + RNCallKeepPerformPlayDTMFCallAction, + RNCallKeepProviderReset, + RNCallKeepCheckReachability, + ]; +} + +//- (void)startObserving { +// NSLog(@"[Callingx][startObserving]"); +// _hasListeners = YES; +// if ([_delayedEvents count] > 0) { +//// [self sendEventWithName:CallingxDidLoadWithEvents body:_delayedEvents]; +// NSDictionary *dictionary = [ +// NSDictionary dictionaryWithObjectsAndKeys: +// CallingxDidLoadWithEvents, @"name", +// _delayedEvents, @"params", +// nil +// ]; +// [self emitOnNewEvent: dictionary]; +// } +//} +// +//- (void)stopObserving { +// _hasListeners = FALSE; +// +// // Fix for +// // https://github.com/react-native-webrtc/react-native-callkeep/issues/406 We +// // use Objective-C Key Value Coding(KVC) to sync _RTCEventEmitter_ +// // `_listenerCount`. +// @try { +// [self setValue:@0 forKey:@"_listenerCount"]; +// } @catch (NSException *e) { +// NSLog(@"[Callingx][stopObserving] exception: %@", e); +// NSLog(@"[Callingx][stopObserving] Callingx parent class " +// @"RTCEventEmitter might have a broken state."); +// NSLog(@"[Callingx][stopObserving] Please verify that the parent " +// @"RTCEventEmitter.m has iVar `_listenerCount`."); +// } +//} + +- (void)setSettings:(NSDictionary *)options { +#ifdef DEBUG + NSLog(@"[Callingx][setSettings] options = %@", options); +#endif + NSDictionary *settings = [[NSMutableDictionary alloc] initWithDictionary:options]; + // Store settings in NSUserDefault + [[NSUserDefaults standardUserDefaults] setObject:settings forKey:@"CallingxSettings"]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (void)configureAudioSession { +#ifdef DEBUG + NSLog(@"[Callingx][configureAudioSession] Activating audio session"); +#endif + + NSUInteger categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth | + AVAudioSessionCategoryOptionAllowBluetoothA2DP; + NSString *mode = AVAudioSessionModeDefault; + + NSDictionary *settings = [Callingx getSettings]; + if (settings && settings[@"audioSession"]) { + if (settings[@"audioSession"][@"categoryOptions"]) { + categoryOptions = [settings[@"audioSession"][@"categoryOptions"] integerValue]; + } + + if (settings[@"audioSession"][@"mode"]) { + mode = settings[@"audioSession"][@"mode"]; + } + } + + AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord + withOptions:categoryOptions + error:nil]; + + [audioSession setMode:mode error:nil]; + + double sampleRate = 44100.0; + [audioSession setPreferredSampleRate:sampleRate error:nil]; + + NSTimeInterval bufferDuration = .005; + [audioSession setPreferredIOBufferDuration:bufferDuration error:nil]; + [audioSession setActive:TRUE error:nil]; +} + +- (std::shared_ptr)getTurboModule: + (const facebook::react::ObjCTurboModule::InitParams &)params { + return std::make_shared(params); +} + +- (void)requestTransaction:(CXTransaction *)transaction { +#ifdef DEBUG + NSLog(@"[Callingx][requestTransaction] transaction = %@", transaction); +#endif + if (self.callKeepCallController == nil) { + self.callKeepCallController = [[CXCallController alloc] init]; + } + [self.callKeepCallController + requestTransaction:transaction + completion:^(NSError *_Nullable error) { + if (error != nil) { + NSLog(@"[Callingx][requestTransaction] Error requesting " + @"transaction (%@): (%@)", + transaction.actions, error); + } else { + NSLog(@"[Callingx][requestTransaction] Requested " + @"transaction successfully"); + + // CXStartCallAction + if ([[transaction.actions firstObject] isKindOfClass:[CXStartCallAction class]]) { + CXStartCallAction *startCallAction = [transaction.actions firstObject]; + CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + callUpdate.remoteHandle = startCallAction.handle; + callUpdate.hasVideo = startCallAction.video; + callUpdate.localizedCallerName = startCallAction.contactIdentifier; + callUpdate.supportsDTMF = NO; //configurable? + callUpdate.supportsHolding = NO; //configurable? + callUpdate.supportsGrouping = NO; //configurable? + callUpdate.supportsUngrouping = NO; //configurable? + [self.callKeepProvider reportCallWithUUID:startCallAction.callUUID updated:callUpdate]; + } + } + }]; +} + +- (void)sendEventWithNameWrapper:(NSString *)name body:(id)body { + NSLog(@"[Callingx] sendEventWithNameWrapper: %@", name); + + NSDictionary *dictionary = [NSDictionary + dictionaryWithObjectsAndKeys:name, @"eventName", body, @"params", nil]; + + if (_isInitialized) { + [self emitOnNewEvent: dictionary]; + } else { + [_delayedEvents addObject:dictionary]; + } +} + +- (void)onAudioRouteChange:(NSNotification *)notification { + NSDictionary *info = notification.userInfo; + NSInteger reason = [[info valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue]; + NSString *output = [Callingx getAudioOutput]; + + if (output == nil) { + return; + } + + NSDictionary *params = @{ + @"output" : output, + @"reason" : @(reason), + }; + NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys: + CallingxDidChangeAudioRoute, @"eventName", + params, @"params", + nil + ]; + // [self emitOnNewEvent: dictionary]; + // [self sendEventWithNameWrapper:CallingxDidChangeAudioRoute body:params]; +} + +- (NSString *)getIncomingCallErrorCode:(NSError *)error { + if ([error code] == CXErrorCodeIncomingCallErrorUnentitled) { + return @"Unentitled"; + } else if ([error code] == CXErrorCodeIncomingCallErrorCallUUIDAlreadyExists) { + return @"CallUUIDAlreadyExists"; + } else if ([error code] == CXErrorCodeIncomingCallErrorFilteredByDoNotDisturb) { + return @"FilteredByDoNotDisturb"; + } else if ([error code] == CXErrorCodeIncomingCallErrorFilteredByBlockList) { + return @"FilteredByBlockList"; + } else { + return @"Unknown"; + } +} + +- (void)setup:(NSDictionary *)options { + NSLog(@"[Callingx][setup] options = %@", options); + if (isSetupNatively) { +#ifdef DEBUG + NSLog(@"[Callingx][setup] already setup"); + RCTLog(@"[Callingx][setup] already setup in native code"); +#endif + return; + } + +#ifdef DEBUG + NSLog(@"[Callingx][setup] options = %@", options); +#endif + _version = [[[NSProcessInfo alloc] init] operatingSystemVersion]; + self.callKeepCallController = [[CXCallController alloc] init]; + + [self setSettings:options]; + + [Callingx initCallKitProvider]; + [Callingx initUUIDStorage]; + + self.callKeepProvider = sharedProvider; + [self.callKeepProvider setDelegate:nil queue:nil]; + [self.callKeepProvider setDelegate:self queue:nil]; +} + +#pragma mark - Turbo module methods + +- (void)setupiOS:(JS::NativeCallingx::SpecSetupiOSOptions &)options { + NSDictionary *optionsDict = @{ + @"appName" : options.appName(), + @"supportsVideo" : @(options.supportsVideo()), + @"maximumCallsPerCallGroup" : @(options.maximumCallsPerCallGroup()), + @"maximumCallGroups" : @(options.maximumCallGroups()), + @"handleType" : options.handleType() + }; + [self setup:optionsDict]; + + _isInitialized = YES; +} + +- (NSArray *)getInitialEvents { +#ifdef DEBUG + NSLog(@"[Callingx][getInitialEvents] _delayedEvents = %@", _delayedEvents); +#endif + return _delayedEvents; +} + +- (void)clearInitialEvents:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + _delayedEvents = [NSMutableArray array]; + resolve(@YES); +} + +- (void)answerIncomingCall:(nonnull NSString *)callId + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { +#ifdef DEBUG + NSLog(@"[Callingx][answerIncomingCall] callId = %@", callId); +#endif + NSUUID *uuid = [uuidStorage getUUIDForCid:callId]; + if (uuid == nil) { + NSLog(@"[Callingx][answerIncomingCall] callId not found"); + resolve(@NO); + return; + } + + CXAnswerCallAction *answerCallAction = [[CXAnswerCallAction alloc] initWithCallUUID:uuid]; + CXTransaction *transaction = [[CXTransaction alloc] init]; + [transaction addAction:answerCallAction]; + + [self requestTransaction:transaction]; + resolve(@YES); +} + +- (void)displayIncomingCall:(nonnull NSString *)callId + phoneNumber:(nonnull NSString *)phoneNumber + callerName:(nonnull NSString *)callerName + hasVideo:(BOOL)hasVideo + displayOptions: + (JS::NativeCallingx::SpecDisplayIncomingCallDisplayOptions &)displayOptions + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + + [Callingx reportNewIncomingCall:callId + handle:phoneNumber + handleType:@"number" + hasVideo:hasVideo + localizedCallerName:callerName + supportsHolding:NO // parametrize? + supportsDTMF:NO // parametrize? + supportsGrouping:NO // parametrize? + supportsUngrouping:NO // parametrize? + fromPushKit:NO + payload:nil + withCompletionHandler:nil]; + + NSDictionary *settings = [Callingx getSettings]; + NSNumber *timeout = settings[@"displayCallReachabilityTimeout"]; + + if (timeout) { + dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)([timeout intValue] * NSEC_PER_MSEC)); + dispatch_after(popTime, dispatch_get_main_queue(), ^(void) { + if (!self->_isReachable) { +#ifdef DEBUG + NSLog(@"[Callingx] Displayed a call without a reachable app, ending " + @"the call: %@", + callId); +#endif + [Callingx endCall:callId reason:CXCallEndedReasonFailed]; + } + }); + } + resolve(@YES); +} + +- (void)endCallWithReason:(nonnull NSString *)callId + reason:(double)reason + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { +#ifdef DEBUG + NSLog(@"[Callingx][endCallWithReason] callId = %@ reason = %f", callId, reason); +#endif + [Callingx endCall:callId reason:reason]; + resolve(@YES); +} + +- (void)endCall:(nonnull NSString *)callId + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { +#ifdef DEBUG + NSLog(@"[Callingx][endCall] callId = %@", callId); +#endif + NSUUID *uuid = [uuidStorage getUUIDForCid:callId]; + if (uuid == nil) { + NSLog(@"[Callingx][endCall] callId not found"); + resolve(@NO); + return; + } + + CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:uuid]; + CXTransaction *transaction = [[CXTransaction alloc] initWithAction:endCallAction]; + + [self requestTransaction:transaction]; + resolve(@YES); +} + +- (void)setCurrentCallActive:(nonnull NSString *)callId + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { + //TODO: adjust implementation + NSUUID *uuid = [uuidStorage getUUIDForCid:callId]; + if (uuid == nil) { + NSLog(@"[Callingx][setCurrentCallActive] callId not found"); + resolve(@NO); + return; + } + + [self.callKeepProvider reportOutgoingCallWithUUID:uuid startedConnectingAtDate:[NSDate date]]; + resolve(@YES); +} + +- (void)setMutedCall:(nonnull NSString *)callId + isMuted:(BOOL)isMuted + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { +#ifdef DEBUG + NSLog(@"[Callingx][setMutedCall] muted = %i", isMuted); +#endif + NSUUID *uuid = [uuidStorage getUUIDForCid:callId]; + if (uuid == nil) { + NSLog(@"[Callingx][setMutedCall] callId not found"); + resolve(@NO); + return; + } + + CXSetMutedCallAction *setMutedAction = [[CXSetMutedCallAction alloc] initWithCallUUID:uuid muted:isMuted]; + CXTransaction *transaction = [[CXTransaction alloc] init]; + [transaction addAction:setMutedAction]; + + [self requestTransaction:transaction]; + resolve(@YES); +} + +- (void)setOnHoldCall:(nonnull NSString *)callId + isOnHold:(BOOL)isOnHold + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { +#ifdef DEBUG + NSLog(@"[Callingx][setOnHold] uuidString = %@, shouldHold = %d", callId, + isOnHold); +#endif + NSUUID *uuid = [uuidStorage getUUIDForCid:callId]; + if (uuid == nil) { + NSLog(@"[Callingx][setOnHoldCall] callId not found"); + resolve(@NO); + return; + } + + CXSetHeldCallAction *setHeldCallAction = [[CXSetHeldCallAction alloc] initWithCallUUID:uuid onHold:isOnHold]; + CXTransaction *transaction = [[CXTransaction alloc] init]; + [transaction addAction:setHeldCallAction]; + + [self requestTransaction:transaction]; + resolve(@YES); +} + +- (void)startCall:(nonnull NSString *)callId + phoneNumber:(nonnull NSString *)phoneNumber + callerName:(nonnull NSString *)callerName + hasVideo:(BOOL)hasVideo + displayOptions:(JS::NativeCallingx::SpecStartCallDisplayOptions &)displayOptions + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { +#ifdef DEBUG + NSLog(@"[Callingx][startCall] uuidString = %@", callId, phoneNumber); +#endif + CXHandleType _handleType = [Callingx getHandleType:@"generic"]; + NSUUID *uuid = [uuidStorage getOrCreateUUIDForCid:callId]; + CXHandle *callHandle = [[CXHandle alloc] initWithType:_handleType value:phoneNumber]; + CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle]; + [startCallAction setVideo:hasVideo]; + [startCallAction setContactIdentifier:callerName]; + + CXTransaction *transaction = [[CXTransaction alloc] initWithAction:startCallAction]; + + [self requestTransaction:transaction]; +} + +- (void)updateDisplay:(nonnull NSString *)callId + phoneNumber:(nonnull NSString *)phoneNumber + callerName:(nonnull NSString *)callerName + displayOptions: + (JS::NativeCallingx::SpecUpdateDisplayDisplayOptions &)displayOptions + resolve:(nonnull RCTPromiseResolveBlock)resolve + reject:(nonnull RCTPromiseRejectBlock)reject { +#ifdef DEBUG + NSLog( + @"[Callingx][updateDisplay] uuidString = %@ displayName = %@ uri = %@", + callId, callerName, phoneNumber); +#endif + NSUUID *uuid = [uuidStorage getUUIDForCid:callId]; + if (uuid == nil) { + return; + } + + CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:phoneNumber]; + CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + callUpdate.localizedCallerName = callerName; + callUpdate.remoteHandle = callHandle; + + // if ([options valueForKey:@"hasVideo"] != nil) { + // callUpdate.hasVideo = [RCTConvert BOOL:options[@"hasVideo"]]; + // } + // if ([options valueForKey:@"supportsHolding"] != nil) { + // callUpdate.supportsHolding = [RCTConvert + // BOOL:options[@"supportsHolding"]]; + // } + // if ([options valueForKey:@"supportsDTMF"] != nil) { + // callUpdate.supportsDTMF = [RCTConvert + // BOOL:options[@"supportsDTMF"]]; + // } + // if ([options valueForKey:@"supportsGrouping"] != nil) { + // callUpdate.supportsGrouping = [RCTConvert + // BOOL:options[@"supportsGrouping"]]; + // } + // if ([options valueForKey:@"supportsUngrouping"] != nil) { + // callUpdate.supportsUngrouping = [RCTConvert + // BOOL:options[@"supportsUngrouping"]]; + // } + + [self.callKeepProvider reportCallWithUUID:uuid updated:callUpdate]; + resolve(@YES); +} + +- (void)log:(NSString *)message level:(NSString *)level { + NSLog(@"[Callingx][log] %@, %@", message, level); +} + +- (void)setupAndroid:(JS::NativeCallingx::SpecSetupAndroidOptions &)options { + // leave empty +} + +- (void)startBackgroundTask:(NSString *)taskName + timeout:(double)timeout + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + // leave empty + resolve(@YES); +} + +- (void)stopBackgroundTask:(NSString *)taskName + resolve:(RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject { + // leave empty + resolve(@YES); +} + +#pragma mark - CXProviderDelegate + +- (void)providerDidReset:(CXProvider *)provider { +#ifdef DEBUG + NSLog(@"[Callingx][providerDidReset]"); +#endif + // this means something big changed, so tell the JS. The JS should + // probably respond by hanging up all calls. + [self sendEventWithNameWrapper:RNCallKeepProviderReset body:nil]; +} + +- (void)provider:(CXProvider *)provider + performStartCallAction:(CXStartCallAction *)action { +#ifdef DEBUG + NSLog(@"[Callingx][CXProviderDelegate][provider:performStartCallAction]"); +#endif + NSString *callId = [uuidStorage getCidForUUID:action.callUUID]; + if (callId == nil) { + NSLog(@"[Callingx][CXProviderDelegate][provider:performStartCallAction] callId not found"); + return; + } + // do this first, audio sessions are flakey + [self configureAudioSession]; + // tell the JS to actually make the call + [self sendEventWithNameWrapper:CallingxDidReceiveStartCallAction + body:@{ + @"callId" : callId, + @"handle" : action.handle.value + }]; + [action fulfill]; +} + +// Update call contact info +// @deprecated +// RCT_EXPORT_METHOD(reportUpdatedCall : (NSString *)uuidString contactIdentifier : (NSString *)contactIdentifier) { +// #ifdef DEBUG +// NSLog(@"[deprecated][Callingx][reportUpdatedCall] contactIdentifier = %@", +// contactIdentifier); +// #endif +// NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString]; +// CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; +// callUpdate.localizedCallerName = contactIdentifier; + +// [self.callKeepProvider reportCallWithUUID:uuid updated:callUpdate]; +// } + +- (void)provider:(CXProvider *)provider + performAnswerCallAction:(CXAnswerCallAction *)action { +#ifdef DEBUG + NSLog(@"[Callingx][CXProviderDelegate][provider:performAnswerCallAction]"); +#endif + NSString *callId = [uuidStorage getCidForUUID:action.callUUID]; + if (callId == nil) { + NSLog(@"[Callingx][CXProviderDelegate][provider:performAnswerCallAction] callId not found"); + [action fail]; + return; + } + + [self configureAudioSession]; + [self sendEventWithNameWrapper:CallingxPerformAnswerCallAction + body:@{ + @"callId" : callId + }]; + [action fulfill]; +} + +- (void)provider:(CXProvider *)provider + performEndCallAction:(CXEndCallAction *)action { +#ifdef DEBUG + NSLog(@"[Callingx][CXProviderDelegate][provider:performEndCallAction]"); +#endif + NSString *callId = [uuidStorage getCidForUUID:action.callUUID]; + if (callId == nil) { + NSLog(@"[Callingx][CXProviderDelegate][provider:performEndCallAction] callId not found"); + [action fail]; + return; + } + + [self sendEventWithNameWrapper:CallingxPerformEndCallAction + body:@{ + @"callId" : callId + }]; + [uuidStorage removeCid:callId]; + [action fulfill]; +} + +- (void)provider:(CXProvider *)provider + performSetHeldCallAction:(CXSetHeldCallAction *)action { +#ifdef DEBUG + NSLog(@"[Callingx][CXProviderDelegate][provider:performSetHeldCallAction]"); +#endif + NSString *callId = [uuidStorage getCidForUUID:action.callUUID]; + if (callId == nil) { + NSLog(@"[Callingx][CXProviderDelegate][provider:performSetHeldCallAction] callId not found"); + [action fail]; + return; + } + + [self sendEventWithNameWrapper:CallingxDidToggleHoldAction + body:@{ + @"hold" : @(action.onHold), + @"callId" : callId + }]; + [action fulfill]; +} + +- (void)provider:(CXProvider *)provider + performPlayDTMFCallAction:(CXPlayDTMFCallAction *)action { +#ifdef DEBUG + NSLog(@"[Callingx][CXProviderDelegate][provider:performPlayDTMFCallAction]"); +#endif + NSString *callId = [uuidStorage getCidForUUID:action.callUUID]; + if (callId == nil) { + NSLog(@"[Callingx][CXProviderDelegate][provider:performPlayDTMFCallAction] callId not found"); + [action fail]; + return; + } + + [self sendEventWithNameWrapper:RNCallKeepPerformPlayDTMFCallAction + body:@{ + @"digits" : action.digits, + @"callId" : callId + }]; + [action fulfill]; +} + +- (void)provider:(CXProvider *)provider + performSetMutedCallAction:(CXSetMutedCallAction *)action { +#ifdef DEBUG + NSLog(@"[Callingx][CXProviderDelegate][provider:performSetMutedCallAction]"); +#endif + NSString *callId = [uuidStorage getCidForUUID:action.callUUID]; + if (callId == nil) { + NSLog(@"[Callingx][CXProviderDelegate][provider:performSetMutedCallAction] callId not found"); + [action fail]; + return; + } + + [self sendEventWithNameWrapper:CallingxDidPerformSetMutedCallAction + body:@{ + @"muted" : @(action.muted), + @"callId" : callId + }]; + [action fulfill]; +} + +- (void)provider:(CXProvider *)provider + timedOutPerformingAction:(CXAction *)action { +#ifdef DEBUG + NSLog(@"[Callingx][CXProviderDelegate][provider:timedOutPerformingAction]"); +#endif +} + +- (void)provider:(CXProvider *)provider + didActivateAudioSession:(AVAudioSession *)audioSession { +#ifdef DEBUG + NSLog(@"[Callingx][CXProviderDelegate][provider:didActivateAudioSession]"); +#endif + NSDictionary *userInfo = @{ + AVAudioSessionInterruptionTypeKey : + [NSNumber numberWithInt:AVAudioSessionInterruptionTypeEnded], + AVAudioSessionInterruptionOptionKey : + [NSNumber numberWithInt:AVAudioSessionInterruptionOptionShouldResume] + }; + [[NSNotificationCenter defaultCenter] + postNotificationName:AVAudioSessionInterruptionNotification + object:nil + userInfo:userInfo]; + + [self configureAudioSession]; + [self sendEventWithNameWrapper:RNCallKeepDidActivateAudioSession body:nil]; +} + +- (void)provider:(CXProvider *)provider + didDeactivateAudioSession:(AVAudioSession *)audioSession { +#ifdef DEBUG + NSLog( + @"[Callingx][CXProviderDelegate][provider:didDeactivateAudioSession]"); +#endif + [self sendEventWithNameWrapper:RNCallKeepDidDeactivateAudioSession body:nil]; +} + +@end diff --git a/packages/react-native-callingx/ios/CallingxPublic.h b/packages/react-native-callingx/ios/CallingxPublic.h new file mode 100644 index 0000000000..3e872b71b5 --- /dev/null +++ b/packages/react-native-callingx/ios/CallingxPublic.h @@ -0,0 +1,45 @@ +// +// CallingModule.h +// POCCallingX +// +// Created by Artem Grintsevich on 17/11/2025. +// + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface Callingx : NSObject + ++ (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options NS_AVAILABLE_IOS(9_0); + ++ (BOOL)application:(UIApplication *)application +continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler; + ++ (void)reportNewIncomingCall:(NSString *)uuidString + handle:(NSString *)handle + handleType:(NSString *)handleType + hasVideo:(BOOL)hasVideo + localizedCallerName:(NSString * _Nullable)localizedCallerName + supportsHolding:(BOOL)supportsHolding + supportsDTMF:(BOOL)supportsDTMF + supportsGrouping:(BOOL)supportsGrouping + supportsUngrouping:(BOOL)supportsUngrouping + fromPushKit:(BOOL)fromPushKit + payload:(NSDictionary * _Nullable)payload + withCompletionHandler:(void (^_Nullable)(void))completion; + ++ (void)endCallWithUUID:(NSString *)uuidString + reason:(int)reason; + ++ (BOOL)isCallActive:(NSString *)uuidString; + ++ (void)setup:(NSDictionary *)options; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-callingx/ios/UUIDStorage.h b/packages/react-native-callingx/ios/UUIDStorage.h new file mode 100644 index 0000000000..627ef83a5f --- /dev/null +++ b/packages/react-native-callingx/ios/UUIDStorage.h @@ -0,0 +1,16 @@ +#import + +@interface UUIDStorage : NSObject + +- (instancetype)init; +- (NSUUID *)getOrCreateUUIDForCid:(NSString *)cid; +- (NSUUID *)getUUIDForCid:(NSString *)cid; +- (NSString *)getCidForUUID:(NSUUID *)uuid; +- (void)removeCidForUUID:(NSUUID *)uuid; +- (void)removeCid:(NSString *)cid; +- (void)removeAllObjects; +- (NSUInteger)count; +- (BOOL)containsCid:(NSString *)cid; +- (BOOL)containsUUID:(NSUUID *)uuid; + +@end \ No newline at end of file diff --git a/packages/react-native-callingx/ios/UUIDStorage.mm b/packages/react-native-callingx/ios/UUIDStorage.mm new file mode 100644 index 0000000000..d4c92d4628 --- /dev/null +++ b/packages/react-native-callingx/ios/UUIDStorage.mm @@ -0,0 +1,96 @@ +#import "UUIDStorage.h" + +@interface UUIDStorage () +@property (nonatomic, strong) NSMutableDictionary *uuidDict; +@property (nonatomic, strong) NSMutableDictionary *cidDict; +@end + +@implementation UUIDStorage + +- (instancetype)init { + self = [super init]; + if (self) { + _uuidDict = [[NSMutableDictionary alloc] init]; + _cidDict = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (NSUUID *)getOrCreateUUIDForCid:(NSString *)cid { + if ([self containsCid:cid]) { + NSString *existingUUID = self.uuidDict[cid]; + NSLog(@"[UUIDStorage] getUUIDForCid: found existing UUID %@ for cid %@", existingUUID, cid); + return [[NSUUID alloc] initWithUUIDString:existingUUID]; + } + + NSUUID *uuid = [NSUUID UUID]; + NSString *uuidString = [uuid.UUIDString lowercaseString]; + self.uuidDict[cid] = uuidString; + self.cidDict[uuidString] = cid; + NSLog(@"[UUIDStorage] getUUIDForCid: created new UUID %@ for cid %@", uuidString, cid); + + return uuid; +} + +- (NSUUID *)getUUIDForCid:(NSString *)cid { + NSString *uuidString = self.uuidDict[cid]; + if (uuidString) { + return [[NSUUID alloc] initWithUUIDString:uuidString]; + } + return nil; +} + +- (NSString *)getCidForUUID:(NSUUID *)uuid { + NSString *uuidString = [uuid.UUIDString lowercaseString]; + NSString *cid = self.cidDict[uuidString]; + NSLog(@"[UUIDStorage] getCidForUUID: UUID %@ -> cid %@", uuidString, cid ?: @"(not found)"); + return cid; +} + +- (void)removeCidForUUID:(NSUUID *)uuid { + NSString *uuidString = [uuid.UUIDString lowercaseString]; + NSString *cid = self.cidDict[uuidString]; + if (cid) { + [self.uuidDict removeObjectForKey:cid]; + [self.cidDict removeObjectForKey:uuidString]; + NSLog(@"[UUIDStorage] removeCidForUUID: removed cid %@ for UUID %@", cid, uuidString); + } else { + NSLog(@"[UUIDStorage] removeCidForUUID: no cid found for UUID %@", uuidString); + } +} + +- (void)removeCid:(NSString *)cid { + NSString *uuidString = self.uuidDict[cid]; + if (uuidString) { + [self.cidDict removeObjectForKey:uuidString]; + [self.uuidDict removeObjectForKey:cid]; + NSLog(@"[UUIDStorage] removeCid: removed cid %@ with UUID %@", cid, uuidString); + } else { + NSLog(@"[UUIDStorage] removeCid: no UUID found for cid %@", cid); + } +} + +- (void)removeAllObjects { + NSUInteger count = [self.uuidDict count]; + [self.uuidDict removeAllObjects]; + [self.cidDict removeAllObjects]; + NSLog(@"[UUIDStorage] removeAllObjects: cleared %lu entries", (unsigned long)count); +} + +- (NSUInteger)count { + return [self.uuidDict count]; +} + +- (BOOL)containsCid:(NSString *)cid { + return self.uuidDict[cid] != nil; +} + +- (BOOL)containsUUID:(NSUUID *)uuid { + return self.cidDict[[uuid.UUIDString lowercaseString]] != nil; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"UUIDStorage: %@", self.uuidDict]; +} + +@end \ No newline at end of file diff --git a/packages/react-native-callingx/lefthook.yml b/packages/react-native-callingx/lefthook.yml new file mode 100644 index 0000000000..003bcde2c3 --- /dev/null +++ b/packages/react-native-callingx/lefthook.yml @@ -0,0 +1,16 @@ +pre-commit: + parallel: true + commands: + + lint: + glob: "*.{js,ts,jsx,tsx}" + run: npx eslint {staged_files} + + types: + glob: "*.{js,ts, jsx, tsx}" + run: npx tsc +commit-msg: + parallel: true + commands: + commitlint: + run: npx commitlint --edit diff --git a/packages/react-native-callingx/package.json b/packages/react-native-callingx/package.json new file mode 100644 index 0000000000..37350e431b --- /dev/null +++ b/packages/react-native-callingx/package.json @@ -0,0 +1,170 @@ +{ + "name": "react-native-callingx", + "version": "0.1.0", + "description": "test", + "main": "./dist/module/index.js", + "types": "./dist/typescript/src/index.d.ts", + "exports": { + ".": { + "source": "./src/index.tsx", + "types": "./dist/typescript/src/index.d.ts", + "default": "./dist/module/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "src", + "dist", + "android", + "ios", + "cpp", + "*.podspec", + "react-native.config.js", + "!ios/build", + "!android/build", + "!android/gradle", + "!android/gradlew", + "!android/gradlew.bat", + "!android/local.properties", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__", + "!**/.*" + ], + "scripts": { + "example": "yarn workspace react-native-callingx-example", + "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build dist", + "prepare": "bob build", + "build": "bob build", + "typecheck": "tsc", + "lint": "eslint \"**/*.{js,ts,tsx}\"", + "release": "release-it --only-version", + "test": "jest" + }, + "keywords": [ + "react-native", + "ios", + "android" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/GetStream/stream-video-js.git" + }, + "author": "https://getstream.io (https://github.com/GetStream)", + "license": "MIT", + "bugs": { + "url": "https://github.com/GetStream/stream-video-js/issues" + }, + "homepage": "https://github.com/GetStream/stream-video-js#readme", + "publishConfig": { + "registry": "https://registry.npmjs.org/" + }, + "devDependencies": { + "@commitlint/config-conventional": "^19.8.1", + "@eslint/compat": "^1.3.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "^9.35.0", + "@react-native-community/cli": "20.0.1", + "@react-native/babel-preset": "0.81.1", + "@react-native/eslint-config": "^0.81.1", + "@release-it/conventional-changelog": "^10.0.1", + "@types/jest": "^29.5.14", + "@types/react": "^19.1.0", + "commitlint": "^19.8.1", + "del-cli": "^6.0.0", + "eslint": "^9.35.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-prettier": "^5.5.4", + "jest": "^29.7.0", + "lefthook": "^2.0.3", + "prettier": "^3.4.2", + "react": "19.1.0", + "react-native": "0.81.1", + "react-native-builder-bob": "^0.40.15", + "release-it": "^19.0.4", + "turbo": "^2.5.6", + "typescript": "^5.9.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + }, + "react-native-builder-bob": { + "source": "src", + "output": "dist", + "targets": [ + [ + "module", + { + "esm": true + } + ], + [ + "typescript", + { + "project": "tsconfig.build.json" + } + ] + ] + }, + "codegenConfig": { + "name": "CallingxSpec", + "type": "modules", + "jsSrcsDir": "src/spec", + "android": { + "javaPackageName": "com.callingx" + }, + "ios": { + "moduleName": "Callingx" + } + }, + "prettier": { + "quoteProps": "consistent", + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "release-it": { + "git": { + "commitMessage": "chore: release ${version}", + "tagName": "v${version}" + }, + "npm": { + "publish": true + }, + "github": { + "release": true + }, + "plugins": { + "@release-it/conventional-changelog": { + "preset": { + "name": "angular" + } + } + } + }, + "jest": { + "preset": "react-native", + "modulePathIgnorePatterns": [ + "/example/node_modules", + "/dist/" + ] + }, + "create-react-native-library": { + "languages": "kotlin-objc", + "type": "turbo-module", + "tools": [ + "eslint", + "lefthook", + "release-it", + "jest" + ], + "version": "0.55.0" + } +} diff --git a/packages/react-native-callingx/react-native.config.js b/packages/react-native-callingx/react-native.config.js new file mode 100644 index 0000000000..21b3b68dd7 --- /dev/null +++ b/packages/react-native-callingx/react-native.config.js @@ -0,0 +1,8 @@ +module.exports = { + dependency: { + platforms: { + android: {}, + ios: {}, + }, + }, +}; diff --git a/packages/react-native-callingx/src/CallingxModule.ts b/packages/react-native-callingx/src/CallingxModule.ts new file mode 100644 index 0000000000..8571795e18 --- /dev/null +++ b/packages/react-native-callingx/src/CallingxModule.ts @@ -0,0 +1,220 @@ +import { Platform } from 'react-native'; +import NativeCallingModule from './spec/NativeCallingx'; +import { requestCallPermissions } from './utils/permissions'; +import type { PermissionsResult } from './utils/permissions'; +import { + HEADLESS_TASK_NAME, + registerHeadlessTask, + setHeadlessTask, +} from './utils/headlessTask'; +import type { ManagableTask } from './utils/headlessTask'; +import { EventManager } from './EventManager'; +import type { EventListener, EventName, EventParams } from './EventManager'; +import { + type ICallingxModule, + type InfoDisplayOptions, + type AndroidOptions, + type iOSOptions, + type TextTransformer, + type NotificationTransformers, + type EndCallReason, + type EventData, +} from './types'; +import { + androidEndCallReasonMap, + defaultAndroidOptions, + defaultiOSOptions, + defaultTextTransformer, + iosEndCallReasonMap, +} from './utils/constants'; + +class CallingxModule implements ICallingxModule { + private isNotificationsAllowed = false; + + private titleTransformer: TextTransformer = (text: string) => text; + private subtitleTransformer: TextTransformer | undefined = undefined; + + private eventManager: EventManager = new EventManager(); + + get canPostNotifications(): boolean { + return this.isNotificationsAllowed; + } + + setup(options: { + ios: Partial; + android: Partial; + }): void { + if (Platform.OS === 'ios') { + NativeCallingModule.setupiOS({ ...defaultiOSOptions, ...options.ios }); + } + + if (Platform.OS === 'android') { + const { titleTransformer, subtitleTransformer, ...rest } = + options.android; + + this.titleTransformer = titleTransformer ?? defaultTextTransformer; + this.subtitleTransformer = subtitleTransformer; + + const notificationsConfig: Required = { + incomingChannel: { + ...defaultAndroidOptions.incomingChannel, + ...(rest.incomingChannel ?? {}), + }, + outgoingChannel: { + ...defaultAndroidOptions.outgoingChannel, + ...(rest.outgoingChannel ?? {}), + }, + }; + NativeCallingModule.setupAndroid(notificationsConfig); + + registerHeadlessTask(); + } + } + + async requestPermissions(): Promise { + const result: { + recordAudio: boolean; + postNotifications: boolean; + } = await requestCallPermissions(); + + this.isNotificationsAllowed = result.postNotifications; + return result; + } + + async checkPermissions(): Promise { + const result: { + recordAudio: boolean; + postNotifications: boolean; + } = await requestCallPermissions(); + + this.isNotificationsAllowed = result.postNotifications; + return result; + } + + getInitialEvents(): EventData[] { + return NativeCallingModule.getInitialEvents(); + } + + clearInitialEvents(): Promise { + return NativeCallingModule.clearInitialEvents(); + } + + //activates call that was registered with the telecom stack + setCurrentCallActive(callId: string): Promise { + return NativeCallingModule.setCurrentCallActive(callId); + } + + displayIncomingCall( + callId: string, + phoneNumber: string, + callerName: string, + hasVideo: boolean + ): Promise { + const displayOptions: InfoDisplayOptions = { + displayTitle: this.titleTransformer(callerName), + displaySubtitle: this.subtitleTransformer?.(phoneNumber), + }; + return NativeCallingModule.displayIncomingCall( + callId, + phoneNumber, + callerName, + hasVideo, + displayOptions + ); + } + + answerIncomingCall(callId: string): Promise { + return NativeCallingModule.answerIncomingCall(callId); + } + + //registers call with the telecom stack + startCall( + callId: string, + phoneNumber: string, + callerName: string, + hasVideo: boolean + ): Promise { + const displayOptions: InfoDisplayOptions = { + displayTitle: this.titleTransformer(callerName), + displaySubtitle: this.subtitleTransformer?.(phoneNumber), + }; + return NativeCallingModule.startCall( + callId, + phoneNumber, + callerName, + hasVideo, + displayOptions + ); + } + + updateDisplay( + callId: string, + phoneNumber: string, + callerName: string + ): Promise { + const displayOptions: InfoDisplayOptions = { + displayTitle: this.titleTransformer(callerName), + displaySubtitle: this.subtitleTransformer?.(phoneNumber), + }; + return NativeCallingModule.updateDisplay( + callId, + phoneNumber, + callerName, + displayOptions + ); + } + + endCallWithReason(callId: string, reason: EndCallReason): Promise { + const reasons = + Platform.OS === 'ios' ? iosEndCallReasonMap : androidEndCallReasonMap; + + if (Platform.OS === 'ios' && reason === 'local') { + return NativeCallingModule.endCall(callId); + } + + return NativeCallingModule.endCallWithReason(callId, reasons[reason]); + } + + setMutedCall(callId: string, isMuted: boolean): Promise { + return NativeCallingModule.setMutedCall(callId, isMuted); + } + + setOnHoldCall(callId: string, isOnHold: boolean): Promise { + return NativeCallingModule.setOnHoldCall(callId, isOnHold); + } + + startBackgroundTask(taskProvider: ManagableTask): Promise { + const stopTask = () => { + NativeCallingModule.log(`stopBackgroundTask`, 'warn'); + NativeCallingModule.stopBackgroundTask(HEADLESS_TASK_NAME); + }; + + setHeadlessTask((taskData: any) => taskProvider(taskData, stopTask)); + + return NativeCallingModule.startBackgroundTask(HEADLESS_TASK_NAME, 0); + } + + stopBackgroundTask(): Promise { + return NativeCallingModule.stopBackgroundTask(HEADLESS_TASK_NAME); + } + + addEventListener( + eventName: T, + callback: EventListener + ): { remove: () => void } { + this.eventManager.addListener(eventName, callback); + + return { + remove: () => { + this.eventManager.removeListener(eventName, callback); + }, + }; + } + + log(message: string, level: 'debug' | 'info' | 'warn' | 'error'): void { + NativeCallingModule.log(message, level); + } +} + +const module = new CallingxModule(); +export default module; diff --git a/packages/react-native-callingx/src/EventManager.ts b/packages/react-native-callingx/src/EventManager.ts new file mode 100644 index 0000000000..82c5cde2c4 --- /dev/null +++ b/packages/react-native-callingx/src/EventManager.ts @@ -0,0 +1,62 @@ +import type { EventSubscription } from 'react-native'; +import NativeCallingModule from './spec/NativeCallingx'; +import type { EventData, EventName, EventParams } from './types'; + +type EventListener = (params: T) => void; + +class EventManager { + private listenersCount: number = 0; + private eventListeners: Map< + EventName, + EventListener[] + > = new Map(); + private subscription: EventSubscription | null = null; + + addListener( + eventName: T, + callback: EventListener + ): void { + const listeners = this.eventListeners.get(eventName) || []; + listeners.push(callback as EventListener); + this.eventListeners.set(eventName, listeners); + + this.listenersCount++; + + if (this.subscription === null) { + this.subscription = NativeCallingModule.onNewEvent((event: EventData) => { + console.log('[callingx] NativeCallingModule.onNewEvent', event); + const eventListeners = + this.eventListeners.get(event.eventName as EventName) || []; + eventListeners.forEach((listener) => + listener(event.params as EventParams[EventName]) + ); + }); + } + } + + removeListener( + eventName: T, + callback: EventListener + ): void { + const listeners = this.eventListeners.get(eventName) || []; + this.eventListeners.set( + eventName, + listeners.filter((c) => c !== callback) + ); + + this.listenersCount--; + + if (this.listenersCount === 0) { + this.subscription?.remove(); + this.subscription = null; + } + + console.log( + '!!! remove listener', + this.listenersCount, + this.subscription === null ? 'null' : 'not null' + ); + } +} + +export { EventManager, type EventName, type EventParams, type EventListener }; diff --git a/packages/react-native-callingx/src/__tests__/index.test.tsx b/packages/react-native-callingx/src/__tests__/index.test.tsx new file mode 100644 index 0000000000..bf84291a5e --- /dev/null +++ b/packages/react-native-callingx/src/__tests__/index.test.tsx @@ -0,0 +1 @@ +it.todo('write a test'); diff --git a/packages/react-native-callingx/src/index.tsx b/packages/react-native-callingx/src/index.tsx new file mode 100644 index 0000000000..0381bf9540 --- /dev/null +++ b/packages/react-native-callingx/src/index.tsx @@ -0,0 +1,2 @@ +export { default as CallingxModule } from './CallingxModule'; +export * from './types'; diff --git a/packages/react-native-callingx/src/spec/NativeCallingx.ts b/packages/react-native-callingx/src/spec/NativeCallingx.ts new file mode 100644 index 0000000000..3be13f3486 --- /dev/null +++ b/packages/react-native-callingx/src/spec/NativeCallingx.ts @@ -0,0 +1,102 @@ +import { TurboModuleRegistry, type TurboModule } from 'react-native'; + +// @ts-expect-error - CodegenTypes is not properly typed +import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypes'; + +export interface Spec extends TurboModule { + setupiOS(options: { + appName: string; + supportsVideo: boolean; + maximumCallsPerCallGroup: number; + maximumCallGroups: number; + handleType: string; + }): void; + + setupAndroid(options: { + incomingChannel?: { + id: string; + name: string; + sound?: string; + vibration?: boolean; + }; + outgoingChannel?: { + id: string; + name: string; + sound?: string; + vibration?: boolean; + }; + }): void; + + getInitialEvents(): Array<{ + eventName: string; + params: { + callId: string; + cause?: string; + }; + }>; + + clearInitialEvents(): Promise; + + setCurrentCallActive(callId: string): Promise; + + displayIncomingCall( + callId: string, + phoneNumber: string, + callerName: string, + hasVideo: boolean, + displayOptions?: { + displayTitle?: string; + displaySubtitle?: string; + } + ): Promise; + + //use when need to answer an incoming call withing app UI + answerIncomingCall(callId: string): Promise; + + startCall( + callId: string, + phoneNumber: string, + callerName: string, + hasVideo: boolean, + displayOptions?: { + displayTitle?: string; + displaySubtitle?: string; + } + ): Promise; + + updateDisplay( + callId: string, + phoneNumber: string, + callerName: string, + displayOptions?: { + displayTitle?: string; + displaySubtitle?: string; + } + ): Promise; + + endCallWithReason(callId: string, reason: number): Promise; + + endCall(callId: string): Promise; + + setMutedCall(callId: string, isMuted: boolean): Promise; + + setOnHoldCall(callId: string, isOnHold: boolean): Promise; + + startBackgroundTask(taskName: string, timeout: number): Promise; + + stopBackgroundTask(taskName: string): Promise; + + readonly onNewEvent: EventEmitter<{ + eventName: string; + params: { + callId: string; + cause?: string; + muted?: boolean; + hold?: boolean; + }; + }>; + + log(message: string, level: 'debug' | 'info' | 'warn' | 'error'): void; +} + +export default TurboModuleRegistry.getEnforcing('Callingx'); diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts new file mode 100644 index 0000000000..98586cdb2e --- /dev/null +++ b/packages/react-native-callingx/src/types.ts @@ -0,0 +1,146 @@ +import type { EventListener } from './EventManager'; +import type { ManagableTask } from './utils/headlessTask'; +import type { PermissionsResult } from './utils/permissions'; + +export interface ICallingxModule { + setup(options: Options): void; + + checkPermissions(): Promise; + + requestPermissions(): Promise; + + setCurrentCallActive(callId: string): Promise; + + getInitialEvents(): EventData[]; + + displayIncomingCall( + callId: string, + phoneNumber: string, + callerName: string, + hasVideo: boolean, + displayOptions?: InfoDisplayOptions + ): Promise; + answerIncomingCall(callId: string): Promise; + + startCall( + callId: string, + phoneNumber: string, + callerName: string, + hasVideo: boolean, + displayOptions?: InfoDisplayOptions + ): void; + + updateDisplay( + callId: string, + phoneNumber: string, + callerName: string, + displayOptions?: InfoDisplayOptions + ): Promise; + + endCallWithReason(callId: string, reason: EndCallReason): Promise; + + setMutedCall(callId: string, isMuted: boolean): Promise; + + setOnHoldCall(callId: string, isOnHold: boolean): Promise; + + startBackgroundTask(taskProvider: ManagableTask): Promise; + + stopBackgroundTask(taskName: string): Promise; + + addEventListener( + eventName: T, + callback: EventListener + ): { remove: () => void }; + + log(message: string, level: 'debug' | 'info' | 'warn' | 'error'): void; +} + +export type iOSOptions = { + appName: string; + supportsVideo?: boolean; + maximumCallsPerCallGroup?: number; + maximumCallGroups?: number; + handleType?: string; //'generic' | 'number' | 'phone' | 'email'; +}; + +export type AndroidOptions = { + incomingChannel?: { + id: string; + name: string; + sound?: string; + vibration?: boolean; + }; + outgoingChannel?: { + id: string; + name: string; + sound?: string; + vibration?: boolean; + }; +}; + +export type TextTransformer = (text: string) => string; +export type NotificationTransformers = { + titleTransformer: TextTransformer; + subtitleTransformer?: TextTransformer; +}; + +export type Options = { + ios: iOSOptions; + android: AndroidOptions & NotificationTransformers; +}; + +export type InfoDisplayOptions = { + displayTitle?: string; + displaySubtitle?: string; +}; + +export type EventData = { + eventName: string; + params: { callId: string; cause?: string }; +}; + +export type EventName = + | 'answerCall' + | 'endCall' + | 'didDisplayIncomingCall' + | 'didToggleHoldCallAction' + | 'didChangeAudioRoute' + | 'didReceiveStartCallAction' + | 'didPerformSetMutedCallAction'; + +export type EventParams = { + answerCall: { + callId: string; + }; + endCall: { + callId: string; + cause: string; + }; + didDisplayIncomingCall: { + callId: string; + }; + didToggleHoldCallAction: { + callId: string; + hold: boolean; + }; + didPerformSetMutedCallAction: { + callId: string; + muted: boolean; + }; + didChangeAudioRoute: { + callId: string; + output: string; + }; + didReceiveStartCallAction: { + callId: string; + }; +}; + +export type EndCallReason = + | 'local' // when call is ended by the user + | 'remote' + | 'rejected' + | 'busy' + | 'answeredElsewhere' + | 'missed' + | 'error'; diff --git a/packages/react-native-callingx/src/utils/constants.ts b/packages/react-native-callingx/src/utils/constants.ts new file mode 100644 index 0000000000..510bacbcc2 --- /dev/null +++ b/packages/react-native-callingx/src/utils/constants.ts @@ -0,0 +1,53 @@ +import type { + AndroidOptions, + iOSOptions, + EndCallReason, + TextTransformer, +} from '../types'; + +export const defaultTextTransformer: TextTransformer = (text: string) => text; + +export const defaultiOSOptions: Required = { + appName: 'My App', + supportsVideo: true, + maximumCallsPerCallGroup: 1, + maximumCallGroups: 1, + handleType: 'generic', +}; + +export const defaultAndroidOptions: Required = { + incomingChannel: { + id: 'telecom_incoming_channel', + name: 'Incoming calls', + sound: '', + vibration: false, + }, + outgoingChannel: { + id: 'telecom_ongoing_channel', + name: 'Ongoing calls', + sound: '', + vibration: false, + }, +}; + +// See ios/Callingx.mm for native iOS logic and constants mapping. +export const iosEndCallReasonMap: Record = { + local: -1, + remote: 1, + rejected: 4, + busy: 2, + answeredElsewhere: 3, + missed: 2, + error: 0, +}; + +// https://developer.android.com/reference/android/telecom/DisconnectCause +export const androidEndCallReasonMap: Record = { + local: 2, + remote: 3, + rejected: 6, + busy: 7, + answeredElsewhere: 11, + missed: 5, + error: 1, +}; diff --git a/packages/react-native-callingx/src/utils/headlessTask.ts b/packages/react-native-callingx/src/utils/headlessTask.ts new file mode 100644 index 0000000000..63c739d520 --- /dev/null +++ b/packages/react-native-callingx/src/utils/headlessTask.ts @@ -0,0 +1,52 @@ +import { AppRegistry } from 'react-native'; + +export const HEADLESS_TASK_NAME = 'HandleIncomingCall'; + +export type ManagableTask = ( + taskData: any, + stopTask: () => void +) => Promise; + +type HeadlessTask = (taskData: any) => Promise; + +export const defaultBackgroundTask: ManagableTask = ( + taskData: any, + stopTask: () => void +) => { + return new Promise((resolve) => { + console.log('Default background task data', taskData); + let i = 0; + const totalIterations = 5; + + function loop() { + if (i < totalIterations) { + setTimeout(() => { + console.log(`Iteration: ${i + 1}`); + i++; + loop(); + }, 1000); + } else { + console.log('Default background task finished'); + resolve(undefined); + stopTask(); + } + } + + loop(); + }); +}; + +let headlessTask: HeadlessTask = (taskData: any) => + defaultBackgroundTask(taskData, () => { + console.log('Cancel callback called'); + }); + +export const setHeadlessTask = (task: HeadlessTask) => { + headlessTask = task; +}; + +export const registerHeadlessTask = () => { + AppRegistry.registerHeadlessTask(HEADLESS_TASK_NAME, () => { + return headlessTask; + }); +}; diff --git a/packages/react-native-callingx/src/utils/permissions.ts b/packages/react-native-callingx/src/utils/permissions.ts new file mode 100644 index 0000000000..77e68c073d --- /dev/null +++ b/packages/react-native-callingx/src/utils/permissions.ts @@ -0,0 +1,109 @@ +import { Platform, PermissionsAndroid } from 'react-native'; +import type { Permission } from 'react-native'; + +export interface PermissionsResult { + recordAudio: boolean; + postNotifications: boolean; +} + +export const requestCallPermissions = async (): Promise => { + if (Platform.OS !== 'android') { + return { recordAudio: true, postNotifications: true }; // iOS handles permissions differently + } + + const permissions: string[] = [PermissionsAndroid.PERMISSIONS.RECORD_AUDIO]; + + // Add POST_NOTIFICATIONS for Android 13+ + if (Platform.Version >= 33) { + permissions.push(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS); + } + + // Add WRITE_CALL_LOG for call history (required for Android 6.0+) + // Note: PermissionsAndroid doesn't have a constant for this, so we use the string directly + // permissions.push('android.permission.WRITE_CALL_LOG'); + + try { + const results = await PermissionsAndroid.requestMultiple( + permissions as Permission[] + ); + + // Check if all permissions are granted + const allGranted = Object.values(results).every( + (status) => status === PermissionsAndroid.RESULTS.GRANTED + ); + + if (!allGranted) { + const deniedPermissions = Object.entries(results) + .filter(([, status]) => status !== PermissionsAndroid.RESULTS.GRANTED) + .map(([permission]) => permission); + + console.warn('Denied permissions:', deniedPermissions); + } + + return { + recordAudio: + results[PermissionsAndroid.PERMISSIONS.RECORD_AUDIO] === + PermissionsAndroid.RESULTS.GRANTED, + postNotifications: + results[PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS] === + PermissionsAndroid.RESULTS.GRANTED, + }; + } catch (err) { + console.warn('Error requesting permissions:', err); + return { recordAudio: false, postNotifications: false }; + } +}; + +export const checkCallPermissions = async (): Promise => { + if (Platform.OS !== 'android') { + return true; + } + + const permissions: string[] = [PermissionsAndroid.PERMISSIONS.RECORD_AUDIO]; + + // if (Platform.Version >= 26) { + // permissions.push(PermissionsAndroid.PERMISSIONS.MANAGE_OWN_CALLS); + // } + + if (Platform.Version >= 33) { + permissions.push(PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS); + } + + // Add WRITE_CALL_LOG for call history (required for Android 6.0+) + // Note: PermissionsAndroid doesn't have a constant for this, so we use the string directly + // permissions.push('android.permission.WRITE_CALL_LOG'); + + try { + const results = await Promise.all( + permissions.map((permission) => + PermissionsAndroid.check(permission as Permission) + ) + ); + return Object.values(results).every((granted) => granted === true); + } catch (err) { + console.warn('Error checking permissions:', err); + return false; + } +}; + +export const requestPostNotificationPermissions = + async (): Promise => { + if (Platform.OS !== 'android') { + return true; + } + + const results = await PermissionsAndroid.request( + PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS + ); + return results === PermissionsAndroid.RESULTS.GRANTED; + }; + +export const canPostNotifications = async (): Promise => { + if (Platform.OS !== 'android') { + return true; + } + + return PermissionsAndroid.check( + PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS + ); +}; diff --git a/packages/react-native-callingx/tsconfig.build.json b/packages/react-native-callingx/tsconfig.build.json new file mode 100644 index 0000000000..3c0636adf2 --- /dev/null +++ b/packages/react-native-callingx/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig", + "exclude": ["example", "lib"] +} diff --git a/packages/react-native-callingx/tsconfig.json b/packages/react-native-callingx/tsconfig.json new file mode 100644 index 0000000000..6d5506c5f8 --- /dev/null +++ b/packages/react-native-callingx/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "rootDir": ".", + "paths": { + "react-native-callingx": ["./src/index"] + }, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "customConditions": ["react-native-strict-api"], + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "lib": ["ESNext"], + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "verbatimModuleSyntax": true + } +} diff --git a/packages/react-native-callingx/turbo.json b/packages/react-native-callingx/turbo.json new file mode 100644 index 0000000000..c4d78c4961 --- /dev/null +++ b/packages/react-native-callingx/turbo.json @@ -0,0 +1,42 @@ +{ + "$schema": "https://turbo.build/schema.json", + "globalDependencies": [".nvmrc", ".yarnrc.yml"], + "globalEnv": ["NODE_ENV"], + "tasks": { + "build:android": { + "env": ["ANDROID_HOME", "ORG_GRADLE_PROJECT_newArchEnabled"], + "inputs": [ + "package.json", + "android", + "!android/build", + "src/*.ts", + "src/*.tsx", + "example/package.json", + "example/android", + "!example/android/.gradle", + "!example/android/build", + "!example/android/app/build" + ], + "outputs": [] + }, + "build:ios": { + "env": [ + "RCT_NEW_ARCH_ENABLED", + "RCT_USE_RN_DEP", + "RCT_USE_PREBUILT_RNCORE" + ], + "inputs": [ + "package.json", + "*.podspec", + "ios", + "src/*.ts", + "src/*.tsx", + "example/package.json", + "example/ios", + "!example/ios/build", + "!example/ios/Pods" + ], + "outputs": [] + } + } +} diff --git a/packages/react-native-sdk/ios/StreamVideoReactNative.h b/packages/react-native-sdk/ios/StreamVideoReactNative.h index 9683580f3b..bd0aa76556 100644 --- a/packages/react-native-sdk/ios/StreamVideoReactNative.h +++ b/packages/react-native-sdk/ios/StreamVideoReactNative.h @@ -1,11 +1,12 @@ #import #import +#import @interface StreamVideoReactNative : RCTEventEmitter - (void)screenShareEventReceived:(NSString *)event; -+ (void)registerIncomingCall:(NSString *)cid uuid:(NSString *)uuid; ++ (void)didReceiveIncomingPush:(PKPushPayload *)payload completionHandler: (void (^_Nullable)(void)) completion; + (void)setup DEPRECATED_MSG_ATTRIBUTE("No need to use setup() anymore"); diff --git a/packages/react-native-sdk/ios/StreamVideoReactNative.m b/packages/react-native-sdk/ios/StreamVideoReactNative.m index 5d2c293dfa..54db9d0994 100644 --- a/packages/react-native-sdk/ios/StreamVideoReactNative.m +++ b/packages/react-native-sdk/ios/StreamVideoReactNative.m @@ -4,6 +4,7 @@ #import #import #import +#import #import "StreamVideoReactNative.h" #import "WebRTCModule.h" #import "WebRTCModuleOptions.h" @@ -14,8 +15,6 @@ NSNotificationName const kBroadcastStartedNotification = @"iOS_BroadcastStarted"; NSNotificationName const kBroadcastStoppedNotification = @"iOS_BroadcastStopped"; -static NSMutableDictionary *_incomingCallUUIDsByCallID = nil; -static NSMutableDictionary *_incomingCallCidsByUUID = nil; static dispatch_queue_t _dictionaryQueue = nil; static BOOL _shouldRejectCallWhenBusy = NO; @@ -60,11 +59,62 @@ +(void)initializeSharedDictionaries { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _dictionaryQueue = dispatch_queue_create("com.stream.video.dictionary", DISPATCH_QUEUE_SERIAL); - _incomingCallUUIDsByCallID = [NSMutableDictionary dictionary]; - _incomingCallCidsByUUID = [NSMutableDictionary dictionary]; }); } ++(void)didReceiveIncomingPush:(PKPushPayload *)payload completionHandler: (void (^_Nullable)(void)) completion { + NSDictionary *streamPayload = payload.dictionaryPayload[@"stream"]; + if (!streamPayload) { + NSLog(@"[StreamVideoReactNative][didReceiveIncomingPush] Stream payload not found"); + return; + } + + Class callingxClass = NSClassFromString(@"Callingx"); + if (!callingxClass) { + NSLog(@"[StreamVideoReactNative][didReceiveIncomingPush] Callingx not available"); + return; + } + + NSLog(@"[StreamVideoReactNative][didReceiveIncomingPush] Callingx available"); + + SEL selector = @selector(reportNewIncomingCall:handle:handleType:hasVideo:localizedCallerName:supportsHolding:supportsDTMF:supportsGrouping:supportsUngrouping:fromPushKit:payload:withCompletionHandler:); + + if (![callingxClass respondsToSelector:selector]) { + NSLog(@"[StreamVideoReactNative][didReceiveIncomingPush] Callingx does not respond to selector"); + return; + } + + NSString *callCid = streamPayload[@"call_cid"]; + NSString *createdCallerName = streamPayload[@"created_by_display_name"]; + NSString *videoIncluded = streamPayload[@"video"]; + BOOL hasVideo = [videoIncluded isEqualToString:@"false"] ? NO : YES; + NSString *handleType = @"generic"; + BOOL supportsHolding = NO; + BOOL supportsDTMF = NO; + BOOL supportsGrouping = NO; + BOOL supportsUngrouping = NO; + BOOL fromPushKit = YES; + void (^completionHandler)(void) = nil; + + NSMethodSignature *signature = [callingxClass methodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + [invocation setTarget:callingxClass]; + [invocation setSelector:selector]; + [invocation setArgument:&callCid atIndex:2]; + [invocation setArgument:&createdCallerName atIndex:3]; + [invocation setArgument:&handleType atIndex:4]; + [invocation setArgument:&hasVideo atIndex:5]; + [invocation setArgument:&createdCallerName atIndex:6]; + [invocation setArgument:&supportsHolding atIndex:7]; + [invocation setArgument:&supportsDTMF atIndex:8]; + [invocation setArgument:&supportsGrouping atIndex:9]; + [invocation setArgument:&supportsUngrouping atIndex:10]; + [invocation setArgument:&fromPushKit atIndex:11]; + [invocation setArgument:&streamPayload atIndex:12]; + [invocation setArgument:&completionHandler atIndex:13]; + [invocation invoke]; +} + -(instancetype)init { if ((self = [super init])) { _notificationCenter = CFNotificationCenterGetDarwinNotifyCenter(); @@ -191,71 +241,6 @@ -(void)screenShareEventReceived:(NSString*)event { } } -+(void)registerIncomingCall:(NSString *)cid uuid:(NSString *)uuid { - [StreamVideoReactNative initializeSharedDictionaries]; - dispatch_sync(_dictionaryQueue, ^{ - -#ifdef DEBUG - NSLog(@"registerIncomingCall cid:%@ -> uuid:%@",cid,uuid); -#endif - NSString *lowercaseUUID = [uuid lowercaseString]; - _incomingCallUUIDsByCallID[cid] = lowercaseUUID; - _incomingCallCidsByUUID[lowercaseUUID] = cid; - }); -} - -RCT_EXPORT_METHOD(getIncomingCallUUid:(NSString *)cid - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) -{ - dispatch_sync(_dictionaryQueue, ^{ - NSString *uuid = _incomingCallUUIDsByCallID[cid]; - if (uuid) { - resolve(uuid); - } else { - NSString *errorString = [NSString stringWithFormat:@"requested incoming call not found for cid: %@", cid]; - reject(@"access_failure", errorString, nil); - } - }); -} - -RCT_EXPORT_METHOD(getIncomingCallCid:(NSString *)uuid - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) -{ - dispatch_sync(_dictionaryQueue, ^{ - NSString *lowercaseUUID = [uuid lowercaseString]; - NSString *foundCid = _incomingCallCidsByUUID[lowercaseUUID]; - - if (foundCid) { - resolve(foundCid); - } else { - NSString *errorString = [NSString stringWithFormat:@"requested incoming call not found for uuid: %@", uuid]; - reject(@"access_failure", errorString, nil); - } - }); -} - -RCT_EXPORT_METHOD(removeIncomingCall:(NSString *)cid - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject) -{ - dispatch_sync(_dictionaryQueue, ^{ - NSString *uuid = _incomingCallUUIDsByCallID[cid]; - if (uuid) { -#ifdef DEBUG - NSLog(@"removeIncomingCall cid:%@ -> uuid:%@",cid,uuid); -#endif - - [_incomingCallUUIDsByCallID removeObjectForKey:cid]; - [_incomingCallCidsByUUID removeObjectForKey:uuid]; - resolve(@YES); - } else { - resolve(@NO); - } - }); -} - RCT_EXPORT_METHOD(captureRef:(nonnull NSNumber *)reactTag options:(NSDictionary *)options resolver:(RCTPromiseResolveBlock)resolve diff --git a/packages/react-native-sdk/package.json b/packages/react-native-sdk/package.json index 3f62437a12..d695b17c8e 100644 --- a/packages/react-native-sdk/package.json +++ b/packages/react-native-sdk/package.json @@ -67,7 +67,7 @@ "expo-notifications": "*", "react": ">=17.0.0", "react-native": ">=0.73.0", - "react-native-callkeep": ">=4.3.11", + "react-native-callingx": "workspace:^", "react-native-gesture-handler": ">=2.8.0", "react-native-reanimated": ">=2.7.0", "react-native-svg": ">=13.6.0", @@ -101,7 +101,7 @@ "expo-notifications": { "optional": true }, - "react-native-callkeep": { + "react-native-callingx": { "optional": true }, "react-native-gesture-handler": { @@ -143,7 +143,7 @@ "react": "19.1.0", "react-native": "^0.81.5", "react-native-builder-bob": "~0.23", - "react-native-callkeep": "^4.3.16", + "react-native-callingx": "workspace:^", "react-native-gesture-handler": "^2.28.0", "react-native-reanimated": "~4.1.2", "react-native-svg": "^15.14.0", diff --git a/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts new file mode 100644 index 0000000000..84c5c295ce --- /dev/null +++ b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts @@ -0,0 +1,180 @@ +import { + CallingState, + RxUtils, + videoLoggerSystem, +} from '@stream-io/video-client'; +import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings'; +import { useEffect, useRef } from 'react'; +import { voipPushNotificationCallCId$ } from '../../utils/push/internal/rxSubjects'; +import { getCallingxLibIfAvailable } from '../../utils/push/libs/callingx'; + +//calling state methods are not exhaustive, so we need to add more methods to cover all the cases +//for now we can check is state is joined or !joined +const isAcceptedCallingState = (callingState: CallingState | undefined) => { + if (!callingState) { + return false; + } + + return callingState === CallingState.JOINED; +}; + +const logger = videoLoggerSystem.getLogger( + 'useCallingExpWithCallingStateEffect', +); + +/** + * This hook is used to inform sync call state with CallKit/Telecom (e.i. start call, end call, mute/unmute call). + */ +export const useCallingExpWithCallingStateEffect = () => { + const { useCallCallingState, useMicrophoneState } = useCallStateHooks(); + + const activeCall = useCall(); + const callingState = useCallCallingState(); + const { isMute, microphone } = useMicrophoneState(); + + const prevState = useRef(undefined); + + const activeCallCid = activeCall?.cid; + // const isOutcomingCall = activeCall?.isCreatedByMe; //can be used to trigger CallKit/Telecom start call for outcoming calls + + useEffect(() => { + return () => { + const callingx = getCallingxLibIfAvailable(); + if (!callingx) { + return; + } + //as an alternative think about method which will return CallKit/Telecom state for given cid + const incomingCallCid = RxUtils.getCurrentValue( + voipPushNotificationCallCId$, + ); + if ( + !activeCallCid || + !incomingCallCid || + incomingCallCid !== activeCallCid + ) { + logger.debug( + `No active call cid to end in calling exp: ${activeCallCid} incomingCallCid: ${incomingCallCid}`, + ); + return; + } + //if incoming stream call was unmounted, we need to end the call in CallKit/Telecom + //TODO: think about sending appropriate reason for end call + callingx.endCallWithReason(activeCallCid, 'local'); + voipPushNotificationCallCId$.next(undefined); + }; + }, [activeCallCid]); + + useEffect(() => { + const callingx = getCallingxLibIfAvailable(); + if (!callingx) { + return; + } + + //as an alternative think about method which will return CallKit/Telecom state for given cid + const incomingCallCid = RxUtils.getCurrentValue( + voipPushNotificationCallCId$, + ); + if ( + !incomingCallCid || + !activeCallCid || + incomingCallCid !== activeCallCid + ) { + logger.debug( + `No active call cid to end in calling exp: ${activeCallCid} incomingCallCid: ${incomingCallCid}`, + ); + return; + } + + logger.debug( + `useEffect: ${activeCallCid} incomingCallCid: ${incomingCallCid}`, + ); + logger.debug( + `prevState.current: ${prevState.current}, current callingState: ${callingState}`, + ); + + if (prevState.current !== callingState) { + if ( + !isAcceptedCallingState(prevState.current) && + isAcceptedCallingState(callingState) + ) { + //in case call was registered as incoming and state changed to joined, we need to answer the call + logger.debug( + `Should accept call in callkeep: ${activeCallCid} callCid: ${incomingCallCid}`, + ); + callingx.answerIncomingCall(activeCallCid); + } else if ( + isAcceptedCallingState(prevState.current) && + !isAcceptedCallingState(callingState) + ) { + //in case call was registered as incoming and state changed to "not joined", we need to end the call and clear rxjs subject + logger.debug(`Should end call in callkeep: ${activeCallCid}`); + //TODO: think about sending appropriate reason for end call + callingx.endCallWithReason(activeCallCid, 'local'); + voipPushNotificationCallCId$.next(undefined); + } + } + + prevState.current = callingState; + }, [activeCallCid, callingState]); + + useEffect(() => { + const callingx = getCallingxLibIfAvailable(); + if (!callingx) { + return; + } + + //for now supports only incoming calls + const incomingCallCid = RxUtils.getCurrentValue( + voipPushNotificationCallCId$, + ); + if ( + !incomingCallCid || + !activeCallCid || + incomingCallCid !== activeCallCid + ) { + logger.debug( + `No active call cid to set muted in calling exp: ${activeCallCid} incomingCallCid: ${incomingCallCid}`, + ); + return; + } + + callingx.setMutedCall(activeCallCid, isMute); + }, [activeCallCid, isMute]); + + useEffect(() => { + const callingx = getCallingxLibIfAvailable(); + if (!callingx || !activeCallCid) { + return; + } + + //listen to mic toggle events from CallKit/Telecom and update stream call microphone state + const subscription = callingx.addEventListener( + 'didPerformSetMutedCallAction', + async (event) => { + const { callId, muted } = event; + + if (callId === activeCallCid) { + const isCurrentlyMuted = + RxUtils.getCurrentValue(microphone.state.status$) === 'disabled'; + if (isCurrentlyMuted === muted) { + logger.debug( + `Mic toggle is already in the desired state: ${muted} for call: ${activeCallCid}`, + ); + //this check prevents mic toggle when state change was initiated from client and not from CallKit/Telecom + return; + } + + if (muted) { + await microphone.disable(); + } else { + await microphone.enable(); + } + } + }, + ); + + return () => { + subscription.remove(); + }; + }, [activeCallCid, microphone]); +}; diff --git a/packages/react-native-sdk/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts b/packages/react-native-sdk/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts deleted file mode 100644 index 24b6de9b4d..0000000000 --- a/packages/react-native-sdk/src/hooks/push/useIosCallkeepWithCallingStateEffect.ts +++ /dev/null @@ -1,235 +0,0 @@ -import { - CallingState, - RxUtils, - videoLoggerSystem, -} from '@stream-io/video-client'; -import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings'; -import { NativeModules, Platform } from 'react-native'; -import { useEffect, useState } from 'react'; -import { StreamVideoRN } from '../../utils'; -import { getCallKeepLib } from '../../utils/push/libs'; -import { - voipCallkeepAcceptedCallOnNativeDialerMap$, - voipCallkeepCallOnForegroundMap$, - voipPushNotificationCallCId$, -} from '../../utils/push/internal/rxSubjects'; - -const isNonActiveCallingState = (callingState: CallingState) => { - return ( - callingState === CallingState.IDLE || - callingState === CallingState.UNKNOWN || - callingState === CallingState.LEFT - ); -}; - -const isAcceptedCallingState = (callingState: CallingState) => { - return ( - callingState === CallingState.JOINING || - callingState === CallingState.JOINED - ); -}; - -const unsubscribeCallkeepEvents = async (activeCallCid: string | undefined) => { - const voipPushNotificationCallCId = RxUtils.getCurrentValue( - voipPushNotificationCallCId$, - ); - if (activeCallCid && activeCallCid === voipPushNotificationCallCId) { - // callkeep events should not be listened anymore so clear the call cid - voipPushNotificationCallCId$.next(undefined); - } - return await NativeModules.StreamVideoReactNative?.removeIncomingCall( - activeCallCid, - ); -}; - -const logger = videoLoggerSystem.getLogger( - 'useIosCallkeepWithCallingStateEffect', -); -const log = (message: string) => { - logger.warn(message); -}; - -/** - * This hook is used to inform the callkeep library that the call has been joined or ended. - */ -export const useIosCallkeepWithCallingStateEffect = () => { - const activeCall = useCall(); - const { useCallCallingState } = useCallStateHooks(); - const callingState = useCallCallingState(); - const [acceptedForegroundCallkeepMap, setAcceptedForegroundCallkeepMap] = - useState<{ - uuid: string; - cid: string; - }>(); - - useEffect(() => { - return () => { - const pushConfig = StreamVideoRN.getConfig().push; - if ( - Platform.OS !== 'ios' || - !pushConfig || - !pushConfig.ios?.pushProviderName - ) { - return; - } - if (!pushConfig.android.incomingCallChannel) { - // TODO: remove this check and find a better way once we have telecom integration for android - return; - } - - const callkeep = getCallKeepLib(); - // if the component is unmounted and the callID was not reported to callkeep, then report it now - if (acceptedForegroundCallkeepMap) { - log( - `Ending call in callkeep: ${acceptedForegroundCallkeepMap.cid}, reason: component unmounted and call was present in acceptedForegroundCallkeepMap`, - ); - unsubscribeCallkeepEvents(acceptedForegroundCallkeepMap.cid).then(() => - callkeep.endCall(acceptedForegroundCallkeepMap.uuid), - ); - } - }; - }, [acceptedForegroundCallkeepMap]); - - const activeCallCid = activeCall?.cid; - - useEffect(() => { - return () => { - const pushConfig = StreamVideoRN.getConfig().push; - if ( - Platform.OS !== 'ios' || - !pushConfig || - !pushConfig.ios?.pushProviderName || - !activeCallCid - ) { - return; - } - if (!pushConfig.android.incomingCallChannel) { - // TODO: remove this check and find a better way once we have telecom integration for android - return; - } - const nativeDialerAcceptedCallMap = RxUtils.getCurrentValue( - voipCallkeepAcceptedCallOnNativeDialerMap$, - ); - const foregroundIncomingCallkeepMap = RxUtils.getCurrentValue( - voipCallkeepCallOnForegroundMap$, - ); - const callkeep = getCallKeepLib(); - if (activeCallCid === nativeDialerAcceptedCallMap?.cid) { - log( - `Ending call in callkeep: ${activeCallCid}, reason: activeCallCid changed or was removed and call was present in nativeDialerAcceptedCallMap`, - ); - unsubscribeCallkeepEvents(activeCallCid).then(() => - callkeep.endCall(nativeDialerAcceptedCallMap.uuid), - ); - // no need to keep this reference anymore - voipCallkeepAcceptedCallOnNativeDialerMap$.next(undefined); - } else if (activeCallCid === foregroundIncomingCallkeepMap?.cid) { - log( - `Ending call in callkeep: ${activeCallCid}, reason: activeCallCid changed or was removed and call was present in foregroundIncomingCallkeepMap`, - ); - unsubscribeCallkeepEvents(activeCallCid).then(() => - callkeep.endCall(foregroundIncomingCallkeepMap.uuid), - ); - } - }; - }, [activeCallCid]); - - const pushConfig = StreamVideoRN.getConfig().push; - if ( - Platform.OS !== 'ios' || - !pushConfig || - !pushConfig.ios.pushProviderName || - !activeCallCid - ) { - return; - } - if (!pushConfig.android.incomingCallChannel) { - // TODO: remove this check and find a better way once we have telecom integration for android - return; - } - - /** - * Check if current call is still needed to be accepted in callkeep - */ - if ( - isAcceptedCallingState(callingState) && - acceptedForegroundCallkeepMap?.cid !== activeCallCid - ) { - const callkeep = getCallKeepLib(); - // push notification was displayed - // but the call has been accepted through the app and not through the native dialer - const foregroundCallkeepMap = RxUtils.getCurrentValue( - voipCallkeepCallOnForegroundMap$, - ); - if (foregroundCallkeepMap && foregroundCallkeepMap.cid === activeCallCid) { - log( - // @ts-expect-error - types issue - `Accepting call in callkeep: ${activeCallCid}, reason: callingstate went to ${CallingState[callingState]} and call was present in foregroundCallkeepMap`, - ); - // no need to keep this reference anymore - voipCallkeepCallOnForegroundMap$.next(undefined); - NativeModules.StreamVideoReactNative?.removeIncomingCall( - activeCallCid, - ).then(() => callkeep.answerIncomingCall(foregroundCallkeepMap.uuid)); - // this call should be accepted in callkeep - setAcceptedForegroundCallkeepMap(foregroundCallkeepMap); - } - } - - /** - * Check if current call is still needed to be ended in callkeep - */ - if (isNonActiveCallingState(callingState)) { - const callkeep = getCallKeepLib(); - - // this was a previously joined call which had push notification displayed - // the call was accepted through the app and not through native dialer - // the call was left using the leave button in the app and not through native dialer - if (activeCallCid === acceptedForegroundCallkeepMap?.cid) { - log( - // @ts-expect-error - types issue - `Ending call in callkeep: ${activeCallCid}, reason: callingstate went to ${CallingState[callingState]} and call was present in acceptedForegroundCallkeepMap`, - ); - unsubscribeCallkeepEvents(activeCallCid).then(() => - callkeep.endCall(acceptedForegroundCallkeepMap.uuid), - ); - setAcceptedForegroundCallkeepMap(undefined); - return; - } - // this was a call which had push notification displayed but never joined - // the user rejected in the app and not from native dialer - const foregroundIncomingCallkeepMap = RxUtils.getCurrentValue( - voipCallkeepCallOnForegroundMap$, - ); - if (activeCallCid === foregroundIncomingCallkeepMap?.cid) { - log( - // @ts-expect-error - types issue - `Ending call in callkeep: ${activeCallCid}, reason: callingstate went to ${CallingState[callingState]} and call was present in foregroundIncomingCallkeepMap`, - ); - unsubscribeCallkeepEvents(activeCallCid).then(() => - callkeep.endCall(foregroundIncomingCallkeepMap.uuid), - ); - // no need to keep this reference anymore - voipCallkeepCallOnForegroundMap$.next(undefined); - return; - } - // this was a previously joined call - // it was an accepted call from native dialer and not from the app - // the user left using the leave button in the app - const nativeDialerAcceptedCallMap = RxUtils.getCurrentValue( - voipCallkeepAcceptedCallOnNativeDialerMap$, - ); - if (activeCallCid === nativeDialerAcceptedCallMap?.cid) { - log( - // @ts-expect-error - types issue - `Ending call in callkeep: ${activeCallCid}, reason: callingstate went to ${CallingState[callingState]} and call was present in nativeDialerAcceptedCallMap`, - ); - unsubscribeCallkeepEvents(activeCallCid).then(() => - callkeep.endCall(nativeDialerAcceptedCallMap.uuid), - ); - // no need to keep this reference anymore - voipCallkeepAcceptedCallOnNativeDialerMap$.next(undefined); - return; - } - } -}; diff --git a/packages/react-native-sdk/src/providers/StreamCall/index.tsx b/packages/react-native-sdk/src/providers/StreamCall/index.tsx index 6748eaac21..174bf037e4 100644 --- a/packages/react-native-sdk/src/providers/StreamCall/index.tsx +++ b/packages/react-native-sdk/src/providers/StreamCall/index.tsx @@ -1,12 +1,12 @@ import { StreamCallProvider } from '@stream-io/video-react-bindings'; import React, { type PropsWithChildren, useEffect } from 'react'; import { Call } from '@stream-io/video-client'; -import { useIosCallkeepWithCallingStateEffect } from '../../hooks/push/useIosCallkeepWithCallingStateEffect'; import { canAddPushWSSubscriptionsRef } from '../../utils/push/internal/utils'; import { useAndroidKeepCallAliveEffect } from '../../hooks/useAndroidKeepCallAliveEffect'; import { AppStateListener } from './AppStateListener'; import { DeviceStats } from './DeviceStats'; import { pushUnsubscriptionCallbacks } from '../../utils/push/internal/constants'; +import { useCallingExpWithCallingStateEffect } from '../../hooks/push/useCallingExpWithCallingStateEffect'; // const PIP_CHANGE_EVENT = 'StreamVideoReactNative_PIP_CHANGE_EVENT'; @@ -34,7 +34,7 @@ export const StreamCall = ({ - + {children} @@ -52,11 +52,11 @@ const AndroidKeepCallAlive = () => { }; /** - * This is a renderless component to end the call in callkeep for ios. - * useAndroidKeepCallAliveEffect needs to called inside a child of StreamCallProvider. + * This is a renderless component to sync state between stream call and CallKit/Telecom. + * useCallingExpWithCallingStateEffect needs to called inside a child of StreamCallProvider. */ -const IosInformCallkeepCallEnd = () => { - useIosCallkeepWithCallingStateEffect(); +const CallingExpWithCallingState = () => { + useCallingExpWithCallingStateEffect(); return null; }; diff --git a/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts b/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts index ca45cf4c50..f9d1d8fca6 100644 --- a/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts +++ b/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts @@ -1,11 +1,14 @@ +import { type Options as CallingxOptions } from 'react-native-callingx'; import type { StreamVideoConfig } from './types'; import pushLogoutCallbacks from '../internal/pushLogoutCallback'; import newNotificationCallbacks, { type NewCallNotificationCallback, } from '../internal/newNotificationCallbacks'; -import { setupIosCallKeepEvents } from '../push/setupIosCallKeepEvents'; import { setupIosVoipPushEvents } from '../push/setupIosVoipPushEvents'; +import { setupCallingExpEvents } from '../push/setupCallingExpEvents'; +import { getCallingxLib } from '../push/libs/callingx'; import { NativeModules, Platform } from 'react-native'; +import { videoLoggerSystem } from '@stream-io/video-client'; // Utility type for deep partial type DeepPartial = { @@ -123,10 +126,21 @@ export class StreamVideoRN { this.config.push = pushConfig; - setupIosCallKeepEvents(pushConfig); + setupCallingExpEvents(pushConfig); setupIosVoipPushEvents(pushConfig); } + static setupCallingExp(options: CallingxOptions) { + //check if calling exp pacakge is installed + const callingx = getCallingxLib(); + callingx.setup(options); + callingx.checkPermissions().catch((error) => { + videoLoggerSystem + .getLogger('setupCallingExp') + .error('failed to check permissions', error); + }); + } + static getConfig() { return this.config; } diff --git a/packages/react-native-sdk/src/utils/push/android.ts b/packages/react-native-sdk/src/utils/push/android.ts index 0937fd3a26..659c39df31 100644 --- a/packages/react-native-sdk/src/utils/push/android.ts +++ b/packages/react-native-sdk/src/utils/push/android.ts @@ -1,10 +1,10 @@ import { - Call, CallingState, + RxUtils, StreamVideoClient, videoLoggerSystem, } from '@stream-io/video-client'; -import { AppState, Platform } from 'react-native'; +import { Platform } from 'react-native'; import type { NonRingingPushEvent, StreamVideoConfig, @@ -15,30 +15,18 @@ import { getExpoNotificationsLibNoThrow, getFirebaseMessagingLib, getFirebaseMessagingLibNoThrow, - getIncomingCallForegroundServiceTypes, getNotifeeLibThrowIfNotInstalledForPush, type NotifeeLib, } from './libs'; import { - pushAcceptedIncomingCallCId$, - pushAndroidBackgroundDeliveredIncomingCallCId$, pushNonRingingCallData$, - pushRejectedIncomingCallCId$, - pushTappedIncomingCallCId$, + voipPushNotificationCallCId$, } from './internal/rxSubjects'; import { pushUnsubscriptionCallbacks } from './internal/constants'; -import { - canAddPushWSSubscriptionsRef, - clearPushWSEventSubscriptions, - processCallFromPushInBackground, - shouldCallBeEnded, -} from './internal/utils'; +import { canListenToWS, shouldCallBeClosed } from './internal/utils'; import { setPushLogoutCallback } from '../internal/pushLogoutCallback'; -import { getAndroidDefaultRingtoneUrl } from '../getAndroidDefaultRingtoneUrl'; import { StreamVideoRN } from '../StreamVideoRN'; - -const ACCEPT_CALL_ACTION_ID = 'accept'; -const DECLINE_CALL_ACTION_ID = 'decline'; +import { getCallingxLib } from './libs/callingx'; type PushConfig = NonNullable; @@ -127,10 +115,10 @@ export async function initAndroidPushToken( * Creates notification from the push message data. * For Ringing and Non-Ringing calls. */ + export const firebaseDataHandler = async ( data: FirebaseMessagingTypes.RemoteMessage['data'], ) => { - if (Platform.OS !== 'android') return; /* Example data from firebase "message": { "data": { @@ -146,279 +134,258 @@ export const firebaseDataHandler = async ( // other stuff } */ + if (Platform.OS !== 'android') return; + + const logger = videoLoggerSystem.getLogger('firebaseDataHandler'); const pushConfig = StreamVideoRN.getConfig().push; if (!pushConfig || !data || data.sender !== 'stream.video') { return; } - const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); - const notifee = notifeeLib.default; - const settings = await notifee.getNotificationSettings(); - if (settings.authorizationStatus !== 1) { - const logger = videoLoggerSystem.getLogger('firebaseDataHandler'); - logger.debug( - `Notification permission not granted, unable to post ${data.type} notifications`, - ); - return; - } if (data.type === 'call.ring') { const call_cid = data.call_cid as string; - const created_by_id = data.created_by_id as string; - const receiver_id = data.receiver_id as string; + if (!call_cid) { + logger.debug( + `call_cid is not provided, skipping the call.ring notification`, + ); + return; + } - const video_client = await pushConfig.createStreamVideoClient(); - await video_client?.onRingingCall(call_cid); + if (RxUtils.getCurrentValue(voipPushNotificationCallCId$)) { + logger.debug( + `call.ring notification already processed, skipping the call.ring notification`, + ); + return; + } - const shouldCallBeClosed = (callToCheck: Call) => { - const { mustEndCall } = shouldCallBeEnded( - callToCheck, - created_by_id, - receiver_id, + const callingx = getCallingxLib(); + const permissions = await callingx.checkPermissions(); + if (!permissions.postNotifications) { + logger.debug( + `Notification permission not granted, unable to post ${data.type} notifications`, ); - return mustEndCall; - }; + return; + } + + const client = await pushConfig.createStreamVideoClient(); + if (!client) { + logger.debug( + `video client not found, skipping the call.ring notification`, + ); + return; + } - const canListenToWS = () => - canAddPushWSSubscriptionsRef.current && - AppState.currentState !== 'active'; const asForegroundService = canListenToWS(); + const phoneNumber = data.created_by_display_name as string; + const callerName = data.call_display_name as string; + const hasVideo = data.video === 'true'; + + await callingx.displayIncomingCall( + call_cid, + phoneNumber, + callerName, + hasVideo, + ); + logger.debug( + `Displaying incoming call notification with callCid: ${call_cid} asForegroundService: ${asForegroundService}`, + ); + voipPushNotificationCallCId$.next(call_cid); + if (asForegroundService) { // Listen to call events from WS through fg service // note: this will replace the current empty fg service runner - notifee.registerForegroundService(() => { - return new Promise(async () => { - const client = await pushConfig.createStreamVideoClient(); - if (!client) { - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Closing fg service as there is no client to create from push config`, - ); - notifee.stopForegroundService(); + //we need to start service (e.g. by calling display incoming call) and than launch bg task, consider making those steps independent + await callingx.startBackgroundTask((_: unknown, stopTask: () => void) => { + return new Promise(async (resolve) => { + const finishBackgroundTask = () => { + resolve(undefined); + stopTask(); + }; + + const _client = await pushConfig.createStreamVideoClient(); + if (!_client) { + logger.debug( + `Closing fg service as there is no client to create from push config`, + ); + finishBackgroundTask(); return; } - const callFromPush = await client.onRingingCall(call_cid); - let _shouldCallBeClosed = shouldCallBeClosed(callFromPush); - if (_shouldCallBeClosed) { - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Closing fg service callCid: ${call_cid} shouldCallBeClosed: ${_shouldCallBeClosed}`, - ); - notifee.stopForegroundService(); + + const callFromPush = await _client.onRingingCall(call_cid); + if (shouldCallBeClosed(callFromPush, data)) { + logger.debug( + `Closing fg service callCid: ${call_cid} shouldCallBeClosed`, + ); + + finishBackgroundTask(); + callingx.log( + `Ending call with callCid: ${call_cid} shouldCallBeClosed`, + 'debug', + ); + voipPushNotificationCallCId$.next(undefined); + //TODO: think about sending appropriate reason for end call + callingx.endCallWithReason(call_cid, 'remote'); return; } + const unsubscribeFunctions: Array<() => void> = []; // check if service needs to be closed if accept/decline event was done on another device const unsubscribe = callFromPush.on('all', (event) => { const _canListenToWS = canListenToWS(); if (!_canListenToWS) { - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Closing fg service from event callCid: ${call_cid} canListenToWS: ${_canListenToWS}`, - { event }, - ); + logger.debug( + `Closing fg service from event callCid: ${call_cid} canListenToWS: ${_canListenToWS}`, + { event }, + ); unsubscribeFunctions.forEach((fn) => fn()); - notifee.stopForegroundService(); + + finishBackgroundTask(); return; } - _shouldCallBeClosed = shouldCallBeClosed(callFromPush); - if (_shouldCallBeClosed) { - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Closing fg service from event callCid: ${call_cid} canListenToWS: ${_canListenToWS} shouldCallBeClosed: ${_shouldCallBeClosed}`, - { event }, - ); + + if (shouldCallBeClosed(callFromPush, data)) { + logger.debug( + `Closing fg service from event callCid: ${call_cid} canListenToWS: ${_canListenToWS} shouldCallBeClosed`, + { event }, + ); unsubscribeFunctions.forEach((fn) => fn()); - notifee.stopForegroundService(); + + voipPushNotificationCallCId$.next(undefined); + finishBackgroundTask(); + //TODO: think about sending appropriate reason for end call + callingx.endCallWithReason(call_cid, 'rejected'); } }); + // check if service needs to be closed if call was left - const subscription = callFromPush.state.callingState$.subscribe( + const stateSubscription = callFromPush.state.callingState$.subscribe( (callingState) => { if ( callingState === CallingState.IDLE || callingState === CallingState.LEFT ) { - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Closing fg service from callingState callCid: ${call_cid} callingState: ${callingState}`, - ); + logger.debug( + `Closing fg service from callingState callCid: ${call_cid} callingState: ${callingState}`, + ); unsubscribeFunctions.forEach((fn) => fn()); - notifee.stopForegroundService(); + + //TODO: think about sending appropriate reason for end call + callingx.log( + `Ending call with callCid: ${call_cid} callingState: ${callingState}`, + 'debug', + ); + voipPushNotificationCallCId$.next(undefined); + finishBackgroundTask(); + callingx.endCallWithReason(call_cid, 'remote'); } }, ); + + const endCallSubscription = callingx.addEventListener( + 'endCall', + async ({ callId }: { callId: string }) => { + unsubscribeFunctions.forEach((fn) => fn()); + try { + await callFromPush.leave({ reject: true, reason: 'decline' }); + } catch (error) { + logger.error( + `Failed to leave call with callCid: ${call_cid} error: ${error}`, + ); + } finally { + callingx.log( + `Ending call with callCid: ${call_cid} callId: ${callId}`, + 'debug', + ); + voipPushNotificationCallCId$.next(undefined); + finishBackgroundTask(); + callingx.endCallWithReason(callId, 'rejected'); + } + }, + ); + unsubscribeFunctions.push(unsubscribe); - unsubscribeFunctions.push(() => subscription.unsubscribe()); + unsubscribeFunctions.push(() => stateSubscription.unsubscribe()); + unsubscribeFunctions.push(() => endCallSubscription.remove()); pushUnsubscriptionCallbacks.get(call_cid)?.forEach((cb) => cb()); pushUnsubscriptionCallbacks.set(call_cid, unsubscribeFunctions); }); }); } - const incomingCallChannel = pushConfig.android.incomingCallChannel; - const incomingCallNotificationTextGetters = - pushConfig.android.incomingCallNotificationTextGetters; - if (!incomingCallChannel || !incomingCallNotificationTextGetters) { - const logger = videoLoggerSystem.getLogger( - 'firebaseMessagingOnMessageHandler', - ); - logger.error( - "Can't show incoming call notification as either or both incomingCallChannel and incomingCallNotificationTextGetters were not provided", - ); - return; - } - /* - * Sound has to be set on channel level for android 8 and above and cant be updated later after creation! - * For android 7 and below, sound should be set on notification level - */ - // set default ringtone if not provided - if (!incomingCallChannel.sound) { - incomingCallChannel.sound = await getAndroidDefaultRingtoneUrl(); - } - await notifee.createChannel(incomingCallChannel); - const { getTitle, getBody, getAcceptButtonTitle, getDeclineButtonTitle } = - incomingCallNotificationTextGetters; - const createdUserName = data.created_by_display_name as string; - - const title = getTitle(createdUserName); - const body = getBody(createdUserName); - - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Displaying incoming call notification with callCid: ${call_cid} title: ${title} body: ${body} asForegroundService: ${asForegroundService}`, - ); - - const channelId = incomingCallChannel.id; - await notifee.displayNotification({ - id: call_cid, - title: getTitle(createdUserName), - body: getBody(createdUserName), - data, - android: { - channelId, - smallIcon: pushConfig.android.smallIcon, - importance: 4, // high importance - foregroundServiceTypes: getIncomingCallForegroundServiceTypes(), - asForegroundService, - ongoing: true, - sound: incomingCallChannel.sound, - vibrationPattern: incomingCallChannel.vibrationPattern, - loopSound: true, - pressAction: { - id: 'default', - launchActivity: 'default', // open the app when the notification is pressed - }, - actions: [ - { - title: getDeclineButtonTitle?.() ?? 'Decline', - pressAction: { - id: DECLINE_CALL_ACTION_ID, - }, - }, - { - title: getAcceptButtonTitle?.() ?? 'Accept', - pressAction: { - id: ACCEPT_CALL_ACTION_ID, - launchActivity: 'default', // open the app when the notification is pressed - }, - }, - ], - category: notifeeLib.AndroidCategory.CALL, - fullScreenAction: { - id: 'stream_ringing_incoming_call', - }, - timeoutAfter: 60000, // 60 seconds, after which the notification will be dismissed automatically - }, - }); if (asForegroundService) { // no need to check if call has be closed as that will be handled by the fg service return; } - // check if call needs to be closed if accept/decline event was done - // before the notification was shown - const client = await pushConfig.createStreamVideoClient(); - if (!client) { - return; - } const callFromPush = await client.onRingingCall(call_cid); - if (shouldCallBeClosed(callFromPush)) { - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( - `Removing incoming call notification immediately with callCid: ${call_cid} as it should be closed`, - ); - notifee.cancelDisplayedNotification(call_cid); + if (shouldCallBeClosed(callFromPush, data)) { + logger.debug( + `Removing incoming call notification immediately with callCid: ${call_cid} as it should be closed`, + ); + //TODO: think about sending appropriate reason for end call + callingx.endCallWithReason(call_cid, 'remote'); } } else { - // the other types are call.live_started and call.notification - const callChannel = pushConfig.android.callChannel; - const callNotificationTextGetters = - pushConfig.android.callNotificationTextGetters; - if (!callChannel || !callNotificationTextGetters) { - const logger = videoLoggerSystem.getLogger( - 'firebaseMessagingOnMessageHandler', - ); + const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); + const notifee = notifeeLib.default; + const settings = await notifee.getNotificationSettings(); + if (settings.authorizationStatus !== 1) { logger.debug( - "Can't show call notification as either or both callChannel and callNotificationTextGetters is not provided", + `Notification permission not granted, unable to post ${data.type} notifications`, ); - return; - } - await notifee.createChannel(callChannel); - const channelId = callChannel.id; - const { getTitle, getBody } = callNotificationTextGetters; - const createdUserName = data.created_by_display_name as string; - // we can safely cast to string because the data is from "stream.video" - const type = data.type as NonRingingPushEvent; - const title = getTitle(type, createdUserName); - const body = getBody(type, createdUserName); + // the other types are call.live_started and call.notification + const callChannel = pushConfig.android.callChannel; + const callNotificationTextGetters = + pushConfig.android.callNotificationTextGetters; + if (!callChannel || !callNotificationTextGetters) { + logger.debug( + "Can't show call notification as either or both callChannel and callNotificationTextGetters is not provided", + ); + return; + } + await notifee.createChannel(callChannel); + const channelId = callChannel.id; + const { getTitle, getBody } = callNotificationTextGetters; + const createdUserName = data.created_by_display_name as string; + // we can safely cast to string because the data is from "stream.video" + const type = data.type as NonRingingPushEvent; - videoLoggerSystem - .getLogger('firebaseMessagingOnMessageHandler') - .debug( + const title = getTitle(type, createdUserName); + const body = getBody(type, createdUserName); + + logger.debug( `Displaying NonRingingPushEvent ${type} notification with title: ${title} body: ${body}`, ); - await notifee.displayNotification({ - title: getTitle(type, createdUserName), - body: getBody(type, createdUserName), - data, - android: { - sound: callChannel.sound, - smallIcon: pushConfig.android.smallIcon, - vibrationPattern: callChannel.vibrationPattern, - channelId, - importance: 4, // high importance - pressAction: { - id: 'default', - launchActivity: 'default', // open the app when the notification is pressed + await notifee.displayNotification({ + title: getTitle(type, createdUserName), + body: getBody(type, createdUserName), + data, + android: { + sound: callChannel.sound, + smallIcon: pushConfig.android.smallIcon, + vibrationPattern: callChannel.vibrationPattern, + channelId, + importance: 4, // high importance + pressAction: { + id: 'default', + launchActivity: 'default', // open the app when the notification is pressed + }, + timeoutAfter: 60000, // 60 seconds, after which the notification will be dismissed automatically }, - timeoutAfter: 60000, // 60 seconds, after which the notification will be dismissed automatically - }, - }); - const cid = data.call_cid as string; - pushNonRingingCallData$.next({ cid, type }); + }); + const cid = data.call_cid as string; + pushNonRingingCallData$.next({ cid, type }); + } } }; -export const onAndroidNotifeeEvent = async ({ - event, - isBackground, -}: { - event: Event; - isBackground: boolean; -}) => { +export const onAndroidNotifeeEvent = async ({ event }: { event: Event }) => { if (Platform.OS !== 'android') return; const { type, detail } = event; - const { notification, pressAction } = detail; + const { notification } = detail; const notificationId = notification?.id; const data = notification?.data; const pushConfig = StreamVideoRN.getConfig().push; @@ -434,92 +401,14 @@ export const onAndroidNotifeeEvent = async ({ // we can safely cast to string because the data is from "stream.video" const call_cid = data.call_cid as string; - if (data.type === 'call.ring') { - // check if we have observers for the call cid (this means the app is in the foreground state) - const hasObservers = - pushAcceptedIncomingCallCId$.observed && - pushRejectedIncomingCallCId$.observed; - - const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); - const notifee = notifeeLib.default; - // Check if we need to decline the call - const didPressDecline = - type === notifeeLib.EventType.ACTION_PRESS && - pressAction?.id === DECLINE_CALL_ACTION_ID; - const didDismiss = type === notifeeLib.EventType.DISMISSED; - const mustDecline = didPressDecline || didDismiss; - // Check if we need to accept the call - const mustAccept = - type === notifeeLib.EventType.ACTION_PRESS && - pressAction?.id === ACCEPT_CALL_ACTION_ID; - - if ( - mustAccept || - mustDecline || - type === notifeeLib.EventType.ACTION_PRESS - ) { - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug( - `clearPushWSEventSubscriptions for callCId: ${call_cid} mustAccept: ${mustAccept} mustDecline: ${mustDecline}`, - ); - clearPushWSEventSubscriptions(call_cid); - notifee.stopForegroundService(); - } - - if (mustAccept) { - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug(`pushAcceptedIncomingCallCId$ added with callCId: ${call_cid}`); - pushAcceptedIncomingCallCId$.next(call_cid); - // NOTE: accept will be handled by the app with rxjs observers as the app will go to foreground always - } else if (mustDecline) { - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug(`pushRejectedIncomingCallCId$ added with callCId: ${call_cid}`); - pushRejectedIncomingCallCId$.next(call_cid); - if (hasObservers) { - // if we had observers we can return here as the observers will handle the call as the app is in the foreground state - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug( - `Skipped processCallFromPushInBackground for Declining call with callCId: ${call_cid} as the app is in the foreground state`, - ); - return; - } - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug( - `start processCallFromPushInBackground - Declining call with callCId: ${call_cid}`, - ); - await processCallFromPushInBackground(pushConfig, call_cid, 'decline'); - } else { - if (type === notifeeLib.EventType.PRESS) { - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug(`pushTappedIncomingCallCId$ added with callCId: ${call_cid}`); - pushTappedIncomingCallCId$.next(call_cid); - // pressed state will be handled by the app with rxjs observers as the app will go to foreground always - } else if (isBackground && type === notifeeLib.EventType.DELIVERED) { - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug( - `pushAndroidBackgroundDeliveredIncomingCallCId$ added with callCId: ${call_cid}`, - ); - pushAndroidBackgroundDeliveredIncomingCallCId$.next(call_cid); - // background delivered state will be handled by the app with rxjs observers as processing needs to happen only when app is opened - } - } - } else { - const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); - if (type === notifeeLib.EventType.PRESS) { - videoLoggerSystem - .getLogger('onAndroidNotifeeEvent') - .debug(`onTapNonRingingCallNotification with callCId: ${call_cid}`); - pushConfig.onTapNonRingingCallNotification?.( - call_cid, - data.type as NonRingingPushEvent, - ); - } + const notifeeLib = getNotifeeLibThrowIfNotInstalledForPush(); + if (type === notifeeLib.EventType.PRESS) { + videoLoggerSystem + .getLogger('onAndroidNotifeeEvent') + .debug(`onTapNonRingingCallNotification with callCId: ${call_cid}`); + pushConfig.onTapNonRingingCallNotification?.( + call_cid, + data.type as NonRingingPushEvent, + ); } }; diff --git a/packages/react-native-sdk/src/utils/push/internal/ios.ts b/packages/react-native-sdk/src/utils/push/internal/ios.ts index 8801017b45..613f3b04de 100644 --- a/packages/react-native-sdk/src/utils/push/internal/ios.ts +++ b/packages/react-native-sdk/src/utils/push/internal/ios.ts @@ -1,10 +1,11 @@ -import { AppState, NativeModules, Platform } from 'react-native'; -import { getCallKeepLib, getVoipPushNotificationLib } from '../libs'; +import { NativeModules, Platform } from 'react-native'; +import { getVoipPushNotificationLib } from '../libs'; import { voipPushNotificationCallCId$ } from './rxSubjects'; import { pushUnsubscriptionCallbacks } from './constants'; -import { canAddPushWSSubscriptionsRef, shouldCallBeEnded } from './utils'; +import { canListenToWS, shouldCallBeClosed } from './utils'; import { StreamVideoConfig } from '../../StreamVideoRN/types'; -import { videoLoggerSystem } from '@stream-io/video-client'; +import { RxUtils, videoLoggerSystem } from '@stream-io/video-client'; +import { getCallingxLib } from '../libs/callingx'; export const onVoipNotificationReceived = async ( notification: any, @@ -32,6 +33,8 @@ export const onVoipNotificationReceived = async ( "version": "v2" } } */ + const logger = videoLoggerSystem.getLogger('setupIosVoipPushEvents'); + const sender = notification?.stream?.sender; const type = notification?.stream?.type; // do not process any other notifications other than stream.video or ringing @@ -42,9 +45,15 @@ export const onVoipNotificationReceived = async ( if (!call_cid || Platform.OS !== 'ios' || !pushConfig.ios.pushProviderName) { return; } - const logger = videoLoggerSystem.getLogger('setupIosVoipPushEvents'); - const client = await pushConfig.createStreamVideoClient(); + if (RxUtils.getCurrentValue(voipPushNotificationCallCId$)) { + logger.debug( + `call.ring notification already processed, skipping the call.ring notification`, + ); + return; + } + + const client = await pushConfig.createStreamVideoClient(); if (!client) { logger.debug( 'client not found, not processing call.ring voip push notification', @@ -59,44 +68,23 @@ export const onVoipNotificationReceived = async ( ); } const callFromPush = await client.onRingingCall(call_cid); - let uuid = ''; - try { - uuid = - await NativeModules?.StreamVideoReactNative?.getIncomingCallUUid( - call_cid, - ); - } catch (error) { - logger.error('Error in getting call uuid from native module', error); - } - if (!uuid) { - logger.error( - `Not processing call.ring push notification, as no uuid found for call_cid: ${call_cid}`, - ); - return; - } - const created_by_id = notification?.stream?.created_by_id; - const receiver_id = notification?.stream?.receiver_id; + function closeCallIfNecessary() { - const { mustEndCall, callkeepReason } = shouldCallBeEnded( - callFromPush, - created_by_id, - receiver_id, - ); + const mustEndCall = shouldCallBeClosed(callFromPush, notification?.stream); if (mustEndCall) { - const callkeep = getCallKeepLib(); - logger.debug( - `callkeep.reportEndCallWithUUID for uuid: ${uuid}, call_cid: ${call_cid}, reason: ${callkeepReason}`, - ); - callkeep.reportEndCallWithUUID(uuid, callkeepReason); + logger.debug(`callkeep.reportEndCallWithUUID for call_cid: ${call_cid}`); + //TODO: think about sending appropriate reason for end call + const callingx = getCallingxLib(); + callingx.endCallWithReason(call_cid, 'remote'); + const voipPushNotification = getVoipPushNotificationLib(); - voipPushNotification.onVoipNotificationCompleted(uuid); + voipPushNotification.onVoipNotificationCompleted(call_cid); return true; } return false; } + const closed = closeCallIfNecessary(); - const canListenToWS = () => - canAddPushWSSubscriptionsRef.current && AppState.currentState !== 'active'; if (!closed && canListenToWS()) { const unsubscribe = callFromPush.on('all', (event) => { const _canListenToWS = canListenToWS(); @@ -121,10 +109,11 @@ export const onVoipNotificationReceived = async ( pushUnsubscriptionCallbacks.get(call_cid)?.forEach((cb) => cb()); pushUnsubscriptionCallbacks.set(call_cid, [unsubscribe]); } + // send the info to this subject, it is listened by callkeep events // callkeep events will then accept/reject the call logger.debug( - `call_cid:${call_cid} uuid:${uuid} received and processed from call.ring push notification`, + `call_cid:${call_cid} received and processed from call.ring push notification`, ); voipPushNotificationCallCId$.next(call_cid); }; diff --git a/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts b/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts index 5b9eab7f58..67b4b3eeab 100644 --- a/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts +++ b/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts @@ -42,30 +42,8 @@ export const pushRejectedIncomingCallCId$ = new BehaviorSubject< >(undefined); /** - * This rxjs subject is used to store the call cid of the incoming call from ios voip pushkit notification + * This rxjs subject is used to store the call cid of the incoming call from voip notification */ export const voipPushNotificationCallCId$ = new BehaviorSubject< string | undefined >(undefined); - -/** The pair of cid of a call and its corresponding uuid created in the native side */ -type CallkeepMap = { - uuid: string; - cid: string; -}; - -/* - * This rxjs subject should only used to store the CallkeepMap - * for the incoming call when on foreground - * or in other words, when we get didDisplayIncomingCall from callkeep lib - */ -export const voipCallkeepCallOnForegroundMap$ = new BehaviorSubject< - CallkeepMap | undefined ->(undefined); - -/* - * This rxjs subject should only used to store the CallkeepMap when it was accepted in the native dialer - */ -export const voipCallkeepAcceptedCallOnNativeDialerMap$ = new BehaviorSubject< - CallkeepMap | undefined ->(undefined); diff --git a/packages/react-native-sdk/src/utils/push/internal/utils.ts b/packages/react-native-sdk/src/utils/push/internal/utils.ts index c31ce73c78..48b5d3af2f 100644 --- a/packages/react-native-sdk/src/utils/push/internal/utils.ts +++ b/packages/react-native-sdk/src/utils/push/internal/utils.ts @@ -10,6 +10,7 @@ import type { } from '../../StreamVideoRN/types'; import { onNewCallNotification } from '../../internal/newNotificationCallbacks'; import { pushUnsubscriptionCallbacks } from './constants'; +import { AppState } from 'react-native'; type PushConfig = NonNullable; @@ -122,6 +123,16 @@ export const processCallFromPush = async ( .debug( `joining call from push notification with callCid: ${callFromPush.cid}`, ); + + if (callFromPush.state.callingState === CallingState.JOINED) { + videoLoggerSystem + .getLogger('processCallFromPush') + .debug( + `call already joined from push notification with callCid: ${callFromPush.cid}`, + ); + return; + } + await callFromPush.join(); } else if (action === 'decline') { const canReject = @@ -191,3 +202,17 @@ export const clearPushWSEventSubscriptions = (call_cid: string) => { export const canAddPushWSSubscriptionsRef: CanAddPushWSSubscriptionsRef = { current: true, }; + +export const canListenToWS = () => + canAddPushWSSubscriptionsRef.current && AppState.currentState !== 'active'; + +export const shouldCallBeClosed = ( + call: Call, + pushData: { [key: string]: string | object }, +) => { + const created_by_id = pushData?.created_by_id as string; + const receiver_id = pushData?.receiver_id as string; + + const { mustEndCall } = shouldCallBeEnded(call, created_by_id, receiver_id); + return mustEndCall; +}; diff --git a/packages/react-native-sdk/src/utils/push/libs/callingx.ts b/packages/react-native-sdk/src/utils/push/libs/callingx.ts new file mode 100644 index 0000000000..f0a4fe1c07 --- /dev/null +++ b/packages/react-native-sdk/src/utils/push/libs/callingx.ts @@ -0,0 +1,18 @@ +export type RNCallingxType = import('react-native-callingx').ICallingxModule; + +let callingx: RNCallingxType | undefined; + +try { + callingx = require('react-native-callingx').CallingxModule; +} catch {} + +export function getCallingxLib() { + if (!callingx) { + throw Error('react-native-callingx library is not installed.'); + } + return callingx; +} + +export function getCallingxLibIfAvailable() { + return callingx ?? undefined; +} diff --git a/packages/react-native-sdk/src/utils/push/libs/callkeep.ts b/packages/react-native-sdk/src/utils/push/libs/callkeep.ts deleted file mode 100644 index fe8a20bf68..0000000000 --- a/packages/react-native-sdk/src/utils/push/libs/callkeep.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type RNCallKeepType = typeof import('react-native-callkeep').default; - -let callkeep: RNCallKeepType | undefined; - -try { - callkeep = require('react-native-callkeep').default; -} catch {} - -export function getCallKeepLib() { - if (!callkeep) { - throw Error( - 'react-native-callkeep library is not installed. Please see https://github.com/react-native-webrtc/react-native-callkeep#Installation for installation instructions', - ); - } - return callkeep; -} diff --git a/packages/react-native-sdk/src/utils/push/libs/index.ts b/packages/react-native-sdk/src/utils/push/libs/index.ts index 73a249953b..437da4d834 100644 --- a/packages/react-native-sdk/src/utils/push/libs/index.ts +++ b/packages/react-native-sdk/src/utils/push/libs/index.ts @@ -2,8 +2,8 @@ export * from './expoNotifications'; export * from './firebaseMessaging'; export * from './iosPushNotification'; export * from './voipPushNotification'; -export * from './callkeep'; export * from './notifee'; +export * from './callingx'; /* NOTE: must keep each libs in different files diff --git a/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts b/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts new file mode 100644 index 0000000000..fd806ad1a3 --- /dev/null +++ b/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts @@ -0,0 +1,100 @@ +import { pushAcceptedIncomingCallCId$ } from './internal/rxSubjects'; +import { videoLoggerSystem } from '@stream-io/video-client'; +import type { StreamVideoConfig } from '../StreamVideoRN/types'; +import { + clearPushWSEventSubscriptions, + processCallFromPushInBackground, +} from './internal/utils'; +import { setPushLogoutCallback } from '../internal/pushLogoutCallback'; + +import { getCallingxLib, getCallingxLibIfAvailable } from './libs/callingx'; + +type PushConfig = NonNullable; + +/** + * This hook is used to listen to callkeep events and do the necessary actions + */ +export function setupCallingExpEvents(pushConfig: NonNullable) { + if ( + !(pushConfig.android.pushProviderName && pushConfig.ios.pushProviderName) + ) { + return; + } + + const callingx = getCallingxLib(); + // const logger = videoLoggerSystem.getLogger('setupCallingExpEvents'); + + const { remove: removeAnswerCall } = callingx.addEventListener( + 'answerCall', + callingExpAcceptCall, + ); + const { remove: removeEndCall } = callingx.addEventListener( + 'endCall', + callingExpRejectCall(pushConfig), + ); + + //TODO: need to find cases where delayed events can appear + // const events = callingx.getInitialEvents(); + // events.forEach((event: EventData) => { + // const { eventName, params } = event; + // if (eventName === 'didDisplayIncomingCall') { + // logger.debug(`[delayed] didDisplayIncomingCall event callId: ${params.callId}`); + // } else if (eventName === 'answerCall') { + // logger.debug(`[delayed] answerCall event callId: ${params.callId}`); + // callingExpAcceptCall(params); + // } else if (eventName === 'endCall') { + // logger.debug(`[delayed] endCall event callId: ${params.callId}`); + // callingExpRejectCall(pushConfig)(params); + // } + // }); + + setPushLogoutCallback(async () => { + removeAnswerCall(); + removeEndCall(); + }); +} + +const callingExpAcceptCall = ({ callId: call_cid }: { callId: string }) => { + videoLoggerSystem + .getLogger('callingExpAcceptCall') + .debug(`callingExpAcceptCall event callId: ${call_cid}`); + + if (!call_cid) { + getCallingxLibIfAvailable()?.log( + `call_cid is undefined, so returning early`, + 'debug', + ); + return; + } + + clearPushWSEventSubscriptions(call_cid); + // to process the call in the app + pushAcceptedIncomingCallCId$.next(call_cid); +}; + +const callingExpRejectCall = + (pushConfig: PushConfig) => + async ({ callId: call_cid }: { callId: string }) => { + getCallingxLibIfAvailable()?.log( + `callingExpRejectCall call_cid: ${call_cid}`, + 'debug', + ); + videoLoggerSystem + .getLogger('callingExpRejectCall') + .debug(`call_cid: ${call_cid}`); + + if (!call_cid) { + videoLoggerSystem + .getLogger('callingExpRejectCall') + .debug('call_cid is undefined, so returning early'); + return; + } + + clearPushWSEventSubscriptions(call_cid); + // remove the references if the call_cid matches + + videoLoggerSystem + .getLogger('callingExpRejectCall') + .debug(`ending call with call_cid: ${call_cid}`); + await processCallFromPushInBackground(pushConfig, call_cid, 'decline'); + }; diff --git a/packages/react-native-sdk/src/utils/push/setupIosCallKeepEvents.ts b/packages/react-native-sdk/src/utils/push/setupIosCallKeepEvents.ts deleted file mode 100644 index c9d9bcad50..0000000000 --- a/packages/react-native-sdk/src/utils/push/setupIosCallKeepEvents.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { - pushAcceptedIncomingCallCId$, - voipCallkeepAcceptedCallOnNativeDialerMap$, - voipCallkeepCallOnForegroundMap$, - voipPushNotificationCallCId$, -} from './internal/rxSubjects'; -import { RxUtils, videoLoggerSystem } from '@stream-io/video-client'; -import { getCallKeepLib, getVoipPushNotificationLib } from './libs'; -import type { StreamVideoConfig } from '../StreamVideoRN/types'; -import { - clearPushWSEventSubscriptions, - processCallFromPushInBackground, -} from './internal/utils'; -import { AppState, NativeModules, Platform } from 'react-native'; -import { setPushLogoutCallback } from '../internal/pushLogoutCallback'; - -type PushConfig = NonNullable; - -/** - * This hook is used to listen to callkeep events and do the necessary actions - */ -export function setupIosCallKeepEvents( - pushConfig: NonNullable, -) { - if (Platform.OS !== 'ios' || !pushConfig.ios.pushProviderName) { - return; - } - if (!pushConfig.android.incomingCallChannel) { - // TODO: remove this check and find a better way once we have telecom integration for android - videoLoggerSystem - .getLogger('setupIosCallKeepEvents') - .debug( - 'android incomingCallChannel is not defined, so skipping the setupIosCallKeepEvents', - ); - return; - } - const logger = videoLoggerSystem.getLogger('setupIosCallKeepEvents'); - const callkeep = getCallKeepLib(); - - async function getCallCid(callUUID: string): Promise { - try { - const call_cid = - await NativeModules.StreamVideoReactNative.getIncomingCallCid(callUUID); - // in a case that voipPushNotificationCallCId$ is empty (this should not happen as voipPushNotificationCallCId$ is updated in push reception)] - // update it with this call_cid - const voipPushNotificationCallCId = RxUtils.getCurrentValue( - voipPushNotificationCallCId$, - ); - if (!voipPushNotificationCallCId) { - logger.debug( - `voipPushNotificationCallCId$ is empty, updating it with the call_cid: ${call_cid} for callUUID: ${callUUID}`, - ); - voipPushNotificationCallCId$.next(call_cid); - } - return call_cid; - } catch { - logger.debug( - `Error in getting call cid from native module for callUUID: ${callUUID} - probably the call was already processed, so ignoring this callkeep event`, - ); - } - return undefined; - } - - function answerCall(callUUID: string) { - getCallCid(callUUID).then((call_cid) => { - logger.debug(`answerCall event with call_cid: ${call_cid}`); - iosCallkeepAcceptCall(call_cid, callUUID); - }); - } - - function endCall(callUUID: string) { - getCallCid(callUUID).then((call_cid) => { - logger.debug(`endCall event with call_cid: ${call_cid}`); - iosCallkeepRejectCall(call_cid, callUUID, pushConfig!); - }); - } - - function didDisplayIncomingCall(callUUID: string, payload: object) { - const voipPushNotification = getVoipPushNotificationLib(); - // @ts-expect-error - call_cid is not part of RNCallKeepEventPayload - const call_cid = payload?.call_cid as string | undefined; - logger.debug( - `didDisplayIncomingCall event with callUUID: ${callUUID} call_cid: ${call_cid}`, - ); - if (call_cid) { - if (AppState.currentState === 'background') { - processCallFromPushInBackground( - pushConfig!, - call_cid, - 'backgroundDelivered', - ); - } - voipCallkeepCallOnForegroundMap$.next({ - uuid: callUUID, - cid: call_cid, - }); - } - voipPushNotification.onVoipNotificationCompleted(callUUID); - } - - const { remove: removeAnswerCall } = callkeep.addEventListener( - 'answerCall', - ({ callUUID }) => { - answerCall(callUUID); - }, - ); - const { remove: removeEndCall } = callkeep.addEventListener( - 'endCall', - ({ callUUID }) => { - endCall(callUUID); - }, - ); - - const { remove: removeDisplayIncomingCall } = callkeep.addEventListener( - 'didDisplayIncomingCall', - ({ callUUID, payload }) => { - didDisplayIncomingCall(callUUID, payload); - }, - ); - - const { remove: removeDidLoadWithEvents } = callkeep.addEventListener( - 'didLoadWithEvents', - (events) => { - if (!events || !Array.isArray(events) || events.length < 1) { - return; - } - - events.forEach((event) => { - const { name, data } = event; - if (name === 'RNCallKeepDidDisplayIncomingCall') { - didDisplayIncomingCall(data.callUUID, data.payload); - } else if (name === 'RNCallKeepPerformAnswerCallAction') { - answerCall(data.callUUID); - } else if (name === 'RNCallKeepPerformEndCallAction') { - endCall(data.callUUID); - } - }); - }, - ); - - setPushLogoutCallback(async () => { - removeAnswerCall(); - removeEndCall(); - removeDisplayIncomingCall(); - removeDidLoadWithEvents(); - }); -} - -const iosCallkeepAcceptCall = ( - call_cid: string | undefined, - callUUIDFromCallkeep: string, -) => { - if (!shouldProcessCallFromCallkeep(call_cid, callUUIDFromCallkeep)) { - return; - } - clearPushWSEventSubscriptions(call_cid); - // to call end callkeep later if ended in app and not through callkeep - voipCallkeepAcceptedCallOnNativeDialerMap$.next({ - uuid: callUUIDFromCallkeep, - cid: call_cid, - }); - // to process the call in the app - pushAcceptedIncomingCallCId$.next(call_cid); - // no need to keep these references anymore - voipCallkeepCallOnForegroundMap$.next(undefined); -}; - -const iosCallkeepRejectCall = async ( - call_cid: string | undefined, - callUUIDFromCallkeep: string, - pushConfig: PushConfig, -) => { - if (!shouldProcessCallFromCallkeep(call_cid, callUUIDFromCallkeep)) { - return; - } - clearPushWSEventSubscriptions(call_cid); - // remove the references if the call_cid matches - const voipPushNotificationCallCId = RxUtils.getCurrentValue( - voipPushNotificationCallCId$, - ); - if (voipPushNotificationCallCId === call_cid) { - voipCallkeepAcceptedCallOnNativeDialerMap$.next(undefined); - voipCallkeepCallOnForegroundMap$.next(undefined); - voipPushNotificationCallCId$.next(undefined); - } - - await processCallFromPushInBackground(pushConfig, call_cid, 'decline'); - await NativeModules.StreamVideoReactNative?.removeIncomingCall(call_cid); -}; - -/** - * Helper function to determine if the answer/end call event from callkeep must be processed - * Just checks if we have a valid call_cid and acts as a type guard for call_cid - */ -const shouldProcessCallFromCallkeep = ( - call_cid: string | undefined, - callUUIDFromCallkeep: string, -): call_cid is string => { - if (!call_cid || !callUUIDFromCallkeep) { - return false; - } - return true; -}; diff --git a/packages/react-native-sdk/src/utils/push/setupIosVoipPushEvents.ts b/packages/react-native-sdk/src/utils/push/setupIosVoipPushEvents.ts index 6646e6498b..96256924f9 100644 --- a/packages/react-native-sdk/src/utils/push/setupIosVoipPushEvents.ts +++ b/packages/react-native-sdk/src/utils/push/setupIosVoipPushEvents.ts @@ -23,6 +23,15 @@ export function setupIosVoipPushEvents( const voipPushNotification = getVoipPushNotificationLib(); logger.debug('notification event listener added'); + voipPushNotification.addEventListener('didLoadWithEvents', (events) => { + //we need this for cold start scenario where the app is not running and the events are not processed when the app is launched + for (const event of events) { + const { name, data } = event; + if (name === 'RNVoipPushRemoteNotificationReceivedEvent') { + onVoipNotificationReceived(data, pushConfig); + } + } + }); voipPushNotification.addEventListener('notification', (notification) => { onVoipNotificationReceived(notification, pushConfig); }); diff --git a/sample-apps/react-native/dogfood/android/app/build.gradle b/sample-apps/react-native/dogfood/android/app/build.gradle index 708b257ee9..6250da9eca 100644 --- a/sample-apps/react-native/dogfood/android/app/build.gradle +++ b/sample-apps/react-native/dogfood/android/app/build.gradle @@ -117,8 +117,6 @@ android { } dependencies { - implementation (project(':react-native-callkeep')) - // The version of react-native is set by the React Native Gradle Plugin implementation("com.facebook.react:react-android") diff --git a/sample-apps/react-native/dogfood/android/app/src/main/java/io/getstream/rnvideosample/MainActivity.kt b/sample-apps/react-native/dogfood/android/app/src/main/java/io/getstream/rnvideosample/MainActivity.kt index 1920222c0a..2a6e9f8763 100644 --- a/sample-apps/react-native/dogfood/android/app/src/main/java/io/getstream/rnvideosample/MainActivity.kt +++ b/sample-apps/react-native/dogfood/android/app/src/main/java/io/getstream/rnvideosample/MainActivity.kt @@ -1,8 +1,10 @@ package io.getstream.rnvideosample import android.content.res.Configuration +import android.content.Intent import android.os.Build import android.os.Bundle +import android.util.Log import androidx.lifecycle.Lifecycle import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate @@ -10,6 +12,7 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnable import com.facebook.react.defaults.DefaultReactActivityDelegate import com.oney.WebRTCModule.WebRTCModuleOptions import com.streamvideo.reactnative.StreamVideoReactNative +import com.callingx.CallingxModule class MainActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -18,8 +21,13 @@ class MainActivity : ReactActivity() { // for react-navigation super.onCreate(null) StreamVideoReactNative.setupCallActivity(this) + CallingxModule.handleCallingIntent(this, intent) } + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + CallingxModule.handleCallingIntent(this, intent) + } /** * Returns the name of the main component registered from JavaScript. This is used to schedule * rendering of the component. diff --git a/sample-apps/react-native/dogfood/ios/AppDelegate.swift b/sample-apps/react-native/dogfood/ios/AppDelegate.swift index 010d5e82e8..7d11bd5825 100644 --- a/sample-apps/react-native/dogfood/ios/AppDelegate.swift +++ b/sample-apps/react-native/dogfood/ios/AppDelegate.swift @@ -11,11 +11,12 @@ import ReactAppDependencyProvider import UserNotifications import RNCPushNotificationIOS -import RNCallKeep +import Callingx import PushKit import WebRTC import RNVoipPushNotification import stream_io_noise_cancellation_react_native +import stream_video_react_native @main class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, PKPushRegistryDelegate { @@ -75,7 +76,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD completion() // Ensure completion handler is called even if parsing fails return } - + // Check if user is busy BEFORE registering the call let shouldReject = StreamVideoReactNative.shouldRejectCallWhenBusy() let hasAnyActiveCall = StreamVideoReactNative.hasAnyActiveCall() @@ -86,30 +87,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return } - let uuid = UUID().uuidString - let videoIncluded = stream["video"] as? String - let hasVideo = videoIncluded == "false" ? false : true - - StreamVideoReactNative.registerIncomingCall(cid, uuid: uuid) - // required if you want to call `completion()` on the js side - RNVoipPushNotificationManager.addCompletionHandler(uuid, completionHandler: completion) + RNVoipPushNotificationManager.addCompletionHandler(cid, completionHandler: completion) // Process the received push // fire 'notification' event to JS RNVoipPushNotificationManager.didReceiveIncomingPush(with: payload, forType: type.rawValue) // type is enum, use rawValue - RNCallKeep.reportNewIncomingCall(uuid, - handle: createdCallerName, - handleType: "generic", - hasVideo: hasVideo, - localizedCallerName: createdCallerName, - supportsHolding: false, - supportsDTMF: false, - supportsGrouping: false, - supportsUngrouping: false, - fromPushKit: true, - payload: stream, - withCompletionHandler: nil) // Completion handler is already handled above + StreamVideoReactNative.didReceiveIncomingPush(payload, completionHandler: nil) //for now left completion handler empty } //Called when a notification is delivered to a foreground app. @@ -132,14 +116,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // Uncomment the next line to enable verbose WebRTC logs // WebRTCModuleOptions.sharedInstance().loggingSeverity = .verbose - let localizedAppName = Bundle.main.localizedInfoDictionary?["CFBundleDisplayName"] as? String - let appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String - RNCallKeep.setup([ - "appName": localizedAppName != nil ? localizedAppName! : appName as Any, // Forced unwrap is safe here due to nil check - "supportsVideo": true, - "includesCallsInRecents": false, - ]) - RNVoipPushNotificationManager.voipRegistration() let center = UNUserNotificationCenter.current() diff --git a/sample-apps/react-native/dogfood/ios/Podfile.lock b/sample-apps/react-native/dogfood/ios/Podfile.lock index c549c224c5..e86ba5f89f 100644 --- a/sample-apps/react-native/dogfood/ios/Podfile.lock +++ b/sample-apps/react-native/dogfood/ios/Podfile.lock @@ -1,8 +1,36 @@ PODS: - boost (1.84.0) + - Callingx (0.1.0): + - boost + - DoubleConversion + - fast_float + - fmt + - glog + - hermes-engine + - RCT-Folly + - RCT-Folly/Fabric + - RCTRequired + - RCTTypeSafety + - React-Core + - React-debug + - React-Fabric + - React-featureflags + - React-graphics + - React-ImageManager + - React-jsi + - React-NativeModulesApple + - React-RCTFabric + - React-renderercss + - React-rendererdebug + - React-utils + - ReactCodegen + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - SocketRocket + - Yoga - DoubleConversion (1.1.6) - fast_float (8.0.0) - - FBLazyVector (0.81.4) + - FBLazyVector (0.81.5) - fmt (11.0.2) - glog (0.3.5) - hermes-engine (0.81.4): @@ -27,28 +55,28 @@ PODS: - fast_float (= 8.0.0) - fmt (= 11.0.2) - glog - - RCTDeprecation (0.81.4) - - RCTRequired (0.81.4) - - RCTTypeSafety (0.81.4): - - FBLazyVector (= 0.81.4) - - RCTRequired (= 0.81.4) - - React-Core (= 0.81.4) - - React (0.81.4): - - React-Core (= 0.81.4) - - React-Core/DevSupport (= 0.81.4) - - React-Core/RCTWebSocket (= 0.81.4) - - React-RCTActionSheet (= 0.81.4) - - React-RCTAnimation (= 0.81.4) - - React-RCTBlob (= 0.81.4) - - React-RCTImage (= 0.81.4) - - React-RCTLinking (= 0.81.4) - - React-RCTNetwork (= 0.81.4) - - React-RCTSettings (= 0.81.4) - - React-RCTText (= 0.81.4) - - React-RCTVibration (= 0.81.4) - - React-callinvoker (0.81.4) + - RCTDeprecation (0.81.5) + - RCTRequired (0.81.5) + - RCTTypeSafety (0.81.5): + - FBLazyVector (= 0.81.5) + - RCTRequired (= 0.81.5) + - React-Core (= 0.81.5) + - React (0.81.5): + - React-Core (= 0.81.5) + - React-Core/DevSupport (= 0.81.5) + - React-Core/RCTWebSocket (= 0.81.5) + - React-RCTActionSheet (= 0.81.5) + - React-RCTAnimation (= 0.81.5) + - React-RCTBlob (= 0.81.5) + - React-RCTImage (= 0.81.5) + - React-RCTLinking (= 0.81.5) + - React-RCTNetwork (= 0.81.5) + - React-RCTSettings (= 0.81.5) + - React-RCTText (= 0.81.5) + - React-RCTVibration (= 0.81.5) + - React-callinvoker (0.81.5) - React-Codegen (0.1.0) - - React-Core (0.81.4): + - React-Core (0.81.5): - boost - DoubleConversion - fast_float @@ -58,7 +86,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - RCTDeprecation - - React-Core/Default (= 0.81.4) + - React-Core/Default (= 0.81.5) - React-cxxreact - React-featureflags - React-hermes @@ -73,7 +101,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/CoreModulesHeaders (0.81.4): + - React-Core/CoreModulesHeaders (0.81.5): - boost - DoubleConversion - fast_float @@ -98,7 +126,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/Default (0.81.4): + - React-Core/Default (0.81.5): - boost - DoubleConversion - fast_float @@ -122,7 +150,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/DevSupport (0.81.4): + - React-Core/DevSupport (0.81.5): - boost - DoubleConversion - fast_float @@ -132,8 +160,8 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - RCTDeprecation - - React-Core/Default (= 0.81.4) - - React-Core/RCTWebSocket (= 0.81.4) + - React-Core/Default (= 0.81.5) + - React-Core/RCTWebSocket (= 0.81.5) - React-cxxreact - React-featureflags - React-hermes @@ -148,7 +176,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTActionSheetHeaders (0.81.4): + - React-Core/RCTActionSheetHeaders (0.81.5): - boost - DoubleConversion - fast_float @@ -173,7 +201,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTAnimationHeaders (0.81.4): + - React-Core/RCTAnimationHeaders (0.81.5): - boost - DoubleConversion - fast_float @@ -198,7 +226,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTBlobHeaders (0.81.4): + - React-Core/RCTBlobHeaders (0.81.5): - boost - DoubleConversion - fast_float @@ -223,7 +251,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTImageHeaders (0.81.4): + - React-Core/RCTImageHeaders (0.81.5): - boost - DoubleConversion - fast_float @@ -248,7 +276,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTLinkingHeaders (0.81.4): + - React-Core/RCTLinkingHeaders (0.81.5): - boost - DoubleConversion - fast_float @@ -273,7 +301,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTNetworkHeaders (0.81.4): + - React-Core/RCTNetworkHeaders (0.81.5): - boost - DoubleConversion - fast_float @@ -298,7 +326,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTSettingsHeaders (0.81.4): + - React-Core/RCTSettingsHeaders (0.81.5): - boost - DoubleConversion - fast_float @@ -323,7 +351,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTTextHeaders (0.81.4): + - React-Core/RCTTextHeaders (0.81.5): - boost - DoubleConversion - fast_float @@ -348,7 +376,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTVibrationHeaders (0.81.4): + - React-Core/RCTVibrationHeaders (0.81.5): - boost - DoubleConversion - fast_float @@ -373,7 +401,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-Core/RCTWebSocket (0.81.4): + - React-Core/RCTWebSocket (0.81.5): - boost - DoubleConversion - fast_float @@ -383,7 +411,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - RCTDeprecation - - React-Core/Default (= 0.81.4) + - React-Core/Default (= 0.81.5) - React-cxxreact - React-featureflags - React-hermes @@ -398,7 +426,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-CoreModules (0.81.4): + - React-CoreModules (0.81.5): - boost - DoubleConversion - fast_float @@ -406,20 +434,20 @@ PODS: - glog - RCT-Folly - RCT-Folly/Fabric - - RCTTypeSafety (= 0.81.4) - - React-Core/CoreModulesHeaders (= 0.81.4) - - React-jsi (= 0.81.4) + - RCTTypeSafety (= 0.81.5) + - React-Core/CoreModulesHeaders (= 0.81.5) + - React-jsi (= 0.81.5) - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing - React-NativeModulesApple - React-RCTBlob - React-RCTFBReactNativeSpec - - React-RCTImage (= 0.81.4) + - React-RCTImage (= 0.81.5) - React-runtimeexecutor - ReactCommon - SocketRocket - - React-cxxreact (0.81.4): + - React-cxxreact (0.81.5): - boost - DoubleConversion - fast_float @@ -428,19 +456,19 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-callinvoker (= 0.81.4) - - React-debug (= 0.81.4) - - React-jsi (= 0.81.4) + - React-callinvoker (= 0.81.5) + - React-debug (= 0.81.5) + - React-jsi (= 0.81.5) - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing - - React-logger (= 0.81.4) - - React-perflogger (= 0.81.4) + - React-logger (= 0.81.5) + - React-perflogger (= 0.81.5) - React-runtimeexecutor - - React-timing (= 0.81.4) + - React-timing (= 0.81.5) - SocketRocket - - React-debug (0.81.4) - - React-defaultsnativemodule (0.81.4): + - React-debug (0.81.5) + - React-defaultsnativemodule (0.81.5): - boost - DoubleConversion - fast_float @@ -457,7 +485,7 @@ PODS: - React-microtasksnativemodule - React-RCTFBReactNativeSpec - SocketRocket - - React-domnativemodule (0.81.4): + - React-domnativemodule (0.81.5): - boost - DoubleConversion - fast_float @@ -477,7 +505,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-Fabric (0.81.4): + - React-Fabric (0.81.5): - boost - DoubleConversion - fast_float @@ -491,23 +519,23 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/animations (= 0.81.4) - - React-Fabric/attributedstring (= 0.81.4) - - React-Fabric/bridging (= 0.81.4) - - React-Fabric/componentregistry (= 0.81.4) - - React-Fabric/componentregistrynative (= 0.81.4) - - React-Fabric/components (= 0.81.4) - - React-Fabric/consistency (= 0.81.4) - - React-Fabric/core (= 0.81.4) - - React-Fabric/dom (= 0.81.4) - - React-Fabric/imagemanager (= 0.81.4) - - React-Fabric/leakchecker (= 0.81.4) - - React-Fabric/mounting (= 0.81.4) - - React-Fabric/observers (= 0.81.4) - - React-Fabric/scheduler (= 0.81.4) - - React-Fabric/telemetry (= 0.81.4) - - React-Fabric/templateprocessor (= 0.81.4) - - React-Fabric/uimanager (= 0.81.4) + - React-Fabric/animations (= 0.81.5) + - React-Fabric/attributedstring (= 0.81.5) + - React-Fabric/bridging (= 0.81.5) + - React-Fabric/componentregistry (= 0.81.5) + - React-Fabric/componentregistrynative (= 0.81.5) + - React-Fabric/components (= 0.81.5) + - React-Fabric/consistency (= 0.81.5) + - React-Fabric/core (= 0.81.5) + - React-Fabric/dom (= 0.81.5) + - React-Fabric/imagemanager (= 0.81.5) + - React-Fabric/leakchecker (= 0.81.5) + - React-Fabric/mounting (= 0.81.5) + - React-Fabric/observers (= 0.81.5) + - React-Fabric/scheduler (= 0.81.5) + - React-Fabric/telemetry (= 0.81.5) + - React-Fabric/templateprocessor (= 0.81.5) + - React-Fabric/uimanager (= 0.81.5) - React-featureflags - React-graphics - React-jsi @@ -519,7 +547,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/animations (0.81.4): + - React-Fabric/animations (0.81.5): - boost - DoubleConversion - fast_float @@ -544,7 +572,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/attributedstring (0.81.4): + - React-Fabric/attributedstring (0.81.5): - boost - DoubleConversion - fast_float @@ -569,7 +597,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/bridging (0.81.4): + - React-Fabric/bridging (0.81.5): - boost - DoubleConversion - fast_float @@ -594,7 +622,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/componentregistry (0.81.4): + - React-Fabric/componentregistry (0.81.5): - boost - DoubleConversion - fast_float @@ -619,7 +647,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/componentregistrynative (0.81.4): + - React-Fabric/componentregistrynative (0.81.5): - boost - DoubleConversion - fast_float @@ -644,7 +672,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components (0.81.4): + - React-Fabric/components (0.81.5): - boost - DoubleConversion - fast_float @@ -658,10 +686,10 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/components/legacyviewmanagerinterop (= 0.81.4) - - React-Fabric/components/root (= 0.81.4) - - React-Fabric/components/scrollview (= 0.81.4) - - React-Fabric/components/view (= 0.81.4) + - React-Fabric/components/legacyviewmanagerinterop (= 0.81.5) + - React-Fabric/components/root (= 0.81.5) + - React-Fabric/components/scrollview (= 0.81.5) + - React-Fabric/components/view (= 0.81.5) - React-featureflags - React-graphics - React-jsi @@ -673,7 +701,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components/legacyviewmanagerinterop (0.81.4): + - React-Fabric/components/legacyviewmanagerinterop (0.81.5): - boost - DoubleConversion - fast_float @@ -698,7 +726,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components/root (0.81.4): + - React-Fabric/components/root (0.81.5): - boost - DoubleConversion - fast_float @@ -723,7 +751,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components/scrollview (0.81.4): + - React-Fabric/components/scrollview (0.81.5): - boost - DoubleConversion - fast_float @@ -748,7 +776,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/components/view (0.81.4): + - React-Fabric/components/view (0.81.5): - boost - DoubleConversion - fast_float @@ -775,7 +803,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-Fabric/consistency (0.81.4): + - React-Fabric/consistency (0.81.5): - boost - DoubleConversion - fast_float @@ -800,7 +828,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/core (0.81.4): + - React-Fabric/core (0.81.5): - boost - DoubleConversion - fast_float @@ -825,7 +853,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/dom (0.81.4): + - React-Fabric/dom (0.81.5): - boost - DoubleConversion - fast_float @@ -850,7 +878,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/imagemanager (0.81.4): + - React-Fabric/imagemanager (0.81.5): - boost - DoubleConversion - fast_float @@ -875,7 +903,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/leakchecker (0.81.4): + - React-Fabric/leakchecker (0.81.5): - boost - DoubleConversion - fast_float @@ -900,7 +928,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/mounting (0.81.4): + - React-Fabric/mounting (0.81.5): - boost - DoubleConversion - fast_float @@ -925,7 +953,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/observers (0.81.4): + - React-Fabric/observers (0.81.5): - boost - DoubleConversion - fast_float @@ -939,7 +967,7 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/observers/events (= 0.81.4) + - React-Fabric/observers/events (= 0.81.5) - React-featureflags - React-graphics - React-jsi @@ -951,7 +979,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/observers/events (0.81.4): + - React-Fabric/observers/events (0.81.5): - boost - DoubleConversion - fast_float @@ -976,7 +1004,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/scheduler (0.81.4): + - React-Fabric/scheduler (0.81.5): - boost - DoubleConversion - fast_float @@ -1003,7 +1031,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/telemetry (0.81.4): + - React-Fabric/telemetry (0.81.5): - boost - DoubleConversion - fast_float @@ -1028,7 +1056,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/templateprocessor (0.81.4): + - React-Fabric/templateprocessor (0.81.5): - boost - DoubleConversion - fast_float @@ -1053,7 +1081,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/uimanager (0.81.4): + - React-Fabric/uimanager (0.81.5): - boost - DoubleConversion - fast_float @@ -1067,7 +1095,7 @@ PODS: - React-Core - React-cxxreact - React-debug - - React-Fabric/uimanager/consistency (= 0.81.4) + - React-Fabric/uimanager/consistency (= 0.81.5) - React-featureflags - React-graphics - React-jsi @@ -1080,7 +1108,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-Fabric/uimanager/consistency (0.81.4): + - React-Fabric/uimanager/consistency (0.81.5): - boost - DoubleConversion - fast_float @@ -1106,7 +1134,7 @@ PODS: - React-utils - ReactCommon/turbomodule/core - SocketRocket - - React-FabricComponents (0.81.4): + - React-FabricComponents (0.81.5): - boost - DoubleConversion - fast_float @@ -1121,8 +1149,8 @@ PODS: - React-cxxreact - React-debug - React-Fabric - - React-FabricComponents/components (= 0.81.4) - - React-FabricComponents/textlayoutmanager (= 0.81.4) + - React-FabricComponents/components (= 0.81.5) + - React-FabricComponents/textlayoutmanager (= 0.81.5) - React-featureflags - React-graphics - React-jsi @@ -1135,7 +1163,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components (0.81.4): + - React-FabricComponents/components (0.81.5): - boost - DoubleConversion - fast_float @@ -1150,17 +1178,17 @@ PODS: - React-cxxreact - React-debug - React-Fabric - - React-FabricComponents/components/inputaccessory (= 0.81.4) - - React-FabricComponents/components/iostextinput (= 0.81.4) - - React-FabricComponents/components/modal (= 0.81.4) - - React-FabricComponents/components/rncore (= 0.81.4) - - React-FabricComponents/components/safeareaview (= 0.81.4) - - React-FabricComponents/components/scrollview (= 0.81.4) - - React-FabricComponents/components/switch (= 0.81.4) - - React-FabricComponents/components/text (= 0.81.4) - - React-FabricComponents/components/textinput (= 0.81.4) - - React-FabricComponents/components/unimplementedview (= 0.81.4) - - React-FabricComponents/components/virtualview (= 0.81.4) + - React-FabricComponents/components/inputaccessory (= 0.81.5) + - React-FabricComponents/components/iostextinput (= 0.81.5) + - React-FabricComponents/components/modal (= 0.81.5) + - React-FabricComponents/components/rncore (= 0.81.5) + - React-FabricComponents/components/safeareaview (= 0.81.5) + - React-FabricComponents/components/scrollview (= 0.81.5) + - React-FabricComponents/components/switch (= 0.81.5) + - React-FabricComponents/components/text (= 0.81.5) + - React-FabricComponents/components/textinput (= 0.81.5) + - React-FabricComponents/components/unimplementedview (= 0.81.5) + - React-FabricComponents/components/virtualview (= 0.81.5) - React-featureflags - React-graphics - React-jsi @@ -1173,7 +1201,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/inputaccessory (0.81.4): + - React-FabricComponents/components/inputaccessory (0.81.5): - boost - DoubleConversion - fast_float @@ -1200,7 +1228,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/iostextinput (0.81.4): + - React-FabricComponents/components/iostextinput (0.81.5): - boost - DoubleConversion - fast_float @@ -1227,7 +1255,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/modal (0.81.4): + - React-FabricComponents/components/modal (0.81.5): - boost - DoubleConversion - fast_float @@ -1254,7 +1282,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/rncore (0.81.4): + - React-FabricComponents/components/rncore (0.81.5): - boost - DoubleConversion - fast_float @@ -1281,7 +1309,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/safeareaview (0.81.4): + - React-FabricComponents/components/safeareaview (0.81.5): - boost - DoubleConversion - fast_float @@ -1308,7 +1336,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/scrollview (0.81.4): + - React-FabricComponents/components/scrollview (0.81.5): - boost - DoubleConversion - fast_float @@ -1335,7 +1363,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/switch (0.81.4): + - React-FabricComponents/components/switch (0.81.5): - boost - DoubleConversion - fast_float @@ -1362,7 +1390,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/text (0.81.4): + - React-FabricComponents/components/text (0.81.5): - boost - DoubleConversion - fast_float @@ -1389,7 +1417,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/textinput (0.81.4): + - React-FabricComponents/components/textinput (0.81.5): - boost - DoubleConversion - fast_float @@ -1416,7 +1444,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/unimplementedview (0.81.4): + - React-FabricComponents/components/unimplementedview (0.81.5): - boost - DoubleConversion - fast_float @@ -1443,7 +1471,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/components/virtualview (0.81.4): + - React-FabricComponents/components/virtualview (0.81.5): - boost - DoubleConversion - fast_float @@ -1470,7 +1498,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricComponents/textlayoutmanager (0.81.4): + - React-FabricComponents/textlayoutmanager (0.81.5): - boost - DoubleConversion - fast_float @@ -1497,7 +1525,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-FabricImage (0.81.4): + - React-FabricImage (0.81.5): - boost - DoubleConversion - fast_float @@ -1506,21 +1534,21 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - RCTRequired (= 0.81.4) - - RCTTypeSafety (= 0.81.4) + - RCTRequired (= 0.81.5) + - RCTTypeSafety (= 0.81.5) - React-Fabric - React-featureflags - React-graphics - React-ImageManager - React-jsi - - React-jsiexecutor (= 0.81.4) + - React-jsiexecutor (= 0.81.5) - React-logger - React-rendererdebug - React-utils - ReactCommon - SocketRocket - Yoga - - React-featureflags (0.81.4): + - React-featureflags (0.81.5): - boost - DoubleConversion - fast_float @@ -1529,7 +1557,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-featureflagsnativemodule (0.81.4): + - React-featureflagsnativemodule (0.81.5): - boost - DoubleConversion - fast_float @@ -1544,7 +1572,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - SocketRocket - - React-graphics (0.81.4): + - React-graphics (0.81.5): - boost - DoubleConversion - fast_float @@ -1557,7 +1585,7 @@ PODS: - React-jsiexecutor - React-utils - SocketRocket - - React-hermes (0.81.4): + - React-hermes (0.81.5): - boost - DoubleConversion - fast_float @@ -1566,16 +1594,16 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-cxxreact (= 0.81.4) + - React-cxxreact (= 0.81.5) - React-jsi - - React-jsiexecutor (= 0.81.4) + - React-jsiexecutor (= 0.81.5) - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing - - React-perflogger (= 0.81.4) + - React-perflogger (= 0.81.5) - React-runtimeexecutor - SocketRocket - - React-idlecallbacksnativemodule (0.81.4): + - React-idlecallbacksnativemodule (0.81.5): - boost - DoubleConversion - fast_float @@ -1591,7 +1619,7 @@ PODS: - React-runtimescheduler - ReactCommon/turbomodule/core - SocketRocket - - React-ImageManager (0.81.4): + - React-ImageManager (0.81.5): - boost - DoubleConversion - fast_float @@ -1606,7 +1634,7 @@ PODS: - React-rendererdebug - React-utils - SocketRocket - - React-jserrorhandler (0.81.4): + - React-jserrorhandler (0.81.5): - boost - DoubleConversion - fast_float @@ -1621,7 +1649,7 @@ PODS: - React-jsi - ReactCommon/turbomodule/bridging - SocketRocket - - React-jsi (0.81.4): + - React-jsi (0.81.5): - boost - DoubleConversion - fast_float @@ -1631,7 +1659,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-jsiexecutor (0.81.4): + - React-jsiexecutor (0.81.5): - boost - DoubleConversion - fast_float @@ -1640,15 +1668,15 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-cxxreact (= 0.81.4) - - React-jsi (= 0.81.4) + - React-cxxreact (= 0.81.5) + - React-jsi (= 0.81.5) - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing - - React-perflogger (= 0.81.4) + - React-perflogger (= 0.81.5) - React-runtimeexecutor - SocketRocket - - React-jsinspector (0.81.4): + - React-jsinspector (0.81.5): - boost - DoubleConversion - fast_float @@ -1663,10 +1691,10 @@ PODS: - React-jsinspectornetwork - React-jsinspectortracing - React-oscompat - - React-perflogger (= 0.81.4) + - React-perflogger (= 0.81.5) - React-runtimeexecutor - SocketRocket - - React-jsinspectorcdp (0.81.4): + - React-jsinspectorcdp (0.81.5): - boost - DoubleConversion - fast_float @@ -1675,7 +1703,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-jsinspectornetwork (0.81.4): + - React-jsinspectornetwork (0.81.5): - boost - DoubleConversion - fast_float @@ -1688,7 +1716,7 @@ PODS: - React-performancetimeline - React-timing - SocketRocket - - React-jsinspectortracing (0.81.4): + - React-jsinspectortracing (0.81.5): - boost - DoubleConversion - fast_float @@ -1699,7 +1727,7 @@ PODS: - React-oscompat - React-timing - SocketRocket - - React-jsitooling (0.81.4): + - React-jsitooling (0.81.5): - boost - DoubleConversion - fast_float @@ -1707,16 +1735,16 @@ PODS: - glog - RCT-Folly - RCT-Folly/Fabric - - React-cxxreact (= 0.81.4) - - React-jsi (= 0.81.4) + - React-cxxreact (= 0.81.5) + - React-jsi (= 0.81.5) - React-jsinspector - React-jsinspectorcdp - React-jsinspectortracing - React-runtimeexecutor - SocketRocket - - React-jsitracing (0.81.4): + - React-jsitracing (0.81.5): - React-jsi - - React-logger (0.81.4): + - React-logger (0.81.5): - boost - DoubleConversion - fast_float @@ -1725,7 +1753,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-Mapbuffer (0.81.4): + - React-Mapbuffer (0.81.5): - boost - DoubleConversion - fast_float @@ -1735,7 +1763,7 @@ PODS: - RCT-Folly/Fabric - React-debug - SocketRocket - - React-microtasksnativemodule (0.81.4): + - React-microtasksnativemodule (0.81.5): - boost - DoubleConversion - fast_float @@ -2008,7 +2036,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - React-NativeModulesApple (0.81.4): + - React-NativeModulesApple (0.81.5): - boost - DoubleConversion - fast_float @@ -2028,8 +2056,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - SocketRocket - - React-oscompat (0.81.4) - - React-perflogger (0.81.4): + - React-oscompat (0.81.5) + - React-perflogger (0.81.5): - boost - DoubleConversion - fast_float @@ -2038,7 +2066,7 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - SocketRocket - - React-performancetimeline (0.81.4): + - React-performancetimeline (0.81.5): - boost - DoubleConversion - fast_float @@ -2051,9 +2079,9 @@ PODS: - React-perflogger - React-timing - SocketRocket - - React-RCTActionSheet (0.81.4): - - React-Core/RCTActionSheetHeaders (= 0.81.4) - - React-RCTAnimation (0.81.4): + - React-RCTActionSheet (0.81.5): + - React-Core/RCTActionSheetHeaders (= 0.81.5) + - React-RCTAnimation (0.81.5): - boost - DoubleConversion - fast_float @@ -2069,7 +2097,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon - SocketRocket - - React-RCTAppDelegate (0.81.4): + - React-RCTAppDelegate (0.81.5): - boost - DoubleConversion - fast_float @@ -2103,7 +2131,7 @@ PODS: - React-utils - ReactCommon - SocketRocket - - React-RCTBlob (0.81.4): + - React-RCTBlob (0.81.5): - boost - DoubleConversion - fast_float @@ -2122,7 +2150,7 @@ PODS: - React-RCTNetwork - ReactCommon - SocketRocket - - React-RCTFabric (0.81.4): + - React-RCTFabric (0.81.5): - boost - DoubleConversion - fast_float @@ -2157,7 +2185,7 @@ PODS: - React-utils - SocketRocket - Yoga - - React-RCTFBReactNativeSpec (0.81.4): + - React-RCTFBReactNativeSpec (0.81.5): - boost - DoubleConversion - fast_float @@ -2171,10 +2199,10 @@ PODS: - React-Core - React-jsi - React-NativeModulesApple - - React-RCTFBReactNativeSpec/components (= 0.81.4) + - React-RCTFBReactNativeSpec/components (= 0.81.5) - ReactCommon - SocketRocket - - React-RCTFBReactNativeSpec/components (0.81.4): + - React-RCTFBReactNativeSpec/components (0.81.5): - boost - DoubleConversion - fast_float @@ -2197,7 +2225,7 @@ PODS: - ReactCommon - SocketRocket - Yoga - - React-RCTImage (0.81.4): + - React-RCTImage (0.81.5): - boost - DoubleConversion - fast_float @@ -2213,14 +2241,14 @@ PODS: - React-RCTNetwork - ReactCommon - SocketRocket - - React-RCTLinking (0.81.4): - - React-Core/RCTLinkingHeaders (= 0.81.4) - - React-jsi (= 0.81.4) + - React-RCTLinking (0.81.5): + - React-Core/RCTLinkingHeaders (= 0.81.5) + - React-jsi (= 0.81.5) - React-NativeModulesApple - React-RCTFBReactNativeSpec - ReactCommon - - ReactCommon/turbomodule/core (= 0.81.4) - - React-RCTNetwork (0.81.4): + - ReactCommon/turbomodule/core (= 0.81.5) + - React-RCTNetwork (0.81.5): - boost - DoubleConversion - fast_float @@ -2238,7 +2266,7 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon - SocketRocket - - React-RCTRuntime (0.81.4): + - React-RCTRuntime (0.81.5): - boost - DoubleConversion - fast_float @@ -2258,7 +2286,7 @@ PODS: - React-runtimeexecutor - React-RuntimeHermes - SocketRocket - - React-RCTSettings (0.81.4): + - React-RCTSettings (0.81.5): - boost - DoubleConversion - fast_float @@ -2273,10 +2301,10 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon - SocketRocket - - React-RCTText (0.81.4): - - React-Core/RCTTextHeaders (= 0.81.4) + - React-RCTText (0.81.5): + - React-Core/RCTTextHeaders (= 0.81.5) - Yoga - - React-RCTVibration (0.81.4): + - React-RCTVibration (0.81.5): - boost - DoubleConversion - fast_float @@ -2290,11 +2318,11 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon - SocketRocket - - React-rendererconsistency (0.81.4) - - React-renderercss (0.81.4): + - React-rendererconsistency (0.81.5) + - React-renderercss (0.81.5): - React-debug - React-utils - - React-rendererdebug (0.81.4): + - React-rendererdebug (0.81.5): - boost - DoubleConversion - fast_float @@ -2304,7 +2332,7 @@ PODS: - RCT-Folly/Fabric - React-debug - SocketRocket - - React-RuntimeApple (0.81.4): + - React-RuntimeApple (0.81.5): - boost - DoubleConversion - fast_float @@ -2333,7 +2361,7 @@ PODS: - React-runtimescheduler - React-utils - SocketRocket - - React-RuntimeCore (0.81.4): + - React-RuntimeCore (0.81.5): - boost - DoubleConversion - fast_float @@ -2355,7 +2383,7 @@ PODS: - React-runtimescheduler - React-utils - SocketRocket - - React-runtimeexecutor (0.81.4): + - React-runtimeexecutor (0.81.5): - boost - DoubleConversion - fast_float @@ -2365,10 +2393,10 @@ PODS: - RCT-Folly/Fabric - React-debug - React-featureflags - - React-jsi (= 0.81.4) + - React-jsi (= 0.81.5) - React-utils - SocketRocket - - React-RuntimeHermes (0.81.4): + - React-RuntimeHermes (0.81.5): - boost - DoubleConversion - fast_float @@ -2389,7 +2417,7 @@ PODS: - React-runtimeexecutor - React-utils - SocketRocket - - React-runtimescheduler (0.81.4): + - React-runtimescheduler (0.81.5): - boost - DoubleConversion - fast_float @@ -2411,9 +2439,9 @@ PODS: - React-timing - React-utils - SocketRocket - - React-timing (0.81.4): + - React-timing (0.81.5): - React-debug - - React-utils (0.81.4): + - React-utils (0.81.5): - boost - DoubleConversion - fast_float @@ -2423,11 +2451,11 @@ PODS: - RCT-Folly - RCT-Folly/Fabric - React-debug - - React-jsi (= 0.81.4) + - React-jsi (= 0.81.5) - SocketRocket - - ReactAppDependencyProvider (0.81.4): + - ReactAppDependencyProvider (0.81.5): - ReactCodegen - - ReactCodegen (0.81.4): + - ReactCodegen (0.81.5): - boost - DoubleConversion - fast_float @@ -2453,7 +2481,7 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - SocketRocket - - ReactCommon (0.81.4): + - ReactCommon (0.81.5): - boost - DoubleConversion - fast_float @@ -2461,9 +2489,9 @@ PODS: - glog - RCT-Folly - RCT-Folly/Fabric - - ReactCommon/turbomodule (= 0.81.4) + - ReactCommon/turbomodule (= 0.81.5) - SocketRocket - - ReactCommon/turbomodule (0.81.4): + - ReactCommon/turbomodule (0.81.5): - boost - DoubleConversion - fast_float @@ -2472,15 +2500,15 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-callinvoker (= 0.81.4) - - React-cxxreact (= 0.81.4) - - React-jsi (= 0.81.4) - - React-logger (= 0.81.4) - - React-perflogger (= 0.81.4) - - ReactCommon/turbomodule/bridging (= 0.81.4) - - ReactCommon/turbomodule/core (= 0.81.4) + - React-callinvoker (= 0.81.5) + - React-cxxreact (= 0.81.5) + - React-jsi (= 0.81.5) + - React-logger (= 0.81.5) + - React-perflogger (= 0.81.5) + - ReactCommon/turbomodule/bridging (= 0.81.5) + - ReactCommon/turbomodule/core (= 0.81.5) - SocketRocket - - ReactCommon/turbomodule/bridging (0.81.4): + - ReactCommon/turbomodule/bridging (0.81.5): - boost - DoubleConversion - fast_float @@ -2489,13 +2517,13 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-callinvoker (= 0.81.4) - - React-cxxreact (= 0.81.4) - - React-jsi (= 0.81.4) - - React-logger (= 0.81.4) - - React-perflogger (= 0.81.4) + - React-callinvoker (= 0.81.5) + - React-cxxreact (= 0.81.5) + - React-jsi (= 0.81.5) + - React-logger (= 0.81.5) + - React-perflogger (= 0.81.5) - SocketRocket - - ReactCommon/turbomodule/core (0.81.4): + - ReactCommon/turbomodule/core (0.81.5): - boost - DoubleConversion - fast_float @@ -2504,17 +2532,15 @@ PODS: - hermes-engine - RCT-Folly - RCT-Folly/Fabric - - React-callinvoker (= 0.81.4) - - React-cxxreact (= 0.81.4) - - React-debug (= 0.81.4) - - React-featureflags (= 0.81.4) - - React-jsi (= 0.81.4) - - React-logger (= 0.81.4) - - React-perflogger (= 0.81.4) - - React-utils (= 0.81.4) + - React-callinvoker (= 0.81.5) + - React-cxxreact (= 0.81.5) + - React-debug (= 0.81.5) + - React-featureflags (= 0.81.5) + - React-jsi (= 0.81.5) + - React-logger (= 0.81.5) + - React-perflogger (= 0.81.5) + - React-utils (= 0.81.5) - SocketRocket - - RNCallKeep (4.3.16): - - React - RNCClipboard (1.16.3): - boost - DoubleConversion @@ -2937,7 +2963,7 @@ PODS: - SocketRocket - Yoga - SocketRocket (0.7.1) - - stream-chat-react-native (8.6.0): + - stream-chat-react-native (8.9.1): - boost - DoubleConversion - fast_float @@ -2966,7 +2992,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - stream-io-noise-cancellation-react-native (0.4.0): + - stream-io-noise-cancellation-react-native (0.4.2): - boost - DoubleConversion - fast_float @@ -2996,7 +3022,7 @@ PODS: - stream-react-native-webrtc - StreamVideoNoiseCancellation - Yoga - - stream-io-video-filters-react-native (0.8.0): + - stream-io-video-filters-react-native (0.9.1): - boost - DoubleConversion - fast_float @@ -3027,7 +3053,7 @@ PODS: - Yoga - stream-react-native-webrtc (137.0.0): - React-Core - - stream-video-react-native (1.24.1): + - stream-video-react-native (1.25.0): - boost - DoubleConversion - fast_float @@ -3067,6 +3093,7 @@ PODS: DEPENDENCIES: - boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`) + - Callingx (from `../node_modules/react-native-callingx`) - DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) - fast_float (from `../node_modules/react-native/third-party-podspecs/fast_float.podspec`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) @@ -3143,7 +3170,6 @@ DEPENDENCIES: - ReactAppDependencyProvider (from `build/generated/ios`) - ReactCodegen (from `build/generated/ios`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - - RNCallKeep (from `../node_modules/react-native-callkeep`) - "RNCClipboard (from `../node_modules/@react-native-clipboard/clipboard`)" - "RNCPushNotificationIOS (from `../node_modules/@react-native-community/push-notification-ios`)" - RNDeviceInfo (from `../node_modules/react-native-device-info`) @@ -3174,6 +3200,8 @@ SPEC REPOS: EXTERNAL SOURCES: boost: :podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec" + Callingx: + :path: "../node_modules/react-native-callingx" DoubleConversion: :podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" fast_float: @@ -3325,8 +3353,6 @@ EXTERNAL SOURCES: :path: build/generated/ios ReactCommon: :path: "../node_modules/react-native/ReactCommon" - RNCallKeep: - :path: "../node_modules/react-native-callkeep" RNCClipboard: :path: "../node_modules/@react-native-clipboard/clipboard" RNCPushNotificationIOS: @@ -3368,105 +3394,105 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 + Callingx: af9fe08b1b553f61f11bd68f05eb731b1910b72a DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb fast_float: b32c788ed9c6a8c584d114d0047beda9664e7cc6 - FBLazyVector: 941bef1c8eeabd9fe1f501e30a5220beee913886 + FBLazyVector: 5beb8028d5a2e75dd9634917f23e23d3a061d2aa fmt: a40bb5bd0294ea969aaaba240a927bd33d878cdd glog: 5683914934d5b6e4240e497e0f4a3b42d1854183 hermes-engine: 35c763d57c9832d0eef764316ca1c4d043581394 - RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 - RCTDeprecation: c0ed3249a97243002615517dff789bf4666cf585 - RCTRequired: 58719f5124f9267b5f9649c08bf23d9aea845b23 - RCTTypeSafety: 4aefa8328ab1f86da273f08517f1f6b343f6c2cc - React: 2073376f47c71b7e9a0af7535986a77522ce1049 - React-callinvoker: 751b6f2c83347a0486391c3f266f291f0f53b27e + RCT-Folly: 59ec0ac1f2f39672a0c6e6cecdd39383b764646f + RCTDeprecation: 5eb1d2eeff5fb91151e8a8eef45b6c7658b6c897 + RCTRequired: cebcf9442fc296c9b89ac791dfd463021d9f6f23 + RCTTypeSafety: b99aa872829ee18f6e777e0ef55852521c5a6788 + React: 914f8695f9bf38e6418228c2ffb70021e559f92f + React-callinvoker: 23cd4e33928608bd0cc35357597568b8b9a5f068 React-Codegen: 4b8b4817cea7a54b83851d4c1f91f79aa73de30a - React-Core: dff5d29973349b11dd6631c9498456d75f846d5e - React-CoreModules: c0ae04452e4c5d30e06f8e94692a49107657f537 - React-cxxreact: 376fd672c95dfb64ad5cc246e6a1e9edb78dec4c - React-debug: d4955c86870792887ed695df6ebf0e94e39dc7e1 - React-defaultsnativemodule: bd2b805c6daa85d430d034aa748544b377ada152 - React-domnativemodule: b5c04a4a74ed9c3cb25adc72583b017868600464 - React-Fabric: 93a9ff378f1edf29e9a22a24ad55a1be061e7985 - React-FabricComponents: 83bd54366d4ecb8bec563aa1a78d49915763d503 - React-FabricImage: 8bcd88e553047d4ed5c7ea3def8d6c0e3dd88cfc - React-featureflags: 4ea691ab154d505277859416aa226ae32edeef5f - React-featureflagsnativemodule: b8f00b01436294a30dc62fb5e50b70aa3910309c - React-graphics: d6207795fe822668daeb9c6e1f1470a8500d9eec - React-hermes: fcbdc45ecf38259fe3b12642bd0757c52270a107 - React-idlecallbacksnativemodule: f390a518e1a862453f45f86a1bc248350634d858 - React-ImageManager: acb99e093632b7fc2953dd45f2abaeeea2d9588e - React-jserrorhandler: 958ab9afbe7acdbfe8ca225f7503313409b1319a - React-jsi: 59ec3190dd364cca86a58869e7755477d2468948 - React-jsiexecutor: b87d78a2e8dd7a6f56e9cdac038da45de98c944f - React-jsinspector: 9c33e0c4eeeb10a23b61c4501947b57977980e0e - React-jsinspectorcdp: d7b2c3feddd3669f0eaad2ac1e0f7afbc1d1cf18 - React-jsinspectornetwork: 696d0cf07016e69c053deffba30003fa448904a3 - React-jsinspectortracing: 05d49cd8795db15a279eab6f7604dfa9fe9622f1 - React-jsitooling: 0f9894c3656c3c13d4fcfe6e1dc964fd340acf49 - React-jsitracing: dc11027f9e4e829d32bf17626ec831581ea05223 - React-logger: a3cb5b29c32b8e447b5a96919340e89334062b48 - React-Mapbuffer: e4a65db5f4df53369f39558c0cf2f480f6d3d6c7 - React-microtasksnativemodule: 86334c5c06315e0bccb7b6e6f2c905e92f98b615 - react-native-blob-util: 7f71e0af02279ef38a2ba43e8c2fcb79cf927732 - react-native-image-picker: 6051cfd030121b880a58f1cc0e5e33e9887804e4 - react-native-mmkv: 7b9c7469fa0a7e463f9411ad3e4fe273bd5ff030 - react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187 - react-native-safe-area-context: ee1e8e2a7abf737a8d4d9d1a5686a7f2e7466236 - react-native-video: d9d12aa2325ae06222e97e8bd801bbc31df2675d - React-NativeModulesApple: 8c7eb6057b00c191a11ad5ced41826ec5a0e4d78 - React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d - React-perflogger: 5536d2df3d18fe0920263466f7b46a56351c0510 - React-performancetimeline: c6c9393c1a0453a51e1852e3531defe60790b36c - React-RCTActionSheet: 42195ae666e6d79b4af2346770f765b7c29435b9 - React-RCTAnimation: fa103ccc3503b1ed8dedca7e62e7823937748843 - React-RCTAppDelegate: 665d4baf19424cef08276e9ac0d8771eec4519f9 - React-RCTBlob: 0fa9530c255644db095f2c4fd8d89738d9d9ecc0 - React-RCTFabric: 95eb4a92c5c166e21bae07231d327174e56f202d - React-RCTFBReactNativeSpec: fd66225b71f902a8bfa939fb5f7ec743958298df - React-RCTImage: ba824e61ce2e920a239a65d130b83c3a1d426dff - React-RCTLinking: d2dc199c37e71e6f505d9eca3e5c33be930014d4 - React-RCTNetwork: 87137d4b9bd77e5068f854dd5c1f30d4b072faf6 - React-RCTRuntime: b10bd5e5506af0d6205c4101dd1560fe7beead95 - React-RCTSettings: 71f5c7fd7b5f4e725a4e2114a4b4373d0e46048f - React-RCTText: b94d4699b49285bee22b8ebf768924d607eccee3 - React-RCTVibration: 6e3993c4f6c36a3899059f9a9ead560ddaf5a7d7 - React-rendererconsistency: 612d0f6603d9837bb1236d7fd5194203b35c8799 - React-renderercss: e5c2c3b84976f7a587cde8423c671db07a6a77da - React-rendererdebug: cc7a6131733605b8897754f72c0c35c79f77da9e - React-RuntimeApple: 3f96102fc1ebf738d36719cdce5422a5769293fb - React-RuntimeCore: f05563107927f155180dfa008fed2ac1316a6aec - React-runtimeexecutor: dd3ec3b76761b43e7b37d07a70de91fc1dd24e7e - React-RuntimeHermes: 7fcb384acc111ea21bcffe2e4a15f31b58bb702e - React-runtimescheduler: 7d2eaa4e7d652a391f47df7ff510260413429bd9 - React-timing: f5d4ba74be96a24b9b2a1a910142ed14e03013d9 - React-utils: eb92d1db56a9bb5911b2c77fb4c2e8d331c8b9dd - ReactAppDependencyProvider: 433ddfb4536948630aadd5bd925aff8a632d2fe3 - ReactCodegen: 2cfa890e84ecf7f3a708f1ed9c0f2c0b22a23c9a - ReactCommon: e9ab32f1d1482d207867b4fdd139361302b9dcc6 - RNCallKeep: 1930a01d8caf48f018be4f2db0c9f03405c2f977 - RNCClipboard: e560338bf6cc4656a09ff90610b62ddc0dbdad65 - RNCPushNotificationIOS: 6c4ca3388c7434e4a662b92e4dfeeee858e6f440 - RNDeviceInfo: bcce8752b5043a623fe3c26789679b473f705d3c - RNGestureHandler: b8d2e75c2e88fc2a1f6be3b3beeeed80b88fa37d - RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168 - RNPermissions: ee88ba7d9fb3f9b3650ff7e5a78e7ee2d1c7e200 - RNReactNativeHapticFeedback: 5f1542065f0b24c9252bd8cf3e83bc9c548182e4 - RNReanimated: 44af5b24b999a70c737df8e407582c32d12b3a87 - RNScreens: 2e9c41cd099b1ca50136af8d57c3594214d0086a - RNSVG: 65335d69b8e372837ccad79307b1190d5cc1d0a9 - RNVoipPushNotification: 4998fe6724d421da616dca765da7dc421ff54c4e - RNWorklets: ad0606bee2a8103c14adb412149789c60b72bfb2 + React-Core: 895a479e2e0331f48af3ad919bece917926a0b7d + React-CoreModules: dfa38212cf3a91f2eb84ccd43d747d006d33449e + React-cxxreact: 7a4e2c77e564792252131e63270f6184d93343b3 + React-debug: 29aed758c756956a51b4560223edbd15191ca4c5 + React-defaultsnativemodule: ee4e3ca63b29c8b91448a6760d72063196ed0796 + React-domnativemodule: 4d29aad12ebb2b5aa34043e5bdd191a92821f3aa + React-Fabric: 21f78a4856240d39a663a52c452e223c5e412098 + React-FabricComponents: 13fc0ac39a488cea00c83ffa7b16113f024d66e6 + React-FabricImage: 8961abe0372d20679ee093d144aaf5fb1227bf41 + React-featureflags: 018934f958e6b83907e71631599b02144e6b17f4 + React-featureflagsnativemodule: 89fef5751203b7d3cdde43e1e10407983735a4b4 + React-graphics: 1c62dd11f47071482ca90238981f0147cce4089d + React-hermes: 36704d7354fff9c9e3fbb2490e8eeb2ac027f6f0 + React-idlecallbacksnativemodule: 5f7cbecc1479b53e665f2cd6c2af2c21a80d2ffd + React-ImageManager: 4cb6318bb2bcc947106e29f9297a1c24c85a9233 + React-jserrorhandler: 4b9344f5794cfe8946f8752d38094649f53dd3f3 + React-jsi: 3a8c6f94a52033b0cca53c38d9bb568306aa9dc1 + React-jsiexecutor: d7cf79b1c2771c6b21c46691a96dd2e32d4454c7 + React-jsinspector: 651483ea1d79859e0ed21b86e9042b2a3f4d2b40 + React-jsinspectorcdp: c800035023789b8bf32b4f5a4c9003c2dc28ee49 + React-jsinspectornetwork: 249ee46e9de930d773ff6e4726aa8eeb5854b589 + React-jsinspectortracing: 80e251e13a6071607f06f0e39e03d3f2ce2645cb + React-jsitooling: 6ce395671d0139ec1c4721953a4d3d92172fc06f + React-jsitracing: 4a4d89334b14d96de0387876751528433d1d2fbd + React-logger: 8bcfaf06f8c536fb9e9760526cf3d17ccd53e4ce + React-Mapbuffer: 4649384414066eb77e30a3124dbb48732a3aa173 + React-microtasksnativemodule: e39f94cc96d61b8174a5cfb2d5862a73fa8c0d35 + react-native-blob-util: 9027c999d7d2b7d4b087ea40413eddc899af46fe + react-native-image-picker: b99d9c1f2ddbef491eb49e1103e5c2ce78b9ab37 + react-native-mmkv: c70c58437c0f5e5fe6992f1de1e350dcdd236332 + react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac + react-native-safe-area-context: 3e0a25a843c40ad5efdf2bef93fdd95ad7229650 + react-native-video: 201e71d168053580319a4741e1ef44380cdcb06f + React-NativeModulesApple: 8ce162c145e6b9767bb37a090c77d3d28f7d32b5 + React-oscompat: eb0626e8ba1a2c61673c991bf9dc21834898475d + React-perflogger: d0d0d1b884120fa0a13bd38ac5e9c3c8e8bfe82a + React-performancetimeline: ae60fb7a7447c44d4d3227fc4eeba606403aaee3 + React-RCTActionSheet: 30fe8f9f8d86db4a25ff34595a658ecd837485fc + React-RCTAnimation: e86dacf8a982f42341a44ec87ea8a30964a15f9f + React-RCTAppDelegate: d7214067e796732b5d22960270593945f1ab8a14 + React-RCTBlob: af1fc227a5aa55564afbe84530a8bd28834fda15 + React-RCTFabric: 8d92e851cc6cdf9771c52a18b144424c92b72082 + React-RCTFBReactNativeSpec: c9ec2130e3c9366d30a85886e1776210054763f5 + React-RCTImage: 70a10a5b957ca124b8d0b0fdeec369f11194782c + React-RCTLinking: 67f8a024192b4844c40ace955c54bb34f40d47f0 + React-RCTNetwork: a7679ee67e7d34797a00cefaa879a3f9ea8cee9c + React-RCTRuntime: 3d25c69970924b597c339aead60168026d7cbc2c + React-RCTSettings: 18d8674195383c4fd51b9fc98ee815b329fba7e4 + React-RCTText: 125574af8e29d0ceb430cbe2a03381d62ba45a47 + React-RCTVibration: e96d43017757197d46834c50a0acfb78429c9b30 + React-rendererconsistency: a7b47f8b186af64ff8509c8caec4114a2f1ae63f + React-renderercss: 0a5b6b7aefc3f5e46a61b0e41b1179a0750cf077 + React-rendererdebug: 7da01af21ab31661c3040ef647e6e2bc55575771 + React-RuntimeApple: 788ca3b8e5485a46654e8a316d4c1e40bf82c5d4 + React-RuntimeCore: 1730e6e5cba6f0af4e0319f891da6426b491e39f + React-runtimeexecutor: 79894322e55268854dc04ff1bee083f24725f6c8 + React-RuntimeHermes: 86bf03cbf11ef05803a2e32087667c8a3cc45f72 + React-runtimescheduler: 70601d598a8a71582fa69a9ba488a27c5d12790d + React-timing: 5717558f0bea206d7557df53015ee9efe1eb57b2 + React-utils: 55e54e497e3d3f373ebfcf844eb77e24ed013356 + ReactAppDependencyProvider: c277c5b231881ad4f00cd59e3aa0671b99d7ebee + ReactCodegen: 1e847cf77c1402fe7394dae41b3829c95569e76e + ReactCommon: 5cfd842fcd893bb40fc835f98fadc60c42906a20 + RNCClipboard: 4eea71d801c880175c12f6ab14332ca86fed935f + RNCPushNotificationIOS: 64218f3c776c03d7408284a819b2abfda1834bc8 + RNDeviceInfo: 8b6fa8379062949dd79a009cf3d6b02a9c03ca59 + RNGestureHandler: 9339994ea5d1ff6ad2679b7d0cc3d49053111369 + RNNotifee: 4a6ee5c7deaf00e005050052d73ee6315dff7ec9 + RNPermissions: f16d5f721280a171831d992c5886761f08075926 + RNReactNativeHapticFeedback: 43c09eb41d8321be2e1375cb87ae734e58f677b0 + RNReanimated: 91d075aaf0c89d51a0708cd64cd6c77f7fa42cdc + RNScreens: 871305075ddf1291411b051d86da9e0d9289de02 + RNSVG: 596e9946ddc0e023c6e6931038c9e49b553f742d + RNVoipPushNotification: a6f7c09e1ca7220e2be1c45e9b6b897c9600024b + RNWorklets: 582ee5c59370d483a7a3c3f8a8014d48b1bff6bd SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 - stream-chat-react-native: c88c3a1087393358e660885479e21be1f2c286a3 - stream-io-noise-cancellation-react-native: ed874466f2e7967ada45a9e4dfad147dabe8f9dd - stream-io-video-filters-react-native: 1336c7f604d99d452817b90828389f47771f9417 - stream-react-native-webrtc: dcf95d3bd7f0503245b553af02484423805212ed - stream-video-react-native: 5a29ec88115d77bc6bc36623c4bfe3b8ce9ca2e6 + stream-chat-react-native: 615d9d16c569045da3737a03f54ec3a1d781be81 + stream-io-noise-cancellation-react-native: 6ba3a3818974c0afc8e39689ba50d65ef5728cb4 + stream-io-video-filters-react-native: 98fbda802ededc1983d1f20655bdc1a290d2d9ca + stream-react-native-webrtc: 129dae4801746eb01c27f280410f4fb0918f1fca + stream-video-react-native: e82a00cdb199cdb320fd4aa4bbfab5205b952fc5 StreamVideoNoiseCancellation: 41f5a712aba288f9636b64b17ebfbdff52c61490 - VisionCamera: 891edb31806dd3a239c8a9d6090d6ec78e11ee80 - Yoga: 9b30b783a17681321b52ac507a37219d7d795ace + VisionCamera: 05e4bc4783174689a5878a0797015ab32afae9e4 + Yoga: cc4a6600d61e4e9276e860d4d68eebb834a050ba PODFILE CHECKSUM: aa62ba474533b73121c2068a13a8b909b17efbaa -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/sample-apps/react-native/dogfood/metro.config.js b/sample-apps/react-native/dogfood/metro.config.js index a414f24daa..0dc43e348f 100644 --- a/sample-apps/react-native/dogfood/metro.config.js +++ b/sample-apps/react-native/dogfood/metro.config.js @@ -14,6 +14,7 @@ config.watchFolders = [ path.join(workspaceRoot, 'packages/react-native-sdk'), path.join(workspaceRoot, 'packages/video-filters-react-native'), path.join(workspaceRoot, 'packages/noise-cancellation-react-native'), + path.join(workspaceRoot, 'packages/react-native-callingx'), ]; // find what all modules need to be unique for the app diff --git a/sample-apps/react-native/dogfood/package.json b/sample-apps/react-native/dogfood/package.json index 6ec0872a35..8e6cfa2418 100644 --- a/sample-apps/react-native/dogfood/package.json +++ b/sample-apps/react-native/dogfood/package.json @@ -28,7 +28,7 @@ "react": "19.1.0", "react-native": "^0.81.5", "react-native-blob-util": "^0.22.2", - "react-native-callkeep": "^4.3.16", + "react-native-callingx": "workspace:^", "react-native-device-info": "^14.1.1", "react-native-dotenv": "^3.4.11", "react-native-gesture-handler": "^2.28.0", diff --git a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts index 58158bd415..cb5aeb2dd3 100644 --- a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts +++ b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts @@ -84,6 +84,25 @@ export function setPushConfig() { }, }); + StreamVideoRN.setupCallingExp({ + ios: { + appName: 'Dogfood app', + }, + android: { + incomingChannel: { + id: 'stream_incoming_call_channel_update2', + name: 'Incoming call notifications', + }, + outgoingChannel: { + id: 'stream_outgoing_call_channel_update2', + name: 'Outgoing call notifications', + }, + titleTransformer: (callerName: string) => + `Incoming call from ${callerName}`, + subtitleTransformer: (phoneNumber: string) => `Tap to open the call`, + }, + }); + setFirebaseListeners(); if (Platform.OS === 'android') { // on press handlers of background notifications diff --git a/yarn.lock b/yarn.lock index 4ebe2080b9..c37b1af1d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -59,6 +59,22 @@ __metadata: languageName: node linkType: hard +"@ark/schema@npm:0.55.0": + version: 0.55.0 + resolution: "@ark/schema@npm:0.55.0" + dependencies: + "@ark/util": "npm:0.55.0" + checksum: 10/18fbfe27f3da911ba38d063ba07b35d3236df124a32bb47ce1ed9cbf25185fe337b37a87f0776831cc698d7295fe4c9ec5547c444c5648ac5205f45c4514aa71 + languageName: node + linkType: hard + +"@ark/util@npm:0.55.0": + version: 0.55.0 + resolution: "@ark/util@npm:0.55.0" + checksum: 10/a172db34c566e6677f210b6b1fbfab49f86a5c81c7ae6633be43860cac60b67245f7af518b4db92fa9947eb2fb854e1166df3358e60556b2621ecb7547ffd1ef + languageName: node + linkType: hard + "@babel/cli@npm:^7.23.4": version: 7.26.4 resolution: "@babel/cli@npm:7.26.4" @@ -136,6 +152,20 @@ __metadata: languageName: node linkType: hard +"@babel/eslint-parser@npm:^7.25.1": + version: 7.28.5 + resolution: "@babel/eslint-parser@npm:7.28.5" + dependencies: + "@nicolo-ribaudo/eslint-scope-5-internals": "npm:5.1.1-v1" + eslint-visitor-keys: "npm:^2.1.0" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.11.0 + eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 + checksum: 10/ec8eb061f319fe3854f4c720303bf239625e63c5ddc9391a3c00f17339d417dc5482dc043b64d15506b0e87f26820dfec9ff4880a1862f7c05f4cf1bbf6e34be + languageName: node + linkType: hard + "@babel/generator@npm:^7.20.0, @babel/generator@npm:^7.20.5, @babel/generator@npm:^7.25.0, @babel/generator@npm:^7.25.7, @babel/generator@npm:^7.28.3, @babel/generator@npm:^7.7.2": version: 7.28.3 resolution: "@babel/generator@npm:7.28.3" @@ -565,6 +595,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-flow@npm:^7.27.1": + version: 7.27.1 + resolution: "@babel/plugin-syntax-flow@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.27.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/7baca3171ed595d04c865b0ce46fca7f21900686df9d7fcd1017036ce78bb5483e33803de810831e68d39cf478953db69f49ae3f3de2e3207bc4ba49a96b6739 + languageName: node + linkType: hard + "@babel/plugin-syntax-import-assertions@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-syntax-import-assertions@npm:7.27.1" @@ -935,6 +976,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-flow-strip-types@npm:^7.26.5": + version: 7.27.1 + resolution: "@babel/plugin-transform-flow-strip-types@npm:7.27.1" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.27.1" + "@babel/plugin-syntax-flow": "npm:^7.27.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/22e260866b122b7d0c35f2c55b2d422b175606b4d14c9ba116b1fbe88e08cc8b024c1c41bb62527cfc5f7ccc0ed06c752e5945cb1ee22465a30aa5623e617940 + languageName: node + linkType: hard + "@babel/plugin-transform-for-of@npm:^7.24.7, @babel/plugin-transform-for-of@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-transform-for-of@npm:7.27.1" @@ -1669,6 +1722,197 @@ __metadata: languageName: node linkType: hard +"@commitlint/cli@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/cli@npm:19.8.1" + dependencies: + "@commitlint/format": "npm:^19.8.1" + "@commitlint/lint": "npm:^19.8.1" + "@commitlint/load": "npm:^19.8.1" + "@commitlint/read": "npm:^19.8.1" + "@commitlint/types": "npm:^19.8.1" + tinyexec: "npm:^1.0.0" + yargs: "npm:^17.0.0" + bin: + commitlint: ./cli.js + checksum: 10/c41f5a42319e38e1da2addd27b32ced461eae2d01c9bbfc75be069d88467974255dfbe4593d3e107a0e3f68350f482490bcda69d9d6a4192cde8084f203e7c8b + languageName: node + linkType: hard + +"@commitlint/config-conventional@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/config-conventional@npm:19.8.1" + dependencies: + "@commitlint/types": "npm:^19.8.1" + conventional-changelog-conventionalcommits: "npm:^7.0.2" + checksum: 10/aa61837baaf49e8ccf5eb8fa1bd78656abbea58edb73dacddc64c8915f1d28b27590005d66664c88b9a28a57e9a03ff11cf3b1d913af1ea4e86f3b66678ce630 + languageName: node + linkType: hard + +"@commitlint/config-validator@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/config-validator@npm:19.8.1" + dependencies: + "@commitlint/types": "npm:^19.8.1" + ajv: "npm:^8.11.0" + checksum: 10/26eee15c1c0564fc8857b4bbc4f06305a32e049a724ede73753f66fc15316eb79a5dde4c8e2765bd75952a27b138cd80cffc49491281f122b834f8467c658d80 + languageName: node + linkType: hard + +"@commitlint/ensure@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/ensure@npm:19.8.1" + dependencies: + "@commitlint/types": "npm:^19.8.1" + lodash.camelcase: "npm:^4.3.0" + lodash.kebabcase: "npm:^4.1.1" + lodash.snakecase: "npm:^4.1.1" + lodash.startcase: "npm:^4.4.0" + lodash.upperfirst: "npm:^4.3.1" + checksum: 10/af342f61b246c301937cc03477c64b86ca6dea47de23f94d237181d346d020ec23c8a458f56aec8bfe9cdcb80a06adcc34964f32c05a2649282a959ce6fae39d + languageName: node + linkType: hard + +"@commitlint/execute-rule@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/execute-rule@npm:19.8.1" + checksum: 10/a39d9a87c0962c290e4f7d7438e8fca7642384a5aa97ec84c0b3dbbf91dc048496dd25447ba3dbec37b00006eec1951f8f22f30a98448e90e22d44d585d8a68f + languageName: node + linkType: hard + +"@commitlint/format@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/format@npm:19.8.1" + dependencies: + "@commitlint/types": "npm:^19.8.1" + chalk: "npm:^5.3.0" + checksum: 10/5af80e489c1470e20519780867145492c145690bd8e6b0dc049f53d317b045fa39ba012faed2715307e105ca439e6b16bdd4fe9c39c146d38bb5d93f1542fc5f + languageName: node + linkType: hard + +"@commitlint/is-ignored@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/is-ignored@npm:19.8.1" + dependencies: + "@commitlint/types": "npm:^19.8.1" + semver: "npm:^7.6.0" + checksum: 10/a70631bb7825ed49f2d6164c7547d025ca184a5e65eb7b1bd63f041ae7aa9189991c2dbef18b1160951aeb59595307b75d5ba151ea10e0de4d36f22709b9c877 + languageName: node + linkType: hard + +"@commitlint/lint@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/lint@npm:19.8.1" + dependencies: + "@commitlint/is-ignored": "npm:^19.8.1" + "@commitlint/parse": "npm:^19.8.1" + "@commitlint/rules": "npm:^19.8.1" + "@commitlint/types": "npm:^19.8.1" + checksum: 10/6206236649b2214c9c81d76e00bb9d010fc5be6c990d14e473a01253e178ad3a4e081b5c458154314291805de843eebfdd35d720d2eff889f86f43269c9d392a + languageName: node + linkType: hard + +"@commitlint/load@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/load@npm:19.8.1" + dependencies: + "@commitlint/config-validator": "npm:^19.8.1" + "@commitlint/execute-rule": "npm:^19.8.1" + "@commitlint/resolve-extends": "npm:^19.8.1" + "@commitlint/types": "npm:^19.8.1" + chalk: "npm:^5.3.0" + cosmiconfig: "npm:^9.0.0" + cosmiconfig-typescript-loader: "npm:^6.1.0" + lodash.isplainobject: "npm:^4.0.6" + lodash.merge: "npm:^4.6.2" + lodash.uniq: "npm:^4.5.0" + checksum: 10/e78c997ef529f80f8b62f686e553d0f2cb33d88b8b907d2e3890195851cd783fd44bd780addaa49f1cceb12ed073c10bb10e11dc082f51e4fdc54640f5ac1cca + languageName: node + linkType: hard + +"@commitlint/message@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/message@npm:19.8.1" + checksum: 10/e365590dd539fe2519a15bd99ee8499c3ffbd80852839783ae6fd0b65feef08b26d2134a4e9ea32e006c2b3aa04447a38b011e73975b4fc3d7c7380a0fbf2377 + languageName: node + linkType: hard + +"@commitlint/parse@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/parse@npm:19.8.1" + dependencies: + "@commitlint/types": "npm:^19.8.1" + conventional-changelog-angular: "npm:^7.0.0" + conventional-commits-parser: "npm:^5.0.0" + checksum: 10/f6264bb30399b420a875532905e18049b4ab6f24d79f42d20fa06e64b9f355649ac18a33874e02643f0a826f3cec69423d6bc96cf852fa692338603ce910a95f + languageName: node + linkType: hard + +"@commitlint/read@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/read@npm:19.8.1" + dependencies: + "@commitlint/top-level": "npm:^19.8.1" + "@commitlint/types": "npm:^19.8.1" + git-raw-commits: "npm:^4.0.0" + minimist: "npm:^1.2.8" + tinyexec: "npm:^1.0.0" + checksum: 10/ee0f42e2e5a3ade673b2d14f3b2056a86804afe7d09b6703d51b41edc099b33b9c09dc715b30d7113879999381a198d78b4fcbc649785ed3beb9c3f7d1dc2bb2 + languageName: node + linkType: hard + +"@commitlint/resolve-extends@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/resolve-extends@npm:19.8.1" + dependencies: + "@commitlint/config-validator": "npm:^19.8.1" + "@commitlint/types": "npm:^19.8.1" + global-directory: "npm:^4.0.1" + import-meta-resolve: "npm:^4.0.0" + lodash.mergewith: "npm:^4.6.2" + resolve-from: "npm:^5.0.0" + checksum: 10/736e62f5fe819337a95de8ac50b65b04bdd472a652ebe18ac3a92efc3428d62dcf16d9c62b222ef2e8a7e2e8737bd49d13b9c9d3b061a588869a42acdc620bf0 + languageName: node + linkType: hard + +"@commitlint/rules@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/rules@npm:19.8.1" + dependencies: + "@commitlint/ensure": "npm:^19.8.1" + "@commitlint/message": "npm:^19.8.1" + "@commitlint/to-lines": "npm:^19.8.1" + "@commitlint/types": "npm:^19.8.1" + checksum: 10/dc3a90b4561369991b851224c5cc1c0e2297c68ce148e21a7a5893a0556fffced192d59bf491a6c80270da012840fafdb34d991b7048170f4b2e7b0122211cee + languageName: node + linkType: hard + +"@commitlint/to-lines@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/to-lines@npm:19.8.1" + checksum: 10/47f33d5e0d77aa0cc2fc14daa3e73661c64c9cffb5fc9c723714ced4fcfc758bf5ba2e084143fa55bc512ad896d115b9983a308a97a005200484f04f2ed0fd90 + languageName: node + linkType: hard + +"@commitlint/top-level@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/top-level@npm:19.8.1" + dependencies: + find-up: "npm:^7.0.0" + checksum: 10/c875b6c1be495675c77d86e80419d27fd5eb70fc061ef412d041541219c3222d9c4dbd6f0353247d49e9b2cd6d86a7ffc9df1ba20f96c77726c1f9a0edeeb8fe + languageName: node + linkType: hard + +"@commitlint/types@npm:^19.8.1": + version: 19.8.1 + resolution: "@commitlint/types@npm:19.8.1" + dependencies: + "@types/conventional-commits-parser": "npm:^5.0.0" + chalk: "npm:^5.3.0" + checksum: 10/d1943a5789a02c75b0c72755673ab8d50c850b025abb7806b7eef83b373591948f5d1d9cd22022f89302a256546934d797445913c5c495d8e92711cf17b0fbf0 + languageName: node + linkType: hard + "@config-plugins/react-native-callkeep@npm:^12.0.0": version: 12.0.0 resolution: "@config-plugins/react-native-callkeep@npm:12.0.0" @@ -1687,6 +1931,25 @@ __metadata: languageName: node linkType: hard +"@conventional-changelog/git-client@npm:^2.5.1": + version: 2.5.1 + resolution: "@conventional-changelog/git-client@npm:2.5.1" + dependencies: + "@simple-libs/child-process-utils": "npm:^1.0.0" + "@simple-libs/stream-utils": "npm:^1.1.0" + semver: "npm:^7.5.2" + peerDependencies: + conventional-commits-filter: ^5.0.0 + conventional-commits-parser: ^6.1.0 + peerDependenciesMeta: + conventional-commits-filter: + optional: true + conventional-commits-parser: + optional: true + checksum: 10/39ed505f8b93529ae278f71a75a67c52be54b20a374c7a209918e6c0374e3a4bebd8f8ca7497cbc28c8c062a3923890f54fe8077bca5424c9c42fb7fb4daf71f + languageName: node + linkType: hard + "@egjs/hammerjs@npm:^2.0.17": version: 2.0.17 resolution: "@egjs/hammerjs@npm:2.0.17" @@ -2058,7 +2321,7 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.1.2, @eslint-community/eslint-utils@npm:^4.5.0, @eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0": +"@eslint-community/eslint-utils@npm:^4.1.2, @eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0, @eslint-community/eslint-utils@npm:^4.5.0, @eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0": version: 4.9.0 resolution: "@eslint-community/eslint-utils@npm:4.9.0" dependencies: @@ -2076,6 +2339,20 @@ __metadata: languageName: node linkType: hard +"@eslint/compat@npm:^1.3.2": + version: 1.4.1 + resolution: "@eslint/compat@npm:1.4.1" + dependencies: + "@eslint/core": "npm:^0.17.0" + peerDependencies: + eslint: ^8.40 || 9 + peerDependenciesMeta: + eslint: + optional: true + checksum: 10/2345ba0991aaf57f79feed0417eac61fd0e09fb1d2f5bc3f723d5790a4f0881cca16b7a48c82555ab907a3469dce7d3cb43cc5e5100c22e2a369a561f4b421cd + languageName: node + linkType: hard + "@eslint/config-array@npm:^0.21.0": version: 0.21.0 resolution: "@eslint/config-array@npm:0.21.0" @@ -2087,6 +2364,17 @@ __metadata: languageName: node linkType: hard +"@eslint/config-array@npm:^0.21.1": + version: 0.21.1 + resolution: "@eslint/config-array@npm:0.21.1" + dependencies: + "@eslint/object-schema": "npm:^2.1.7" + debug: "npm:^4.3.1" + minimatch: "npm:^3.1.2" + checksum: 10/6eaa0435972f735ce52d581f355a0b616e50a9b8a73304a7015398096e252798b9b3b968a67b524eefb0fdeacc57c4d960f0ec6432abe1c1e24be815b88c5d18 + languageName: node + linkType: hard + "@eslint/config-helpers@npm:^0.4.0": version: 0.4.0 resolution: "@eslint/config-helpers@npm:0.4.0" @@ -2096,6 +2384,15 @@ __metadata: languageName: node linkType: hard +"@eslint/config-helpers@npm:^0.4.2": + version: 0.4.2 + resolution: "@eslint/config-helpers@npm:0.4.2" + dependencies: + "@eslint/core": "npm:^0.17.0" + checksum: 10/3f2b4712d8e391c36ec98bc200f7dea423dfe518e42956569666831b89ede83b33120c761dfd3ab6347d8e8894a6d4af47254a18d464a71c6046fd88065f6daf + languageName: node + linkType: hard + "@eslint/core@npm:^0.16.0": version: 0.16.0 resolution: "@eslint/core@npm:0.16.0" @@ -2105,6 +2402,15 @@ __metadata: languageName: node linkType: hard +"@eslint/core@npm:^0.17.0": + version: 0.17.0 + resolution: "@eslint/core@npm:0.17.0" + dependencies: + "@types/json-schema": "npm:^7.0.15" + checksum: 10/f9a428cc651ec15fb60d7d60c2a7bacad4666e12508320eafa98258e976fafaa77d7be7be91519e75f801f15f830105420b14a458d4aab121a2b0a59bc43517b + languageName: node + linkType: hard + "@eslint/eslintrc@npm:^3.3.1": version: 3.3.1 resolution: "@eslint/eslintrc@npm:3.3.1" @@ -2129,6 +2435,13 @@ __metadata: languageName: node linkType: hard +"@eslint/js@npm:9.39.1, @eslint/js@npm:^9.35.0": + version: 9.39.1 + resolution: "@eslint/js@npm:9.39.1" + checksum: 10/b10b9b953212c0f3ffca475159bbe519e9e98847200c7432d1637d444fddcd7b712d2b7710a7dc20510f9cfbe8db330039b2aad09cb55d9545b116d940dbeed2 + languageName: node + linkType: hard + "@eslint/object-schema@npm:^2.1.6": version: 2.1.6 resolution: "@eslint/object-schema@npm:2.1.6" @@ -2136,6 +2449,13 @@ __metadata: languageName: node linkType: hard +"@eslint/object-schema@npm:^2.1.7": + version: 2.1.7 + resolution: "@eslint/object-schema@npm:2.1.7" + checksum: 10/946ef5d6235b4d1c0907c6c6e6429c8895f535380c562b7705c131f63f2e961b06e8785043c86a293da48e0a60c6286d98ba395b8b32ea55561fe6e4417cb7e4 + languageName: node + linkType: hard + "@eslint/plugin-kit@npm:^0.4.0": version: 0.4.0 resolution: "@eslint/plugin-kit@npm:0.4.0" @@ -2146,6 +2466,16 @@ __metadata: languageName: node linkType: hard +"@eslint/plugin-kit@npm:^0.4.1": + version: 0.4.1 + resolution: "@eslint/plugin-kit@npm:0.4.1" + dependencies: + "@eslint/core": "npm:^0.17.0" + levn: "npm:^0.4.1" + checksum: 10/c5947d0ffeddca77d996ac1b886a66060c1a15ed1d5e425d0c7e7d7044a4bd3813fc968892d03950a7831c9b89368a2f7b281e45dd3c74a048962b74bf3a1cb4 + languageName: node + linkType: hard + "@expo/cli@npm:54.0.10": version: 54.0.10 resolution: "@expo/cli@npm:54.0.10" @@ -3553,95 +3883,342 @@ __metadata: languageName: node linkType: hard -"@inquirer/external-editor@npm:^1.0.0": +"@inquirer/ansi@npm:^1.0.0, @inquirer/ansi@npm:^1.0.2": version: 1.0.2 - resolution: "@inquirer/external-editor@npm:1.0.2" + resolution: "@inquirer/ansi@npm:1.0.2" + checksum: 10/d1496e573a63ee6752bcf3fc93375cdabc55b0d60f0588fe7902282c710b223252ad318ff600ee904e48555634663b53fda517f5b29ce9fbda90bfae18592fbc + languageName: node + linkType: hard + +"@inquirer/checkbox@npm:^4.3.2": + version: 4.3.2 + resolution: "@inquirer/checkbox@npm:4.3.2" dependencies: - chardet: "npm:^2.1.0" - iconv-lite: "npm:^0.7.0" + "@inquirer/ansi": "npm:^1.0.2" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/figures": "npm:^1.0.15" + "@inquirer/type": "npm:^3.0.10" + yoctocolors-cjs: "npm:^2.1.3" peerDependencies: "@types/node": ">=18" peerDependenciesMeta: "@types/node": optional: true - checksum: 10/d0c5c73249b8153f4cf872c4fba01c57a7653142a4cad496f17ed03ef3769330a4b3c519b68d70af69d4bb33003d2599b66b2242be85411c0b027ff383619666 + checksum: 10/4ac5dd2679981e23f066c51c605cb1c63ccda9ea6e1ad895e675eb26702aaf6cf961bf5ca3acd832efba5edcf9883b6742002c801673d2b35c123a7fa7db7b23 languageName: node linkType: hard -"@isaacs/balanced-match@npm:^4.0.1": - version: 4.0.1 - resolution: "@isaacs/balanced-match@npm:4.0.1" - checksum: 10/102fbc6d2c0d5edf8f6dbf2b3feb21695a21bc850f11bc47c4f06aa83bd8884fde3fe9d6d797d619901d96865fdcb4569ac2a54c937992c48885c5e3d9967fe8 +"@inquirer/confirm@npm:^5.1.21": + version: 5.1.21 + resolution: "@inquirer/confirm@npm:5.1.21" + dependencies: + "@inquirer/core": "npm:^10.3.2" + "@inquirer/type": "npm:^3.0.10" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/a107aa0073965ea510affb9e5b55baf40333503d600970c458c07770cd4e0eee01efc4caba66f0409b0fadc9550d127329622efb543cffcabff3ad0e7f865372 languageName: node linkType: hard -"@isaacs/brace-expansion@npm:^5.0.0": - version: 5.0.0 - resolution: "@isaacs/brace-expansion@npm:5.0.0" +"@inquirer/core@npm:^10.2.2, @inquirer/core@npm:^10.3.2": + version: 10.3.2 + resolution: "@inquirer/core@npm:10.3.2" dependencies: - "@isaacs/balanced-match": "npm:^4.0.1" - checksum: 10/cf3b7f206aff12128214a1df764ac8cdbc517c110db85249b945282407e3dfc5c6e66286383a7c9391a059fc8e6e6a8ca82262fc9d2590bd615376141fbebd2d + "@inquirer/ansi": "npm:^1.0.2" + "@inquirer/figures": "npm:^1.0.15" + "@inquirer/type": "npm:^3.0.10" + cli-width: "npm:^4.1.0" + mute-stream: "npm:^2.0.0" + signal-exit: "npm:^4.1.0" + wrap-ansi: "npm:^6.2.0" + yoctocolors-cjs: "npm:^2.1.3" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/eb434bdf0ae7d904367003c772bcd80cbf679f79c087c99a4949fd7288e9a2f713ec3ea63381b9a001f52389ab56a77fcd88d64d81a03b1195193410ce8971c2 languageName: node linkType: hard -"@isaacs/cliui@npm:^8.0.2": - version: 8.0.2 - resolution: "@isaacs/cliui@npm:8.0.2" +"@inquirer/editor@npm:^4.2.23": + version: 4.2.23 + resolution: "@inquirer/editor@npm:4.2.23" dependencies: - string-width: "npm:^5.1.2" - string-width-cjs: "npm:string-width@^4.2.0" - strip-ansi: "npm:^7.0.1" - strip-ansi-cjs: "npm:strip-ansi@^6.0.1" - wrap-ansi: "npm:^8.1.0" - wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" - checksum: 10/e9ed5fd27c3aec1095e3a16e0c0cf148d1fee55a38665c35f7b3f86a9b5d00d042ddaabc98e8a1cb7463b9378c15f22a94eb35e99469c201453eb8375191f243 + "@inquirer/core": "npm:^10.3.2" + "@inquirer/external-editor": "npm:^1.0.3" + "@inquirer/type": "npm:^3.0.10" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/f91b9aadba6ea28a0f4ea5f075af421e076262aebbd737e1b9779f086fa9d559d064e9942a581544645d1dcf56d6b685e8063fe46677880fbca73f6de4e4e7c5 languageName: node linkType: hard -"@isaacs/fs-minipass@npm:^4.0.0": - version: 4.0.1 - resolution: "@isaacs/fs-minipass@npm:4.0.1" +"@inquirer/expand@npm:^4.0.23": + version: 4.0.23 + resolution: "@inquirer/expand@npm:4.0.23" dependencies: - minipass: "npm:^7.0.4" - checksum: 10/4412e9e6713c89c1e66d80bb0bb5a2a93192f10477623a27d08f228ba0316bb880affabc5bfe7f838f58a34d26c2c190da726e576cdfc18c49a72e89adabdcf5 + "@inquirer/core": "npm:^10.3.2" + "@inquirer/type": "npm:^3.0.10" + yoctocolors-cjs: "npm:^2.1.3" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/73ad1d6376e5efe2a452c33494d6d16ee2670c638ae470a795fdff4acb59a8e032e38e141f87b603b6e96320977519b375dac6471d86d5e3087a9c1db40e3111 languageName: node linkType: hard -"@isaacs/ttlcache@npm:^1.4.1": - version: 1.4.1 - resolution: "@isaacs/ttlcache@npm:1.4.1" - checksum: 10/57f2b00b58845d48a173c7668c58c27c3e6f91a56c17d6d4c58b38780a475a858ce3b4fc2cd4304469eee9f49818b79a187f0e13120b3617c4f67e4abc475698 +"@inquirer/external-editor@npm:^1.0.0": + version: 1.0.2 + resolution: "@inquirer/external-editor@npm:1.0.2" + dependencies: + chardet: "npm:^2.1.0" + iconv-lite: "npm:^0.7.0" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/d0c5c73249b8153f4cf872c4fba01c57a7653142a4cad496f17ed03ef3769330a4b3c519b68d70af69d4bb33003d2599b66b2242be85411c0b027ff383619666 languageName: node linkType: hard -"@istanbuljs/load-nyc-config@npm:^1.0.0": - version: 1.1.0 - resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" +"@inquirer/external-editor@npm:^1.0.3": + version: 1.0.3 + resolution: "@inquirer/external-editor@npm:1.0.3" dependencies: - camelcase: "npm:^5.3.1" - find-up: "npm:^4.1.0" - get-package-type: "npm:^0.1.0" - js-yaml: "npm:^3.13.1" - resolve-from: "npm:^5.0.0" - checksum: 10/b000a5acd8d4fe6e34e25c399c8bdbb5d3a202b4e10416e17bfc25e12bab90bb56d33db6089ae30569b52686f4b35ff28ef26e88e21e69821d2b85884bd055b8 + chardet: "npm:^2.1.1" + iconv-lite: "npm:^0.7.0" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/c95d7237a885b32031715089f92820525731d4d3c2bd7afdb826307dc296cc2b39e7a644b0bb265441963348cca42e7785feb29c3aaf18fd2b63131769bf6587 languageName: node linkType: hard -"@istanbuljs/schema@npm:^0.1.2": - version: 0.1.3 - resolution: "@istanbuljs/schema@npm:0.1.3" - checksum: 10/a9b1e49acdf5efc2f5b2359f2df7f90c5c725f2656f16099e8b2cd3a000619ecca9fc48cf693ba789cf0fd989f6e0df6a22bc05574be4223ecdbb7997d04384b +"@inquirer/figures@npm:^1.0.15": + version: 1.0.15 + resolution: "@inquirer/figures@npm:1.0.15" + checksum: 10/3f858807f361ca29f41ec1076bbece4098cc140d86a06159d42c6e3f6e4d9bec9e10871ccfcbbaa367d6a8462b01dff89f2b1b157d9de6e8726bec85533f525c languageName: node linkType: hard -"@jest/console@npm:^29.7.0": - version: 29.7.0 - resolution: "@jest/console@npm:29.7.0" +"@inquirer/input@npm:^4.3.1": + version: 4.3.1 + resolution: "@inquirer/input@npm:4.3.1" dependencies: - "@jest/types": "npm:^29.6.3" - "@types/node": "npm:*" - chalk: "npm:^4.0.0" - jest-message-util: "npm:^29.7.0" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/type": "npm:^3.0.10" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/713aaa4c94263299fbd7adfd65378f788cac1b5047f2b7e1ea349ca669db6c7c91b69ab6e2f6660cdbc28c7f7888c5c77ab4433bd149931597e43976d1ba5f34 + languageName: node + linkType: hard + +"@inquirer/number@npm:^3.0.23": + version: 3.0.23 + resolution: "@inquirer/number@npm:3.0.23" + dependencies: + "@inquirer/core": "npm:^10.3.2" + "@inquirer/type": "npm:^3.0.10" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/50694807b71746e15ed69d100aae3c8014d83c90aa660e8a179fe0db1046f26d727947542f64e24cc8b969a61659cb89fe36208cc2b59c1816382b598e686dd2 + languageName: node + linkType: hard + +"@inquirer/password@npm:^4.0.23": + version: 4.0.23 + resolution: "@inquirer/password@npm:4.0.23" + dependencies: + "@inquirer/ansi": "npm:^1.0.2" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/type": "npm:^3.0.10" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/97364970b01c85946a4a50ad876c53ef0c1857a9144e24fad65e5dfa4b4e5dd42564fbcdfa2b49bb049a25d127efbe0882cb18afcdd47b166ebd01c6c4b5e825 + languageName: node + linkType: hard + +"@inquirer/prompts@npm:^7.8.6": + version: 7.10.1 + resolution: "@inquirer/prompts@npm:7.10.1" + dependencies: + "@inquirer/checkbox": "npm:^4.3.2" + "@inquirer/confirm": "npm:^5.1.21" + "@inquirer/editor": "npm:^4.2.23" + "@inquirer/expand": "npm:^4.0.23" + "@inquirer/input": "npm:^4.3.1" + "@inquirer/number": "npm:^3.0.23" + "@inquirer/password": "npm:^4.0.23" + "@inquirer/rawlist": "npm:^4.1.11" + "@inquirer/search": "npm:^3.2.2" + "@inquirer/select": "npm:^4.4.2" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/b3e3386edd255e4e91c7908050674f8a2e69b043883c00feec2f87d697be37bc6e8cd4a360e7e3233a9825ae7ea044a2ac63d5700926d27f9959013d8566f890 + languageName: node + linkType: hard + +"@inquirer/rawlist@npm:^4.1.11": + version: 4.1.11 + resolution: "@inquirer/rawlist@npm:4.1.11" + dependencies: + "@inquirer/core": "npm:^10.3.2" + "@inquirer/type": "npm:^3.0.10" + yoctocolors-cjs: "npm:^2.1.3" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/0d8f6484cfc20749190e95eecfb2d034bafb3644ec4907b84b1673646f5dd71730e38e35565ea98dfd240d8851e3cff653edafcc4e0af617054b127b407e3229 + languageName: node + linkType: hard + +"@inquirer/search@npm:^3.2.2": + version: 3.2.2 + resolution: "@inquirer/search@npm:3.2.2" + dependencies: + "@inquirer/core": "npm:^10.3.2" + "@inquirer/figures": "npm:^1.0.15" + "@inquirer/type": "npm:^3.0.10" + yoctocolors-cjs: "npm:^2.1.3" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/abaed2df7763633ff4414b58d1c87233b69ed3cd2ac77629f0d54b72b8b585dc4806c7a2a8261daba58af5b0a2147e586d079fdc82060b6bcf56b75d3d03f3a7 + languageName: node + linkType: hard + +"@inquirer/select@npm:^4.4.2": + version: 4.4.2 + resolution: "@inquirer/select@npm:4.4.2" + dependencies: + "@inquirer/ansi": "npm:^1.0.2" + "@inquirer/core": "npm:^10.3.2" + "@inquirer/figures": "npm:^1.0.15" + "@inquirer/type": "npm:^3.0.10" + yoctocolors-cjs: "npm:^2.1.3" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/795ec0ac77d575f20bd6a12fb1c040093e62217ac0c80194829a8d3c3d1e09f70ad738e9a9dd6095cc8358fff4e13882209c09bdf8eb0864a86dcabef5b0a6a6 + languageName: node + linkType: hard + +"@inquirer/type@npm:^3.0.10, @inquirer/type@npm:^3.0.8": + version: 3.0.10 + resolution: "@inquirer/type@npm:3.0.10" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/57d113a9db7abc73326491e29bedc88ef362e53779f9f58a1b61225e0be068ce0c54e33cd65f4a13ca46131676fb72c3ef488463c4c9af0aa89680684c55d74c + languageName: node + linkType: hard + +"@isaacs/balanced-match@npm:^4.0.1": + version: 4.0.1 + resolution: "@isaacs/balanced-match@npm:4.0.1" + checksum: 10/102fbc6d2c0d5edf8f6dbf2b3feb21695a21bc850f11bc47c4f06aa83bd8884fde3fe9d6d797d619901d96865fdcb4569ac2a54c937992c48885c5e3d9967fe8 + languageName: node + linkType: hard + +"@isaacs/brace-expansion@npm:^5.0.0": + version: 5.0.0 + resolution: "@isaacs/brace-expansion@npm:5.0.0" + dependencies: + "@isaacs/balanced-match": "npm:^4.0.1" + checksum: 10/cf3b7f206aff12128214a1df764ac8cdbc517c110db85249b945282407e3dfc5c6e66286383a7c9391a059fc8e6e6a8ca82262fc9d2590bd615376141fbebd2d + languageName: node + linkType: hard + +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: "npm:^5.1.2" + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: "npm:^7.0.1" + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: "npm:^8.1.0" + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: 10/e9ed5fd27c3aec1095e3a16e0c0cf148d1fee55a38665c35f7b3f86a9b5d00d042ddaabc98e8a1cb7463b9378c15f22a94eb35e99469c201453eb8375191f243 + languageName: node + linkType: hard + +"@isaacs/fs-minipass@npm:^4.0.0": + version: 4.0.1 + resolution: "@isaacs/fs-minipass@npm:4.0.1" + dependencies: + minipass: "npm:^7.0.4" + checksum: 10/4412e9e6713c89c1e66d80bb0bb5a2a93192f10477623a27d08f228ba0316bb880affabc5bfe7f838f58a34d26c2c190da726e576cdfc18c49a72e89adabdcf5 + languageName: node + linkType: hard + +"@isaacs/ttlcache@npm:^1.4.1": + version: 1.4.1 + resolution: "@isaacs/ttlcache@npm:1.4.1" + checksum: 10/57f2b00b58845d48a173c7668c58c27c3e6f91a56c17d6d4c58b38780a475a858ce3b4fc2cd4304469eee9f49818b79a187f0e13120b3617c4f67e4abc475698 + languageName: node + linkType: hard + +"@istanbuljs/load-nyc-config@npm:^1.0.0": + version: 1.1.0 + resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" + dependencies: + camelcase: "npm:^5.3.1" + find-up: "npm:^4.1.0" + get-package-type: "npm:^0.1.0" + js-yaml: "npm:^3.13.1" + resolve-from: "npm:^5.0.0" + checksum: 10/b000a5acd8d4fe6e34e25c399c8bdbb5d3a202b4e10416e17bfc25e12bab90bb56d33db6089ae30569b52686f4b35ff28ef26e88e21e69821d2b85884bd055b8 + languageName: node + linkType: hard + +"@istanbuljs/schema@npm:^0.1.2": + version: 0.1.3 + resolution: "@istanbuljs/schema@npm:0.1.3" + checksum: 10/a9b1e49acdf5efc2f5b2359f2df7f90c5c725f2656f16099e8b2cd3a000619ecca9fc48cf693ba789cf0fd989f6e0df6a22bc05574be4223ecdbb7997d04384b + languageName: node + linkType: hard + +"@jest/console@npm:^29.7.0": + version: 29.7.0 + resolution: "@jest/console@npm:29.7.0" + dependencies: + "@jest/types": "npm:^29.6.3" + "@types/node": "npm:*" + chalk: "npm:^4.0.0" + jest-message-util: "npm:^29.7.0" jest-util: "npm:^29.7.0" slash: "npm:^3.0.0" checksum: 10/4a80c750e8a31f344233cb9951dee9b77bf6b89377cb131f8b3cde07ff218f504370133a5963f6a786af4d2ce7f85642db206ff7a15f99fe58df4c38ac04899e @@ -4375,6 +4952,15 @@ __metadata: languageName: node linkType: hard +"@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1": + version: 5.1.1-v1 + resolution: "@nicolo-ribaudo/eslint-scope-5-internals@npm:5.1.1-v1" + dependencies: + eslint-scope: "npm:5.1.1" + checksum: 10/f2e3b2d6a6e2d9f163ca22105910c9f850dc4897af0aea3ef0a5886b63d8e1ba6505b71c99cb78a3bba24a09557d601eb21c8dede3f3213753fcfef364eb0e57 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -4402,6 +4988,15 @@ __metadata: languageName: node linkType: hard +"@nodeutils/defaults-deep@npm:1.1.0": + version: 1.1.0 + resolution: "@nodeutils/defaults-deep@npm:1.1.0" + dependencies: + lodash: "npm:^4.15.0" + checksum: 10/4651c6e2179b0207f1a848f1e13eff7f2c24e7b6e52c965a8a5ef5140aea5df1fdafbf714026559e4f1f5828d3723eb35fe1b18dc2c7b0ec2265dc70b577bc34 + languageName: node + linkType: hard + "@notifee/react-native@npm:9.1.8": version: 9.1.8 resolution: "@notifee/react-native@npm:9.1.8" @@ -4542,6 +5137,146 @@ __metadata: languageName: node linkType: hard +"@octokit/auth-token@npm:^6.0.0": + version: 6.0.0 + resolution: "@octokit/auth-token@npm:6.0.0" + checksum: 10/a30f5c4c984964b57193de5b6f67169f74e4779fedbe716157dd3558dd9de3ca5c105cae521b7bd8ce1ae180773a2ef01afe2306ad5a329f4fd291eba2b7c7d1 + languageName: node + linkType: hard + +"@octokit/core@npm:^7.0.2": + version: 7.0.6 + resolution: "@octokit/core@npm:7.0.6" + dependencies: + "@octokit/auth-token": "npm:^6.0.0" + "@octokit/graphql": "npm:^9.0.3" + "@octokit/request": "npm:^10.0.6" + "@octokit/request-error": "npm:^7.0.2" + "@octokit/types": "npm:^16.0.0" + before-after-hook: "npm:^4.0.0" + universal-user-agent: "npm:^7.0.0" + checksum: 10/852d41fc3150d2a891156427dd0575c77889f1c7a109894ee541594e3fd47c0d4e0a93fee22966c507dfd6158b522e42846c2ac46b9d896078194c95fa81f4ae + languageName: node + linkType: hard + +"@octokit/endpoint@npm:^11.0.2": + version: 11.0.2 + resolution: "@octokit/endpoint@npm:11.0.2" + dependencies: + "@octokit/types": "npm:^16.0.0" + universal-user-agent: "npm:^7.0.2" + checksum: 10/0d088747baf94eafbba69da23ba840b40cd3f5d0bfbc51c692ff9d9d78de6d81f06366e6e30df8c1783355be826c27d38ab9ab0708396af8f430b06cfa29db35 + languageName: node + linkType: hard + +"@octokit/graphql@npm:^9.0.3": + version: 9.0.3 + resolution: "@octokit/graphql@npm:9.0.3" + dependencies: + "@octokit/request": "npm:^10.0.6" + "@octokit/types": "npm:^16.0.0" + universal-user-agent: "npm:^7.0.0" + checksum: 10/7b16f281f8571dce55280b3986fbb8d15465a7236164a5f6497ded7597ff9ee95d5796924555b979903fe8c6706fe6be1b3e140d807297f85ac8edeadc28f9fe + languageName: node + linkType: hard + +"@octokit/openapi-types@npm:^26.0.0": + version: 26.0.0 + resolution: "@octokit/openapi-types@npm:26.0.0" + checksum: 10/b9e1b1230b0a3d280b48902a927ce4e7df0d51096c928e2ee929035b0bce779fe7748a1ae58696f1c3080bf8338b6388d5caba5b0dbf254e9713303ed3abf7c2 + languageName: node + linkType: hard + +"@octokit/openapi-types@npm:^27.0.0": + version: 27.0.0 + resolution: "@octokit/openapi-types@npm:27.0.0" + checksum: 10/5cd2cdf4e41fdf522e15e3d53f3ece8380d98dda9173a6fc905828fb2c33e8733d5f5d2a757ae3a572525f4749748e66cb40e7939372132988d8eb4ba978d54f + languageName: node + linkType: hard + +"@octokit/plugin-paginate-rest@npm:^13.0.1": + version: 13.2.1 + resolution: "@octokit/plugin-paginate-rest@npm:13.2.1" + dependencies: + "@octokit/types": "npm:^15.0.1" + peerDependencies: + "@octokit/core": ">=6" + checksum: 10/72ad8822594435e766acb968dcdfc3f15779aab9721c51febf078450805cd30c98f5dcaa397f9b20c166b2b75cb1a3b2e3da0354a770fdb502569ef90225e9b1 + languageName: node + linkType: hard + +"@octokit/plugin-request-log@npm:^6.0.0": + version: 6.0.0 + resolution: "@octokit/plugin-request-log@npm:6.0.0" + peerDependencies: + "@octokit/core": ">=6" + checksum: 10/8a79973b1429bfead9113c4117f418aaef5ff368795daded3415ba14623d97d5fc08d1e822dbd566ecc9f041119e1a48a11853a9c48d9eb1caa62baa79c17f83 + languageName: node + linkType: hard + +"@octokit/plugin-rest-endpoint-methods@npm:^16.0.0": + version: 16.1.1 + resolution: "@octokit/plugin-rest-endpoint-methods@npm:16.1.1" + dependencies: + "@octokit/types": "npm:^15.0.1" + peerDependencies: + "@octokit/core": ">=6" + checksum: 10/230ad8bd9300e7e4a5e1b1f2c5ebf5f14e0a7e508bcf9355b926e5bcef4b481337ccd912cd3cd5a2498277326615d40805d35176801f7de59336683a3ef0434c + languageName: node + linkType: hard + +"@octokit/request-error@npm:^7.0.2": + version: 7.1.0 + resolution: "@octokit/request-error@npm:7.1.0" + dependencies: + "@octokit/types": "npm:^16.0.0" + checksum: 10/c1d447ff7482382c69f7a4b2eaa44c672906dd111d8a9196a5d07f2adc4ae0f0e12ec4ce0063f14f9b2fb5f0cef4451c95ec961a7a711bd900e5d6441d546570 + languageName: node + linkType: hard + +"@octokit/request@npm:^10.0.6": + version: 10.0.7 + resolution: "@octokit/request@npm:10.0.7" + dependencies: + "@octokit/endpoint": "npm:^11.0.2" + "@octokit/request-error": "npm:^7.0.2" + "@octokit/types": "npm:^16.0.0" + fast-content-type-parse: "npm:^3.0.0" + universal-user-agent: "npm:^7.0.2" + checksum: 10/eaf6d347340729b47d482b487411e3543384d1c07f9125c4e53c11ece53f22a0245b71be22dd48bd6ad16af48c7c323c7317da49a238206d07fb7cd3ef0c6e74 + languageName: node + linkType: hard + +"@octokit/rest@npm:22.0.0": + version: 22.0.0 + resolution: "@octokit/rest@npm:22.0.0" + dependencies: + "@octokit/core": "npm:^7.0.2" + "@octokit/plugin-paginate-rest": "npm:^13.0.1" + "@octokit/plugin-request-log": "npm:^6.0.0" + "@octokit/plugin-rest-endpoint-methods": "npm:^16.0.0" + checksum: 10/d2b80fefd6aed307cb728980cb1d94cb484d48fabf0055198664287a7fb50544d312b005e4fb8dec2a6e97a153ec0ad7654d62f59898e1077a4cfba64e6d5c3e + languageName: node + linkType: hard + +"@octokit/types@npm:^15.0.1": + version: 15.0.2 + resolution: "@octokit/types@npm:15.0.2" + dependencies: + "@octokit/openapi-types": "npm:^26.0.0" + checksum: 10/4f40a3eb65fab1370f8c988e6f8281265238fd1a4d69218eb7b496703c31c652aa27a834b134f6c8679f9029d6f547c61ad588a7a21e3d98fdc57448174ca9f8 + languageName: node + linkType: hard + +"@octokit/types@npm:^16.0.0": + version: 16.0.0 + resolution: "@octokit/types@npm:16.0.0" + dependencies: + "@octokit/openapi-types": "npm:^27.0.0" + checksum: 10/03d5cfc29556a9b53eae8beb1bf15c0b704dc722db2c51b53f093f3c3ee6c1d8e20b682be8117a3a17034b458be7746d1b22aaefb959ceb5152ad7589b39e2c9 + languageName: node + linkType: hard + "@openapitools/openapi-generator-cli@npm:^2.25.0": version: 2.25.0 resolution: "@openapitools/openapi-generator-cli@npm:2.25.0" @@ -5122,6 +5857,13 @@ __metadata: languageName: node linkType: hard +"@phun-ky/typeof@npm:2.0.3": + version: 2.0.3 + resolution: "@phun-ky/typeof@npm:2.0.3" + checksum: 10/ca7daa8e520ca3e947c2cd47e25ab1f299acc87c05d5750747665c531a70395b8e4e5e510347bebc98d89d6c65dbe16b8d6604da2327d832aa5abe91703d0598 + languageName: node + linkType: hard + "@pkgjs/parseargs@npm:^0.11.0": version: 0.11.0 resolution: "@pkgjs/parseargs@npm:0.11.0" @@ -5750,6 +6492,18 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-clean@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-clean@npm:20.0.1" + dependencies: + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + fast-glob: "npm:^3.3.2" + checksum: 10/4389266313a0fe2baf0423b4860117040d91920d29a6430f65f8ab62b779efb3e082fc4d6eefb36180ba3aff2c02b34f2dc2cbf632ad18df749608701c1d1f05 + languageName: node + linkType: hard + "@react-native-community/cli-config-android@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-config-android@npm:20.0.0" @@ -5762,6 +6516,18 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-config-android@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-config-android@npm:20.0.1" + dependencies: + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + fast-glob: "npm:^3.3.2" + fast-xml-parser: "npm:^4.4.1" + checksum: 10/6e3f146509ea3f18c2be9e1c626511d9128519aa7e99b584f576257124c33e9d41d51c6e53ea52c1fcf6a06f30eab9eceee4dff37c91f13b273e4b8c5e94e3fe + languageName: node + linkType: hard + "@react-native-community/cli-config-apple@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-config-apple@npm:20.0.0" @@ -5774,6 +6540,18 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-config-apple@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-config-apple@npm:20.0.1" + dependencies: + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + fast-glob: "npm:^3.3.2" + checksum: 10/2cab86e8a156b2a23b31e3064593271d233267a9c746c42075864d9c11bea8de35d0956bf7b01f3f446a299f07d60edbb11da4f3b056b5850a6b35159f37ad52 + languageName: node + linkType: hard + "@react-native-community/cli-config@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-config@npm:20.0.0" @@ -5788,6 +6566,20 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-config@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-config@npm:20.0.1" + dependencies: + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + cosmiconfig: "npm:^9.0.0" + deepmerge: "npm:^4.3.0" + fast-glob: "npm:^3.3.2" + joi: "npm:^17.2.1" + checksum: 10/aac2bb49079585532af854ee3e29b18eba6cc94dcfae831c492379659292a93555dcbf690af14fecadb1ae0f0f9d08a6649e51f6821d3d9baf5983bccdb421da + languageName: node + linkType: hard + "@react-native-community/cli-doctor@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-doctor@npm:20.0.0" @@ -5811,6 +6603,29 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-doctor@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-doctor@npm:20.0.1" + dependencies: + "@react-native-community/cli-config": "npm:20.0.1" + "@react-native-community/cli-platform-android": "npm:20.0.1" + "@react-native-community/cli-platform-apple": "npm:20.0.1" + "@react-native-community/cli-platform-ios": "npm:20.0.1" + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + command-exists: "npm:^1.2.8" + deepmerge: "npm:^4.3.0" + envinfo: "npm:^7.13.0" + execa: "npm:^5.0.0" + node-stream-zip: "npm:^1.9.1" + ora: "npm:^5.4.1" + semver: "npm:^7.5.2" + wcwidth: "npm:^1.0.1" + yaml: "npm:^2.2.1" + checksum: 10/3f70f8a9c98f1b4b39ff7073628e595620e24520a744af4c6ca3c453cd3f6df3486132fa344b6a0c2d4ff130de65f38e83498a04b9a8e61b1d6a5d080c0c34bc + languageName: node + linkType: hard + "@react-native-community/cli-platform-android@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-platform-android@npm:20.0.0" @@ -5824,6 +6639,19 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-android@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-platform-android@npm:20.0.1" + dependencies: + "@react-native-community/cli-config-android": "npm:20.0.1" + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + logkitty: "npm:^0.7.1" + checksum: 10/d27eb40b7e7f76cc2f114a9b81600eae19b6bf08791c1e1a0feed6c602792a81e0a749e9e0e008e4eab5cf9b216d476b71de85cad03609343494a516902fb17f + languageName: node + linkType: hard + "@react-native-community/cli-platform-apple@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-platform-apple@npm:20.0.0" @@ -5837,6 +6665,19 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-apple@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-platform-apple@npm:20.0.1" + dependencies: + "@react-native-community/cli-config-apple": "npm:20.0.1" + "@react-native-community/cli-tools": "npm:20.0.1" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + fast-xml-parser: "npm:^4.4.1" + checksum: 10/3f1c2dbfbdf4b89a9403500fbe9c850e8fbe15e3f8869ac063dca2b181f7f21c1d99c984ec517ee682d0163f5347857ecd31ebfff52e63565583fb358850458e + languageName: node + linkType: hard + "@react-native-community/cli-platform-ios@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-platform-ios@npm:20.0.0" @@ -5846,6 +6687,15 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-platform-ios@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-platform-ios@npm:20.0.1" + dependencies: + "@react-native-community/cli-platform-apple": "npm:20.0.1" + checksum: 10/8abfd1872cecfa6a70790bac831fa3fea6e19e58e3ff9a70475d65b1c17af7426f4a97ea786cfe428dbf1cf2e64bdf1899b9b30eb00445311e54c78163b701b4 + languageName: node + linkType: hard + "@react-native-community/cli-server-api@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-server-api@npm:20.0.0" @@ -5864,6 +6714,24 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-server-api@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-server-api@npm:20.0.1" + dependencies: + "@react-native-community/cli-tools": "npm:20.0.1" + body-parser: "npm:^1.20.3" + compression: "npm:^1.7.1" + connect: "npm:^3.6.5" + errorhandler: "npm:^1.5.1" + nocache: "npm:^3.0.1" + open: "npm:^6.2.0" + pretty-format: "npm:^29.7.0" + serve-static: "npm:^1.13.1" + ws: "npm:^6.2.3" + checksum: 10/36bf723828ff03ba86f33489c4296e2d46ba94b04edc3f5c23b680232a68a33547ea3d8b1fc7e8012c105ec97f6704e04d38e412c8e1349ca20157c55ea92616 + languageName: node + linkType: hard + "@react-native-community/cli-tools@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-tools@npm:20.0.0" @@ -5882,6 +6750,24 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-tools@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-tools@npm:20.0.1" + dependencies: + "@vscode/sudo-prompt": "npm:^9.0.0" + appdirsjs: "npm:^1.2.4" + chalk: "npm:^4.1.2" + execa: "npm:^5.0.0" + find-up: "npm:^5.0.0" + launch-editor: "npm:^2.9.1" + mime: "npm:^2.4.1" + ora: "npm:^5.4.1" + prompts: "npm:^2.4.2" + semver: "npm:^7.5.2" + checksum: 10/dacb2007419c2428cf7a91aeace8cb342c24bee48e09b9bc77cbef4c5b3c6ff67a60e8934d96e046aa00e4da0e2d0c0e70c761c0ab19e06554bdca6d3939e79f + languageName: node + linkType: hard + "@react-native-community/cli-types@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli-types@npm:20.0.0" @@ -5891,6 +6777,15 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli-types@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli-types@npm:20.0.1" + dependencies: + joi: "npm:^17.2.1" + checksum: 10/8d5153b92b0b2ca409ae31eafa1ad1d5c332ead7a19b896e2be4003a3ae34aac6d999825f1281966e4ed5f88e0479f0b6147d6fb5e6fbb3b7ee378c725889ec1 + languageName: node + linkType: hard + "@react-native-community/cli@npm:20.0.0": version: 20.0.0 resolution: "@react-native-community/cli@npm:20.0.0" @@ -5916,6 +6811,31 @@ __metadata: languageName: node linkType: hard +"@react-native-community/cli@npm:20.0.1": + version: 20.0.1 + resolution: "@react-native-community/cli@npm:20.0.1" + dependencies: + "@react-native-community/cli-clean": "npm:20.0.1" + "@react-native-community/cli-config": "npm:20.0.1" + "@react-native-community/cli-doctor": "npm:20.0.1" + "@react-native-community/cli-server-api": "npm:20.0.1" + "@react-native-community/cli-tools": "npm:20.0.1" + "@react-native-community/cli-types": "npm:20.0.1" + chalk: "npm:^4.1.2" + commander: "npm:^9.4.1" + deepmerge: "npm:^4.3.0" + execa: "npm:^5.0.0" + find-up: "npm:^5.0.0" + fs-extra: "npm:^8.1.0" + graceful-fs: "npm:^4.1.3" + prompts: "npm:^2.4.2" + semver: "npm:^7.5.2" + bin: + rnc-cli: build/bin.js + checksum: 10/63354c1f1b0bd8c69c15596026d8be950f7a8c570c6260d5d73c82e7e43e0f8e57d820ea68ac36140c13841cc54c64db18d1d0c1c6228f2342821f7c021ea393 + languageName: node + linkType: hard + "@react-native-community/netinfo@npm:11.4.1, @react-native-community/netinfo@npm:^11.4.1": version: 11.4.1 resolution: "@react-native-community/netinfo@npm:11.4.1" @@ -5966,6 +6886,13 @@ __metadata: languageName: node linkType: hard +"@react-native/assets-registry@npm:0.81.1": + version: 0.81.1 + resolution: "@react-native/assets-registry@npm:0.81.1" + checksum: 10/eb9a69695f6354eb61b68a5ff056ee91f430ae435311f1e8aa72b2c40fdeecd0ad7c7f2334f9b7b6e60e645f6f900353405e4e95145e95fb02aadd13fe867a76 + languageName: node + linkType: hard + "@react-native/assets-registry@npm:0.81.5": version: 0.81.5 resolution: "@react-native/assets-registry@npm:0.81.5" @@ -5973,6 +6900,16 @@ __metadata: languageName: node linkType: hard +"@react-native/babel-plugin-codegen@npm:0.81.1": + version: 0.81.1 + resolution: "@react-native/babel-plugin-codegen@npm:0.81.1" + dependencies: + "@babel/traverse": "npm:^7.25.3" + "@react-native/codegen": "npm:0.81.1" + checksum: 10/4321ba81ff440cc2b9bb593c865e660f643054629c6363e8a13c6b8ec678dd3086b3fadf877863676939fc4b32db4c4ce956f2b5d99f0abc946310458bb3d8f4 + languageName: node + linkType: hard + "@react-native/babel-plugin-codegen@npm:0.81.4": version: 0.81.4 resolution: "@react-native/babel-plugin-codegen@npm:0.81.4" @@ -5993,6 +6930,61 @@ __metadata: languageName: node linkType: hard +"@react-native/babel-preset@npm:0.81.1": + version: 0.81.1 + resolution: "@react-native/babel-preset@npm:0.81.1" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/plugin-proposal-export-default-from": "npm:^7.24.7" + "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" + "@babel/plugin-syntax-export-default-from": "npm:^7.24.7" + "@babel/plugin-syntax-nullish-coalescing-operator": "npm:^7.8.3" + "@babel/plugin-syntax-optional-chaining": "npm:^7.8.3" + "@babel/plugin-transform-arrow-functions": "npm:^7.24.7" + "@babel/plugin-transform-async-generator-functions": "npm:^7.25.4" + "@babel/plugin-transform-async-to-generator": "npm:^7.24.7" + "@babel/plugin-transform-block-scoping": "npm:^7.25.0" + "@babel/plugin-transform-class-properties": "npm:^7.25.4" + "@babel/plugin-transform-classes": "npm:^7.25.4" + "@babel/plugin-transform-computed-properties": "npm:^7.24.7" + "@babel/plugin-transform-destructuring": "npm:^7.24.8" + "@babel/plugin-transform-flow-strip-types": "npm:^7.25.2" + "@babel/plugin-transform-for-of": "npm:^7.24.7" + "@babel/plugin-transform-function-name": "npm:^7.25.1" + "@babel/plugin-transform-literals": "npm:^7.25.2" + "@babel/plugin-transform-logical-assignment-operators": "npm:^7.24.7" + "@babel/plugin-transform-modules-commonjs": "npm:^7.24.8" + "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.24.7" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.24.7" + "@babel/plugin-transform-numeric-separator": "npm:^7.24.7" + "@babel/plugin-transform-object-rest-spread": "npm:^7.24.7" + "@babel/plugin-transform-optional-catch-binding": "npm:^7.24.7" + "@babel/plugin-transform-optional-chaining": "npm:^7.24.8" + "@babel/plugin-transform-parameters": "npm:^7.24.7" + "@babel/plugin-transform-private-methods": "npm:^7.24.7" + "@babel/plugin-transform-private-property-in-object": "npm:^7.24.7" + "@babel/plugin-transform-react-display-name": "npm:^7.24.7" + "@babel/plugin-transform-react-jsx": "npm:^7.25.2" + "@babel/plugin-transform-react-jsx-self": "npm:^7.24.7" + "@babel/plugin-transform-react-jsx-source": "npm:^7.24.7" + "@babel/plugin-transform-regenerator": "npm:^7.24.7" + "@babel/plugin-transform-runtime": "npm:^7.24.7" + "@babel/plugin-transform-shorthand-properties": "npm:^7.24.7" + "@babel/plugin-transform-spread": "npm:^7.24.7" + "@babel/plugin-transform-sticky-regex": "npm:^7.24.7" + "@babel/plugin-transform-typescript": "npm:^7.25.2" + "@babel/plugin-transform-unicode-regex": "npm:^7.24.7" + "@babel/template": "npm:^7.25.0" + "@react-native/babel-plugin-codegen": "npm:0.81.1" + babel-plugin-syntax-hermes-parser: "npm:0.29.1" + babel-plugin-transform-flow-enums: "npm:^0.0.2" + react-refresh: "npm:^0.14.0" + peerDependencies: + "@babel/core": "*" + checksum: 10/fa11e95535233294059f46821173b9c2cba219e03c4d00f110e231728ccd2b2086ba04e9bde352f4c85306859f45ef2213b02f787b8521109f7f416df9f86c30 + languageName: node + linkType: hard + "@react-native/babel-preset@npm:0.81.4": version: 0.81.4 resolution: "@react-native/babel-preset@npm:0.81.4" @@ -6103,6 +7095,23 @@ __metadata: languageName: node linkType: hard +"@react-native/codegen@npm:0.81.1": + version: 0.81.1 + resolution: "@react-native/codegen@npm:0.81.1" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/parser": "npm:^7.25.3" + glob: "npm:^7.1.1" + hermes-parser: "npm:0.29.1" + invariant: "npm:^2.2.4" + nullthrows: "npm:^1.1.1" + yargs: "npm:^17.6.2" + peerDependencies: + "@babel/core": "*" + checksum: 10/9097dbcf9025dd580c2fbf0c0162c63a0b748916fce6afb159a1f3ee4cfd77fda391b8b260396bf2a298d4de8a61b5e7fb2315a4b160997dcfee3c7d2d802d5e + languageName: node + linkType: hard + "@react-native/codegen@npm:0.81.4": version: 0.81.4 resolution: "@react-native/codegen@npm:0.81.4" @@ -6137,6 +7146,29 @@ __metadata: languageName: node linkType: hard +"@react-native/community-cli-plugin@npm:0.81.1": + version: 0.81.1 + resolution: "@react-native/community-cli-plugin@npm:0.81.1" + dependencies: + "@react-native/dev-middleware": "npm:0.81.1" + debug: "npm:^4.4.0" + invariant: "npm:^2.2.4" + metro: "npm:^0.83.1" + metro-config: "npm:^0.83.1" + metro-core: "npm:^0.83.1" + semver: "npm:^7.1.3" + peerDependencies: + "@react-native-community/cli": "*" + "@react-native/metro-config": "*" + peerDependenciesMeta: + "@react-native-community/cli": + optional: true + "@react-native/metro-config": + optional: true + checksum: 10/d6dd960010f0cb2be94fbc48b7636af808c36732cc0533a70e69abf6a484ee40e735ca835b0d1ac97809ea9dd28f591c0abe2fb6c7b679a746eae3374a62ee8b + languageName: node + linkType: hard + "@react-native/community-cli-plugin@npm:0.81.5": version: 0.81.5 resolution: "@react-native/community-cli-plugin@npm:0.81.5" @@ -6160,6 +7192,13 @@ __metadata: languageName: node linkType: hard +"@react-native/debugger-frontend@npm:0.81.1": + version: 0.81.1 + resolution: "@react-native/debugger-frontend@npm:0.81.1" + checksum: 10/ce772bf32675234d66c539616e0524e08bfe9165bd2c93496974509d94ebc2b772cd5ed311adaa0f44a6e45e74c156df49063e8ae79b28f9ecd913fbc5318c7d + languageName: node + linkType: hard + "@react-native/debugger-frontend@npm:0.81.4": version: 0.81.4 resolution: "@react-native/debugger-frontend@npm:0.81.4" @@ -6174,6 +7213,25 @@ __metadata: languageName: node linkType: hard +"@react-native/dev-middleware@npm:0.81.1": + version: 0.81.1 + resolution: "@react-native/dev-middleware@npm:0.81.1" + dependencies: + "@isaacs/ttlcache": "npm:^1.4.1" + "@react-native/debugger-frontend": "npm:0.81.1" + chrome-launcher: "npm:^0.15.2" + chromium-edge-launcher: "npm:^0.2.0" + connect: "npm:^3.6.5" + debug: "npm:^4.4.0" + invariant: "npm:^2.2.4" + nullthrows: "npm:^1.1.1" + open: "npm:^7.0.3" + serve-static: "npm:^1.16.2" + ws: "npm:^6.2.3" + checksum: 10/99b06f02d85ddd42d23762fc09b3955d6eb5c03c50cbff897595fdd3504f30a20cfb59fc6a3ef2a7c9c159c5ed254974bede2a28fb08129ece0c03610d154f02 + languageName: node + linkType: hard + "@react-native/dev-middleware@npm:0.81.4": version: 0.81.4 resolution: "@react-native/dev-middleware@npm:0.81.4" @@ -6212,6 +7270,43 @@ __metadata: languageName: node linkType: hard +"@react-native/eslint-config@npm:^0.81.1": + version: 0.81.5 + resolution: "@react-native/eslint-config@npm:0.81.5" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/eslint-parser": "npm:^7.25.1" + "@react-native/eslint-plugin": "npm:0.81.5" + "@typescript-eslint/eslint-plugin": "npm:^7.1.1" + "@typescript-eslint/parser": "npm:^7.1.1" + eslint-config-prettier: "npm:^8.5.0" + eslint-plugin-eslint-comments: "npm:^3.2.0" + eslint-plugin-ft-flow: "npm:^2.0.1" + eslint-plugin-jest: "npm:^27.9.0" + eslint-plugin-react: "npm:^7.30.1" + eslint-plugin-react-hooks: "npm:^5.2.0" + eslint-plugin-react-native: "npm:^4.0.0" + peerDependencies: + eslint: ">=8" + prettier: ">=2" + checksum: 10/59b017aed747a177caa95672158188d0ce1eee3fb9b60bd73a51841f0f28abab56b4c9d4056bdf8d2469579207d8afab433b60290b8f6a0c3f949505464a316d + languageName: node + linkType: hard + +"@react-native/eslint-plugin@npm:0.81.5": + version: 0.81.5 + resolution: "@react-native/eslint-plugin@npm:0.81.5" + checksum: 10/57bea5be68fd90ac6e0b699fc0ce814e5cbda9f9319dfedb36b186e2d022102b44c9db7aed71464f8c631a5efa0af0bfc44d154c26f498a478feb29c4a0141f7 + languageName: node + linkType: hard + +"@react-native/gradle-plugin@npm:0.81.1": + version: 0.81.1 + resolution: "@react-native/gradle-plugin@npm:0.81.1" + checksum: 10/9d75807431bd8671147eb5b2e894de3d1f8a6bd6b20f3478158fb10696e4505d8888934c909b5774aa48a1cfee2c1dcd0e4c6d0b3c88bfa69bba61466b314582 + languageName: node + linkType: hard + "@react-native/gradle-plugin@npm:0.81.5": version: 0.81.5 resolution: "@react-native/gradle-plugin@npm:0.81.5" @@ -6219,6 +7314,13 @@ __metadata: languageName: node linkType: hard +"@react-native/js-polyfills@npm:0.81.1": + version: 0.81.1 + resolution: "@react-native/js-polyfills@npm:0.81.1" + checksum: 10/2cd9ccb46bec96c849537087458769d85952a2e9200b7d9a66a2db65dca9a0a3c21d7339e7bd63a8fef21c60f0ab77e6686fbebd277c9e7f708720c720d9a9f4 + languageName: node + linkType: hard + "@react-native/js-polyfills@npm:0.81.5": version: 0.81.5 resolution: "@react-native/js-polyfills@npm:0.81.5" @@ -6252,6 +7354,13 @@ __metadata: languageName: node linkType: hard +"@react-native/normalize-colors@npm:0.81.1": + version: 0.81.1 + resolution: "@react-native/normalize-colors@npm:0.81.1" + checksum: 10/c1dae5e6494b7acea6cd40ac70a2f35b10ec09034dfbf504b633b9478ebe75212274302b032cad95073267e96a5658ff35978e718562ce9310afeea0d5009d2f + languageName: node + linkType: hard + "@react-native/normalize-colors@npm:0.81.4": version: 0.81.4 resolution: "@react-native/normalize-colors@npm:0.81.4" @@ -6280,6 +7389,23 @@ __metadata: languageName: node linkType: hard +"@react-native/virtualized-lists@npm:0.81.1": + version: 0.81.1 + resolution: "@react-native/virtualized-lists@npm:0.81.1" + dependencies: + invariant: "npm:^2.2.4" + nullthrows: "npm:^1.1.1" + peerDependencies: + "@types/react": ^19.1.0 + react: "*" + react-native: "*" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10/3baea44be69d15f94fbd0bcefd92b76ebf5c30e1b402eee3546cc7db306c0fa31cb054c5461bc978a0a5952c59e1ac3be346b067296e81a51da9d95eabb109e4 + languageName: node + linkType: hard + "@react-native/virtualized-lists@npm:0.81.5": version: 0.81.5 resolution: "@react-native/virtualized-lists@npm:0.81.5" @@ -6420,6 +7546,23 @@ __metadata: languageName: node linkType: hard +"@release-it/conventional-changelog@npm:^10.0.1": + version: 10.0.2 + resolution: "@release-it/conventional-changelog@npm:10.0.2" + dependencies: + "@conventional-changelog/git-client": "npm:^2.5.1" + concat-stream: "npm:^2.0.0" + conventional-changelog: "npm:^7.1.1" + conventional-changelog-angular: "npm:^8.1.0" + conventional-changelog-conventionalcommits: "npm:^9.1.0" + conventional-recommended-bump: "npm:^11.2.0" + semver: "npm:^7.7.3" + peerDependencies: + release-it: ^18.0.0 || ^19.0.0 + checksum: 10/3337224f1f1fe65ba384f4d350996cfeb2241874b870de943ec785404c11dfa92283531767d15923f087820f30c585f8b2b0fba92e5eac7e892a87261a3f378d + languageName: node + linkType: hard + "@remix-run/router@npm:1.23.0": version: 1.23.0 resolution: "@remix-run/router@npm:1.23.0" @@ -7142,6 +8285,25 @@ __metadata: languageName: node linkType: hard +"@simple-libs/child-process-utils@npm:^1.0.0": + version: 1.0.1 + resolution: "@simple-libs/child-process-utils@npm:1.0.1" + dependencies: + "@simple-libs/stream-utils": "npm:^1.1.0" + "@types/node": "npm:^22.0.0" + checksum: 10/5a8aa820fa635a116027b782fe601ec673a98f302e991e4b84097e007d7d65106caad96d5674d38a673c8fb63e0a737345498e982db5d53562b6cae5a680ee6c + languageName: node + linkType: hard + +"@simple-libs/stream-utils@npm:^1.1.0": + version: 1.1.0 + resolution: "@simple-libs/stream-utils@npm:1.1.0" + dependencies: + "@types/node": "npm:^22.0.0" + checksum: 10/13cb27400fa96c3d212d273ebc501851eaf4b13067be9961cfcc51e19e715e698c12cebd974ffa3135e94c7fd59c5bc857135cc64c1f61f3dd5f38e1a80d88a1 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -7156,6 +8318,13 @@ __metadata: languageName: node linkType: hard +"@sindresorhus/merge-streams@npm:^2.1.0": + version: 2.3.0 + resolution: "@sindresorhus/merge-streams@npm:2.3.0" + checksum: 10/798bcb53cd1ace9df84fcdd1ba86afdc9e0cd84f5758d26ae9b1eefd8e8887e5fc30051132b9e74daf01bb41fa5a2faf1369361f83d76a3b3d7ee938058fd71c + languageName: node + linkType: hard + "@sinonjs/commons@npm:^2.0.0": version: 2.0.0 resolution: "@sinonjs/commons@npm:2.0.0" @@ -7634,7 +8803,7 @@ __metadata: react: "npm:19.1.0" react-native: "npm:^0.81.5" react-native-blob-util: "npm:^0.22.2" - react-native-callkeep: "npm:^4.3.16" + react-native-callingx: "workspace:^" react-native-device-info: "npm:^14.1.1" react-native-dotenv: "npm:^3.4.11" react-native-gesture-handler: "npm:^2.28.0" @@ -7747,7 +8916,7 @@ __metadata: react: "npm:19.1.0" react-native: "npm:^0.81.5" react-native-builder-bob: "npm:~0.23" - react-native-callkeep: "npm:^4.3.16" + react-native-callingx: "workspace:^" react-native-gesture-handler: "npm:^2.28.0" react-native-reanimated: "npm:~4.1.2" react-native-svg: "npm:^15.14.0" @@ -7773,7 +8942,7 @@ __metadata: expo-notifications: "*" react: ">=17.0.0" react-native: ">=0.73.0" - react-native-callkeep: ">=4.3.11" + react-native-callingx: "workspace:^" react-native-gesture-handler: ">=2.8.0" react-native-reanimated: ">=2.7.0" react-native-svg: ">=13.6.0" @@ -7797,7 +8966,7 @@ __metadata: optional: true expo-notifications: optional: true - react-native-callkeep: + react-native-callingx: optional: true react-native-gesture-handler: optional: true @@ -8046,6 +9215,15 @@ __metadata: languageName: node linkType: hard +"@types/conventional-commits-parser@npm:^5.0.0": + version: 5.0.2 + resolution: "@types/conventional-commits-parser@npm:5.0.2" + dependencies: + "@types/node": "npm:*" + checksum: 10/c6f9a47f66dc4f8d0de89ba44f47a1574db010b012c082c43ff1a86856053424e84dd53f59f88ab7cd64b9b20f7ee8957553aba2408768a128a9feb8d0ecfd60 + languageName: node + linkType: hard + "@types/css-font-loading-module@npm:0.0.7": version: 0.0.7 resolution: "@types/css-font-loading-module@npm:0.0.7" @@ -8186,7 +9364,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:^7.0.15": +"@types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" checksum: 10/1a3c3e06236e4c4aab89499c428d585527ce50c24fe8259e8b3926d3df4cfbbbcf306cfc73ddfb66cbafc973116efd15967020b0f738f63e09e64c7d260519e7 @@ -8287,7 +9465,16 @@ __metadata: languageName: node linkType: hard -"@types/normalize-package-data@npm:^2.4.1": +"@types/node@npm:^22.0.0": + version: 22.19.1 + resolution: "@types/node@npm:22.19.1" + dependencies: + undici-types: "npm:~6.21.0" + checksum: 10/40d5368faa6d9be6c27ebca2362734bc9e035a742e0b5cafee40ba3b355d7cfcaedbc93618c76465451e53f1af0c811b4b85ee9b85e2e942f34a4c5310fa047b + languageName: node + linkType: hard + +"@types/normalize-package-data@npm:^2.4.1, @types/normalize-package-data@npm:^2.4.4": version: 2.4.4 resolution: "@types/normalize-package-data@npm:2.4.4" checksum: 10/65dff72b543997b7be8b0265eca7ace0e34b75c3e5fee31de11179d08fa7124a7a5587265d53d0409532ecb7f7fba662c2012807963e1f9b059653ec2c83ee05 @@ -8301,6 +9488,13 @@ __metadata: languageName: node linkType: hard +"@types/parse-path@npm:^7.0.0": + version: 7.0.3 + resolution: "@types/parse-path@npm:7.0.3" + checksum: 10/21a12c228d38f5a75659dfd7cb127dc2001ed3f6acbd1b2e0575d1348c735594c0bab06a97fe849c151438384829f20ea5971cb045f7ecd37d53c76a9fcb9de3 + languageName: node + linkType: hard + "@types/pg-pool@npm:2.0.6": version: 2.0.6 resolution: "@types/pg-pool@npm:2.0.6" @@ -8364,6 +9558,15 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^19.1.0": + version: 19.2.7 + resolution: "@types/react@npm:19.2.7" + dependencies: + csstype: "npm:^3.2.2" + checksum: 10/dc0b756eee2c9782d282ae47eaa8d537b2a569eb889a6808c4b172d70fb690b2b1d8fe6239db451aa1c90d2a947cc21c9b537ce177ba9e6121468e403e4079c5 + languageName: node + linkType: hard + "@types/sdp-transform@npm:^2.15.0": version: 2.15.0 resolution: "@types/sdp-transform@npm:2.15.0" @@ -8371,6 +9574,13 @@ __metadata: languageName: node linkType: hard +"@types/semver@npm:^7.3.12": + version: 7.7.1 + resolution: "@types/semver@npm:7.7.1" + checksum: 10/8f09e7e6ca3ded67d78ba7a8f7535c8d9cf8ced83c52e7f3ac3c281fe8c689c3fe475d199d94390dc04fc681d51f2358b430bb7b2e21c62de24f2bee2c719068 + languageName: node + linkType: hard + "@types/shimmer@npm:^1.2.0": version: 1.2.0 resolution: "@types/shimmer@npm:1.2.0" @@ -8482,6 +9692,29 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/eslint-plugin@npm:^7.1.1": + version: 7.18.0 + resolution: "@typescript-eslint/eslint-plugin@npm:7.18.0" + dependencies: + "@eslint-community/regexpp": "npm:^4.10.0" + "@typescript-eslint/scope-manager": "npm:7.18.0" + "@typescript-eslint/type-utils": "npm:7.18.0" + "@typescript-eslint/utils": "npm:7.18.0" + "@typescript-eslint/visitor-keys": "npm:7.18.0" + graphemer: "npm:^1.4.0" + ignore: "npm:^5.3.1" + natural-compare: "npm:^1.4.0" + ts-api-utils: "npm:^1.3.0" + peerDependencies: + "@typescript-eslint/parser": ^7.0.0 + eslint: ^8.56.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/6ee4c61f145dc05f0a567b8ac01b5399ef9c75f58bc6e9a3ffca8927b15e2be2d4c3fd32a2c1a7041cc0848fdeadac30d9cb0d3bcd3835d301847a88ffd19c4d + languageName: node + linkType: hard + "@typescript-eslint/parser@npm:8.46.0, @typescript-eslint/parser@npm:^8.29.1": version: 8.46.0 resolution: "@typescript-eslint/parser@npm:8.46.0" @@ -8498,6 +9731,24 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/parser@npm:^7.1.1": + version: 7.18.0 + resolution: "@typescript-eslint/parser@npm:7.18.0" + dependencies: + "@typescript-eslint/scope-manager": "npm:7.18.0" + "@typescript-eslint/types": "npm:7.18.0" + "@typescript-eslint/typescript-estree": "npm:7.18.0" + "@typescript-eslint/visitor-keys": "npm:7.18.0" + debug: "npm:^4.3.4" + peerDependencies: + eslint: ^8.56.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/36b00e192a96180220ba100fcce3c777fc3e61a6edbdead4e6e75a744d9f0cbe3fabb5f1c94a31cce6b28a4e4d5de148098eec01296026c3c8e16f7f0067cb1e + languageName: node + linkType: hard + "@typescript-eslint/project-service@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/project-service@npm:8.46.0" @@ -8511,6 +9762,26 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/scope-manager@npm:5.62.0" + dependencies: + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/visitor-keys": "npm:5.62.0" + checksum: 10/e827770baa202223bc0387e2fd24f630690809e460435b7dc9af336c77322290a770d62bd5284260fa881c86074d6a9fd6c97b07382520b115f6786b8ed499da + languageName: node + linkType: hard + +"@typescript-eslint/scope-manager@npm:7.18.0": + version: 7.18.0 + resolution: "@typescript-eslint/scope-manager@npm:7.18.0" + dependencies: + "@typescript-eslint/types": "npm:7.18.0" + "@typescript-eslint/visitor-keys": "npm:7.18.0" + checksum: 10/9eb2ae5d69d9f723e706c16b2b97744fc016996a5473bed596035ac4d12429b3d24e7340a8235d704efa57f8f52e1b3b37925ff7c2e3384859d28b23a99b8bcc + languageName: node + linkType: hard + "@typescript-eslint/scope-manager@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/scope-manager@npm:8.46.0" @@ -8530,6 +9801,23 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/type-utils@npm:7.18.0": + version: 7.18.0 + resolution: "@typescript-eslint/type-utils@npm:7.18.0" + dependencies: + "@typescript-eslint/typescript-estree": "npm:7.18.0" + "@typescript-eslint/utils": "npm:7.18.0" + debug: "npm:^4.3.4" + ts-api-utils: "npm:^1.3.0" + peerDependencies: + eslint: ^8.56.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/bcc7958a4ecdddad8c92e17265175773e7dddf416a654c1a391e69cb16e43960b39d37b6ffa349941bf3635e050f0ca7cd8f56ec9dd774168f2bbe7afedc9676 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/type-utils@npm:8.46.0" @@ -8546,6 +9834,20 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/types@npm:5.62.0" + checksum: 10/24e8443177be84823242d6729d56af2c4b47bfc664dd411a1d730506abf2150d6c31bdefbbc6d97c8f91043e3a50e0c698239dcb145b79bb6b0c34469aaf6c45 + languageName: node + linkType: hard + +"@typescript-eslint/types@npm:7.18.0": + version: 7.18.0 + resolution: "@typescript-eslint/types@npm:7.18.0" + checksum: 10/0e30c73a3cc3c67dd06360a5a12fd12cee831e4092750eec3d6c031bdc4feafcb0ab1d882910a73e66b451a4f6e1dd015e9e2c4d45bf6bf716a474e5d123ddf0 + languageName: node + linkType: hard + "@typescript-eslint/types@npm:8.46.0, @typescript-eslint/types@npm:^8.46.0": version: 8.46.0 resolution: "@typescript-eslint/types@npm:8.46.0" @@ -8553,6 +9855,43 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" + dependencies: + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/visitor-keys": "npm:5.62.0" + debug: "npm:^4.3.4" + globby: "npm:^11.1.0" + is-glob: "npm:^4.0.3" + semver: "npm:^7.3.7" + tsutils: "npm:^3.21.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/06c975eb5f44b43bd19fadc2e1023c50cf87038fe4c0dd989d4331c67b3ff509b17fa60a3251896668ab4d7322bdc56162a9926971218d2e1a1874d2bef9a52e + languageName: node + linkType: hard + +"@typescript-eslint/typescript-estree@npm:7.18.0": + version: 7.18.0 + resolution: "@typescript-eslint/typescript-estree@npm:7.18.0" + dependencies: + "@typescript-eslint/types": "npm:7.18.0" + "@typescript-eslint/visitor-keys": "npm:7.18.0" + debug: "npm:^4.3.4" + globby: "npm:^11.1.0" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/b01e66235a91aa4439d02081d4a5f8b4a7cf9cb24f26b334812f657e3c603493e5f41e5c1e89cf4efae7d64509fa1f73affc16afc5e15cb7f83f724577c82036 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/typescript-estree@npm:8.46.0" @@ -8573,6 +9912,20 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:7.18.0": + version: 7.18.0 + resolution: "@typescript-eslint/utils@npm:7.18.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:7.18.0" + "@typescript-eslint/types": "npm:7.18.0" + "@typescript-eslint/typescript-estree": "npm:7.18.0" + peerDependencies: + eslint: ^8.56.0 + checksum: 10/f43fedb4f4d2e3836bdf137889449063a55c0ece74fdb283929cd376197b992313be8ef4df920c1c801b5c3076b92964c84c6c3b9b749d263b648d0011f5926e + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/utils@npm:8.46.0" @@ -8588,6 +9941,44 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^5.10.0": + version: 5.62.0 + resolution: "@typescript-eslint/utils@npm:5.62.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.2.0" + "@types/json-schema": "npm:^7.0.9" + "@types/semver": "npm:^7.3.12" + "@typescript-eslint/scope-manager": "npm:5.62.0" + "@typescript-eslint/types": "npm:5.62.0" + "@typescript-eslint/typescript-estree": "npm:5.62.0" + eslint-scope: "npm:^5.1.1" + semver: "npm:^7.3.7" + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + checksum: 10/15ef13e43998a082b15f85db979f8d3ceb1f9ce4467b8016c267b1738d5e7cdb12aa90faf4b4e6dd6486c236cf9d33c463200465cf25ff997dbc0f12358550a1 + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:5.62.0": + version: 5.62.0 + resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" + dependencies: + "@typescript-eslint/types": "npm:5.62.0" + eslint-visitor-keys: "npm:^3.3.0" + checksum: 10/dc613ab7569df9bbe0b2ca677635eb91839dfb2ca2c6fa47870a5da4f160db0b436f7ec0764362e756d4164e9445d49d5eb1ff0b87f4c058946ae9d8c92eb388 + languageName: node + linkType: hard + +"@typescript-eslint/visitor-keys@npm:7.18.0": + version: 7.18.0 + resolution: "@typescript-eslint/visitor-keys@npm:7.18.0" + dependencies: + "@typescript-eslint/types": "npm:7.18.0" + eslint-visitor-keys: "npm:^3.4.3" + checksum: 10/b7cfe6fdeae86c507357ac6b2357813c64fb2fbf1aaf844393ba82f73a16e2599b41981b34200d9fc7765d70bc3a8181d76b503051e53f04bcb7c9afef637eab + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:8.46.0": version: 8.46.0 resolution: "@typescript-eslint/visitor-keys@npm:8.46.0" @@ -9180,6 +10571,26 @@ __metadata: languageName: node linkType: hard +"arkregex@npm:0.0.3": + version: 0.0.3 + resolution: "arkregex@npm:0.0.3" + dependencies: + "@ark/util": "npm:0.55.0" + checksum: 10/41969316fa1b134a2b112943bb8d3f236140435cb3bf612add7b6ec982469e0eb0dbc113216e492f71a0c9b8c237c80a22cb82f7226cf9549b8e9112396a4312 + languageName: node + linkType: hard + +"arktype@npm:^2.1.15": + version: 2.1.27 + resolution: "arktype@npm:2.1.27" + dependencies: + "@ark/schema": "npm:0.55.0" + "@ark/util": "npm:0.55.0" + arkregex: "npm:0.0.3" + checksum: 10/89b9c8b8b8441105ab441a40a989e1c84eb930dc2f1e4f1c8304dc84625e51de0da19178e7f425267cbae46007794a38f895e15c0807aa78fe6c3b748d707952 + languageName: node + linkType: hard + "array-buffer-byte-length@npm:^1.0.1, array-buffer-byte-length@npm:^1.0.2": version: 1.0.2 resolution: "array-buffer-byte-length@npm:1.0.2" @@ -9369,6 +10780,15 @@ __metadata: languageName: node linkType: hard +"async-retry@npm:1.3.3": + version: 1.3.3 + resolution: "async-retry@npm:1.3.3" + dependencies: + retry: "npm:0.13.1" + checksum: 10/38a7152ff7265a9321ea214b9c69e8224ab1febbdec98efbbde6e562f17ff68405569b796b1c5271f354aef8783665d29953f051f68c1fc45306e61aec82fdc4 + languageName: node + linkType: hard + "async@npm:^3.2.3": version: 3.2.6 resolution: "async@npm:3.2.6" @@ -9589,6 +11009,15 @@ __metadata: languageName: node linkType: hard +"babel-plugin-syntax-hermes-parser@npm:^0.28.0": + version: 0.28.1 + resolution: "babel-plugin-syntax-hermes-parser@npm:0.28.1" + dependencies: + hermes-parser: "npm:0.28.1" + checksum: 10/2cbc921e663463480ead9ccc8bb229a5196032367ba2b5ccb18a44faa3afa84b4dc493297749983b9a837a3d76b0b123664aecc06f9122618c3246f03e076a9d + languageName: node + linkType: hard + "babel-plugin-transform-flow-enums@npm:^0.0.2": version: 0.0.2 resolution: "babel-plugin-transform-flow-enums@npm:0.0.2" @@ -9729,6 +11158,13 @@ __metadata: languageName: node linkType: hard +"before-after-hook@npm:^4.0.0": + version: 4.0.0 + resolution: "before-after-hook@npm:4.0.0" + checksum: 10/9fd52bc0c3cca0fb115e04dacbeeaacff38fa23e1af725d62392254c31ef433b15da60efcba61552e44d64e26f25ea259f72dba005115924389e88d2fd56e19f + languageName: node + linkType: hard + "better-opn@npm:~3.0.2": version: 3.0.2 resolution: "better-opn@npm:3.0.2" @@ -9902,6 +11338,15 @@ __metadata: languageName: node linkType: hard +"bundle-name@npm:^4.1.0": + version: 4.1.0 + resolution: "bundle-name@npm:4.1.0" + dependencies: + run-applescript: "npm:^7.0.0" + checksum: 10/1d966c8d2dbf4d9d394e53b724ac756c2414c45c01340b37743621f59cc565a435024b394ddcb62b9b335d1c9a31f4640eb648c3fec7f97ee74dc0694c9beb6c + languageName: node + linkType: hard + "bytes@npm:3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" @@ -9909,6 +11354,31 @@ __metadata: languageName: node linkType: hard +"c12@npm:3.3.1": + version: 3.3.1 + resolution: "c12@npm:3.3.1" + dependencies: + chokidar: "npm:^4.0.3" + confbox: "npm:^0.2.2" + defu: "npm:^6.1.4" + dotenv: "npm:^17.2.3" + exsolve: "npm:^1.0.7" + giget: "npm:^2.0.0" + jiti: "npm:^2.6.1" + ohash: "npm:^2.0.11" + pathe: "npm:^2.0.3" + perfect-debounce: "npm:^2.0.0" + pkg-types: "npm:^2.3.0" + rc9: "npm:^2.1.2" + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + checksum: 10/e736fc498e73cba8d555ea984544127026ff45fe86f5a2dbc7ff8cc971d2e0ecfa213ef7bd0a7b2d2450dfd7798febd98c9fb189dbb6f16f7c78c6ce3a5de040 + languageName: node + linkType: hard + "cac@npm:^6.7.14": version: 6.7.14 resolution: "cac@npm:6.7.14" @@ -10085,6 +11555,13 @@ __metadata: languageName: node linkType: hard +"chalk@npm:^5.3.0, chalk@npm:^5.6.2": + version: 5.6.2 + resolution: "chalk@npm:5.6.2" + checksum: 10/1b2f48f6fba1370670d5610f9cd54c391d6ede28f4b7062dd38244ea5768777af72e5be6b74fb6c6d54cb84c4a2dff3f3afa9b7cb5948f7f022cfd3d087989e0 + languageName: node + linkType: hard + "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -10141,6 +11618,13 @@ __metadata: languageName: node linkType: hard +"chardet@npm:^2.1.1": + version: 2.1.1 + resolution: "chardet@npm:2.1.1" + checksum: 10/d56913b65e45c5c86f331988e2ef6264c131bfeadaae098ee719bf6610546c77740e37221ffec802dde56b5e4466613a4c754786f4da6b5f6c5477243454d324 + languageName: node + linkType: hard + "chart.js@npm:^4.4.4": version: 4.4.4 resolution: "chart.js@npm:4.4.4" @@ -10176,7 +11660,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:^4.0.0": +"chokidar@npm:^4.0.0, chokidar@npm:^4.0.3": version: 4.0.3 resolution: "chokidar@npm:4.0.3" dependencies: @@ -10241,6 +11725,22 @@ __metadata: languageName: node linkType: hard +"ci-info@npm:^4.3.0": + version: 4.3.1 + resolution: "ci-info@npm:4.3.1" + checksum: 10/9dc952bef67e665ccde2e7a552d42d5d095529d21829ece060a00925ede2dfa136160c70ef2471ea6ed6c9b133218b47c007f56955c0f1734a2e57f240aa7445 + languageName: node + linkType: hard + +"citty@npm:^0.1.6": + version: 0.1.6 + resolution: "citty@npm:0.1.6" + dependencies: + consola: "npm:^3.2.3" + checksum: 10/3208947e73abb699a12578ee2bfee254bf8dd1ce0d5698e8a298411cabf16bd3620d63433aef5bd88cdb2b9da71aef18adefa3b4ffd18273bb62dd1d28c344f5 + languageName: node + linkType: hard + "cjs-module-lexer@npm:^1.0.0, cjs-module-lexer@npm:^1.2.2": version: 1.3.1 resolution: "cjs-module-lexer@npm:1.3.1" @@ -10296,6 +11796,13 @@ __metadata: languageName: node linkType: hard +"cli-spinners@npm:^3.2.0": + version: 3.3.0 + resolution: "cli-spinners@npm:3.3.0" + checksum: 10/d95f69f4a6a4efab2104ca5d4723c9f6fae9a4006df7fdcc1f79ea6539324e274b85bf6f5931146d84296b0f71814f4c1ff1acc158f2e1107c0c9797c1291bcc + languageName: node + linkType: hard + "cli-truncate@npm:^5.0.0": version: 5.1.0 resolution: "cli-truncate@npm:5.1.0" @@ -10313,6 +11820,13 @@ __metadata: languageName: node linkType: hard +"cli-width@npm:^4.1.0": + version: 4.1.0 + resolution: "cli-width@npm:4.1.0" + checksum: 10/b58876fbf0310a8a35c79b72ecfcf579b354e18ad04e6b20588724ea2b522799a758507a37dfe132fafaf93a9922cafd9514d9e1598e6b2cd46694853aed099f + languageName: node + linkType: hard + "client-only@npm:0.0.1, client-only@npm:^0.0.1": version: 0.0.1 resolution: "client-only@npm:0.0.1" @@ -10535,6 +12049,18 @@ __metadata: languageName: node linkType: hard +"commitlint@npm:^19.8.1": + version: 19.8.1 + resolution: "commitlint@npm:19.8.1" + dependencies: + "@commitlint/cli": "npm:^19.8.1" + "@commitlint/types": "npm:^19.8.1" + bin: + commitlint: cli.js + checksum: 10/23e9a34b074361ec66c89573b1eba3ab65e7fe8044e22c3f044db87071817d8fe32e9e63313703c65385f5db1ab8e204eb2b3fa5e5a9481bc2fdef56eab478c1 + languageName: node + linkType: hard + "commondir@npm:^1.0.1": version: 1.0.1 resolution: "commondir@npm:1.0.1" @@ -10590,6 +12116,18 @@ __metadata: languageName: node linkType: hard +"concat-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "concat-stream@npm:2.0.0" + dependencies: + buffer-from: "npm:^1.0.0" + inherits: "npm:^2.0.3" + readable-stream: "npm:^3.0.2" + typedarray: "npm:^0.0.6" + checksum: 10/250e576d0617e7c58e1c4b2dd6fe69560f316d2c962a409f9f3aac794018499ddb31948b1e4296f217008e124cd5d526432097745157fe504b5d9f3dc469eadb + languageName: node + linkType: hard + "concurrently@npm:9.2.1": version: 9.2.1 resolution: "concurrently@npm:9.2.1" @@ -10607,6 +12145,13 @@ __metadata: languageName: node linkType: hard +"confbox@npm:^0.2.2": + version: 0.2.2 + resolution: "confbox@npm:0.2.2" + checksum: 10/988c7216f9b5aee5d8a8f32153a9164e1b58d92d8335c5daa323fd3fdee91f742ffc25f6c28b059474b6319204085eca985ab14c5a246988dc7ef1fe29414108 + languageName: node + linkType: hard + "connect@npm:^3.6.5, connect@npm:^3.7.0": version: 3.7.0 resolution: "connect@npm:3.7.0" @@ -10626,7 +12171,7 @@ __metadata: languageName: node linkType: hard -"consola@npm:^3.2.3": +"consola@npm:^3.2.3, consola@npm:^3.4.0, consola@npm:^3.4.2": version: 3.4.2 resolution: "consola@npm:3.4.2" checksum: 10/32192c9f50d7cac27c5d7c4ecd3ff3679aea863e6bf5bd6a9cc2b05d1cd78addf5dae71df08c54330c142be8e7fbd46f051030129b57c6aacdd771efe409c4b2 @@ -10665,6 +12210,15 @@ __metadata: languageName: node linkType: hard +"conventional-changelog-angular@npm:^8.1.0": + version: 8.1.0 + resolution: "conventional-changelog-angular@npm:8.1.0" + dependencies: + compare-func: "npm:^2.0.0" + checksum: 10/2211efa2bebbb00c3976d7b860979d3c04c2bcbb661cfc0c61445986dd3efa391f6e56636482cda83dfb3da3e2327a05c80f647a9147072627046bcbe0de7d39 + languageName: node + linkType: hard + "conventional-changelog-atom@npm:^4.0.0": version: 4.0.0 resolution: "conventional-changelog-atom@npm:4.0.0" @@ -10688,6 +12242,15 @@ __metadata: languageName: node linkType: hard +"conventional-changelog-conventionalcommits@npm:^9.1.0": + version: 9.1.0 + resolution: "conventional-changelog-conventionalcommits@npm:9.1.0" + dependencies: + compare-func: "npm:^2.0.0" + checksum: 10/932522a9eb2f19f8b6efc05f1de0b8d4775842e2156c2c58358d25069bfc43ca1a6198fb07666d7abc83695a10591787c23b7ff2e1e2d73ac484cfb2f57f5f7f + languageName: node + linkType: hard + "conventional-changelog-core@npm:^7.0.0": version: 7.0.0 resolution: "conventional-changelog-core@npm:7.0.0" @@ -10750,6 +12313,13 @@ __metadata: languageName: node linkType: hard +"conventional-changelog-preset-loader@npm:^5.0.0": + version: 5.0.0 + resolution: "conventional-changelog-preset-loader@npm:5.0.0" + checksum: 10/7630c2826b43f8f546f0575b46d3eb8c2ac2b5bcfae60b7d1186e9a87f07b7a689d9463afc125a40ab84a030574c9ce7965dd96e6506323e5a7d1ac2b9f2df19 + languageName: node + linkType: hard + "conventional-changelog-writer@npm:^7.0.0": version: 7.0.1 resolution: "conventional-changelog-writer@npm:7.0.1" @@ -10766,6 +12336,20 @@ __metadata: languageName: node linkType: hard +"conventional-changelog-writer@npm:^8.2.0": + version: 8.2.0 + resolution: "conventional-changelog-writer@npm:8.2.0" + dependencies: + conventional-commits-filter: "npm:^5.0.0" + handlebars: "npm:^4.7.7" + meow: "npm:^13.0.0" + semver: "npm:^7.5.2" + bin: + conventional-changelog-writer: dist/cli/index.js + checksum: 10/050387a37a295bf8d0f91733432ea1e3959478d5b7d71f19654b83943503f2a37f24d69cf7688d9bb371c537693da024bc4e7c72a7029df8d121a44035ff7949 + languageName: node + linkType: hard + "conventional-changelog@npm:^5.1.0": version: 5.1.0 resolution: "conventional-changelog@npm:5.1.0" @@ -10785,6 +12369,24 @@ __metadata: languageName: node linkType: hard +"conventional-changelog@npm:^7.1.1": + version: 7.1.1 + resolution: "conventional-changelog@npm:7.1.1" + dependencies: + "@conventional-changelog/git-client": "npm:^2.5.1" + "@types/normalize-package-data": "npm:^2.4.4" + conventional-changelog-preset-loader: "npm:^5.0.0" + conventional-changelog-writer: "npm:^8.2.0" + conventional-commits-parser: "npm:^6.2.0" + fd-package-json: "npm:^2.0.0" + meow: "npm:^13.0.0" + normalize-package-data: "npm:^7.0.0" + bin: + conventional-changelog: dist/cli/index.js + checksum: 10/983ce0498fa9e6103c602342f93b3a9b13dd414be594cd1ec4f7715916253caca1f42a98df7646a8ee629108e0f9bb33b1e45c58f2e689918097063c783cb0d9 + languageName: node + linkType: hard + "conventional-commits-filter@npm:^4.0.0": version: 4.0.0 resolution: "conventional-commits-filter@npm:4.0.0" @@ -10792,6 +12394,13 @@ __metadata: languageName: node linkType: hard +"conventional-commits-filter@npm:^5.0.0": + version: 5.0.0 + resolution: "conventional-commits-filter@npm:5.0.0" + checksum: 10/2345546ea9e40412558d508311d7729b38f8d4c0fd554837c10721a432e8598ec1152320f6b601a9c11c023a31bccbb5a12067736b2227de8591f4de707e11a7 + languageName: node + linkType: hard + "conventional-commits-parser@npm:^5.0.0": version: 5.0.0 resolution: "conventional-commits-parser@npm:5.0.0" @@ -10801,8 +12410,34 @@ __metadata: meow: "npm:^12.0.1" split2: "npm:^4.0.0" bin: - conventional-commits-parser: cli.mjs - checksum: 10/3b56a9313127f18c56b7fc0fdb0c49d2184ec18e0574e64580a0d5a3c3e0f3eecfb8bc3131dce967bfe9fd27debd5f42b7fc1f09e8e541e688e1dd2b57f49278 + conventional-commits-parser: cli.mjs + checksum: 10/3b56a9313127f18c56b7fc0fdb0c49d2184ec18e0574e64580a0d5a3c3e0f3eecfb8bc3131dce967bfe9fd27debd5f42b7fc1f09e8e541e688e1dd2b57f49278 + languageName: node + linkType: hard + +"conventional-commits-parser@npm:^6.1.0, conventional-commits-parser@npm:^6.2.0": + version: 6.2.1 + resolution: "conventional-commits-parser@npm:6.2.1" + dependencies: + meow: "npm:^13.0.0" + bin: + conventional-commits-parser: dist/cli/index.js + checksum: 10/342764ac7c8114e3030d9d86968eafa3023ed887bc66412f89891fb55f09179d151a02342142a4039ca3375a7e39553d29789e022afd08edddbd995a1d5d9c24 + languageName: node + linkType: hard + +"conventional-recommended-bump@npm:^11.2.0": + version: 11.2.0 + resolution: "conventional-recommended-bump@npm:11.2.0" + dependencies: + "@conventional-changelog/git-client": "npm:^2.5.1" + conventional-changelog-preset-loader: "npm:^5.0.0" + conventional-commits-filter: "npm:^5.0.0" + conventional-commits-parser: "npm:^6.1.0" + meow: "npm:^13.0.0" + bin: + conventional-recommended-bump: dist/cli/index.js + checksum: 10/b6330542675399d1053c9092c5ddcc4ab0904046b5f767478f9434ada2cb1e9adbd9555e6952e1ad5a3bae6775cf67a14062b0be3b5c61b93c22abc702b00175 languageName: node linkType: hard @@ -10859,6 +12494,19 @@ __metadata: languageName: node linkType: hard +"cosmiconfig-typescript-loader@npm:^6.1.0": + version: 6.2.0 + resolution: "cosmiconfig-typescript-loader@npm:6.2.0" + dependencies: + jiti: "npm:^2.6.1" + peerDependencies: + "@types/node": "*" + cosmiconfig: ">=9" + typescript: ">=5" + checksum: 10/f905077c7233e561810b030342e6b85a2a58ddcfdaaad7d7b0a52f6376581957826e3aa64238d80d1aaf4d24013342d7c780888cffb36ba445d728d6e12588f3 + languageName: node + linkType: hard + "cosmiconfig@npm:^5.0.5": version: 5.2.1 resolution: "cosmiconfig@npm:5.2.1" @@ -11030,6 +12678,13 @@ __metadata: languageName: node linkType: hard +"csstype@npm:^3.2.2": + version: 3.2.3 + resolution: "csstype@npm:3.2.3" + checksum: 10/ad41baf7e2ffac65ab544d79107bf7cd1a4bb9bab9ac3302f59ab4ba655d5e30942a8ae46e10ba160c6f4ecea464cc95b975ca2fefbdeeacd6ac63f12f99fe1f + languageName: node + linkType: hard + "d@npm:1, d@npm:^1.0.1": version: 1.0.1 resolution: "d@npm:1.0.1" @@ -11224,6 +12879,23 @@ __metadata: languageName: node linkType: hard +"default-browser-id@npm:^5.0.0": + version: 5.0.1 + resolution: "default-browser-id@npm:5.0.1" + checksum: 10/52c637637bcd76bfe974462a2f1dd75cb04784c2852935575760f82e1fd338e5e80d3c45a9b01fdbb1e450553a830bb163b004d2eca223c5573989f82232a072 + languageName: node + linkType: hard + +"default-browser@npm:^5.2.1": + version: 5.4.0 + resolution: "default-browser@npm:5.4.0" + dependencies: + bundle-name: "npm:^4.1.0" + default-browser-id: "npm:^5.0.0" + checksum: 10/cac0222ca5c9a3387d25337228689652ab33679a6566995c7194a75af7e554e91ec9ac92a70bfaa8e8089eae9f466ae99267bb38601282aade89b200f50a765c + languageName: node + linkType: hard + "defaults@npm:^1.0.3": version: 1.0.4 resolution: "defaults@npm:1.0.4" @@ -11251,6 +12923,13 @@ __metadata: languageName: node linkType: hard +"define-lazy-prop@npm:^3.0.0": + version: 3.0.0 + resolution: "define-lazy-prop@npm:3.0.0" + checksum: 10/f28421cf9ee86eecaf5f3b8fe875f13d7009c2625e97645bfff7a2a49aca678270b86c39f9c32939e5ca7ab96b551377ed4139558c795e076774287ad3af1aa4 + languageName: node + linkType: hard + "define-properties@npm:^1.1.3, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" @@ -11262,6 +12941,13 @@ __metadata: languageName: node linkType: hard +"defu@npm:^6.1.4": + version: 6.1.4 + resolution: "defu@npm:6.1.4" + checksum: 10/aeffdb47300f45b4fdef1c5bd3880ac18ea7a1fd5b8a8faf8df29350ff03bf16dd34f9800205cab513d476e4c0a3783aa0cff0a433aff0ac84a67ddc4c8a2d64 + languageName: node + linkType: hard + "degenerator@npm:^5.0.0": version: 5.0.1 resolution: "degenerator@npm:5.0.1" @@ -11273,6 +12959,19 @@ __metadata: languageName: node linkType: hard +"del-cli@npm:^6.0.0": + version: 6.0.0 + resolution: "del-cli@npm:6.0.0" + dependencies: + del: "npm:^8.0.0" + meow: "npm:^13.2.0" + bin: + del: cli.js + del-cli: cli.js + checksum: 10/5441e55c9181f364e84a1d211e53a03476a806e795f6b7d576673f164a9f76e0dc846413a47730f6224913974ba54dabb1884f854a9cf4d418084872bdaaa229 + languageName: node + linkType: hard + "del@npm:^6.1.1": version: 6.1.1 resolution: "del@npm:6.1.1" @@ -11289,6 +12988,21 @@ __metadata: languageName: node linkType: hard +"del@npm:^8.0.0": + version: 8.0.1 + resolution: "del@npm:8.0.1" + dependencies: + globby: "npm:^14.0.2" + is-glob: "npm:^4.0.3" + is-path-cwd: "npm:^3.0.0" + is-path-inside: "npm:^4.0.0" + p-map: "npm:^7.0.2" + presentable-error: "npm:^0.0.1" + slash: "npm:^5.1.0" + checksum: 10/53ed4a379a68c90e7d6d3bcce09c49229e77de9a946d0a5fc25f45b16c950cb8665986b7d0d0423416c03bfd43e0f31e528c5a19c558fe47449be9d6fae7f846 + languageName: node + linkType: hard + "delayed-stream@npm:~1.0.0": version: 1.0.0 resolution: "delayed-stream@npm:1.0.0" @@ -11324,6 +13038,13 @@ __metadata: languageName: node linkType: hard +"destr@npm:^2.0.3": + version: 2.0.5 + resolution: "destr@npm:2.0.5" + checksum: 10/0e4fba62a55a4188c7ab13eed5ebeeda037ead1ab21cf6be40ca39828b258475ad9eb1e7de50a5ea8041705d454a4d090caf9f92b89f03b04d2e229716f7da0a + languageName: node + linkType: hard + "destroy@npm:1.2.0": version: 1.2.0 resolution: "destroy@npm:1.2.0" @@ -11498,6 +13219,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^17.2.3": + version: 17.2.3 + resolution: "dotenv@npm:17.2.3" + checksum: 10/f8b78626ebfff6e44420f634773375c9651808b3e1a33df6d4cc19120968eea53e100f59f04ec35f2a20b2beb334b6aba4f24040b2f8ad61773f158ac042a636 + languageName: node + linkType: hard + "dotenv@npm:~16.4.5": version: 16.4.7 resolution: "dotenv@npm:16.4.7" @@ -12071,6 +13799,28 @@ __metadata: languageName: node linkType: hard +"eslint-config-prettier@npm:^10.1.8": + version: 10.1.8 + resolution: "eslint-config-prettier@npm:10.1.8" + peerDependencies: + eslint: ">=7.0.0" + bin: + eslint-config-prettier: bin/cli.js + checksum: 10/03f8e6ea1a6a9b8f9eeaf7c8c52a96499ec4b275b9ded33331a6cc738ed1d56de734097dbd0091f136f0e84bc197388bd8ec22a52a4658105883f8c8b7d8921a + languageName: node + linkType: hard + +"eslint-config-prettier@npm:^8.5.0": + version: 8.10.2 + resolution: "eslint-config-prettier@npm:8.10.2" + peerDependencies: + eslint: ">=7.0.0" + bin: + eslint-config-prettier: bin/cli.js + checksum: 10/9818f26eebf32c5698bcc68d9b05e985ccaa6862488a32305681f9f025248c4b9192e587969594b3e79a814f965f808f513f63921dbb14639501fa61d6e6560d + languageName: node + linkType: hard + "eslint-config-prettier@npm:^9.1.0": version: 9.1.0 resolution: "eslint-config-prettier@npm:9.1.0" @@ -12154,6 +13904,31 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-eslint-comments@npm:^3.2.0": + version: 3.2.0 + resolution: "eslint-plugin-eslint-comments@npm:3.2.0" + dependencies: + escape-string-regexp: "npm:^1.0.5" + ignore: "npm:^5.0.5" + peerDependencies: + eslint: ">=4.19.1" + checksum: 10/4aa0d31a78ac7746002e37ca0cb436f3e5b481a97d28be07bad831e161a2ffcc4dedff44820edef9a1e80f6a0ab1ef44ed9a46e3a4c4a050350438451908972b + languageName: node + linkType: hard + +"eslint-plugin-ft-flow@npm:^2.0.1": + version: 2.0.3 + resolution: "eslint-plugin-ft-flow@npm:2.0.3" + dependencies: + lodash: "npm:^4.17.21" + string-natural-compare: "npm:^3.0.1" + peerDependencies: + "@babel/eslint-parser": ^7.12.0 + eslint: ^8.1.0 + checksum: 10/ea03496d247b9de915f0c5cee3724d4cbec8c0ab22029e4c06301c524bd8a7cbc20598971bed792304c5b3a17c1a1004a1bf7c7f59b55d3887aa7581e00ad0e1 + languageName: node + linkType: hard + "eslint-plugin-import@npm:^2.31.0, eslint-plugin-import@npm:^2.32.0": version: 2.32.0 resolution: "eslint-plugin-import@npm:2.32.0" @@ -12183,6 +13958,24 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-jest@npm:^27.9.0": + version: 27.9.0 + resolution: "eslint-plugin-jest@npm:27.9.0" + dependencies: + "@typescript-eslint/utils": "npm:^5.10.0" + peerDependencies: + "@typescript-eslint/eslint-plugin": ^5.0.0 || ^6.0.0 || ^7.0.0 + eslint: ^7.0.0 || ^8.0.0 + jest: "*" + peerDependenciesMeta: + "@typescript-eslint/eslint-plugin": + optional: true + jest: + optional: true + checksum: 10/bca54347280c06c56516faea76042134dd74355c2de6c23361ba0e8736ecc01c62b144eea7eda7570ea4f4ee511c583bb8dab00d7153a1bd1740eb77b0038fd4 + languageName: node + linkType: hard + "eslint-plugin-n@npm:^17.17.0": version: 17.23.1 resolution: "eslint-plugin-n@npm:17.23.1" @@ -12218,7 +14011,7 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-prettier@npm:^5.2.6": +"eslint-plugin-prettier@npm:^5.2.6, eslint-plugin-prettier@npm:^5.5.4": version: 5.5.4 resolution: "eslint-plugin-prettier@npm:5.5.4" dependencies: @@ -12262,7 +14055,25 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-react@npm:^7.37.5": +"eslint-plugin-react-native-globals@npm:^0.1.1": + version: 0.1.2 + resolution: "eslint-plugin-react-native-globals@npm:0.1.2" + checksum: 10/ab91e8ecbb51718fb0763f29226b1c2d402251ab2c4730a8bf85f38b805e32d4243da46d07ccdb12cb9dcce9e7514364a1706142cf970f58dcc9a820bcf4b732 + languageName: node + linkType: hard + +"eslint-plugin-react-native@npm:^4.0.0": + version: 4.1.0 + resolution: "eslint-plugin-react-native@npm:4.1.0" + dependencies: + eslint-plugin-react-native-globals: "npm:^0.1.1" + peerDependencies: + eslint: ^3.17.0 || ^4 || ^5 || ^6 || ^7 || ^8 + checksum: 10/fb2d65a3faca9bf775a0fa430eb7e86b7c27d0b256916d4f79a94def9ad353c8a10605f1f0dc9a5fb10e446b003341d53af9d8cbca4dd7ba394350355efa30c6 + languageName: node + linkType: hard + +"eslint-plugin-react@npm:^7.30.1, eslint-plugin-react@npm:^7.37.5": version: 7.37.5 resolution: "eslint-plugin-react@npm:7.37.5" dependencies: @@ -12290,6 +14101,16 @@ __metadata: languageName: node linkType: hard +"eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1": + version: 5.1.1 + resolution: "eslint-scope@npm:5.1.1" + dependencies: + esrecurse: "npm:^4.3.0" + estraverse: "npm:^4.1.1" + checksum: 10/c541ef384c92eb5c999b7d3443d80195fcafb3da335500946f6db76539b87d5826c8f2e1d23bf6afc3154ba8cd7c8e566f8dc00f1eea25fdf3afc8fb9c87b238 + languageName: node + linkType: hard + "eslint-scope@npm:^8.4.0": version: 8.4.0 resolution: "eslint-scope@npm:8.4.0" @@ -12316,7 +14137,14 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.4.3": +"eslint-visitor-keys@npm:^2.1.0": + version: 2.1.0 + resolution: "eslint-visitor-keys@npm:2.1.0" + checksum: 10/db4547eef5039122d518fa307e938ceb8589da5f6e8f5222efaf14dd62f748ce82e2d2becd3ff9412a50350b726bda95dbea8515a471074547daefa58aee8735 + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" checksum: 10/3f357c554a9ea794b094a09bd4187e5eacd1bc0d0653c3adeb87962c548e6a1ab8f982b86963ae1337f5d976004146536dcee5d0e2806665b193fbfbf1a9231b @@ -12330,6 +14158,55 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^9.35.0": + version: 9.39.1 + resolution: "eslint@npm:9.39.1" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.8.0" + "@eslint-community/regexpp": "npm:^4.12.1" + "@eslint/config-array": "npm:^0.21.1" + "@eslint/config-helpers": "npm:^0.4.2" + "@eslint/core": "npm:^0.17.0" + "@eslint/eslintrc": "npm:^3.3.1" + "@eslint/js": "npm:9.39.1" + "@eslint/plugin-kit": "npm:^0.4.1" + "@humanfs/node": "npm:^0.16.6" + "@humanwhocodes/module-importer": "npm:^1.0.1" + "@humanwhocodes/retry": "npm:^0.4.2" + "@types/estree": "npm:^1.0.6" + ajv: "npm:^6.12.4" + chalk: "npm:^4.0.0" + cross-spawn: "npm:^7.0.6" + debug: "npm:^4.3.2" + escape-string-regexp: "npm:^4.0.0" + eslint-scope: "npm:^8.4.0" + eslint-visitor-keys: "npm:^4.2.1" + espree: "npm:^10.4.0" + esquery: "npm:^1.5.0" + esutils: "npm:^2.0.2" + fast-deep-equal: "npm:^3.1.3" + file-entry-cache: "npm:^8.0.0" + find-up: "npm:^5.0.0" + glob-parent: "npm:^6.0.2" + ignore: "npm:^5.2.0" + imurmurhash: "npm:^0.1.4" + is-glob: "npm:^4.0.0" + json-stable-stringify-without-jsonify: "npm:^1.0.1" + lodash.merge: "npm:^4.6.2" + minimatch: "npm:^3.1.2" + natural-compare: "npm:^1.4.0" + optionator: "npm:^0.9.3" + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true + bin: + eslint: bin/eslint.js + checksum: 10/c85fefe4a81a1a476e62087366907af830b62a6565ac153f6d50a100a42a946aeb049c3af8f06c0e091105ba0fe97ac109f379f32755a67f66ecb7d4d1e4dca3 + languageName: node + linkType: hard + "eslint@npm:^9.37.0": version: 9.37.0 resolution: "eslint@npm:9.37.0" @@ -12419,6 +14296,13 @@ __metadata: languageName: node linkType: hard +"estraverse@npm:^4.1.1": + version: 4.3.0 + resolution: "estraverse@npm:4.3.0" + checksum: 10/3f67ad02b6dbfaddd9ea459cf2b6ef4ecff9a6082a7af9d22e445b9abc082ad9ca47e1825557b293fcdae477f4714e561123e30bb6a5b2f184fb2bad4a9497eb + languageName: node + linkType: hard + "estraverse@npm:^5.1.0, estraverse@npm:^5.2.0, estraverse@npm:^5.3.0": version: 5.3.0 resolution: "estraverse@npm:5.3.0" @@ -12456,6 +14340,13 @@ __metadata: languageName: node linkType: hard +"eta@npm:4.0.1": + version: 4.0.1 + resolution: "eta@npm:4.0.1" + checksum: 10/390f69d1213d640026e51c993f6c35fa39cbb21c242e1328210329af959844b5b22a5284f9560b8e9be044272ed606dca1053a49656de1eebc5a1d0884e1f171 + languageName: node + linkType: hard + "etag@npm:~1.8.1": version: 1.8.1 resolution: "etag@npm:1.8.1" @@ -12525,6 +14416,23 @@ __metadata: languageName: node linkType: hard +"execa@npm:^8.0.1": + version: 8.0.1 + resolution: "execa@npm:8.0.1" + dependencies: + cross-spawn: "npm:^7.0.3" + get-stream: "npm:^8.0.1" + human-signals: "npm:^5.0.0" + is-stream: "npm:^3.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^5.1.0" + onetime: "npm:^6.0.0" + signal-exit: "npm:^4.1.0" + strip-final-newline: "npm:^3.0.0" + checksum: 10/d2ab5fe1e2bb92b9788864d0713f1fce9a07c4594e272c0c97bc18c90569897ab262e4ea58d27a694d288227a2e24f16f5e2575b44224ad9983b799dc7f1098d + languageName: node + linkType: hard + "exit@npm:^0.1.2": version: 0.1.2 resolution: "exit@npm:0.1.2" @@ -13001,6 +14909,13 @@ __metadata: languageName: node linkType: hard +"exsolve@npm:^1.0.7": + version: 1.0.8 + resolution: "exsolve@npm:1.0.8" + checksum: 10/e7e8eac048af9f6856628a46df15529ab37428bdb5f7bc5b7824614383223de1aff60ebe85f44d9c8d4ee218d98c71df1a3e2d336f7d022a4dccd97a0651ec5b + languageName: node + linkType: hard + "ext@npm:^1.1.2": version: 1.7.0 resolution: "ext@npm:1.7.0" @@ -13028,6 +14943,13 @@ __metadata: languageName: node linkType: hard +"fast-content-type-parse@npm:^3.0.0": + version: 3.0.0 + resolution: "fast-content-type-parse@npm:3.0.0" + checksum: 10/8616a8aa6c9b4f8f4f3c90eaa4e7bfc2240cfa6f41f0eef5b5aa2b2c8b38bd9ad435f1488b6d817ffd725c54651e2777b882ae9dd59366e71e7896f1ec11d473 + languageName: node + linkType: hard + "fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" @@ -13055,6 +14977,19 @@ __metadata: languageName: node linkType: hard +"fast-glob@npm:^3.3.3": + version: 3.3.3 + resolution: "fast-glob@npm:3.3.3" + dependencies: + "@nodelib/fs.stat": "npm:^2.0.2" + "@nodelib/fs.walk": "npm:^1.2.3" + glob-parent: "npm:^5.1.2" + merge2: "npm:^1.3.0" + micromatch: "npm:^4.0.8" + checksum: 10/dcc6432b269762dd47381d8b8358bf964d8f4f60286ac6aa41c01ade70bda459ff2001b516690b96d5365f68a49242966112b5d5cc9cd82395fa8f9d017c90ad + languageName: node + linkType: hard + "fast-json-stable-stringify@npm:2.x, fast-json-stable-stringify@npm:^2.0.0, fast-json-stable-stringify@npm:^2.1.0": version: 2.1.0 resolution: "fast-json-stable-stringify@npm:2.1.0" @@ -13136,6 +15071,15 @@ __metadata: languageName: node linkType: hard +"fd-package-json@npm:^2.0.0": + version: 2.0.0 + resolution: "fd-package-json@npm:2.0.0" + dependencies: + walk-up-path: "npm:^4.0.0" + checksum: 10/e595a1a23f8e208815cdcf26c92218240da00acce80468324408dc4a5cb6c26b6efb5076f0458a02f044562a1e60253731187a627d5416b4961468ddfc0ae426 + languageName: node + linkType: hard + "fdir@npm:^6.2.0, fdir@npm:^6.5.0": version: 6.5.0 resolution: "fdir@npm:6.5.0" @@ -13289,6 +15233,17 @@ __metadata: languageName: node linkType: hard +"find-up@npm:^7.0.0": + version: 7.0.0 + resolution: "find-up@npm:7.0.0" + dependencies: + locate-path: "npm:^7.2.0" + path-exists: "npm:^5.0.0" + unicorn-magic: "npm:^0.1.0" + checksum: 10/7e6b08fbc05a10677e25e74bb0a020054a86b31d1806c5e6a9e32e75472bbf177210bc16e5f97453be8bda7ae2e3d97669dbb2901f8c30b39ce53929cbea6746 + languageName: node + linkType: hard + "firebase@npm:12.2.1": version: 12.2.1 resolution: "firebase@npm:12.2.1" @@ -13718,6 +15673,13 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^8.0.1": + version: 8.0.1 + resolution: "get-stream@npm:8.0.1" + checksum: 10/dde5511e2e65a48e9af80fea64aff11b4921b14b6e874c6f8294c50975095af08f41bfb0b680c887f28b566dd6ec2cb2f960f9d36a323359be324ce98b766e9e + languageName: node + linkType: hard + "get-symbol-description@npm:^1.1.0": version: 1.1.0 resolution: "get-symbol-description@npm:1.1.0" @@ -13756,6 +15718,22 @@ __metadata: languageName: node linkType: hard +"giget@npm:^2.0.0": + version: 2.0.0 + resolution: "giget@npm:2.0.0" + dependencies: + citty: "npm:^0.1.6" + consola: "npm:^3.4.0" + defu: "npm:^6.1.4" + node-fetch-native: "npm:^1.6.6" + nypm: "npm:^0.6.0" + pathe: "npm:^2.0.3" + bin: + giget: dist/cli.mjs + checksum: 10/3ee0f4aa06bdaeda9d4d31791d6a1e4349f15e20ff1dbe60535c709d3acc03f29f36a648cd047851a332fc1a0e9997ab6c5036410cc1629c09ad45ee155ee6dd + languageName: node + linkType: hard + "git-raw-commits@npm:^4.0.0": version: 4.0.0 resolution: "git-raw-commits@npm:4.0.0" @@ -13781,6 +15759,25 @@ __metadata: languageName: node linkType: hard +"git-up@npm:^8.1.0": + version: 8.1.1 + resolution: "git-up@npm:8.1.1" + dependencies: + is-ssh: "npm:^1.4.0" + parse-url: "npm:^9.2.0" + checksum: 10/475bfb816ee6003c505f25f2d6859148eedf684b2381f59774a8b4baf279d603710b888ecd1c5f5619fd7a582b131c2c52f8d247d59c7cc10bb7f7edffc704f7 + languageName: node + linkType: hard + +"git-url-parse@npm:16.1.0": + version: 16.1.0 + resolution: "git-url-parse@npm:16.1.0" + dependencies: + git-up: "npm:^8.1.0" + checksum: 10/1522e86ce89bb854ac5eaab13e71f9a72e93c4eb9897559d9731e03b700a7e2d38d16a77dddede9f79c8dac6f50633e2e420702bb1677580e797312e9b55379d + languageName: node + linkType: hard + "gl-matrix@npm:^3.4.3": version: 3.4.3 resolution: "gl-matrix@npm:3.4.3" @@ -13849,6 +15846,22 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.5.0": + version: 10.5.0 + resolution: "glob@npm:10.5.0" + dependencies: + foreground-child: "npm:^3.1.0" + jackspeak: "npm:^3.1.2" + minimatch: "npm:^9.0.4" + minipass: "npm:^7.1.2" + package-json-from-dist: "npm:^1.0.0" + path-scurry: "npm:^1.11.1" + bin: + glob: dist/esm/bin.mjs + checksum: 10/ab3bccfefcc0afaedbd1f480cd0c4a2c0e322eb3f0aa7ceaa31b3f00b825069f17cf0f1fc8b6f256795074b903f37c0ade37ddda6a176aa57f1c2bbfe7240653 + languageName: node + linkType: hard + "glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.2.0": version: 7.2.3 resolution: "glob@npm:7.2.3" @@ -13888,6 +15901,15 @@ __metadata: languageName: node linkType: hard +"global-directory@npm:^4.0.1": + version: 4.0.1 + resolution: "global-directory@npm:4.0.1" + dependencies: + ini: "npm:4.1.1" + checksum: 10/5b4df24438a4e5f21e43fbdd9e54f5e12bb48dce01a0a83b415d8052ce91be2d3a97e0c8f98a535e69649b2190036155e9f0f7d3c62f9318f31bdc3fd4f235f5 + languageName: node + linkType: hard + "global-dirs@npm:^0.1.1": version: 0.1.1 resolution: "global-dirs@npm:0.1.1" @@ -13935,7 +15957,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.0.1": +"globby@npm:^11.0.1, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -13949,6 +15971,20 @@ __metadata: languageName: node linkType: hard +"globby@npm:^14.0.2": + version: 14.1.0 + resolution: "globby@npm:14.1.0" + dependencies: + "@sindresorhus/merge-streams": "npm:^2.1.0" + fast-glob: "npm:^3.3.3" + ignore: "npm:^7.0.3" + path-type: "npm:^6.0.0" + slash: "npm:^5.1.0" + unicorn-magic: "npm:^0.3.0" + checksum: 10/e527ff54f0dddf60abfabd0d9e799768619d957feecd8b13ef60481f270bfdce0d28f6b09267c60f8064798fb3003b8ec991375f7fe0233fbce5304e1741368c + languageName: node + linkType: hard + "globrex@npm:^0.1.2": version: 0.1.2 resolution: "globrex@npm:0.1.2" @@ -14151,6 +16187,13 @@ __metadata: languageName: node linkType: hard +"hermes-estree@npm:0.28.1": + version: 0.28.1 + resolution: "hermes-estree@npm:0.28.1" + checksum: 10/3195a1aa7035d96b77839e6bfd6832b51830518aaf8dabfca11248b84d6fb6abd27e21c8caa84229954a76b4f8a1e346b65d421a4daecd3053bd2ea08fe6abc9 + languageName: node + linkType: hard + "hermes-estree@npm:0.29.1": version: 0.29.1 resolution: "hermes-estree@npm:0.29.1" @@ -14174,6 +16217,15 @@ __metadata: languageName: node linkType: hard +"hermes-parser@npm:0.28.1": + version: 0.28.1 + resolution: "hermes-parser@npm:0.28.1" + dependencies: + hermes-estree: "npm:0.28.1" + checksum: 10/cb2aa4d386929825c3bd8184eeb4e3dcf34892c1f850624d09a80aee0674bc2eb135eccaeb7ac33675552130229ee6160025c4e4f351d6a61b503bd8bfdf63f5 + languageName: node + linkType: hard + "hermes-parser@npm:0.29.1, hermes-parser@npm:^0.29.1": version: 0.29.1 resolution: "hermes-parser@npm:0.29.1" @@ -14226,6 +16278,15 @@ __metadata: languageName: node linkType: hard +"hosted-git-info@npm:^8.0.0": + version: 8.1.0 + resolution: "hosted-git-info@npm:8.1.0" + dependencies: + lru-cache: "npm:^10.0.1" + checksum: 10/872a1f3b5da6bff9d99410b96cf7ecb6415ef7d8c8842579cfb690144f40be4581cc4ea50d978829a5fc1ef0b1097151a722d14f905beaf3f09330e8ca40fa4c + languageName: node + linkType: hard + "hotkeys-js@npm:^3.13.15": version: 3.13.15 resolution: "hotkeys-js@npm:3.13.15" @@ -14338,6 +16399,13 @@ __metadata: languageName: node linkType: hard +"human-signals@npm:^5.0.0": + version: 5.0.0 + resolution: "human-signals@npm:5.0.0" + checksum: 10/30f8870d831cdcd2d6ec0486a7d35d49384996742052cee792854273fa9dd9e7d5db06bb7985d4953e337e10714e994e0302e90dc6848069171b05ec836d65b0 + languageName: node + linkType: hard + "humanize-ms@npm:^1.2.1": version: 1.2.1 resolution: "humanize-ms@npm:1.2.1" @@ -14418,14 +16486,14 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^5.0.4, ignore@npm:^5.1.1, ignore@npm:^5.2.0, ignore@npm:^5.3.1, ignore@npm:^5.3.2": +"ignore@npm:^5.0.4, ignore@npm:^5.0.5, ignore@npm:^5.1.1, ignore@npm:^5.2.0, ignore@npm:^5.3.1, ignore@npm:^5.3.2": version: 5.3.2 resolution: "ignore@npm:5.3.2" checksum: 10/cceb6a457000f8f6a50e1196429750d782afce5680dd878aa4221bd79972d68b3a55b4b1458fc682be978f4d3c6a249046aa0880637367216444ab7b014cfc98 languageName: node linkType: hard -"ignore@npm:^7.0.0": +"ignore@npm:^7.0.0, ignore@npm:^7.0.3": version: 7.0.5 resolution: "ignore@npm:7.0.5" checksum: 10/f134b96a4de0af419196f52c529d5c6120c4456ff8a6b5a14ceaaa399f883e15d58d2ce651c9b69b9388491d4669dda47285d307e827de9304a53a1824801bc6 @@ -14501,6 +16569,13 @@ __metadata: languageName: node linkType: hard +"import-meta-resolve@npm:^4.0.0": + version: 4.2.0 + resolution: "import-meta-resolve@npm:4.2.0" + checksum: 10/3499ee8b7eddb79be77067b368bcdf39e6f144306dea4686d08071ae7e65a2e3bdca3f98f2a0f4babdcd4ba9d9e7d379ae7e27c4b9bf8b08c1e812a28c674bf3 + languageName: node + linkType: hard + "imurmurhash@npm:^0.1.4": version: 0.1.4 resolution: "imurmurhash@npm:0.1.4" @@ -14546,6 +16621,13 @@ __metadata: languageName: node linkType: hard +"ini@npm:4.1.1": + version: 4.1.1 + resolution: "ini@npm:4.1.1" + checksum: 10/64c7102301742a7527bb17257d18451410eacf63b4b5648a20e108816c355c21c4e8a1761bbcbf3fe8c4ded3297f1b832b885d5e3e485d781e293ebfaf56fea6 + languageName: node + linkType: hard + "ini@npm:^1.3.4, ini@npm:~1.3.0": version: 1.3.8 resolution: "ini@npm:1.3.8" @@ -14569,6 +16651,26 @@ __metadata: languageName: node linkType: hard +"inquirer@npm:12.9.6": + version: 12.9.6 + resolution: "inquirer@npm:12.9.6" + dependencies: + "@inquirer/ansi": "npm:^1.0.0" + "@inquirer/core": "npm:^10.2.2" + "@inquirer/prompts": "npm:^7.8.6" + "@inquirer/type": "npm:^3.0.8" + mute-stream: "npm:^2.0.0" + run-async: "npm:^4.0.5" + rxjs: "npm:^7.8.2" + peerDependencies: + "@types/node": ">=18" + peerDependenciesMeta: + "@types/node": + optional: true + checksum: 10/bcac231b3eba055aa16dbdb60ba6d7bfe66109be654bfb19f92095f703af07fc01528f716e86ec62f7bf7bd17b4e21ad4bb32b677cf42075dee04568afe9686b + languageName: node + linkType: hard + "inquirer@npm:8.2.6": version: 8.2.6 resolution: "inquirer@npm:8.2.6" @@ -14812,6 +16914,15 @@ __metadata: languageName: node linkType: hard +"is-docker@npm:^3.0.0": + version: 3.0.0 + resolution: "is-docker@npm:3.0.0" + bin: + is-docker: cli.js + checksum: 10/b698118f04feb7eaf3338922bd79cba064ea54a1c3db6ec8c0c8d8ee7613e7e5854d802d3ef646812a8a3ace81182a085dfa0a71cc68b06f3fa794b9783b3c90 + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -14906,6 +17017,17 @@ __metadata: languageName: node linkType: hard +"is-inside-container@npm:^1.0.0": + version: 1.0.0 + resolution: "is-inside-container@npm:1.0.0" + dependencies: + is-docker: "npm:^3.0.0" + bin: + is-inside-container: cli.js + checksum: 10/c50b75a2ab66ab3e8b92b3bc534e1ea72ca25766832c0623ac22d134116a98bcf012197d1caabe1d1c4bd5f84363d4aa5c36bb4b585fbcaf57be172cd10a1a03 + languageName: node + linkType: hard + "is-interactive@npm:^1.0.0": version: 1.0.0 resolution: "is-interactive@npm:1.0.0" @@ -14913,6 +17035,13 @@ __metadata: languageName: node linkType: hard +"is-interactive@npm:^2.0.0": + version: 2.0.0 + resolution: "is-interactive@npm:2.0.0" + checksum: 10/e8d52ad490bed7ae665032c7675ec07732bbfe25808b0efbc4d5a76b1a1f01c165f332775c63e25e9a03d319ebb6b24f571a9e902669fc1e40b0a60b5be6e26c + languageName: node + linkType: hard + "is-lambda@npm:^1.0.1": version: 1.0.1 resolution: "is-lambda@npm:1.0.1" @@ -14975,6 +17104,13 @@ __metadata: languageName: node linkType: hard +"is-path-cwd@npm:^3.0.0": + version: 3.0.0 + resolution: "is-path-cwd@npm:3.0.0" + checksum: 10/bc34d13b6a03dfca4a3ab6a8a5ba78ae4b24f4f1db4b2b031d2760c60d0913bd16a4b980dcb4e590adfc906649d5f5132684079a3972bd219da49deebb9adea8 + languageName: node + linkType: hard + "is-path-inside@npm:^3.0.2": version: 3.0.3 resolution: "is-path-inside@npm:3.0.3" @@ -14982,6 +17118,13 @@ __metadata: languageName: node linkType: hard +"is-path-inside@npm:^4.0.0": + version: 4.0.0 + resolution: "is-path-inside@npm:4.0.0" + checksum: 10/8810fa11c58e6360b82c3e0d6cd7d9c7d0392d3ac9eb10f980b81f9839f40ac6d1d6d6f05d069db0d227759801228f0b072e1b6c343e4469b065ab5fe0b68fe5 + languageName: node + linkType: hard + "is-plain-obj@npm:^2.1.0": version: 2.1.0 resolution: "is-plain-obj@npm:2.1.0" @@ -15049,6 +17192,15 @@ __metadata: languageName: node linkType: hard +"is-ssh@npm:^1.4.0": + version: 1.4.1 + resolution: "is-ssh@npm:1.4.1" + dependencies: + protocols: "npm:^2.0.1" + checksum: 10/f60910cd83fa94e9874655a672c3849312c12af83c0fe3dbff9945755fe838a73985d8f94e32ebf5626ba4148ee10eef51b7240b0218dbb6e9a43a06899b0529 + languageName: node + linkType: hard + "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -15056,6 +17208,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^3.0.0": + version: 3.0.0 + resolution: "is-stream@npm:3.0.0" + checksum: 10/172093fe99119ffd07611ab6d1bcccfe8bc4aa80d864b15f43e63e54b7abc71e779acd69afdb854c4e2a67fdc16ae710e370eda40088d1cfc956a50ed82d8f16 + languageName: node + linkType: hard + "is-string@npm:^1.1.1": version: 1.1.1 resolution: "is-string@npm:1.1.1" @@ -15111,6 +17270,13 @@ __metadata: languageName: node linkType: hard +"is-unicode-supported@npm:^2.0.0, is-unicode-supported@npm:^2.1.0": + version: 2.1.0 + resolution: "is-unicode-supported@npm:2.1.0" + checksum: 10/f254e3da6b0ab1a57a94f7273a7798dd35d1d45b227759f600d0fa9d5649f9c07fa8d3c8a6360b0e376adf916d151ec24fc9a50c5295c58bae7ca54a76a063f9 + languageName: node + linkType: hard + "is-weakmap@npm:^2.0.2": version: 2.0.2 resolution: "is-weakmap@npm:2.0.2" @@ -15160,6 +17326,15 @@ __metadata: languageName: node linkType: hard +"is-wsl@npm:^3.1.0": + version: 3.1.0 + resolution: "is-wsl@npm:3.1.0" + dependencies: + is-inside-container: "npm:^1.0.0" + checksum: 10/f9734c81f2f9cf9877c5db8356bfe1ff61680f1f4c1011e91278a9c0564b395ae796addb4bf33956871041476ec82c3e5260ed57b22ac91794d4ae70a1d2f0a9 + languageName: node + linkType: hard + "isarray@npm:^2.0.5": version: 2.0.5 resolution: "isarray@npm:2.0.5" @@ -15190,6 +17365,19 @@ __metadata: languageName: node linkType: hard +"issue-parser@npm:7.0.1": + version: 7.0.1 + resolution: "issue-parser@npm:7.0.1" + dependencies: + lodash.capitalize: "npm:^4.2.1" + lodash.escaperegexp: "npm:^4.1.2" + lodash.isplainobject: "npm:^4.0.6" + lodash.isstring: "npm:^4.0.1" + lodash.uniqby: "npm:^4.7.0" + checksum: 10/04d14d987567008a270f5ed165e9464e131faf9c1d373b13c53bc7a1fe78b4e3aa91fcf2a1be2bac73983624084260b72957926fa35a1df4d60afbb61b645c95 + languageName: node + linkType: hard + "istanbul-lib-coverage@npm:^3.0.0, istanbul-lib-coverage@npm:^3.2.0, istanbul-lib-coverage@npm:^3.2.2": version: 3.2.2 resolution: "istanbul-lib-coverage@npm:3.2.2" @@ -15888,6 +18076,15 @@ __metadata: languageName: node linkType: hard +"jiti@npm:^2.6.1": + version: 2.6.1 + resolution: "jiti@npm:2.6.1" + bin: + jiti: lib/jiti-cli.mjs + checksum: 10/8cd72c5fd03a0502564c3f46c49761090f6dadead21fa191b73535724f095ad86c2fa89ee6fe4bc3515337e8d406cc8fb2d37b73fa0c99a34584bac35cd4a4de + languageName: node + linkType: hard + "joi@npm:^17.2.1": version: 17.9.1 resolution: "joi@npm:17.9.1" @@ -16220,6 +18417,117 @@ __metadata: languageName: node linkType: hard +"lefthook-darwin-arm64@npm:2.0.4": + version: 2.0.4 + resolution: "lefthook-darwin-arm64@npm:2.0.4" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"lefthook-darwin-x64@npm:2.0.4": + version: 2.0.4 + resolution: "lefthook-darwin-x64@npm:2.0.4" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"lefthook-freebsd-arm64@npm:2.0.4": + version: 2.0.4 + resolution: "lefthook-freebsd-arm64@npm:2.0.4" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + +"lefthook-freebsd-x64@npm:2.0.4": + version: 2.0.4 + resolution: "lefthook-freebsd-x64@npm:2.0.4" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"lefthook-linux-arm64@npm:2.0.4": + version: 2.0.4 + resolution: "lefthook-linux-arm64@npm:2.0.4" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"lefthook-linux-x64@npm:2.0.4": + version: 2.0.4 + resolution: "lefthook-linux-x64@npm:2.0.4" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"lefthook-openbsd-arm64@npm:2.0.4": + version: 2.0.4 + resolution: "lefthook-openbsd-arm64@npm:2.0.4" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + +"lefthook-openbsd-x64@npm:2.0.4": + version: 2.0.4 + resolution: "lefthook-openbsd-x64@npm:2.0.4" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"lefthook-windows-arm64@npm:2.0.4": + version: 2.0.4 + resolution: "lefthook-windows-arm64@npm:2.0.4" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"lefthook-windows-x64@npm:2.0.4": + version: 2.0.4 + resolution: "lefthook-windows-x64@npm:2.0.4" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"lefthook@npm:^2.0.3": + version: 2.0.4 + resolution: "lefthook@npm:2.0.4" + dependencies: + lefthook-darwin-arm64: "npm:2.0.4" + lefthook-darwin-x64: "npm:2.0.4" + lefthook-freebsd-arm64: "npm:2.0.4" + lefthook-freebsd-x64: "npm:2.0.4" + lefthook-linux-arm64: "npm:2.0.4" + lefthook-linux-x64: "npm:2.0.4" + lefthook-openbsd-arm64: "npm:2.0.4" + lefthook-openbsd-x64: "npm:2.0.4" + lefthook-windows-arm64: "npm:2.0.4" + lefthook-windows-x64: "npm:2.0.4" + dependenciesMeta: + lefthook-darwin-arm64: + optional: true + lefthook-darwin-x64: + optional: true + lefthook-freebsd-arm64: + optional: true + lefthook-freebsd-x64: + optional: true + lefthook-linux-arm64: + optional: true + lefthook-linux-x64: + optional: true + lefthook-openbsd-arm64: + optional: true + lefthook-openbsd-x64: + optional: true + lefthook-windows-arm64: + optional: true + lefthook-windows-x64: + optional: true + bin: + lefthook: bin/index.js + checksum: 10/dd0eb5737c50a386dbe9a877613ee21f495d86458f96a7ffae6335425232efa400d205c57f205cdba66a454d370557de20dcbda72818bb0e9a45608062c1b38e + languageName: node + linkType: hard + "leven@npm:^3.1.0": version: 3.1.0 resolution: "leven@npm:3.1.0" @@ -16465,7 +18773,7 @@ __metadata: languageName: node linkType: hard -"locate-path@npm:^7.1.0": +"locate-path@npm:^7.1.0, locate-path@npm:^7.2.0": version: 7.2.0 resolution: "locate-path@npm:7.2.0" dependencies: @@ -16488,6 +18796,13 @@ __metadata: languageName: node linkType: hard +"lodash.capitalize@npm:^4.2.1": + version: 4.2.1 + resolution: "lodash.capitalize@npm:4.2.1" + checksum: 10/54d61121bd040212954faee94703a999282987a104fab4ea6a85027d5fb2ce482a737478b76d292d07753da1c15911541adf0f6db840abf121c4cab85b92e962 + languageName: node + linkType: hard + "lodash.debounce@npm:^4.0.8": version: 4.0.8 resolution: "lodash.debounce@npm:4.0.8" @@ -16509,6 +18824,13 @@ __metadata: languageName: node linkType: hard +"lodash.escaperegexp@npm:^4.1.2": + version: 4.1.2 + resolution: "lodash.escaperegexp@npm:4.1.2" + checksum: 10/6d99452b1cfd6073175a9b741a9b09ece159eac463f86f02ea3bee2e2092923fce812c8d2bf446309cc52d1d61bf9af51c8118b0d7421388e6cead7bd3798f0f + languageName: node + linkType: hard + "lodash.includes@npm:^4.3.0": version: 4.3.0 resolution: "lodash.includes@npm:4.3.0" @@ -16551,6 +18873,13 @@ __metadata: languageName: node linkType: hard +"lodash.kebabcase@npm:^4.1.1": + version: 4.1.1 + resolution: "lodash.kebabcase@npm:4.1.1" + checksum: 10/d84ec5441ef8e5c718c50315f35b0a045a77c7e8ee3e54472c06dc31f6f3602e95551a16c0923d689198b51deb8902c4bbc54fc9b965b26c1f86e21df3a05f34 + languageName: node + linkType: hard + "lodash.memoize@npm:4.x": version: 4.1.2 resolution: "lodash.memoize@npm:4.1.2" @@ -16558,7 +18887,7 @@ __metadata: languageName: node linkType: hard -"lodash.merge@npm:^4.6.2": +"lodash.merge@npm:4.6.2, lodash.merge@npm:^4.6.2": version: 4.6.2 resolution: "lodash.merge@npm:4.6.2" checksum: 10/d0ea2dd0097e6201be083865d50c3fb54fbfbdb247d9cc5950e086c991f448b7ab0cdab0d57eacccb43473d3f2acd21e134db39f22dac2d6c9ba6bf26978e3d6 @@ -16579,6 +18908,20 @@ __metadata: languageName: node linkType: hard +"lodash.snakecase@npm:^4.1.1": + version: 4.1.1 + resolution: "lodash.snakecase@npm:4.1.1" + checksum: 10/82ed40935d840477ef8fee64f9f263f75989c6cde36b84aae817246d95826228e1b5a7f6093c51de324084f86433634c7af244cb89496633cacfe443071450d0 + languageName: node + linkType: hard + +"lodash.startcase@npm:^4.4.0": + version: 4.4.0 + resolution: "lodash.startcase@npm:4.4.0" + checksum: 10/3091048a54a2f92bcf2c6441d2bd9a706fb133d5f461ae7c310d6dca1530338a06c91e9e42a5b14b12e875ddae1814d448050dc02afe2cec09b3995d8e836837 + languageName: node + linkType: hard + "lodash.throttle@npm:^4.1.1": version: 4.1.1 resolution: "lodash.throttle@npm:4.1.1" @@ -16586,6 +18929,13 @@ __metadata: languageName: node linkType: hard +"lodash.uniq@npm:^4.5.0": + version: 4.5.0 + resolution: "lodash.uniq@npm:4.5.0" + checksum: 10/86246ca64ac0755c612e5df6d93cfe92f9ecac2e5ff054b965efbbb1d9a647b6310969e78545006f70f52760554b03233ad0103324121ae31474c20d5f7a2812 + languageName: node + linkType: hard + "lodash.uniqby@npm:^4.7.0": version: 4.7.0 resolution: "lodash.uniqby@npm:4.7.0" @@ -16593,7 +18943,14 @@ __metadata: languageName: node linkType: hard -"lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.21": +"lodash.upperfirst@npm:^4.3.1": + version: 4.3.1 + resolution: "lodash.upperfirst@npm:4.3.1" + checksum: 10/3e849d4eb4dbf26faee6435edda8e707b65a5dbd2f10f8def5a16a57bbbf38d3b7506950f0dd455e9c46ba73af35f1de75df4ef83952106949413d64eed59333 + languageName: node + linkType: hard + +"lodash@npm:^4.15.0, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.21": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 @@ -16619,6 +18976,16 @@ __metadata: languageName: node linkType: hard +"log-symbols@npm:^7.0.1": + version: 7.0.1 + resolution: "log-symbols@npm:7.0.1" + dependencies: + is-unicode-supported: "npm:^2.0.0" + yoctocolors: "npm:^2.1.1" + checksum: 10/0862313d84826b551582e39659b8586c56b65130c5f4f976420e2c23985228334f2a26fc4251ac22bf0a5b415d9430e86bf332557d934c10b036f9a549d63a09 + languageName: node + linkType: hard + "log-update@npm:^6.1.0": version: 6.1.0 resolution: "log-update@npm:6.1.0" @@ -16716,6 +19083,13 @@ __metadata: languageName: node linkType: hard +"macos-release@npm:^3.3.0": + version: 3.4.0 + resolution: "macos-release@npm:3.4.0" + checksum: 10/f4c0cb8b3f93b05d73c502b4bbe2b811c44facfc9bd072c13a30ff2a8ba1cad5d9de517d10be8b31e2b917643245a81587a2eec8300e66a7364419d11402ab02 + languageName: node + linkType: hard + "magic-string@npm:0.30.8": version: 0.30.8 resolution: "magic-string@npm:0.30.8" @@ -17100,6 +19474,13 @@ __metadata: languageName: node linkType: hard +"meow@npm:^13.0.0, meow@npm:^13.2.0": + version: 13.2.0 + resolution: "meow@npm:13.2.0" + checksum: 10/4eff5bc921fed0b8a471ad79069d741a0210036d717547d0c7f36fdaf84ef7a3036225f38b6a53830d84dc9cbf8b944b097fde62381b8b5b215119e735ce1063 + languageName: node + linkType: hard + "merge-options@npm:^3.0.4": version: 3.0.4 resolution: "merge-options@npm:3.0.4" @@ -18151,13 +20532,22 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:>= 1.43.0 < 2": +"mime-db@npm:>= 1.43.0 < 2, mime-db@npm:^1.54.0": version: 1.54.0 resolution: "mime-db@npm:1.54.0" checksum: 10/9e7834be3d66ae7f10eaa69215732c6d389692b194f876198dca79b2b90cbf96688d9d5d05ef7987b20f749b769b11c01766564264ea5f919c88b32a29011311 languageName: node linkType: hard +"mime-types@npm:3.0.1": + version: 3.0.1 + resolution: "mime-types@npm:3.0.1" + dependencies: + mime-db: "npm:^1.54.0" + checksum: 10/fa1d3a928363723a8046c346d87bf85d35014dae4285ad70a3ff92bd35957992b3094f8417973cfe677330916c6ef30885109624f1fb3b1e61a78af509dba120 + languageName: node + linkType: hard + "mime-types@npm:^2.1.12, mime-types@npm:^2.1.27, mime-types@npm:^2.1.35, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" @@ -18208,6 +20598,13 @@ __metadata: languageName: node linkType: hard +"mimic-fn@npm:^4.0.0": + version: 4.0.0 + resolution: "mimic-fn@npm:4.0.0" + checksum: 10/995dcece15ee29aa16e188de6633d43a3db4611bcf93620e7e62109ec41c79c0f34277165b8ce5e361205049766e371851264c21ac64ca35499acb5421c2ba56 + languageName: node + linkType: hard + "mimic-function@npm:^5.0.0": version: 5.0.1 resolution: "mimic-function@npm:5.0.1" @@ -18276,7 +20673,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6": +"minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10/908491b6cc15a6c440ba5b22780a0ba89b9810e1aea684e253e43c4e3b8d56ec1dcdd7ea96dde119c29df59c936cde16062159eae4225c691e19c70b432b6e6f @@ -18484,6 +20881,13 @@ __metadata: languageName: node linkType: hard +"mute-stream@npm:^2.0.0": + version: 2.0.0 + resolution: "mute-stream@npm:2.0.0" + checksum: 10/d2e4fd2f5aa342b89b98134a8d899d8ef9b0a6d69274c4af9df46faa2d97aeb1f2ce83d867880d6de63643c52386579b99139801e24e7526c3b9b0a6d1e18d6c + languageName: node + linkType: hard + "mz@npm:^2.7.0": version: 2.7.0 resolution: "mz@npm:2.7.0" @@ -18562,6 +20966,15 @@ __metadata: languageName: node linkType: hard +"new-github-release-url@npm:2.0.0": + version: 2.0.0 + resolution: "new-github-release-url@npm:2.0.0" + dependencies: + type-fest: "npm:^2.5.1" + checksum: 10/3d4ae0f3b775623ceed8e558b6f9850e897aea981a9c937d3ad4e018669c829beccb2c4b5a6af996726ebf86c5b7638368dfc01f3ac2e395d1df29309bc0c5ca + languageName: node + linkType: hard + "next-auth@npm:^4.24.11": version: 4.24.11 resolution: "next-auth@npm:4.24.11" @@ -18679,6 +21092,13 @@ __metadata: languageName: node linkType: hard +"node-fetch-native@npm:^1.6.6": + version: 1.6.7 + resolution: "node-fetch-native@npm:1.6.7" + checksum: 10/b8a99e6adafbdbdd9373a6784c467ca5c7b95eeed4896ee2d604f0729962fda8d07cf7a85edd1e8bb3ee51e791dc55c30cbebeb46cbd1f086d74141b3769a680 + languageName: node + linkType: hard + "node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.12, node-fetch@npm:^2.6.7": version: 2.6.13 resolution: "node-fetch@npm:2.6.13" @@ -18770,6 +21190,17 @@ __metadata: languageName: node linkType: hard +"normalize-package-data@npm:^7.0.0": + version: 7.0.1 + resolution: "normalize-package-data@npm:7.0.1" + dependencies: + hosted-git-info: "npm:^8.0.0" + semver: "npm:^7.3.5" + validate-npm-package-license: "npm:^3.0.4" + checksum: 10/8150d7e663303fb5b06b616416b512812c5805a7a2ed34272448beb000bc8fdfdb0aeea0c997f875a326bc0b8fa263819f765902dc67dd0f5c6b4350ebc22821 + languageName: node + linkType: hard + "normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" @@ -18805,6 +21236,15 @@ __metadata: languageName: node linkType: hard +"npm-run-path@npm:^5.1.0": + version: 5.3.0 + resolution: "npm-run-path@npm:5.3.0" + dependencies: + path-key: "npm:^4.0.0" + checksum: 10/ae8e7a89da9594fb9c308f6555c73f618152340dcaae423e5fb3620026fefbec463618a8b761920382d666fa7a2d8d240b6fe320e8a6cdd54dc3687e2b659d25 + languageName: node + linkType: hard + "npmlog@npm:^6.0.0": version: 6.0.2 resolution: "npmlog@npm:6.0.2" @@ -18925,6 +21365,21 @@ __metadata: languageName: node linkType: hard +"nypm@npm:^0.6.0": + version: 0.6.2 + resolution: "nypm@npm:0.6.2" + dependencies: + citty: "npm:^0.1.6" + consola: "npm:^3.4.2" + pathe: "npm:^2.0.3" + pkg-types: "npm:^2.3.0" + tinyexec: "npm:^1.0.1" + bin: + nypm: dist/cli.mjs + checksum: 10/3bbf25b02b9eab5565a9a11c1f0946d0065cc6a9028e8f438ebf5256f3139cfac0763a3852984a7ae92c761ab1c2ce881272f9b1a863107e195e7f7cae05b598 + languageName: node + linkType: hard + "oauth@npm:^0.9.15": version: 0.9.15 resolution: "oauth@npm:0.9.15" @@ -19065,6 +21520,13 @@ __metadata: languageName: node linkType: hard +"ohash@npm:^2.0.11": + version: 2.0.11 + resolution: "ohash@npm:2.0.11" + checksum: 10/6b0423f42cc95c3d643f390a88364aac824178b7788dccb4e8c64e2124463d0069e60d4d90bad88ed9823808368d051e088aa27058ca4722b1397a201ffbfa4b + languageName: node + linkType: hard + "oidc-token-hash@npm:^5.0.1": version: 5.0.1 resolution: "oidc-token-hash@npm:5.0.1" @@ -19124,6 +21586,15 @@ __metadata: languageName: node linkType: hard +"onetime@npm:^6.0.0": + version: 6.0.0 + resolution: "onetime@npm:6.0.0" + dependencies: + mimic-fn: "npm:^4.0.0" + checksum: 10/0846ce78e440841335d4e9182ef69d5762e9f38aa7499b19f42ea1c4cd40f0b4446094c455c713f9adac3f4ae86f613bb5e30c99e52652764d06a89f709b3788 + languageName: node + linkType: hard + "onetime@npm:^7.0.0": version: 7.0.0 resolution: "onetime@npm:7.0.0" @@ -19133,6 +21604,18 @@ __metadata: languageName: node linkType: hard +"open@npm:10.2.0": + version: 10.2.0 + resolution: "open@npm:10.2.0" + dependencies: + default-browser: "npm:^5.2.1" + define-lazy-prop: "npm:^3.0.0" + is-inside-container: "npm:^1.0.0" + wsl-utils: "npm:^0.1.0" + checksum: 10/e6ad9474734eac3549dcc7d85e952394856ccaee48107c453bd6a725b82e3b8ed5f427658935df27efa76b411aeef62888edea8a9e347e8e7c82632ec966b30e + languageName: node + linkType: hard + "open@npm:^6.2.0": version: 6.4.0 resolution: "open@npm:6.4.0" @@ -19205,6 +21688,23 @@ __metadata: languageName: node linkType: hard +"ora@npm:9.0.0": + version: 9.0.0 + resolution: "ora@npm:9.0.0" + dependencies: + chalk: "npm:^5.6.2" + cli-cursor: "npm:^5.0.0" + cli-spinners: "npm:^3.2.0" + is-interactive: "npm:^2.0.0" + is-unicode-supported: "npm:^2.1.0" + log-symbols: "npm:^7.0.1" + stdin-discarder: "npm:^0.2.2" + string-width: "npm:^8.1.0" + strip-ansi: "npm:^7.1.2" + checksum: 10/b6074c9cec4a39c1b4f41c2ce2741982a99c53c86bd6f07a28fb6274857263af7fe1a340136629939934b553af35b03fc62ca2a88baa6803b2f9bfdf269fb850 + languageName: node + linkType: hard + "ora@npm:^3.4.0": version: 3.4.0 resolution: "ora@npm:3.4.0" @@ -19236,6 +21736,16 @@ __metadata: languageName: node linkType: hard +"os-name@npm:6.1.0": + version: 6.1.0 + resolution: "os-name@npm:6.1.0" + dependencies: + macos-release: "npm:^3.3.0" + windows-release: "npm:^6.1.0" + checksum: 10/d69a2060bea01dc502bd9a08802f43bebce85e95adde7740d0629a8522c16a92c05e0ee052819cac49f82aa61324ff038a3b79e015e26f122bbc08b40aa4ead3 + languageName: node + linkType: hard + "os-tmpdir@npm:~1.0.2": version: 1.0.2 resolution: "os-tmpdir@npm:1.0.2" @@ -19326,6 +21836,13 @@ __metadata: languageName: node linkType: hard +"p-map@npm:^7.0.2": + version: 7.0.4 + resolution: "p-map@npm:7.0.4" + checksum: 10/ef48c3b2e488f31c693c9fcc0df0ef76518cf6426a495cf9486ebbb0fd7f31aef7f90e96f72e0070c0ff6e3177c9318f644b512e2c29e3feee8d7153fcb6782e + languageName: node + linkType: hard + "p-try@npm:^2.0.0": version: 2.2.0 resolution: "p-try@npm:2.2.0" @@ -19425,6 +21942,15 @@ __metadata: languageName: node linkType: hard +"parse-path@npm:^7.0.0": + version: 7.1.0 + resolution: "parse-path@npm:7.1.0" + dependencies: + protocols: "npm:^2.0.0" + checksum: 10/6da6c6803fa73bacfee98e694c6c95fa55caae632c765369e4fd917f1043ef71f35ecaae420ef0e39e933bd1f939c4bc1e01522b62145191cdbe72e58d37a8ab + languageName: node + linkType: hard + "parse-png@npm:^2.1.0": version: 2.1.0 resolution: "parse-png@npm:2.1.0" @@ -19434,6 +21960,16 @@ __metadata: languageName: node linkType: hard +"parse-url@npm:^9.2.0": + version: 9.2.0 + resolution: "parse-url@npm:9.2.0" + dependencies: + "@types/parse-path": "npm:^7.0.0" + parse-path: "npm:^7.0.0" + checksum: 10/d2746f0dbcd34d39df966a0726c00ede272aa34d825513baca721ad95480786c664f91ab22cf4e79cdb130468056e41834f6c9cc912b9180539f73aa5bafa982 + languageName: node + linkType: hard + "parse5@npm:^7.0.0, parse5@npm:^7.1.1": version: 7.2.1 resolution: "parse5@npm:7.2.1" @@ -19495,6 +22031,13 @@ __metadata: languageName: node linkType: hard +"path-key@npm:^4.0.0": + version: 4.0.0 + resolution: "path-key@npm:4.0.0" + checksum: 10/8e6c314ae6d16b83e93032c61020129f6f4484590a777eed709c4a01b50e498822b00f76ceaf94bc64dbd90b327df56ceadce27da3d83393790f1219e07721d7 + languageName: node + linkType: hard + "path-parse@npm:^1.0.5, path-parse@npm:^1.0.7": version: 1.0.7 resolution: "path-parse@npm:1.0.7" @@ -19536,6 +22079,13 @@ __metadata: languageName: node linkType: hard +"path-type@npm:^6.0.0": + version: 6.0.0 + resolution: "path-type@npm:6.0.0" + checksum: 10/b9f6eaf7795c48d5c9bc4c6bc3ac61315b8d36975a73497ab2e02b764c0836b71fb267ea541863153f633a069a1c2ed3c247cb781633842fc571c655ac57c00e + languageName: node + linkType: hard + "path@npm:0.12.7": version: 0.12.7 resolution: "path@npm:0.12.7" @@ -19572,6 +22122,13 @@ __metadata: languageName: node linkType: hard +"perfect-debounce@npm:^2.0.0": + version: 2.0.0 + resolution: "perfect-debounce@npm:2.0.0" + checksum: 10/3f889ab0fe22d84daf49564d4a0b5e823f67beeb1974a2ea26f139b3d90f56a8f81eabc8d317b56caa05c5ee1bf06d2ae6c0aac4bab8f266c6db2aa89ae16ed8 + languageName: node + linkType: hard + "pg-int8@npm:1.0.1": version: 1.0.1 resolution: "pg-int8@npm:1.0.1" @@ -19666,6 +22223,17 @@ __metadata: languageName: node linkType: hard +"pkg-types@npm:^2.3.0": + version: 2.3.0 + resolution: "pkg-types@npm:2.3.0" + dependencies: + confbox: "npm:^0.2.2" + exsolve: "npm:^1.0.7" + pathe: "npm:^2.0.3" + checksum: 10/4b36e4eb12693a1beb145573c564ec6fb74b1008d3b457eaa1f0072331edf05cb7c479c47fe0c4bfdec76c2caff5b68215ff270e5fe49634c07984a7a0197118 + languageName: node + linkType: hard + "pkg-up@npm:^3.1.0": version: 3.1.0 resolution: "pkg-up@npm:3.1.0" @@ -19894,6 +22462,13 @@ __metadata: languageName: node linkType: hard +"presentable-error@npm:^0.0.1": + version: 0.0.1 + resolution: "presentable-error@npm:0.0.1" + checksum: 10/013809ee7a47ced847a8d860e9b89a56cdd8c4f1ad04ad8da1e58fd60843f77f497d204146bb15aaa9793d3b94ad8626eed01256fc9eb5839a545af2000a5fa4 + languageName: node + linkType: hard + "prettier-linter-helpers@npm:^1.0.0": version: 1.0.0 resolution: "prettier-linter-helpers@npm:1.0.0" @@ -19903,7 +22478,7 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^3.6.2": +"prettier@npm:^3.4.2, prettier@npm:^3.6.2": version: 3.6.2 resolution: "prettier@npm:3.6.2" bin: @@ -20066,6 +22641,13 @@ __metadata: languageName: node linkType: hard +"protocols@npm:^2.0.0, protocols@npm:^2.0.1": + version: 2.0.2 + resolution: "protocols@npm:2.0.2" + checksum: 10/031cc068eb800468a50eb7c1e1c528bf142fb8314f5df9b9ea3c3f9df1697a19f97b9915b1229cef694d156812393172d9c3051ef7878d26eaa8c6faa5cccec4 + languageName: node + linkType: hard + "proxy-agent@npm:6.5.0": version: 6.5.0 resolution: "proxy-agent@npm:6.5.0" @@ -20222,6 +22804,16 @@ __metadata: languageName: node linkType: hard +"rc9@npm:^2.1.2": + version: 2.1.2 + resolution: "rc9@npm:2.1.2" + dependencies: + defu: "npm:^6.1.4" + destr: "npm:^2.0.3" + checksum: 10/0694d2a80579983a5e4f0452092d9f6a06b785b104b32f48f3d6bb263f637e53d9ebd1fd77a41b157b84c1c7e8e4ecc87c3824907738653a296e6d2faf3d1844 + languageName: node + linkType: hard + "rc@npm:~1.2.7": version: 1.2.8 resolution: "rc@npm:1.2.8" @@ -20393,6 +22985,38 @@ __metadata: languageName: node linkType: hard +"react-native-builder-bob@npm:^0.40.15": + version: 0.40.16 + resolution: "react-native-builder-bob@npm:0.40.16" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/plugin-transform-flow-strip-types": "npm:^7.26.5" + "@babel/plugin-transform-strict-mode": "npm:^7.24.7" + "@babel/preset-env": "npm:^7.25.2" + "@babel/preset-react": "npm:^7.24.7" + "@babel/preset-typescript": "npm:^7.24.7" + arktype: "npm:^2.1.15" + babel-plugin-syntax-hermes-parser: "npm:^0.28.0" + browserslist: "npm:^4.20.4" + cross-spawn: "npm:^7.0.3" + dedent: "npm:^0.7.0" + del: "npm:^6.1.1" + escape-string-regexp: "npm:^4.0.0" + fs-extra: "npm:^10.1.0" + glob: "npm:^10.5.0" + is-git-dirty: "npm:^2.0.1" + json5: "npm:^2.2.1" + kleur: "npm:^4.1.4" + prompts: "npm:^2.4.2" + react-native-monorepo-config: "npm:^0.1.8" + which: "npm:^2.0.2" + yargs: "npm:^17.5.1" + bin: + bob: bin/bob + checksum: 10/7f9c6e02131a833d68df92e6338f86e2880c7404116c5104c7eaa1cb8517ed559f2de591306b6d67e0af39f4f4fef9894b69b3fd624dce35822fb7d4b752776b + languageName: node + linkType: hard + "react-native-builder-bob@npm:~0.23": version: 0.23.2 resolution: "react-native-builder-bob@npm:0.23.2" @@ -20422,6 +23046,40 @@ __metadata: languageName: node linkType: hard +"react-native-callingx@workspace:^, react-native-callingx@workspace:packages/react-native-callingx": + version: 0.0.0-use.local + resolution: "react-native-callingx@workspace:packages/react-native-callingx" + dependencies: + "@commitlint/config-conventional": "npm:^19.8.1" + "@eslint/compat": "npm:^1.3.2" + "@eslint/eslintrc": "npm:^3.3.1" + "@eslint/js": "npm:^9.35.0" + "@react-native-community/cli": "npm:20.0.1" + "@react-native/babel-preset": "npm:0.81.1" + "@react-native/eslint-config": "npm:^0.81.1" + "@release-it/conventional-changelog": "npm:^10.0.1" + "@types/jest": "npm:^29.5.14" + "@types/react": "npm:^19.1.0" + commitlint: "npm:^19.8.1" + del-cli: "npm:^6.0.0" + eslint: "npm:^9.35.0" + eslint-config-prettier: "npm:^10.1.8" + eslint-plugin-prettier: "npm:^5.5.4" + jest: "npm:^29.7.0" + lefthook: "npm:^2.0.3" + prettier: "npm:^3.4.2" + react: "npm:19.1.0" + react-native: "npm:0.81.1" + react-native-builder-bob: "npm:^0.40.15" + release-it: "npm:^19.0.4" + turbo: "npm:^2.5.6" + typescript: "npm:^5.9.2" + peerDependencies: + react: "*" + react-native: "*" + languageName: unknown + linkType: soft + "react-native-callkeep@npm:^4.3.16": version: 4.3.16 resolution: "react-native-callkeep@npm:4.3.16" @@ -20524,6 +23182,16 @@ __metadata: languageName: node linkType: hard +"react-native-monorepo-config@npm:^0.1.8": + version: 0.1.10 + resolution: "react-native-monorepo-config@npm:0.1.10" + dependencies: + escape-string-regexp: "npm:^5.0.0" + fast-glob: "npm:^3.3.3" + checksum: 10/36611eca9cbda6647111e659d5c466fdba002c608172b9d25880b6e3ac95c51f41d15520e06d9d3188c096b0c9182caeba7b9340c64f6b45f1fee331c08b877b + languageName: node + linkType: hard + "react-native-permissions@npm:^5.4.2": version: 5.4.2 resolution: "react-native-permissions@npm:5.4.2" @@ -20717,6 +23385,56 @@ __metadata: languageName: node linkType: hard +"react-native@npm:0.81.1": + version: 0.81.1 + resolution: "react-native@npm:0.81.1" + dependencies: + "@jest/create-cache-key-function": "npm:^29.7.0" + "@react-native/assets-registry": "npm:0.81.1" + "@react-native/codegen": "npm:0.81.1" + "@react-native/community-cli-plugin": "npm:0.81.1" + "@react-native/gradle-plugin": "npm:0.81.1" + "@react-native/js-polyfills": "npm:0.81.1" + "@react-native/normalize-colors": "npm:0.81.1" + "@react-native/virtualized-lists": "npm:0.81.1" + abort-controller: "npm:^3.0.0" + anser: "npm:^1.4.9" + ansi-regex: "npm:^5.0.0" + babel-jest: "npm:^29.7.0" + babel-plugin-syntax-hermes-parser: "npm:0.29.1" + base64-js: "npm:^1.5.1" + commander: "npm:^12.0.0" + flow-enums-runtime: "npm:^0.0.6" + glob: "npm:^7.1.1" + invariant: "npm:^2.2.4" + jest-environment-node: "npm:^29.7.0" + memoize-one: "npm:^5.0.0" + metro-runtime: "npm:^0.83.1" + metro-source-map: "npm:^0.83.1" + nullthrows: "npm:^1.1.1" + pretty-format: "npm:^29.7.0" + promise: "npm:^8.3.0" + react-devtools-core: "npm:^6.1.5" + react-refresh: "npm:^0.14.0" + regenerator-runtime: "npm:^0.13.2" + scheduler: "npm:0.26.0" + semver: "npm:^7.1.3" + stacktrace-parser: "npm:^0.1.10" + whatwg-fetch: "npm:^3.0.0" + ws: "npm:^6.2.3" + yargs: "npm:^17.6.2" + peerDependencies: + "@types/react": ^19.1.0 + react: ^19.1.0 + peerDependenciesMeta: + "@types/react": + optional: true + bin: + react-native: cli.js + checksum: 10/f6b99f5225ddb2ffa8e10d24e56b3fc47e96fc217c2e1682de6f7fd2728be6d1172f2be21442ca31b34ec552317cb5813288d3921821d1eda22631ecefe1c891 + languageName: node + linkType: hard + "react-native@npm:^0.81.5": version: 0.81.5 resolution: "react-native@npm:0.81.5" @@ -20988,7 +23706,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": +"readable-stream@npm:^3.0.2, readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0, readable-stream@npm:^3.6.0": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -21139,6 +23857,39 @@ __metadata: languageName: node linkType: hard +"release-it@npm:^19.0.4": + version: 19.0.6 + resolution: "release-it@npm:19.0.6" + dependencies: + "@nodeutils/defaults-deep": "npm:1.1.0" + "@octokit/rest": "npm:22.0.0" + "@phun-ky/typeof": "npm:2.0.3" + async-retry: "npm:1.3.3" + c12: "npm:3.3.1" + ci-info: "npm:^4.3.0" + eta: "npm:4.0.1" + git-url-parse: "npm:16.1.0" + inquirer: "npm:12.9.6" + issue-parser: "npm:7.0.1" + lodash.merge: "npm:4.6.2" + mime-types: "npm:3.0.1" + new-github-release-url: "npm:2.0.0" + open: "npm:10.2.0" + ora: "npm:9.0.0" + os-name: "npm:6.1.0" + proxy-agent: "npm:6.5.0" + semver: "npm:7.7.2" + tinyglobby: "npm:0.2.15" + undici: "npm:6.21.3" + url-join: "npm:5.0.0" + wildcard-match: "npm:5.1.4" + yargs-parser: "npm:21.1.1" + bin: + release-it: bin/release-it.js + checksum: 10/99eca72bc55f1dbe6a03e1365f27013d81f502ecc9b74962bafa8524a7ce2d74812441c1deb123ce22fbc77367d5dadc76a0da84a2974ddcc1b17d3e295f45df + languageName: node + linkType: hard + "remark-gfm@npm:^4.0.1": version: 4.0.1 resolution: "remark-gfm@npm:4.0.1" @@ -21441,6 +24192,13 @@ __metadata: languageName: node linkType: hard +"retry@npm:0.13.1": + version: 0.13.1 + resolution: "retry@npm:0.13.1" + checksum: 10/6125ec2e06d6e47e9201539c887defba4e47f63471db304c59e4b82fc63c8e89ca06a77e9d34939a9a42a76f00774b2f46c0d4a4cbb3e287268bd018ed69426d + languageName: node + linkType: hard + "retry@npm:^0.12.0": version: 0.12.0 resolution: "retry@npm:0.12.0" @@ -21600,6 +24358,13 @@ __metadata: languageName: node linkType: hard +"run-applescript@npm:^7.0.0": + version: 7.1.0 + resolution: "run-applescript@npm:7.1.0" + checksum: 10/8659fb5f2717b2b37a68cbfe5f678254cf24b5a82a6df3372b180c80c7c137dcd757a4166c3887e459f59a090ca414e8ea7ca97cf3ee5123db54b3b4006d7b7a + languageName: node + linkType: hard + "run-async@npm:^2.4.0": version: 2.4.1 resolution: "run-async@npm:2.4.1" @@ -21607,6 +24372,13 @@ __metadata: languageName: node linkType: hard +"run-async@npm:^4.0.5": + version: 4.0.6 + resolution: "run-async@npm:4.0.6" + checksum: 10/d23929e36d0422b871a8964d5cfcb1b88295950ea5f72e1dfed458d4c3f3a33a7395e08167d8a4446f2110cfaac7d7653d9c804d2becab8afa8a63e16b97da81 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -21623,7 +24395,7 @@ __metadata: languageName: node linkType: hard -"rxjs@npm:7.8.2, rxjs@npm:^7.5.5, rxjs@npm:~7.8.2": +"rxjs@npm:7.8.2, rxjs@npm:^7.5.5, rxjs@npm:^7.8.2, rxjs@npm:~7.8.2": version: 7.8.2 resolution: "rxjs@npm:7.8.2" dependencies: @@ -21770,6 +24542,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.3.7, semver@npm:^7.7.3": + version: 7.7.3 + resolution: "semver@npm:7.7.3" + bin: + semver: bin/semver.js + checksum: 10/8dbc3168e057a38fc322af909c7f5617483c50caddba135439ff09a754b20bdd6482a5123ff543dad4affa488ecf46ec5fb56d61312ad20bb140199b88dfaea9 + languageName: node + linkType: hard + "semver@npm:~7.6.3": version: 7.6.3 resolution: "semver@npm:7.6.3" @@ -22146,7 +24927,7 @@ __metadata: languageName: node linkType: hard -"slash@npm:^5.0.0": +"slash@npm:^5.0.0, slash@npm:^5.1.0": version: 5.1.0 resolution: "slash@npm:5.1.0" checksum: 10/2c41ec6fb1414cd9bba0fa6b1dd00e8be739e3fe85d079c69d4b09ca5f2f86eafd18d9ce611c0c0f686428638a36c272a6ac14799146a8295f259c10cc45cde4 @@ -22434,6 +25215,13 @@ __metadata: languageName: node linkType: hard +"stdin-discarder@npm:^0.2.2": + version: 0.2.2 + resolution: "stdin-discarder@npm:0.2.2" + checksum: 10/642ffd05bd5b100819d6b24a613d83c6e3857c6de74eb02fc51506fa61dc1b0034665163831873868157c4538d71e31762bcf319be86cea04c3aba5336470478 + languageName: node + linkType: hard + "stop-iteration-iterator@npm:^1.1.0": version: 1.1.0 resolution: "stop-iteration-iterator@npm:1.1.0" @@ -22648,6 +25436,13 @@ __metadata: languageName: node linkType: hard +"string-natural-compare@npm:^3.0.1": + version: 3.0.1 + resolution: "string-natural-compare@npm:3.0.1" + checksum: 10/bc1fd0ee196466489e121bbe11844094ddcdee5a687dca9dbb18ba2ace73b1f6c96c9b448df2dfed0879b781b6b12e329ca1c1fc0a86d70b00c7823b76109b1e + languageName: node + linkType: hard + "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -22681,7 +25476,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^8.0.0": +"string-width@npm:^8.0.0, string-width@npm:^8.1.0": version: 8.1.0 resolution: "string-width@npm:8.1.0" dependencies: @@ -22815,6 +25610,15 @@ __metadata: languageName: node linkType: hard +"strip-ansi@npm:^7.1.2": + version: 7.1.2 + resolution: "strip-ansi@npm:7.1.2" + dependencies: + ansi-regex: "npm:^6.0.1" + checksum: 10/db0e3f9654e519c8a33c50fc9304d07df5649388e7da06d3aabf66d29e5ad65d5e6315d8519d409c15b32fa82c1df7e11ed6f8cd50b0e4404463f0c9d77c8d0b + languageName: node + linkType: hard + "strip-bom@npm:^3.0.0": version: 3.0.0 resolution: "strip-bom@npm:3.0.0" @@ -22836,6 +25640,13 @@ __metadata: languageName: node linkType: hard +"strip-final-newline@npm:^3.0.0": + version: 3.0.0 + resolution: "strip-final-newline@npm:3.0.0" + checksum: 10/23ee263adfa2070cd0f23d1ac14e2ed2f000c9b44229aec9c799f1367ec001478469560abefd00c5c99ee6f0b31c137d53ec6029c53e9f32a93804e18c201050 + languageName: node + linkType: hard + "strip-indent@npm:^3.0.0": version: 3.0.0 resolution: "strip-indent@npm:3.0.0" @@ -23263,7 +26074,14 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": +"tinyexec@npm:^1.0.0, tinyexec@npm:^1.0.1": + version: 1.0.2 + resolution: "tinyexec@npm:1.0.2" + checksum: 10/cb709ed4240e873d3816e67f851d445f5676e0ae3a52931a60ff571d93d388da09108c8057b62351766133ee05ff3159dd56c3a0fbd39a5933c6639ce8771405 + languageName: node + linkType: hard + +"tinyglobby@npm:0.2.15, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -23404,6 +26222,15 @@ __metadata: languageName: node linkType: hard +"ts-api-utils@npm:^1.3.0": + version: 1.4.3 + resolution: "ts-api-utils@npm:1.4.3" + peerDependencies: + typescript: ">=4.2.0" + checksum: 10/713c51e7392323305bd4867422ba130fbf70873ef6edbf80ea6d7e9c8f41eeeb13e40e8e7fe7cd321d74e4864777329797077268c9f570464303a1723f1eed39 + languageName: node + linkType: hard + "ts-api-utils@npm:^2.1.0": version: 2.1.0 resolution: "ts-api-utils@npm:2.1.0" @@ -23506,6 +26333,95 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^1.8.1": + version: 1.14.1 + resolution: "tslib@npm:1.14.1" + checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb + languageName: node + linkType: hard + +"tsutils@npm:^3.21.0": + version: 3.21.0 + resolution: "tsutils@npm:3.21.0" + dependencies: + tslib: "npm:^1.8.1" + peerDependencies: + typescript: ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + checksum: 10/ea036bec1dd024e309939ffd49fda7a351c0e87a1b8eb049570dd119d447250e2c56e0e6c00554e8205760e7417793fdebff752a46e573fbe07d4f375502a5b2 + languageName: node + linkType: hard + +"turbo-darwin-64@npm:2.6.1": + version: 2.6.1 + resolution: "turbo-darwin-64@npm:2.6.1" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"turbo-darwin-arm64@npm:2.6.1": + version: 2.6.1 + resolution: "turbo-darwin-arm64@npm:2.6.1" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"turbo-linux-64@npm:2.6.1": + version: 2.6.1 + resolution: "turbo-linux-64@npm:2.6.1" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"turbo-linux-arm64@npm:2.6.1": + version: 2.6.1 + resolution: "turbo-linux-arm64@npm:2.6.1" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"turbo-windows-64@npm:2.6.1": + version: 2.6.1 + resolution: "turbo-windows-64@npm:2.6.1" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"turbo-windows-arm64@npm:2.6.1": + version: 2.6.1 + resolution: "turbo-windows-arm64@npm:2.6.1" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"turbo@npm:^2.5.6": + version: 2.6.1 + resolution: "turbo@npm:2.6.1" + dependencies: + turbo-darwin-64: "npm:2.6.1" + turbo-darwin-arm64: "npm:2.6.1" + turbo-linux-64: "npm:2.6.1" + turbo-linux-arm64: "npm:2.6.1" + turbo-windows-64: "npm:2.6.1" + turbo-windows-arm64: "npm:2.6.1" + dependenciesMeta: + turbo-darwin-64: + optional: true + turbo-darwin-arm64: + optional: true + turbo-linux-64: + optional: true + turbo-linux-arm64: + optional: true + turbo-windows-64: + optional: true + turbo-windows-arm64: + optional: true + bin: + turbo: bin/turbo + checksum: 10/7bbd7cd3d36ba4d9061c32b07f1d86de946add71d29b5d9098841c195d2d6d60128db2da8b4fa1ed9c69959e544d0e4b063ecf47fdbfb70d7248bfb4dac8f30a + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -23536,6 +26452,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^2.5.1": + version: 2.19.0 + resolution: "type-fest@npm:2.19.0" + checksum: 10/7bf9e8fdf34f92c8bb364c0af14ca875fac7e0183f2985498b77be129dc1b3b1ad0a6b3281580f19e48c6105c037fb966ad9934520c69c6434d17fd0af4eed78 + languageName: node + linkType: hard + "type-fest@npm:^3.8.0": version: 3.13.1 resolution: "type-fest@npm:3.13.1" @@ -23627,6 +26550,13 @@ __metadata: languageName: node linkType: hard +"typedarray@npm:^0.0.6": + version: 0.0.6 + resolution: "typedarray@npm:0.0.6" + checksum: 10/2cc1bcf7d8c1237f6a16c04efc06637b2c5f2d74e58e84665445cf87668b85a21ab18dd751fa49eee6ae024b70326635d7b79ad37b1c370ed2fec6aeeeb52714 + languageName: node + linkType: hard + "typescript-eslint@npm:^8.46.0": version: 8.46.0 resolution: "typescript-eslint@npm:8.46.0" @@ -23736,6 +26666,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:6.21.3": + version: 6.21.3 + resolution: "undici@npm:6.21.3" + checksum: 10/b6b8f4a90e294c11fabbb174b471a310840695ed0154a44b81e9bb4a08867ed738c8a7eac4eb46c7902d502fbccf03fa2cf2e5f18d9a2218d82e4294e3f74a2b + languageName: node + linkType: hard + "undici@npm:^6.18.2": version: 6.21.0 resolution: "undici@npm:6.21.0" @@ -23774,6 +26711,20 @@ __metadata: languageName: node linkType: hard +"unicorn-magic@npm:^0.1.0": + version: 0.1.0 + resolution: "unicorn-magic@npm:0.1.0" + checksum: 10/9b4d0e9809807823dc91d0920a4a4c0cff2de3ebc54ee87ac1ee9bc75eafd609b09d1f14495e0173aef26e01118706196b6ab06a75fe0841028b3983a8af313f + languageName: node + linkType: hard + +"unicorn-magic@npm:^0.3.0": + version: 0.3.0 + resolution: "unicorn-magic@npm:0.3.0" + checksum: 10/bdd7d7c522f9456f32a0b77af23f8854f9a7db846088c3868ec213f9550683ab6a2bdf3803577eacbafddb4e06900974385841ccb75338d17346ccef45f9cb01 + languageName: node + linkType: hard + "unified@npm:^11.0.0": version: 11.0.5 resolution: "unified@npm:11.0.5" @@ -23880,6 +26831,13 @@ __metadata: languageName: node linkType: hard +"universal-user-agent@npm:^7.0.0, universal-user-agent@npm:^7.0.2": + version: 7.0.3 + resolution: "universal-user-agent@npm:7.0.3" + checksum: 10/c497e85f8b11eb8fa4dce584d7a39cc98710164959f494cafc3c269b51abb20fff269951838efd7424d15f6b3d001507f3cb8b52bb5676fdb642019dfd17e63e + languageName: node + linkType: hard + "universalify@npm:^0.1.0": version: 0.1.2 resolution: "universalify@npm:0.1.2" @@ -23943,6 +26901,13 @@ __metadata: languageName: node linkType: hard +"url-join@npm:5.0.0": + version: 5.0.0 + resolution: "url-join@npm:5.0.0" + checksum: 10/5921384a8ad4395b49ce4b50aa26efbc429cebe0bc8b3660ad693dd12fd859747b5369be0443e60e53a7850b2bc9d7d0687bcb94386662b40e743596bbf38101 + languageName: node + linkType: hard + "url-parse@npm:^1.5.3": version: 1.5.10 resolution: "url-parse@npm:1.5.10" @@ -24335,6 +27300,13 @@ __metadata: languageName: node linkType: hard +"walk-up-path@npm:^4.0.0": + version: 4.0.0 + resolution: "walk-up-path@npm:4.0.0" + checksum: 10/6a230b20e5de296895116dc12b09dafaec1f72b8060c089533d296e241aff059dfaebe0d015c77467f857e4b40c78e08f7481add76f340233a1f34fa8af9ed63 + languageName: node + linkType: hard + "walker@npm:^1.0.7, walker@npm:^1.0.8": version: 1.0.8 resolution: "walker@npm:1.0.8" @@ -24590,6 +27562,22 @@ __metadata: languageName: node linkType: hard +"wildcard-match@npm:5.1.4": + version: 5.1.4 + resolution: "wildcard-match@npm:5.1.4" + checksum: 10/4a6821e91def808a38aa7c7c2e774c5b6dc8c6eb37743f18fae182802af52f19f19375de1bc1d465ff1e6b188489eab2716bf3eba1789b25b3f495f55e10fc57 + languageName: node + linkType: hard + +"windows-release@npm:^6.1.0": + version: 6.1.0 + resolution: "windows-release@npm:6.1.0" + dependencies: + execa: "npm:^8.0.1" + checksum: 10/2af39c94d5e4e250c3239e70177f3a97291c505e364b85a7ae63ca9d06c91496e8bd3a75c55e03184d9c27e58c0a0fa21a4a8457ac72cc560d8796a75f12d0a3 + languageName: node + linkType: hard + "wonka@npm:^6.3.2": version: 6.3.4 resolution: "wonka@npm:6.3.4" @@ -24704,6 +27692,15 @@ __metadata: languageName: node linkType: hard +"wsl-utils@npm:^0.1.0": + version: 0.1.0 + resolution: "wsl-utils@npm:0.1.0" + dependencies: + is-wsl: "npm:^3.1.0" + checksum: 10/de4c92187e04c3c27b4478f410a02e81c351dc85efa3447bf1666f34fc80baacd890a6698ec91995631714086992036013286aea3d77e6974020d40a08e00aec + languageName: node + linkType: hard + "xcode@npm:^3.0.1": version: 3.0.1 resolution: "xcode@npm:3.0.1" @@ -24834,7 +27831,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:17.7.2, yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": +"yargs@npm:17.7.2, yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: @@ -24896,6 +27893,20 @@ __metadata: languageName: node linkType: hard +"yoctocolors-cjs@npm:^2.1.3": + version: 2.1.3 + resolution: "yoctocolors-cjs@npm:2.1.3" + checksum: 10/b2144b38807673a4254dae06fe1a212729550609e606289c305e45c585b36fab1dbba44fe6cde90db9b28be465ec63f4c2a50867aeec6672f6bc36b6c9a361a0 + languageName: node + linkType: hard + +"yoctocolors@npm:^2.1.1": + version: 2.1.2 + resolution: "yoctocolors@npm:2.1.2" + checksum: 10/6ee42d665a4cc161c7de3f015b2a65d6c65d2808bfe3b99e228bd2b1b784ef1e54d1907415c025fc12b400f26f372bfc1b71966c6c738d998325ca422eb39363 + languageName: node + linkType: hard + "zod-to-json-schema@npm:^3.24.6": version: 3.24.6 resolution: "zod-to-json-schema@npm:3.24.6" From 06a19b27fc766427b65eebcd56e7c856d706e0b2 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Wed, 3 Dec 2025 21:08:01 +0100 Subject: [PATCH 02/27] feat: improved Android intent handling, fixed permissions check for version <33 --- .../android/src/main/AndroidManifest.xml | 32 +- .../com/callingx/CallNotificationManager.kt | 303 ------------------ .../main/java/com/callingx/CallRepository.kt | 5 +- .../src/main/java/com/callingx/CallService.kt | 5 +- .../main/java/com/callingx/CallingxModule.kt | 25 +- .../src/main/java/com/callingx/Utils.kt | 61 +--- .../java/com/callingx/{ => model}/Call.kt | 2 +- .../com/callingx/{ => model}/CallAction.kt | 2 +- .../notifications/CallNotificationManager.kt | 200 ++++++++++++ .../NotificationIntentFactory.kt | 101 ++++++ .../NotificationReceiverActivity.kt | 40 +++ .../NotificationReceiverService.kt | 45 +++ .../notifications/NotificationsConfig.kt | 108 +++++++ .../src/utils/permissions.ts | 9 +- .../getstream/rnvideosample/MainActivity.kt | 5 - 15 files changed, 546 insertions(+), 397 deletions(-) delete mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/CallNotificationManager.kt rename packages/react-native-callingx/android/src/main/java/com/callingx/{ => model}/Call.kt (98%) rename packages/react-native-callingx/android/src/main/java/com/callingx/{ => model}/CallAction.kt (98%) create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationIntentFactory.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverActivity.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverService.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt diff --git a/packages/react-native-callingx/android/src/main/AndroidManifest.xml b/packages/react-native-callingx/android/src/main/AndroidManifest.xml index 1944cbb8c2..dae1911c08 100644 --- a/packages/react-native-callingx/android/src/main/AndroidManifest.xml +++ b/packages/react-native-callingx/android/src/main/AndroidManifest.xml @@ -1,16 +1,30 @@ - - - - - + + + + + + + + + + - + android:exported="false" + android:foregroundServiceType="phoneCall" + android:stopWithTask="true" /> + + diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallNotificationManager.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallNotificationManager.kt deleted file mode 100644 index ec346b954a..0000000000 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallNotificationManager.kt +++ /dev/null @@ -1,303 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.callingx - -import android.Manifest -import android.app.Notification -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.os.Build -import android.telecom.DisconnectCause -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.core.app.NotificationChannelCompat -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.app.Person -import androidx.core.content.PermissionChecker -import androidx.core.graphics.drawable.IconCompat - -/** - * Handles call status changes and updates the notification accordingly. For more guidance around - * notifications check https://developer.android.com/develop/ui/views/notifications - * - * @see updateCallNotification - */ -@RequiresApi(Build.VERSION_CODES.O) -class CallNotificationManager( - private val context: Context, - private val notificationManager: NotificationManagerCompat = - NotificationManagerCompat.from(context) -) { - - internal companion object { - private const val TAG = "TelecomCallNotificationManager" - - const val NOTIFICATION_ID = 200 - const val NOTIFICATION_ACTION = "notification_action" - const val DEFAULT_INCOMING_CHANNEL_ID = "incoming_channel" - const val DEFAULT_INCOMING_CHANNEL_NAME = "Incoming calls" - const val DEFAULT_ONGOING_CHANNEL_ID = "ongoing_channel" - const val DEFAULT_ONGOING_CHANNEL_NAME = "Ongoing calls" - - private const val PREFS_NAME = "CallingxPrefs" - private const val PREFIX_IN = "incoming_" - private const val PREFIX_OUT = "outgoing_" - private const val KEY_ID = "id" - private const val KEY_NAME = "name" - private const val KEY_SOUND = "sound" - private const val KEY_VIBRATION = "vibration" - - fun saveNotificationsConfig(context: Context, config: NotificationsConfig) { - Log.d(TAG, "saveNotificationsConfig: Saving notifications config: $config") - - val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - prefs.edit() - .apply { - // Incoming channel - putString(PREFIX_IN + KEY_ID, config.incomingChannel.id) - putString(PREFIX_IN + KEY_NAME, config.incomingChannel.name) - putString(PREFIX_IN + KEY_SOUND, config.incomingChannel.sound) - putBoolean(PREFIX_IN + KEY_VIBRATION, config.incomingChannel.vibration) - - // Outgoing channel - putString(PREFIX_OUT + KEY_ID, config.outgoingChannel.id) - putString(PREFIX_OUT + KEY_NAME, config.outgoingChannel.name) - putString(PREFIX_OUT + KEY_SOUND, config.outgoingChannel.sound) - putBoolean(PREFIX_OUT + KEY_VIBRATION, config.outgoingChannel.vibration) - } - .apply() - } - - fun loadNotificationsConfig(context: Context): NotificationsConfig { - val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - Log.d( - TAG, - "loadNotificationsConfig: Loading notifications config ${prefs.getString(PREFIX_IN + KEY_ID, "")}" - ) - return NotificationsConfig( - incomingChannel = - ChannelConfig( - id = prefs.getString(PREFIX_IN + KEY_ID, "") - ?: DEFAULT_INCOMING_CHANNEL_ID, - name = prefs.getString(PREFIX_IN + KEY_NAME, "") - ?: DEFAULT_INCOMING_CHANNEL_NAME, - sound = prefs.getString(PREFIX_IN + KEY_SOUND, "") ?: "", - vibration = prefs.getBoolean(PREFIX_IN + KEY_VIBRATION, false), - importance = NotificationManagerCompat.IMPORTANCE_MAX - ), - outgoingChannel = - ChannelConfig( - id = prefs.getString(PREFIX_OUT + KEY_ID, "") - ?: DEFAULT_ONGOING_CHANNEL_ID, - name = prefs.getString(PREFIX_OUT + KEY_NAME, "") - ?: DEFAULT_ONGOING_CHANNEL_NAME, - sound = prefs.getString(PREFIX_OUT + KEY_SOUND, "") ?: "", - vibration = prefs.getBoolean(PREFIX_OUT + KEY_VIBRATION, false), - importance = NotificationManagerCompat.IMPORTANCE_DEFAULT - ) - ) - } - } - - private var notificationsConfig: NotificationsConfig - - init { - notificationsConfig = CallNotificationManager.loadNotificationsConfig(context) - createNotificationChannels(notificationsConfig) - Log.d(TAG, "CallNotificationManager: Notifications config: $notificationsConfig") - } - - fun createNotification(call: Call.Registered): Notification { - Log.d(TAG, "createNotification: Creating notification for call ID: ${call.id}") - - val contentIntent = getActivityIntent(call.id) - val callStyle = createCallStyle(call) - val channelId = - if (call.isIncoming() && !call.isActive) { - notificationsConfig.incomingChannel.id - } else { - notificationsConfig.outgoingChannel.id - } - - val builder = - NotificationCompat.Builder(context, channelId) - .setContentIntent(contentIntent) - .setFullScreenIntent(contentIntent, true) - .setStyle(callStyle) - .setSmallIcon(R.drawable.ic_round_call_24) - .setCategory(NotificationCompat.CATEGORY_CALL) - .setPriority(NotificationCompat.PRIORITY_MAX) - .setOngoing(true) - - call.displayOptions?.let { - if (it.containsKey(CallService.EXTRA_DISPLAY_SUBTITLE)) { - builder.setContentText(it.getString(CallService.EXTRA_DISPLAY_SUBTITLE)) - } - } - - // if (call.isOnHold) { - // val activateAction = TelecomCallAction.Activate - // builder.addAction( - // R.drawable.ic_phone_paused_24, - // "Resume", - // getPendingIntent(activateAction) - // ) - // } - - return builder.build() - } - - /** Updates, creates or dismisses a CallStyle notification based on the given [TelecomCall] */ - fun updateCallNotification(call: Call) { - if (!canPostNotifications()) { - Log.w(TAG, "updateCallNotification: Notifications are not granted, skipping update") - return - } - - when (call) { - Call.None, is Call.Unregistered -> { - Log.d(TAG, "Dismissing notification (call is None or Unregistered)") - notificationManager.cancel(NOTIFICATION_ID) - } - is Call.Registered -> { - val notification = createNotification(call) - notificationManager.notify(NOTIFICATION_ID, notification) - Log.d(TAG, "updateCallNotification: Notification posted successfully") - } - } - } - - fun cancelNotifications() { - notificationManager.cancel(NOTIFICATION_ID) - } - - fun createNotificationChannels(notificationsConfig: NotificationsConfig) { - val incomingChannel = createNotificationChannel(notificationsConfig.incomingChannel) - val ongoingChannel = createNotificationChannel(notificationsConfig.outgoingChannel) - - notificationManager.createNotificationChannelsCompat( - listOf( - incomingChannel, - ongoingChannel, - ), - ) - Log.d(TAG, "createNotificationChannels: Notification channels registered") - } - - private fun createNotificationChannel(config: ChannelConfig): NotificationChannelCompat { - return NotificationChannelCompat.Builder(config.id, config.importance) - .apply { - setName(config.name) - setVibrationEnabled(config.vibration) - ResourceUtils.getSoundUri(context, config.sound)?.let { setSound(it, null) } - } - .build() - } - - private fun createCallStyle(call: Call.Registered): NotificationCompat.CallStyle { - val caller = createPerson(call) - - if (call.isIncoming() && !call.isActive) { - return NotificationCompat.CallStyle.forIncomingCall( - caller, - getBroadcastIntent(call.id, CallingxModule.CALL_END_ACTION) { - putExtra( - CallingxModule.EXTRA_DISCONNECT_CAUSE, - getDisconnectCauseString(DisconnectCause(DisconnectCause.REJECTED)) - ) - }, - getActivityIntent(call.id, CallingxModule.CALL_ANSWERED_ACTION) - ) - } - - return NotificationCompat.CallStyle.forOngoingCall( - caller, - getBroadcastIntent(call.id, CallingxModule.CALL_END_ACTION) { - putExtra( - CallingxModule.EXTRA_DISCONNECT_CAUSE, - getDisconnectCauseString(DisconnectCause(DisconnectCause.LOCAL)) - ) - } - ) - } - - // this intent will send action directly to the module - private fun getBroadcastIntent( - callId: String, - action: String, - addExtras: Intent.() -> Unit = {} - ): PendingIntent { - val callIntent = - Intent(action).apply { - setPackage(context.packageName) - putExtra(CallingxModule.EXTRA_CALL_ID, callId) - addExtras(this) - } - - return PendingIntent.getBroadcast( - context, - callIntent.hashCode(), - callIntent, - PendingIntent.FLAG_IMMUTABLE, - ) - } - - // this intent will send action to the launch activity, as for asnwering call case we need to - // bring the app to foreground - private fun getActivityIntent( - callId: String, - action: String? = null, - ): PendingIntent { - val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) - val callIntent = - Intent(launchIntent).apply { - action?.let { this.action = it } - putExtra(CallingxModule.EXTRA_CALL_ID, callId) - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) - } - - return PendingIntent.getActivity( - context, - callIntent.hashCode(), - callIntent, - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, - ) - } - - private fun createPerson(call: Call.Registered): Person { - val displayCallerName = call.displayOptions?.getString(CallService.EXTRA_DISPLAY_TITLE) - val address = call.callAttributes.address.toString() - - return Person.Builder() - .setName(displayCallerName ?: call.callAttributes.displayName) - .setUri(address) - .setIcon(IconCompat.createWithResource(context, R.drawable.ic_user)) - .setImportant(true) - .build() - } - - private fun canPostNotifications(): Boolean { - val permission = - PermissionChecker.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS, - ) - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && - permission == PermissionChecker.PERMISSION_GRANTED - } -} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallRepository.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallRepository.kt index 7cc325f72c..029ae91fa3 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallRepository.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallRepository.kt @@ -36,13 +36,14 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import com.callingx.model.Call +import com.callingx.model.CallAction /** * The central repository that keeps track of the current call and allows to register new calls. @@ -66,7 +67,7 @@ class CallRepository(context: Context) { } companion object { - private const val TAG = "TelecomCallRepository" + private const val TAG = "[Callingx] CallRepository" } private var listener: Listener? = null diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt index 46f851c4d2..bcc2e01ade 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt @@ -30,6 +30,9 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import com.callingx.model.Call +import com.callingx.model.CallAction +import com.callingx.notifications.CallNotificationManager /** * This service handles the app call logic (show notification, record mic, display audio, etc..). It @@ -49,7 +52,7 @@ import kotlinx.coroutines.launch class CallService : Service(), CallRepository.Listener { companion object { - private const val TAG = "TelecomCallService" + private const val TAG = "[Callingx] CallService" internal const val EXTRA_CALL_ID = "extra_call_id" internal const val EXTRA_NAME = "extra_name" diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt index 499877dc26..43a1e436b8 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt @@ -12,6 +12,8 @@ import android.os.Bundle import android.os.IBinder import android.telecom.DisconnectCause import android.util.Log +import com.callingx.model.CallAction +import com.callingx.notifications.NotificationsConfig import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise @@ -27,7 +29,7 @@ import com.facebook.react.modules.core.DeviceEventManagerModule class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec(reactContext) { companion object { - const val TAG = "TelecomCallingx" + const val TAG = "[Callingx] CallingxModule" const val NAME = "Callingx" const val EXTRA_CALL_ID = "call_id" @@ -46,19 +48,6 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec const val CALL_END_ACTION = "call_end" const val SERVICE_READY_ACTION = "service_ready" - - public fun handleCallingIntent(context: Context, intent: Intent) { - if (intent?.action == CALL_ANSWERED_ACTION) { - val callId = intent.getStringExtra(EXTRA_CALL_ID) - // Send broadcast or directly call your module to handle the answer - val broadcastIntent = - Intent(CALL_ANSWERED_ACTION).apply { - setPackage(context.packageName) - putExtra(EXTRA_CALL_ID, callId) - } - context.sendBroadcast(broadcastIntent) - } - } } // Track binding state carefully @@ -142,14 +131,9 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec override fun setupAndroid(options: ReadableMap) { Log.d(TAG, "[module] setupAndroid: Setting up Android: $options") - val notificationsConfig = extractNotificationsConfig(options) - CallNotificationManager.saveNotificationsConfig( - reactApplicationContext, - notificationsConfig - ) + NotificationsConfig.saveNotificationsConfig(reactApplicationContext, options) isModuleInitialized = true - Log.d(TAG, "[module] setupAndroid: Notifications config: $notificationsConfig") } override fun getInitialEvents(): WritableArray { @@ -483,6 +467,7 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec if (action == SERVICE_READY_ACTION) { Log.d(TAG, "[module] onReceive: Service is ready, initiating binding") bindToServiceIfNeeded() + return } if (action == null) { diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/Utils.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/Utils.kt index 191b829d98..aa46143723 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/Utils.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/Utils.kt @@ -1,61 +1,16 @@ package com.callingx -import android.content.Context -import android.media.RingtoneManager -import android.net.Uri import android.telecom.DisconnectCause -import com.facebook.react.bridge.ReadableMap - -data class ChannelConfig( - val id: String, - val name: String, - val sound: String, - val vibration: Boolean, - val importance: Int, -) - -data class NotificationsConfig( - val incomingChannel: ChannelConfig, - val outgoingChannel: ChannelConfig, -) - -fun extractNotificationsConfig(config: ReadableMap): NotificationsConfig { - return NotificationsConfig( - incomingChannel = extractChannelConfig(config.getMap("incomingChannel")), - outgoingChannel = extractChannelConfig(config.getMap("outgoingChannel")), - ) -} - -fun extractChannelConfig(channel: ReadableMap?): ChannelConfig { - if (channel == null) { - //return - return ChannelConfig( - id = "", - name = "", - sound = "", - vibration = false, - importance = 0 - ) - } - - return ChannelConfig( - id = channel.getString("id") ?: "", - name = channel.getString("name") ?: "", - sound = channel.getString("sound") ?: "", - vibration = channel.getBoolean("vibration"), - importance = 0 - ) -} fun getDisconnectCauseString(cause: DisconnectCause): String { return when (cause.code) { - DisconnectCause.LOCAL -> "local" - DisconnectCause.REMOTE -> "remote" - DisconnectCause.REJECTED -> "rejected" - DisconnectCause.BUSY -> "busy" - DisconnectCause.ANSWERED_ELSEWHERE -> "answeredElsewhere" - DisconnectCause.MISSED -> "missed" - DisconnectCause.ERROR -> "error" - else -> cause.toString() + DisconnectCause.LOCAL -> "local" + DisconnectCause.REMOTE -> "remote" + DisconnectCause.REJECTED -> "rejected" + DisconnectCause.BUSY -> "busy" + DisconnectCause.ANSWERED_ELSEWHERE -> "answeredElsewhere" + DisconnectCause.MISSED -> "missed" + DisconnectCause.ERROR -> "error" + else -> cause.toString() } } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/Call.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/model/Call.kt similarity index 98% rename from packages/react-native-callingx/android/src/main/java/com/callingx/Call.kt rename to packages/react-native-callingx/android/src/main/java/com/callingx/model/Call.kt index 9f21740df8..513c6b9ea1 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/Call.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/model/Call.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.callingx +package com.callingx.model import android.os.ParcelUuid import android.os.Bundle diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallAction.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/model/CallAction.kt similarity index 98% rename from packages/react-native-callingx/android/src/main/java/com/callingx/CallAction.kt rename to packages/react-native-callingx/android/src/main/java/com/callingx/model/CallAction.kt index 8360cb2dd7..a3ffe40c84 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallAction.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/model/CallAction.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.callingx +package com.callingx.model import android.os.ParcelUuid import android.os.Parcelable diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt new file mode 100644 index 0000000000..785c75123f --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt @@ -0,0 +1,200 @@ +package com.callingx.notifications + +import android.Manifest +import android.app.Notification +import android.content.Context +import android.os.Build +import android.telecom.DisconnectCause +import android.util.Log +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.Person +import androidx.core.content.PermissionChecker +import androidx.core.graphics.drawable.IconCompat +import com.callingx.CallService +import com.callingx.CallingxModule +import com.callingx.R +import com.callingx.getDisconnectCauseString +import com.callingx.model.Call + +/** + * Handles call status changes and updates the notification accordingly. For more guidance around + * notifications check https://developer.android.com/develop/ui/views/notifications + * + * @see updateCallNotification + */ +@RequiresApi(Build.VERSION_CODES.O) +class CallNotificationManager( + private val context: Context, + private val notificationManager: NotificationManagerCompat = + NotificationManagerCompat.from(context) +) { + + internal companion object { + private const val TAG = "[Callingx] CallNotificationManager" + + const val NOTIFICATION_ID = 200 + const val NOTIFICATION_ACTION = "notification_action" + } + + private var notificationsConfig: NotificationsConfig.ChannelsConfig + + init { + notificationsConfig = NotificationsConfig.loadNotificationsConfig(context) + createNotificationChannels(notificationsConfig) + Log.d(TAG, "CallNotificationManager: Notifications config: $notificationsConfig") + } + + fun createNotification(call: Call.Registered): Notification { + Log.d(TAG, "createNotification: Creating notification for call ID: ${call.id}") + + val contentIntent = NotificationIntentFactory.getLaunchActivityIntent(context, CallingxModule.CALL_ANSWERED_ACTION, call.id) + val callStyle = createCallStyle(call) + val channelId = + if (call.isIncoming() && !call.isActive) { + notificationsConfig.incomingChannel.id + } else { + notificationsConfig.outgoingChannel.id + } + + val builder = + NotificationCompat.Builder(context, channelId) + .setContentIntent(contentIntent) + .setFullScreenIntent(contentIntent, true) + .setStyle(callStyle) + .setSmallIcon(R.drawable.ic_round_call_24) + .setCategory(NotificationCompat.CATEGORY_CALL) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setOngoing(true) + + call.displayOptions?.let { + if (it.containsKey(CallService.EXTRA_DISPLAY_SUBTITLE)) { + builder.setContentText(it.getString(CallService.EXTRA_DISPLAY_SUBTITLE)) + } + } + + // if (call.isOnHold) { + // val activateAction = TelecomCallAction.Activate + // builder.addAction( + // R.drawable.ic_phone_paused_24, + // "Resume", + // getPendingIntent(activateAction) + // ) + // } + + return builder.build() + } + + /** Updates, creates or dismisses a CallStyle notification based on the given [TelecomCall] */ + fun updateCallNotification(call: Call) { + if (!canPostNotifications()) { + Log.w(TAG, "updateCallNotification: Notifications are not granted, skipping update") + return + } + + when (call) { + Call.None, is Call.Unregistered -> { + Log.d(TAG, "Dismissing notification (call is None or Unregistered)") + notificationManager.cancel(NOTIFICATION_ID) + } + is Call.Registered -> { + val notification = createNotification(call) + notificationManager.notify(NOTIFICATION_ID, notification) + Log.d(TAG, "updateCallNotification: Notification posted successfully") + } + } + } + + fun cancelNotifications() { + notificationManager.cancel(NOTIFICATION_ID) + } + + fun createNotificationChannels(notificationsConfig: NotificationsConfig.ChannelsConfig) { + val incomingChannel = createNotificationChannel(notificationsConfig.incomingChannel) + val ongoingChannel = createNotificationChannel(notificationsConfig.outgoingChannel) + + notificationManager.createNotificationChannelsCompat( + listOf( + incomingChannel, + ongoingChannel, + ), + ) + Log.d(TAG, "createNotificationChannels: Notification channels registered") + } + + private fun createNotificationChannel(config: NotificationsConfig.ChannelParams): NotificationChannelCompat { + return NotificationChannelCompat.Builder(config.id, config.importance) + .apply { + setName(config.name) + setVibrationEnabled(config.vibration) + ResourceUtils.getSoundUri(context, config.sound)?.let { setSound(it, null) } + } + .build() + } + + private fun createCallStyle(call: Call.Registered): NotificationCompat.CallStyle { + val caller = createPerson(call) + + if (call.isIncoming() && !call.isActive) { + return NotificationCompat.CallStyle.forIncomingCall( + caller, + NotificationIntentFactory.getPendingBroadcastIntent( + context, + CallingxModule.CALL_END_ACTION, + call.id + ) { + putExtra( + CallingxModule.EXTRA_DISCONNECT_CAUSE, + getDisconnectCauseString(DisconnectCause(DisconnectCause.REJECTED)) + ) + }, + NotificationIntentFactory.getPendingNotificationIntent( + context, + CallingxModule.CALL_ANSWERED_ACTION, + call.id + ) + ) + } + + return NotificationCompat.CallStyle.forOngoingCall( + caller, + NotificationIntentFactory.getPendingBroadcastIntent( + context, + CallingxModule.CALL_END_ACTION, + call.id + ) { + putExtra( + CallingxModule.EXTRA_DISCONNECT_CAUSE, + getDisconnectCauseString(DisconnectCause(DisconnectCause.LOCAL)) + ) + }, + ) + } + + private fun createPerson(call: Call.Registered): Person { + val displayCallerName = call.displayOptions?.getString(CallService.EXTRA_DISPLAY_TITLE) + val address = call.callAttributes.address.toString() + + return Person.Builder() + .setName(displayCallerName ?: call.callAttributes.displayName) + .setUri(address) + .setIcon(IconCompat.createWithResource(context, R.drawable.ic_user)) + .setImportant(true) + .build() + } + + private fun canPostNotifications(): Boolean { + // POST_NOTIFICATIONS permission is only required on Android 13+ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return true + } + + val permission = PermissionChecker.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) + return permission == PermissionChecker.PERMISSION_GRANTED + } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationIntentFactory.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationIntentFactory.kt new file mode 100644 index 0000000000..87852a3145 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationIntentFactory.kt @@ -0,0 +1,101 @@ +package com.callingx.notifications + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import com.callingx.CallingxModule + +object NotificationIntentFactory { + // Stable request codes for PendingIntents + private const val REQUEST_CODE_LAUNCH_ACTIVITY = 1001 + private const val REQUEST_CODE_RECEIVER_ACTIVITY = 1002 + private const val REQUEST_CODE_SERVICE = 1003 + + fun getPendingNotificationIntent( + context: Context, + action: String, + callId: String + ): PendingIntent { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + getReceiverActivityIntent(context, action, callId) + } else { + getPendingServiceIntent(context, action, callId) + } + } + + fun getPendingServiceIntent(context: Context, action: String, callId: String): PendingIntent { + val intent = + Intent(context, NotificationReceiverService::class.java).apply { + this.action = action + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + } + + return PendingIntent.getService( + context, + REQUEST_CODE_SERVICE, + intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + ) + } + + fun getReceiverActivityIntent(context: Context, action: String, callId: String): PendingIntent { + val receiverIntent = + Intent(context, NotificationReceiverActivity::class.java).apply { + this.action = action + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + } + + val launchActivity = context.packageManager.getLaunchIntentForPackage(context.packageName) + val launchActivityIntent = + Intent(launchActivity).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + + return PendingIntent.getActivities( + context, + REQUEST_CODE_RECEIVER_ACTIVITY, + arrayOf(launchActivityIntent, receiverIntent), + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + } + + fun getLaunchActivityIntent(context: Context, action: String, callId: String): PendingIntent { + val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + val callIntent = + Intent(launchIntent).apply { + this.action = action + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + + return PendingIntent.getActivity( + context, + REQUEST_CODE_LAUNCH_ACTIVITY, + callIntent, + PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, + ) + } + + fun getPendingBroadcastIntent( + context: Context, + action: String, + callId: String, + addExtras: Intent.() -> Unit = {} + ): PendingIntent { + val intent = + Intent(action).apply { + setPackage(context.packageName) + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + addExtras(this) + } + + // Use action hashCode for unique request code per action type + return PendingIntent.getBroadcast( + context, + action.hashCode(), + intent, + PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT + ) + } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverActivity.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverActivity.kt new file mode 100644 index 0000000000..c91b7e6410 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverActivity.kt @@ -0,0 +1,40 @@ +package com.callingx.notifications + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import com.callingx.CallingxModule + +// For Android 12+ +class NotificationReceiverActivity : Activity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + handleIntent(intent) + finish() + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + handleIntent(intent) + finish() + } + + //re-send intent from notification to the turbo module + private fun handleIntent(intent: Intent?) { + if (intent == null) { + return + } + + //we need it only for answered call event, as for cold start case we need to send broadcast event and to launch the app + if (intent.action == CallingxModule.CALL_ANSWERED_ACTION) { + val callId = intent.getStringExtra(CallingxModule.EXTRA_CALL_ID) + Intent(CallingxModule.CALL_ANSWERED_ACTION) + .apply { + setPackage(packageName) + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + } + .also { sendBroadcast(it) } + } + } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverService.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverService.kt new file mode 100644 index 0000000000..996dd9a0a1 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverService.kt @@ -0,0 +1,45 @@ +package com.callingx.notifications + +import android.app.Service +import android.content.Intent +import android.os.IBinder +import com.callingx.CallingxModule + +class NotificationReceiverService : Service() { + + override fun onBind(intent: Intent?): IBinder? = null + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + val action = intent?.action + if (action == null) { + stopSelf(startId) + return START_NOT_STICKY + } + + when (action) { + CallingxModule.CALL_ANSWERED_ACTION -> onCallAnswered(intent) + } + + stopSelf(startId) + return START_NOT_STICKY + } + + private fun onCallAnswered(intent: Intent) { + val callId = intent.getStringExtra(CallingxModule.EXTRA_CALL_ID) + callId?.let { + NotificationIntentFactory.getPendingBroadcastIntent( + applicationContext, + CallingxModule.CALL_ANSWERED_ACTION, + it + ) + .send() + + NotificationIntentFactory.getLaunchActivityIntent( + applicationContext, + CallingxModule.CALL_ANSWERED_ACTION, + it + ) + .send() + } + } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt new file mode 100644 index 0000000000..2d83f8fad3 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt @@ -0,0 +1,108 @@ +package com.callingx.notifications + +import android.content.Context +import android.util.Log +import androidx.core.app.NotificationManagerCompat +import com.facebook.react.bridge.ReadableMap + +object NotificationsConfig { + private const val TAG = "[Callingx] NotificationsConfig" + private const val PREFS_NAME = "CallingxPrefs" + private const val PREFIX_IN = "incoming_" + private const val PREFIX_OUT = "outgoing_" + private const val KEY_ID = "id" + private const val KEY_NAME = "name" + private const val KEY_SOUND = "sound" + private const val KEY_VIBRATION = "vibration" + + private const val DEFAULT_INCOMING_CHANNEL_ID = "incoming_channel" + private const val DEFAULT_INCOMING_CHANNEL_NAME = "Incoming calls" + private const val DEFAULT_ONGOING_CHANNEL_ID = "ongoing_channel" + private const val DEFAULT_ONGOING_CHANNEL_NAME = "Ongoing calls" + + data class ChannelParams( + val id: String, + val name: String, + val sound: String, + val vibration: Boolean, + val importance: Int, + ) + + data class ChannelsConfig( + val incomingChannel: ChannelParams, + val outgoingChannel: ChannelParams, + ) + + fun saveNotificationsConfig(context: Context, rawConfig: ReadableMap) { + Log.d(TAG, "saveNotificationsConfig: Saving notifications config: $rawConfig") + val config = extractNotificationsConfig(rawConfig) + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + prefs.edit() + .apply { + // Incoming channel + putString(PREFIX_IN + KEY_ID, config.incomingChannel.id) + putString(PREFIX_IN + KEY_NAME, config.incomingChannel.name) + putString(PREFIX_IN + KEY_SOUND, config.incomingChannel.sound) + putBoolean(PREFIX_IN + KEY_VIBRATION, config.incomingChannel.vibration) + + // Outgoing channel + putString(PREFIX_OUT + KEY_ID, config.outgoingChannel.id) + putString(PREFIX_OUT + KEY_NAME, config.outgoingChannel.name) + putString(PREFIX_OUT + KEY_SOUND, config.outgoingChannel.sound) + putBoolean(PREFIX_OUT + KEY_VIBRATION, config.outgoingChannel.vibration) + } + .apply() +} + +fun loadNotificationsConfig(context: Context): ChannelsConfig { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + Log.d( + TAG, + "loadNotificationsConfig: Loading notifications config ${prefs.getString(PREFIX_IN + KEY_ID, "")}" + ) + return ChannelsConfig( + incomingChannel = + ChannelParams( + id = prefs.getString(PREFIX_IN + KEY_ID, "") + ?: DEFAULT_INCOMING_CHANNEL_ID, + name = prefs.getString(PREFIX_IN + KEY_NAME, "") + ?: DEFAULT_INCOMING_CHANNEL_NAME, + sound = prefs.getString(PREFIX_IN + KEY_SOUND, "") ?: "", + vibration = prefs.getBoolean(PREFIX_IN + KEY_VIBRATION, false), + importance = NotificationManagerCompat.IMPORTANCE_MAX + ), + outgoingChannel = + ChannelParams( + id = prefs.getString(PREFIX_OUT + KEY_ID, "") + ?: DEFAULT_ONGOING_CHANNEL_ID, + name = prefs.getString(PREFIX_OUT + KEY_NAME, "") + ?: DEFAULT_ONGOING_CHANNEL_NAME, + sound = prefs.getString(PREFIX_OUT + KEY_SOUND, "") ?: "", + vibration = prefs.getBoolean(PREFIX_OUT + KEY_VIBRATION, false), + importance = NotificationManagerCompat.IMPORTANCE_DEFAULT + ) + ) +} + + fun extractNotificationsConfig(config: ReadableMap): ChannelsConfig { + return ChannelsConfig( + incomingChannel = extractChannelConfig(config.getMap("incomingChannel")), + outgoingChannel = extractChannelConfig(config.getMap("outgoingChannel")), + ) + } + + fun extractChannelConfig(channel: ReadableMap?): ChannelParams { + if (channel == null) { + // return + return ChannelParams(id = "", name = "", sound = "", vibration = false, importance = 0) + } + + return ChannelParams( + id = channel.getString("id") ?: "", + name = channel.getString("name") ?: "", + sound = channel.getString("sound") ?: "", + vibration = channel.getBoolean("vibration"), + importance = 0 + ) + } +} diff --git a/packages/react-native-callingx/src/utils/permissions.ts b/packages/react-native-callingx/src/utils/permissions.ts index 77e68c073d..3ac6e12882 100644 --- a/packages/react-native-callingx/src/utils/permissions.ts +++ b/packages/react-native-callingx/src/utils/permissions.ts @@ -6,6 +6,9 @@ export interface PermissionsResult { postNotifications: boolean; } +const allowedPostNotifications = + Platform.OS === 'android' && Platform.Version < 33; + export const requestCallPermissions = async (): Promise => { if (Platform.OS !== 'android') { return { recordAudio: true, postNotifications: true }; // iOS handles permissions differently @@ -46,7 +49,7 @@ export const requestCallPermissions = async (): Promise => { PermissionsAndroid.RESULTS.GRANTED, postNotifications: results[PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS] === - PermissionsAndroid.RESULTS.GRANTED, + PermissionsAndroid.RESULTS.GRANTED || allowedPostNotifications, }; } catch (err) { console.warn('Error requesting permissions:', err); @@ -95,7 +98,9 @@ export const requestPostNotificationPermissions = const results = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS ); - return results === PermissionsAndroid.RESULTS.GRANTED; + return ( + results === PermissionsAndroid.RESULTS.GRANTED || allowedPostNotifications + ); }; export const canPostNotifications = async (): Promise => { diff --git a/sample-apps/react-native/dogfood/android/app/src/main/java/io/getstream/rnvideosample/MainActivity.kt b/sample-apps/react-native/dogfood/android/app/src/main/java/io/getstream/rnvideosample/MainActivity.kt index 2a6e9f8763..8de5656dfd 100644 --- a/sample-apps/react-native/dogfood/android/app/src/main/java/io/getstream/rnvideosample/MainActivity.kt +++ b/sample-apps/react-native/dogfood/android/app/src/main/java/io/getstream/rnvideosample/MainActivity.kt @@ -21,13 +21,8 @@ class MainActivity : ReactActivity() { // for react-navigation super.onCreate(null) StreamVideoReactNative.setupCallActivity(this) - CallingxModule.handleCallingIntent(this, intent) } - override fun onNewIntent(intent: Intent) { - super.onNewIntent(intent) - CallingxModule.handleCallingIntent(this, intent) - } /** * Returns the name of the main component registered from JavaScript. This is used to schedule * rendering of the component. From 5542009430ef8d759a0b23462f41e1565eeceb85 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Thu, 4 Dec 2025 16:10:50 +0100 Subject: [PATCH 03/27] feat: adjusted notification channels creation and notification posting checks --- .../main/java/com/callingx/CallingxModule.kt | 44 ++++--- .../notifications/CallNotificationManager.kt | 83 +++---------- .../NotificationChannelsManager.kt | 114 ++++++++++++++++++ .../notifications/NotificationsConfig.kt | 64 ++++++---- .../src/CallingxModule.ts | 17 ++- .../src/spec/NativeCallingx.ts | 2 + packages/react-native-callingx/src/types.ts | 2 + .../src/utils/permissions.ts | 29 +++-- .../src/utils/push/android.ts | 4 +- 9 files changed, 237 insertions(+), 122 deletions(-) create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationChannelsManager.kt diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt index 43a1e436b8..1d5cba830c 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt @@ -13,6 +13,7 @@ import android.os.IBinder import android.telecom.DisconnectCause import android.util.Log import com.callingx.model.CallAction +import com.callingx.notifications.NotificationChannelsManager import com.callingx.notifications.NotificationsConfig import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.LifecycleEventListener @@ -50,7 +51,6 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec const val SERVICE_READY_ACTION = "service_ready" } - // Track binding state carefully private enum class BindingState { UNBOUND, BINDING, @@ -60,31 +60,19 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec private var callService: CallService? = null private var bindingState = BindingState.UNBOUND - // private lateinit var notificationsConfig: NotificationsConfig - private var delayedEvents = WritableNativeArray() private var isModuleInitialized = false + private val notificationChannelsManager = NotificationChannelsManager(reactApplicationContext) private val callEventBroadcastReceiver = CallEventBroadcastReceiver() private val appStateListener = object : LifecycleEventListener { - override fun onHostResume() { - // App resumed - ensure service is bound if needed - Log.d(TAG, "[module] onHostResume: App resumed") - // if (!isBound && shouldServiceBeRunning()) { - // bindToService() - // } - } + override fun onHostResume() {} - override fun onHostPause() { - // App paused - start unbind timer - Log.d(TAG, "[module] onHostPause: App paused") - // startUnbindTimer() - } + override fun onHostPause() {} override fun onHostDestroy() { // App destroyed - force unbind - // forceUnbindService() Log.d(TAG, "[module] onHostDestroy: App destroyed") unbindServiceSafely() } @@ -131,11 +119,18 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec override fun setupAndroid(options: ReadableMap) { Log.d(TAG, "[module] setupAndroid: Setting up Android: $options") - NotificationsConfig.saveNotificationsConfig(reactApplicationContext, options) + val notificationsConfig = + NotificationsConfig.saveNotificationsConfig(reactApplicationContext, options) + notificationChannelsManager.setNotificationsConfig(notificationsConfig) + notificationChannelsManager.createNotificationChannels() isModuleInitialized = true } + override fun canPostNotifications(): Boolean { + return notificationChannelsManager.getNotificationStatus().canPost + } + override fun getInitialEvents(): WritableArray { // NOTE: writabel native array can be consumed only once, think of getting rid from clear // event and clear eat immidiate after getting initial events @@ -166,6 +161,11 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec TAG, "[module] displayIncomingCall: Displaying incoming call: $callId, $phoneNumber, $callerName, $hasVideo" ) + if (!notificationChannelsManager.getNotificationStatus().canPost) { + promise.reject("ERROR", "Notifications are not granted") + return + } + startCallService( CallService.ACTION_INCOMING_CALL, callId, @@ -199,6 +199,11 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec TAG, "[module] startCall: Starting outgoing call: $callId, $phoneNumber, $callerName, $hasVideo, $displayOptions" ) + if (!notificationChannelsManager.getNotificationStatus().canPost) { + promise.reject("ERROR", "Notifications are not granted") + return + } + startCallService( CallService.ACTION_OUTGOING_CALL, callId, @@ -218,6 +223,11 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec promise: Promise ) { Log.d(TAG, "[module] updateDisplay: Updating display: $callId, $phoneNumber, $callerName") + if (!notificationChannelsManager.getNotificationStatus().canPost) { + promise.reject("ERROR", "Notifications are not granted") + return + } + startCallService( CallService.ACTION_UPDATE_CALL, callId, diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt index 785c75123f..f0c2c416e7 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt @@ -1,17 +1,14 @@ package com.callingx.notifications -import android.Manifest import android.app.Notification import android.content.Context import android.os.Build import android.telecom.DisconnectCause import android.util.Log import androidx.annotation.RequiresApi -import androidx.core.app.NotificationChannelCompat import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person -import androidx.core.content.PermissionChecker import androidx.core.graphics.drawable.IconCompat import com.callingx.CallService import com.callingx.CallingxModule @@ -36,28 +33,21 @@ class CallNotificationManager( private const val TAG = "[Callingx] CallNotificationManager" const val NOTIFICATION_ID = 200 - const val NOTIFICATION_ACTION = "notification_action" } - private var notificationsConfig: NotificationsConfig.ChannelsConfig - - init { - notificationsConfig = NotificationsConfig.loadNotificationsConfig(context) - createNotificationChannels(notificationsConfig) - Log.d(TAG, "CallNotificationManager: Notifications config: $notificationsConfig") - } + private var notificationsConfig = NotificationsConfig.loadNotificationsConfig(context) fun createNotification(call: Call.Registered): Notification { Log.d(TAG, "createNotification: Creating notification for call ID: ${call.id}") - val contentIntent = NotificationIntentFactory.getLaunchActivityIntent(context, CallingxModule.CALL_ANSWERED_ACTION, call.id) + val contentIntent = + NotificationIntentFactory.getLaunchActivityIntent( + context, + CallingxModule.CALL_ANSWERED_ACTION, + call.id + ) val callStyle = createCallStyle(call) - val channelId = - if (call.isIncoming() && !call.isActive) { - notificationsConfig.incomingChannel.id - } else { - notificationsConfig.outgoingChannel.id - } + val channelId = getChannelId(call) val builder = NotificationCompat.Builder(context, channelId) @@ -68,32 +58,17 @@ class CallNotificationManager( .setCategory(NotificationCompat.CATEGORY_CALL) .setPriority(NotificationCompat.PRIORITY_MAX) .setOngoing(true) - + call.displayOptions?.let { if (it.containsKey(CallService.EXTRA_DISPLAY_SUBTITLE)) { builder.setContentText(it.getString(CallService.EXTRA_DISPLAY_SUBTITLE)) } } - // if (call.isOnHold) { - // val activateAction = TelecomCallAction.Activate - // builder.addAction( - // R.drawable.ic_phone_paused_24, - // "Resume", - // getPendingIntent(activateAction) - // ) - // } - return builder.build() } - /** Updates, creates or dismisses a CallStyle notification based on the given [TelecomCall] */ fun updateCallNotification(call: Call) { - if (!canPostNotifications()) { - Log.w(TAG, "updateCallNotification: Notifications are not granted, skipping update") - return - } - when (call) { Call.None, is Call.Unregistered -> { Log.d(TAG, "Dismissing notification (call is None or Unregistered)") @@ -111,27 +86,12 @@ class CallNotificationManager( notificationManager.cancel(NOTIFICATION_ID) } - fun createNotificationChannels(notificationsConfig: NotificationsConfig.ChannelsConfig) { - val incomingChannel = createNotificationChannel(notificationsConfig.incomingChannel) - val ongoingChannel = createNotificationChannel(notificationsConfig.outgoingChannel) - - notificationManager.createNotificationChannelsCompat( - listOf( - incomingChannel, - ongoingChannel, - ), - ) - Log.d(TAG, "createNotificationChannels: Notification channels registered") - } - - private fun createNotificationChannel(config: NotificationsConfig.ChannelParams): NotificationChannelCompat { - return NotificationChannelCompat.Builder(config.id, config.importance) - .apply { - setName(config.name) - setVibrationEnabled(config.vibration) - ResourceUtils.getSoundUri(context, config.sound)?.let { setSound(it, null) } - } - .build() + private fun getChannelId(call: Call.Registered): String { + return if (call.isIncoming() && !call.isActive) { + notificationsConfig.incomingChannel.id + } else { + notificationsConfig.outgoingChannel.id + } } private fun createCallStyle(call: Call.Registered): NotificationCompat.CallStyle { @@ -184,17 +144,4 @@ class CallNotificationManager( .setImportant(true) .build() } - - private fun canPostNotifications(): Boolean { - // POST_NOTIFICATIONS permission is only required on Android 13+ - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - return true - } - - val permission = PermissionChecker.checkSelfPermission( - context, - Manifest.permission.POST_NOTIFICATIONS, - ) - return permission == PermissionChecker.PERMISSION_GRANTED - } } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationChannelsManager.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationChannelsManager.kt new file mode 100644 index 0000000000..131c1898ea --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationChannelsManager.kt @@ -0,0 +1,114 @@ +package com.callingx.notifications + +import android.Manifest +import android.content.Context +import android.os.Build +import android.util.Log +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.PermissionChecker + +class NotificationChannelsManager( + private val context: Context, + private val notificationManager: NotificationManagerCompat = + NotificationManagerCompat.from(context) +) { + + private var notificationsConfig: NotificationsConfig.ChannelsConfig? = null + + companion object { + private const val TAG = "[Callingx] NotificationChannelsManager" + } + + data class NotificationStatus( + val canPost: Boolean, + val hasPermissions: Boolean, + val areNotificationsEnabled: Boolean, + val isIncomingChannelEnabled: Boolean, + val isOutgoingChannelEnabled: Boolean, + ) + + fun setNotificationsConfig(notificationsConfig: NotificationsConfig.ChannelsConfig) { + this.notificationsConfig = notificationsConfig + } + + fun createNotificationChannels() { + notificationsConfig?.let { + val incomingChannel = createNotificationChannel(it.incomingChannel) + val ongoingChannel = createNotificationChannel(it.outgoingChannel) + + notificationManager.createNotificationChannelsCompat( + listOf( + incomingChannel, + ongoingChannel, + ), + ) + Log.d(TAG, "createNotificationChannels: Notification channels registered") + } + } + + fun getNotificationStatus(): NotificationStatus { + val areNotificationsEnabled = areNotificationsEnabled() + val hasPermissions = hasNotificationPermissions() + val isIncomingChannelEnabled = isChannelEnabled(notificationsConfig?.incomingChannel?.id) + val isOutgoingChannelEnabled = isChannelEnabled(notificationsConfig?.outgoingChannel?.id) + + val canPost = + areNotificationsEnabled && + hasPermissions && + isIncomingChannelEnabled && + isOutgoingChannelEnabled + + return NotificationStatus( + canPost, + hasPermissions, + areNotificationsEnabled, + isIncomingChannelEnabled, + isOutgoingChannelEnabled + ) + } + + private fun createNotificationChannel( + config: NotificationsConfig.ChannelParams + ): NotificationChannelCompat { + Log.d(TAG, "createNotificationChannel: Creating notification channel: ${config.id}, ${config.name}, ${config.importance}, ${config.vibration}, ${config.sound}") + return NotificationChannelCompat.Builder(config.id, config.importance) + .apply { + setName(config.name) + setVibrationEnabled(config.vibration) + ResourceUtils.getSoundUri(context, config.sound)?.let { setSound(it, null) } + } + .build() + } + + private fun areNotificationsEnabled(): Boolean { + return notificationManager.areNotificationsEnabled() + } + + private fun hasNotificationPermissions(): Boolean { + // POST_NOTIFICATIONS permission is only required on Android 13+ + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + return true + } + + val permission = + PermissionChecker.checkSelfPermission( + context, + Manifest.permission.POST_NOTIFICATIONS, + ) + return permission == PermissionChecker.PERMISSION_GRANTED + } + + private fun isChannelEnabled(channelId: String?): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return true + } + + if (channelId == null) { + return false + } + + val channel = notificationManager.getNotificationChannel(channelId) + return channel != null && channel.importance != NotificationManagerCompat.IMPORTANCE_NONE + } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt index 2d83f8fad3..6b2f686256 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt @@ -14,6 +14,7 @@ object NotificationsConfig { private const val KEY_NAME = "name" private const val KEY_SOUND = "sound" private const val KEY_VIBRATION = "vibration" + private const val KEY_TIMEOUT = "timeout" private const val DEFAULT_INCOMING_CHANNEL_ID = "incoming_channel" private const val DEFAULT_INCOMING_CHANNEL_NAME = "Incoming calls" @@ -29,32 +30,34 @@ object NotificationsConfig { ) data class ChannelsConfig( - val incomingChannel: ChannelParams, - val outgoingChannel: ChannelParams, + val incomingChannel: ChannelParams, + val outgoingChannel: ChannelParams, ) - fun saveNotificationsConfig(context: Context, rawConfig: ReadableMap) { + fun saveNotificationsConfig(context: Context, rawConfig: ReadableMap): ChannelsConfig { Log.d(TAG, "saveNotificationsConfig: Saving notifications config: $rawConfig") val config = extractNotificationsConfig(rawConfig) val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) prefs.edit() .apply { - // Incoming channel - putString(PREFIX_IN + KEY_ID, config.incomingChannel.id) - putString(PREFIX_IN + KEY_NAME, config.incomingChannel.name) - putString(PREFIX_IN + KEY_SOUND, config.incomingChannel.sound) - putBoolean(PREFIX_IN + KEY_VIBRATION, config.incomingChannel.vibration) + // Incoming channel + putString(PREFIX_IN + KEY_ID, config.incomingChannel.id) + putString(PREFIX_IN + KEY_NAME, config.incomingChannel.name) + putString(PREFIX_IN + KEY_SOUND, config.incomingChannel.sound) + putBoolean(PREFIX_IN + KEY_VIBRATION, config.incomingChannel.vibration) - // Outgoing channel - putString(PREFIX_OUT + KEY_ID, config.outgoingChannel.id) - putString(PREFIX_OUT + KEY_NAME, config.outgoingChannel.name) - putString(PREFIX_OUT + KEY_SOUND, config.outgoingChannel.sound) - putBoolean(PREFIX_OUT + KEY_VIBRATION, config.outgoingChannel.vibration) + // Outgoing channel + putString(PREFIX_OUT + KEY_ID, config.outgoingChannel.id) + putString(PREFIX_OUT + KEY_NAME, config.outgoingChannel.name) + putString(PREFIX_OUT + KEY_SOUND, config.outgoingChannel.sound) + putBoolean(PREFIX_OUT + KEY_VIBRATION, config.outgoingChannel.vibration) } .apply() -} -fun loadNotificationsConfig(context: Context): ChannelsConfig { + return config + } + + fun loadNotificationsConfig(context: Context): ChannelsConfig { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) Log.d( TAG, @@ -69,7 +72,7 @@ fun loadNotificationsConfig(context: Context): ChannelsConfig { ?: DEFAULT_INCOMING_CHANNEL_NAME, sound = prefs.getString(PREFIX_IN + KEY_SOUND, "") ?: "", vibration = prefs.getBoolean(PREFIX_IN + KEY_VIBRATION, false), - importance = NotificationManagerCompat.IMPORTANCE_MAX + importance = NotificationManagerCompat.IMPORTANCE_MAX, ), outgoingChannel = ChannelParams( @@ -79,22 +82,35 @@ fun loadNotificationsConfig(context: Context): ChannelsConfig { ?: DEFAULT_ONGOING_CHANNEL_NAME, sound = prefs.getString(PREFIX_OUT + KEY_SOUND, "") ?: "", vibration = prefs.getBoolean(PREFIX_OUT + KEY_VIBRATION, false), - importance = NotificationManagerCompat.IMPORTANCE_DEFAULT + importance = NotificationManagerCompat.IMPORTANCE_DEFAULT, ) ) -} + } fun extractNotificationsConfig(config: ReadableMap): ChannelsConfig { return ChannelsConfig( - incomingChannel = extractChannelConfig(config.getMap("incomingChannel")), - outgoingChannel = extractChannelConfig(config.getMap("outgoingChannel")), + incomingChannel = + extractChannelConfig( + config.getMap("incomingChannel"), + NotificationManagerCompat.IMPORTANCE_MAX + ), + outgoingChannel = + extractChannelConfig( + config.getMap("outgoingChannel"), + NotificationManagerCompat.IMPORTANCE_DEFAULT + ), ) } - fun extractChannelConfig(channel: ReadableMap?): ChannelParams { + fun extractChannelConfig(channel: ReadableMap?, importance: Int): ChannelParams { if (channel == null) { - // return - return ChannelParams(id = "", name = "", sound = "", vibration = false, importance = 0) + return ChannelParams( + id = "", + name = "", + sound = "", + vibration = false, + importance = importance, + ) } return ChannelParams( @@ -102,7 +118,7 @@ fun loadNotificationsConfig(context: Context): ChannelsConfig { name = channel.getString("name") ?: "", sound = channel.getString("sound") ?: "", vibration = channel.getBoolean("vibration"), - importance = 0 + importance = importance, ) } } diff --git a/packages/react-native-callingx/src/CallingxModule.ts b/packages/react-native-callingx/src/CallingxModule.ts index 8571795e18..7e5fa35380 100644 --- a/packages/react-native-callingx/src/CallingxModule.ts +++ b/packages/react-native-callingx/src/CallingxModule.ts @@ -1,6 +1,9 @@ import { Platform } from 'react-native'; import NativeCallingModule from './spec/NativeCallingx'; -import { requestCallPermissions } from './utils/permissions'; +import { + checkCallPermissions, + requestCallPermissions, +} from './utils/permissions'; import type { PermissionsResult } from './utils/permissions'; import { HEADLESS_TASK_NAME, @@ -36,8 +39,14 @@ class CallingxModule implements ICallingxModule { private eventManager: EventManager = new EventManager(); - get canPostNotifications(): boolean { - return this.isNotificationsAllowed; + canPostNotifications(): boolean { + if (Platform.OS !== 'android') { + return true; + } + + return ( + this.isNotificationsAllowed && NativeCallingModule.canPostNotifications() + ); } setup(options: { @@ -85,7 +94,7 @@ class CallingxModule implements ICallingxModule { const result: { recordAudio: boolean; postNotifications: boolean; - } = await requestCallPermissions(); + } = await checkCallPermissions(); this.isNotificationsAllowed = result.postNotifications; return result; diff --git a/packages/react-native-callingx/src/spec/NativeCallingx.ts b/packages/react-native-callingx/src/spec/NativeCallingx.ts index 3be13f3486..9a9780fd83 100644 --- a/packages/react-native-callingx/src/spec/NativeCallingx.ts +++ b/packages/react-native-callingx/src/spec/NativeCallingx.ts @@ -27,6 +27,8 @@ export interface Spec extends TurboModule { }; }): void; + canPostNotifications(): boolean; + getInitialEvents(): Array<{ eventName: string; params: { diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index 98586cdb2e..cb5166a355 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -9,6 +9,8 @@ export interface ICallingxModule { requestPermissions(): Promise; + canPostNotifications(): boolean; + setCurrentCallActive(callId: string): Promise; getInitialEvents(): EventData[]; diff --git a/packages/react-native-callingx/src/utils/permissions.ts b/packages/react-native-callingx/src/utils/permissions.ts index 3ac6e12882..1f7cbf5df8 100644 --- a/packages/react-native-callingx/src/utils/permissions.ts +++ b/packages/react-native-callingx/src/utils/permissions.ts @@ -57,9 +57,9 @@ export const requestCallPermissions = async (): Promise => { } }; -export const checkCallPermissions = async (): Promise => { +export const checkCallPermissions = async (): Promise => { if (Platform.OS !== 'android') { - return true; + return { recordAudio: true, postNotifications: true }; } const permissions: string[] = [PermissionsAndroid.PERMISSIONS.RECORD_AUDIO]; @@ -78,14 +78,29 @@ export const checkCallPermissions = async (): Promise => { try { const results = await Promise.all( - permissions.map((permission) => - PermissionsAndroid.check(permission as Permission) - ) + permissions.map(async (permission) => ({ + permission, + granted: await PermissionsAndroid.check(permission as Permission), + })) + ); + + const resultsObject = results.reduce( + (acc, { permission, granted }) => { + acc[permission as Permission] = granted; + return acc; + }, + {} as { [key in Permission]: boolean } ); - return Object.values(results).every((granted) => granted === true); + + return { + recordAudio: resultsObject[PermissionsAndroid.PERMISSIONS.RECORD_AUDIO], + postNotifications: + resultsObject[PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS] || + allowedPostNotifications, + }; } catch (err) { console.warn('Error checking permissions:', err); - return false; + return { recordAudio: false, postNotifications: false }; } }; diff --git a/packages/react-native-sdk/src/utils/push/android.ts b/packages/react-native-sdk/src/utils/push/android.ts index 659c39df31..5fd1e5ffb7 100644 --- a/packages/react-native-sdk/src/utils/push/android.ts +++ b/packages/react-native-sdk/src/utils/push/android.ts @@ -159,8 +159,8 @@ export const firebaseDataHandler = async ( } const callingx = getCallingxLib(); - const permissions = await callingx.checkPermissions(); - if (!permissions.postNotifications) { + await callingx.checkPermissions(); + if (!callingx.canPostNotifications()) { logger.debug( `Notification permission not granted, unable to post ${data.type} notifications`, ); From d6e4585dea3fd3882a6e1c1293a59c9bb3c116e6 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Fri, 5 Dec 2025 09:43:46 +0100 Subject: [PATCH 04/27] feat: added android ringtone support --- .../src/main/java/com/callingx/CallService.kt | 14 +- .../main/java/com/callingx/ResourceUtils.kt | 79 +++++----- .../notifications/CallNotificationManager.kt | 32 +++- .../NotificationChannelsManager.kt | 7 +- .../notifications/NotificationsConfig.kt | 141 +++++++++--------- packages/react-native-callingx/src/types.ts | 2 - .../src/utils/constants.ts | 2 - 7 files changed, 158 insertions(+), 119 deletions(-) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt index bcc2e01ade..5473e46107 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt @@ -26,13 +26,13 @@ import android.os.IBinder import android.telecom.DisconnectCause import android.util.Log import androidx.annotation.RequiresApi +import com.callingx.model.Call +import com.callingx.model.CallAction +import com.callingx.notifications.CallNotificationManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import com.callingx.model.Call -import com.callingx.model.CallAction -import com.callingx.notifications.CallNotificationManager /** * This service handles the app call logic (show notification, record mic, display audio, etc..). It @@ -104,6 +104,7 @@ class CallService : Service(), CallRepository.Listener { Log.d(TAG, "[service] onDestroy: TelecomCallService destroyed") notificationManager.cancelNotifications() + notificationManager.stopRingtone() telecomRepository.release() headlessJSManager.release() @@ -173,6 +174,11 @@ class CallService : Service(), CallRepository.Listener { TAG, "[service] updateServiceState: Call registered - Active: ${call.isActive}, OnHold: ${call.isOnHold}, Muted: ${call.isMuted}" ) + + if (call.isIncoming()) { + if (!call.isActive) notificationManager.startRingtone() + else notificationManager.stopRingtone() + } // Update the call state. if (isInForeground) { Log.d( @@ -199,6 +205,7 @@ class CallService : Service(), CallRepository.Listener { isInForeground = false } + notificationManager.stopRingtone() stopSelf() } is Call.None -> { @@ -209,6 +216,7 @@ class CallService : Service(), CallRepository.Listener { stopForeground(STOP_FOREGROUND_REMOVE) isInForeground = false } + notificationManager.stopRingtone() } } } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/ResourceUtils.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/ResourceUtils.kt index 75a41b5a69..9f13b5009f 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/ResourceUtils.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/ResourceUtils.kt @@ -1,52 +1,57 @@ import android.content.Context import android.media.RingtoneManager import android.net.Uri +import android.util.Log +import androidx.core.net.toUri -class ResourceUtils { - companion object { - private val resourceIdCache = mutableMapOf() - private fun getResourceIdByName(context: Context, name: String?, type: String): Int { - if (name.isNullOrEmpty()) { - return 0 - } +object ResourceUtils { + private const val TAG = "[Callingx] ResourceUtils" - val normalizedName = name.lowercase().replace("-", "_") - val key = "${normalizedName}_$type" + private val resourceIdCache = mutableMapOf() - synchronized(resourceIdCache) { - resourceIdCache[key]?.let { - return it - } + private fun getResourceIdByName(context: Context, name: String?, type: String): Int { + if (name.isNullOrEmpty()) { + return 0 + } - val packageName = context.packageName + val normalizedName = name.lowercase().replace("-", "_") + val key = "${normalizedName}_$type" - val id = context.resources.getIdentifier(normalizedName, type, packageName) - resourceIdCache[key] = id - return id + synchronized(resourceIdCache) { + resourceIdCache[key]?.let { + return it } + + val packageName = context.packageName + + val id = context.resources.getIdentifier(normalizedName, type, packageName) + resourceIdCache[key] = id + return id } + } + + fun getSoundUri(context: Context, sound: String?): Uri? { + Log.d(TAG, "getSoundUri: Getting sound URI for: $sound") + return when { + sound == null -> null + sound.contains("://") -> sound.toUri() + sound.equals("default", ignoreCase = true) -> { + RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) + } - fun getSoundUri(context: Context, sound: String?): Uri? { - return when { - sound == null -> null - sound.contains("://") -> Uri.parse(sound) - sound.equals("default", ignoreCase = true) -> { - RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION) - } - else -> { - // The API user is attempting to set a sound by file name, verify it exists - var soundResourceId = getResourceIdByName(context, sound, "raw") - if (soundResourceId == 0 && sound.contains(".")) { - soundResourceId = getResourceIdByName(context, sound.substringBeforeLast('.'), "raw") - } - if (soundResourceId == 0) { - null - } else { - // Use the actual sound name vs the resource ID, to obtain a stable URI, Issue #341 - Uri.parse("android.resource://${context.packageName}/raw/$sound") - } - } + else -> { + // The API user is attempting to set a sound by file name, verify it exists + var soundResourceId = getResourceIdByName(context, sound, "raw") + if (soundResourceId == 0 && sound.contains(".")) { + soundResourceId = getResourceIdByName(context, sound.substringBeforeLast('.'), "raw") + } + if (soundResourceId == 0) { + null + } else { + // Use the actual sound name vs the resource ID, to obtain a stable URI, Issue #341 + "android.resource://${context.packageName}/raw/$sound".toUri() + } } } } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt index f0c2c416e7..cddd077afc 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt @@ -2,6 +2,8 @@ package com.callingx.notifications import android.app.Notification import android.content.Context +import android.media.Ringtone +import android.media.RingtoneManager import android.os.Build import android.telecom.DisconnectCause import android.util.Log @@ -37,6 +39,8 @@ class CallNotificationManager( private var notificationsConfig = NotificationsConfig.loadNotificationsConfig(context) + private var ringtone: Ringtone? = null + fun createNotification(call: Call.Registered): Notification { Log.d(TAG, "createNotification: Creating notification for call ID: ${call.id}") @@ -58,7 +62,7 @@ class CallNotificationManager( .setCategory(NotificationCompat.CATEGORY_CALL) .setPriority(NotificationCompat.PRIORITY_MAX) .setOngoing(true) - + call.displayOptions?.let { if (it.containsKey(CallService.EXTRA_DISPLAY_SUBTITLE)) { builder.setContentText(it.getString(CallService.EXTRA_DISPLAY_SUBTITLE)) @@ -86,6 +90,32 @@ class CallNotificationManager( notificationManager.cancel(NOTIFICATION_ID) } + fun startRingtone() { + if (ringtone?.isPlaying == true) { + Log.d(TAG, "startRingtone: Ringtone already playing") + return + } + + val soundUri = + ResourceUtils.getSoundUri(context, notificationsConfig.incomingChannel.sound) + ?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) + + ringtone = RingtoneManager.getRingtone(context, soundUri) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ringtone?.isLooping = true + } + ringtone?.play() + Log.d(TAG, "startRingtone: Ringtone started") + } + + fun stopRingtone() { + if (ringtone?.isPlaying == true) { + ringtone?.stop() + Log.d(TAG, "stopRingtone: Ringtone stopped") + } + ringtone = null + } + private fun getChannelId(call: Call.Registered): String { return if (call.isIncoming() && !call.isActive) { notificationsConfig.incomingChannel.id diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationChannelsManager.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationChannelsManager.kt index 131c1898ea..6a5c558e6a 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationChannelsManager.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationChannelsManager.kt @@ -14,7 +14,7 @@ class NotificationChannelsManager( NotificationManagerCompat.from(context) ) { - private var notificationsConfig: NotificationsConfig.ChannelsConfig? = null + private var notificationsConfig: NotificationsConfig.Channels? = null companion object { private const val TAG = "[Callingx] NotificationChannelsManager" @@ -28,7 +28,7 @@ class NotificationChannelsManager( val isOutgoingChannelEnabled: Boolean, ) - fun setNotificationsConfig(notificationsConfig: NotificationsConfig.ChannelsConfig) { + fun setNotificationsConfig(notificationsConfig: NotificationsConfig.Channels) { this.notificationsConfig = notificationsConfig } @@ -71,12 +71,11 @@ class NotificationChannelsManager( private fun createNotificationChannel( config: NotificationsConfig.ChannelParams ): NotificationChannelCompat { - Log.d(TAG, "createNotificationChannel: Creating notification channel: ${config.id}, ${config.name}, ${config.importance}, ${config.vibration}, ${config.sound}") return NotificationChannelCompat.Builder(config.id, config.importance) .apply { setName(config.name) setVibrationEnabled(config.vibration) - ResourceUtils.getSoundUri(context, config.sound)?.let { setSound(it, null) } + setSound(null, null) } .build() } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt index 6b2f686256..4eb3390c8f 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt @@ -3,6 +3,7 @@ package com.callingx.notifications import android.content.Context import android.util.Log import androidx.core.app.NotificationManagerCompat +import androidx.core.content.edit import com.facebook.react.bridge.ReadableMap object NotificationsConfig { @@ -14,7 +15,6 @@ object NotificationsConfig { private const val KEY_NAME = "name" private const val KEY_SOUND = "sound" private const val KEY_VIBRATION = "vibration" - private const val KEY_TIMEOUT = "timeout" private const val DEFAULT_INCOMING_CHANNEL_ID = "incoming_channel" private const val DEFAULT_INCOMING_CHANNEL_NAME = "Incoming calls" @@ -22,103 +22,104 @@ object NotificationsConfig { private const val DEFAULT_ONGOING_CHANNEL_NAME = "Ongoing calls" data class ChannelParams( - val id: String, - val name: String, - val sound: String, - val vibration: Boolean, - val importance: Int, + val id: String, + val name: String, + val sound: String?, + val vibration: Boolean, + val importance: Int, ) - data class ChannelsConfig( - val incomingChannel: ChannelParams, - val outgoingChannel: ChannelParams, + data class Channels( + val incomingChannel: ChannelParams, + val outgoingChannel: ChannelParams, ) - fun saveNotificationsConfig(context: Context, rawConfig: ReadableMap): ChannelsConfig { + fun saveNotificationsConfig(context: Context, rawConfig: ReadableMap): Channels { Log.d(TAG, "saveNotificationsConfig: Saving notifications config: $rawConfig") val config = extractNotificationsConfig(rawConfig) val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - prefs.edit() - .apply { - // Incoming channel - putString(PREFIX_IN + KEY_ID, config.incomingChannel.id) - putString(PREFIX_IN + KEY_NAME, config.incomingChannel.name) - putString(PREFIX_IN + KEY_SOUND, config.incomingChannel.sound) - putBoolean(PREFIX_IN + KEY_VIBRATION, config.incomingChannel.vibration) + prefs.edit { + // Incoming channel + putString(PREFIX_IN + KEY_ID, config.incomingChannel.id) + putString(PREFIX_IN + KEY_NAME, config.incomingChannel.name) + putString(PREFIX_IN + KEY_SOUND, config.incomingChannel.sound) + putBoolean(PREFIX_IN + KEY_VIBRATION, config.incomingChannel.vibration) - // Outgoing channel - putString(PREFIX_OUT + KEY_ID, config.outgoingChannel.id) - putString(PREFIX_OUT + KEY_NAME, config.outgoingChannel.name) - putString(PREFIX_OUT + KEY_SOUND, config.outgoingChannel.sound) - putBoolean(PREFIX_OUT + KEY_VIBRATION, config.outgoingChannel.vibration) - } - .apply() + // Outgoing channel + putString(PREFIX_OUT + KEY_ID, config.outgoingChannel.id) + putString(PREFIX_OUT + KEY_NAME, config.outgoingChannel.name) + } return config } - fun loadNotificationsConfig(context: Context): ChannelsConfig { + fun loadNotificationsConfig(context: Context): Channels { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) Log.d( - TAG, - "loadNotificationsConfig: Loading notifications config ${prefs.getString(PREFIX_IN + KEY_ID, "")}" + TAG, + "loadNotificationsConfig: Loading notifications config ${ + prefs.getString( + PREFIX_IN + KEY_ID, + "" + ) + }" ) - return ChannelsConfig( - incomingChannel = - ChannelParams( - id = prefs.getString(PREFIX_IN + KEY_ID, "") - ?: DEFAULT_INCOMING_CHANNEL_ID, - name = prefs.getString(PREFIX_IN + KEY_NAME, "") - ?: DEFAULT_INCOMING_CHANNEL_NAME, - sound = prefs.getString(PREFIX_IN + KEY_SOUND, "") ?: "", - vibration = prefs.getBoolean(PREFIX_IN + KEY_VIBRATION, false), - importance = NotificationManagerCompat.IMPORTANCE_MAX, - ), - outgoingChannel = - ChannelParams( - id = prefs.getString(PREFIX_OUT + KEY_ID, "") - ?: DEFAULT_ONGOING_CHANNEL_ID, - name = prefs.getString(PREFIX_OUT + KEY_NAME, "") - ?: DEFAULT_ONGOING_CHANNEL_NAME, - sound = prefs.getString(PREFIX_OUT + KEY_SOUND, "") ?: "", - vibration = prefs.getBoolean(PREFIX_OUT + KEY_VIBRATION, false), - importance = NotificationManagerCompat.IMPORTANCE_DEFAULT, - ) + return Channels( + incomingChannel = + ChannelParams( + id = prefs.getString(PREFIX_IN + KEY_ID, "") + ?: DEFAULT_INCOMING_CHANNEL_ID, + name = prefs.getString(PREFIX_IN + KEY_NAME, "") + ?: DEFAULT_INCOMING_CHANNEL_NAME, + sound = prefs.getString(PREFIX_IN + KEY_SOUND, "") ?: "", + vibration = prefs.getBoolean(PREFIX_IN + KEY_VIBRATION, false), + importance = NotificationManagerCompat.IMPORTANCE_MAX, + ), + outgoingChannel = + ChannelParams( + id = prefs.getString(PREFIX_OUT + KEY_ID, "") + ?: DEFAULT_ONGOING_CHANNEL_ID, + name = prefs.getString(PREFIX_OUT + KEY_NAME, "") + ?: DEFAULT_ONGOING_CHANNEL_NAME, + importance = NotificationManagerCompat.IMPORTANCE_DEFAULT, + vibration = false, + sound = null, + ) ) } - fun extractNotificationsConfig(config: ReadableMap): ChannelsConfig { - return ChannelsConfig( - incomingChannel = - extractChannelConfig( - config.getMap("incomingChannel"), - NotificationManagerCompat.IMPORTANCE_MAX - ), - outgoingChannel = - extractChannelConfig( - config.getMap("outgoingChannel"), - NotificationManagerCompat.IMPORTANCE_DEFAULT - ), + fun extractNotificationsConfig(config: ReadableMap): Channels { + return Channels( + incomingChannel = + extractChannelConfig( + config.getMap("incomingChannel"), + NotificationManagerCompat.IMPORTANCE_MAX + ), + outgoingChannel = + extractChannelConfig( + config.getMap("outgoingChannel"), + NotificationManagerCompat.IMPORTANCE_DEFAULT + ), ) } fun extractChannelConfig(channel: ReadableMap?, importance: Int): ChannelParams { if (channel == null) { return ChannelParams( - id = "", - name = "", - sound = "", - vibration = false, - importance = importance, + id = "", + name = "", + sound = "", + vibration = false, + importance = importance, ) } return ChannelParams( - id = channel.getString("id") ?: "", - name = channel.getString("name") ?: "", - sound = channel.getString("sound") ?: "", - vibration = channel.getBoolean("vibration"), - importance = importance, + id = channel.getString("id") ?: "", + name = channel.getString("name") ?: "", + sound = channel.getString("sound"), + vibration = channel.getBoolean("vibration"), + importance = importance, ) } } diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index cb5166a355..6683bb985b 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -75,8 +75,6 @@ export type AndroidOptions = { outgoingChannel?: { id: string; name: string; - sound?: string; - vibration?: boolean; }; }; diff --git a/packages/react-native-callingx/src/utils/constants.ts b/packages/react-native-callingx/src/utils/constants.ts index 510bacbcc2..f6b2554579 100644 --- a/packages/react-native-callingx/src/utils/constants.ts +++ b/packages/react-native-callingx/src/utils/constants.ts @@ -25,8 +25,6 @@ export const defaultAndroidOptions: Required = { outgoingChannel: { id: 'telecom_ongoing_channel', name: 'Ongoing calls', - sound: '', - vibration: false, }, }; From 799e1ec6c76206af6d021d1983ca11451a38495d Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Fri, 5 Dec 2025 10:51:30 +0100 Subject: [PATCH 05/27] feat: added android <26 handling --- .../src/main/java/com/callingx/CallService.kt | 16 +- .../main/java/com/callingx/CallingxModule.kt | 7 +- .../notifications/CallNotificationManager.kt | 2 - .../java/com/callingx/repo/CallRepository.kt | 47 ++++++ .../com/callingx/repo/LegacyCallRepository.kt | 159 ++++++++++++++++++ .../TelecomCallRepository.kt} | 46 +---- 6 files changed, 227 insertions(+), 50 deletions(-) create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/repo/CallRepository.kt create mode 100644 packages/react-native-callingx/android/src/main/java/com/callingx/repo/LegacyCallRepository.kt rename packages/react-native-callingx/android/src/main/java/com/callingx/{CallRepository.kt => repo/TelecomCallRepository.kt} (92%) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt index 5473e46107..c88c2506b0 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt @@ -16,6 +16,7 @@ package com.callingx +import CallRepository import android.app.Service import android.content.Intent import android.net.Uri @@ -48,7 +49,6 @@ import kotlinx.coroutines.launch * calls can consume significant memory, although that would require more complex setup to make it * work across multiple process. */ -@RequiresApi(Build.VERSION_CODES.O) class CallService : Service(), CallRepository.Listener { companion object { @@ -80,7 +80,7 @@ class CallService : Service(), CallRepository.Listener { private lateinit var headlessJSManager: HeadlessTaskManager private lateinit var notificationManager: CallNotificationManager - private lateinit var telecomRepository: CallRepository + private lateinit var callRepository: CallRepository private val binder = CallServiceBinder() private val scope: CoroutineScope = CoroutineScope(SupervisorJob()) @@ -93,8 +93,8 @@ class CallService : Service(), CallRepository.Listener { notificationManager = CallNotificationManager(applicationContext) headlessJSManager = HeadlessTaskManager(applicationContext) - telecomRepository = CallRepository(applicationContext) - telecomRepository.setListener(this) + callRepository = CallRepositoryFactory.create(applicationContext) + callRepository.setListener(this) sendBroadcastEvent(CallingxModule.SERVICE_READY_ACTION) } @@ -105,7 +105,7 @@ class CallService : Service(), CallRepository.Listener { notificationManager.cancelNotifications() notificationManager.stopRingtone() - telecomRepository.release() + callRepository.release() headlessJSManager.release() if (isInForeground) { @@ -272,7 +272,7 @@ class CallService : Service(), CallRepository.Listener { public fun processAction(action: CallAction) { Log.d(TAG, "[service] processAction: Processing action: ${action::class.simpleName}") - val currentCall = telecomRepository.currentCall.value + val currentCall = callRepository.currentCall.value if (currentCall is Call.Registered) { currentCall.processAction(action) } else { @@ -297,7 +297,7 @@ class CallService : Service(), CallRepository.Listener { Log.d(TAG, "[service] registerCall: ${if (incoming) "in" else "out"} call") // If we have an ongoing call ignore command - if (telecomRepository.currentCall.value is Call.Registered) { + if (callRepository.currentCall.value is Call.Registered) { Log.w(TAG, "[service] registerCall: Call already registered, ignoring new call request") return } @@ -316,7 +316,7 @@ class CallService : Service(), CallRepository.Listener { Log.d(TAG, "[service] registerCall: Call details - Name: $name, URI: $uri") scope.launch { - telecomRepository.registerCall( + callRepository.registerCall( callId, name, uri, diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt index 1d5cba830c..27efb903ee 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt @@ -6,12 +6,12 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.ServiceConnection -import android.net.Uri import android.os.Build import android.os.Bundle import android.os.IBinder import android.telecom.DisconnectCause import android.util.Log +import androidx.core.content.ContextCompat import com.callingx.model.CallAction import com.callingx.notifications.NotificationChannelsManager import com.callingx.notifications.NotificationsConfig @@ -25,6 +25,7 @@ import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.module.annotations.ReactModule import com.facebook.react.modules.core.DeviceEventManagerModule +import androidx.core.net.toUri @ReactModule(name = CallingxModule.NAME) class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec(reactContext) { @@ -314,11 +315,11 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec this.action = action putExtra(CallService.EXTRA_CALL_ID, callId) putExtra(CallService.EXTRA_NAME, callerName) - putExtra(CallService.EXTRA_URI, Uri.parse(phoneNumber)) + putExtra(CallService.EXTRA_URI, phoneNumber.toUri()) putExtra(CallService.EXTRA_IS_VIDEO, hasVideo) putExtra(CallService.EXTRA_DISPLAY_OPTIONS, Arguments.toBundle(displayOptions)) } - .also { reactApplicationContext.startForegroundService(it) } + .also { ContextCompat.startForegroundService(reactApplicationContext, it) } } private fun executeServiceAction(action: CallAction, promise: Promise) { diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt index cddd077afc..3cd314ea64 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt @@ -7,7 +7,6 @@ import android.media.RingtoneManager import android.os.Build import android.telecom.DisconnectCause import android.util.Log -import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.Person @@ -24,7 +23,6 @@ import com.callingx.model.Call * * @see updateCallNotification */ -@RequiresApi(Build.VERSION_CODES.O) class CallNotificationManager( private val context: Context, private val notificationManager: NotificationManagerCompat = diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/CallRepository.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/CallRepository.kt new file mode 100644 index 0000000000..4f1be649f3 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/CallRepository.kt @@ -0,0 +1,47 @@ +import android.content.Context +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.telecom.DisconnectCause +import com.callingx.model.Call +import com.callingx.repo.TelecomCallRepository +import kotlinx.coroutines.flow.StateFlow + +interface CallRepository { + + interface Listener { + fun onCallStateChanged(call: Call) + fun onIsCallAnswered(callId: String) + fun onIsCallDisconnected(cause: DisconnectCause) + fun onIsCallInactive(callId: String) + fun onIsCallActive(callId: String) + fun onCallRegistered(callId: String) + fun onMuteCallChanged(callId: String, isMuted: Boolean) + fun onCallEndpointChanged(callId: String, endpoint: String) + } + + val currentCall: StateFlow + + fun setListener(listener: Listener) + fun release() + + suspend fun registerCall( + callId: String, + displayName: String, + address: Uri, + isIncoming: Boolean, + isVideo: Boolean, + displayOptions: Bundle?, + ) +} + +object CallRepositoryFactory { + + fun create(context: Context): CallRepository { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + TelecomCallRepository(context) // Your current CallRepository renamed + } else { + LegacyCallRepository(context) // Fallback implementation + } + } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/LegacyCallRepository.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/LegacyCallRepository.kt new file mode 100644 index 0000000000..f481e70b43 --- /dev/null +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/LegacyCallRepository.kt @@ -0,0 +1,159 @@ +import android.content.Context +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.core.telecom.CallAttributesCompat +import com.callingx.model.Call +import com.callingx.model.CallAction +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +class LegacyCallRepository(private val context: Context) : CallRepository { + + companion object { + private const val TAG = "[Callingx] LegacyCallRepository" + } + + private val _currentCall: MutableStateFlow = MutableStateFlow(Call.None) + override val currentCall = _currentCall.asStateFlow() + + private var listener: CallRepository.Listener? = null + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + override fun setListener(listener: CallRepository.Listener) { + this.listener = listener + // Observe call state changes + scope.launch { currentCall.collect { listener.onCallStateChanged(it) } } + } + + override fun release() { + _currentCall.value = Call.None + + listener = null + + scope.cancel() + } + + override suspend fun registerCall( + callId: String, + displayName: String, + address: Uri, + isIncoming: Boolean, + isVideo: Boolean, + displayOptions: Bundle?, + ) { + + val attributes = createCallAttributes(displayName, address, isIncoming, isVideo) + val actionSource = Channel() + + _currentCall.value = + Call.Registered( + id = callId, + isActive = false, + isOnHold = false, + callAttributes = attributes, + displayOptions = displayOptions, + isMuted = false, + errorCode = null, + currentCallEndpoint = null, + availableCallEndpoints = emptyList(), + actionSource = actionSource, + ) + + listener?.onCallRegistered(callId) + + // Process actions without telecom SDK + scope.launch { + actionSource.consumeAsFlow().collect { action -> processActionLegacy(action) } + } + } + + private fun processActionLegacy(action: CallAction) { + when (action) { + is CallAction.Answer -> { + updateCurrentCall { copy(isActive = true, isOnHold = false) } + (currentCall.value as? Call.Registered)?.let { listener?.onIsCallAnswered(it.id) } + } + is CallAction.Disconnect -> { + val call = currentCall.value as? Call.Registered + if (call != null) { + _currentCall.value = + Call.Unregistered(call.id, call.callAttributes, action.cause) + listener?.onIsCallDisconnected(action.cause) + } + } + is CallAction.ToggleMute -> { + updateCurrentCall { copy(isMuted = action.isMute) } + } + // Handle other actions... + else -> { + /* No-op for unsupported actions */ + } + } + } + + private fun updateCurrentCall(transform: Call.Registered.() -> Call) { + val currentState = _currentCall.value + Log.d( + TAG, + "[repository] updateCurrentCall: Current call state: ${currentState::class.simpleName}" + ) + + _currentCall.update { call -> + if (call is Call.Registered) { + val updated = call.transform() + Log.d( + TAG, + "[repository] updateCurrentCall: Call state updated to: ${updated::class.simpleName}" + ) + updated + } else { + Log.w( + TAG, + "[repository] updateCurrentCall: Call is not Registered, skipping update" + ) + call + } + } + } + + private fun createCallAttributes( + displayName: String, + address: Uri, + isIncoming: Boolean, + isVideo: Boolean + ): CallAttributesCompat { + Log.d( + TAG, + "createCallAttributes: Creating CallAttributes - Direction: ${if (isIncoming) "Incoming" else "Outgoing"}, Type: ${if (isVideo) "Video" else "Audio"}" + ) + return CallAttributesCompat( + displayName = displayName, + address = address, + direction = + if (isIncoming) { + CallAttributesCompat.DIRECTION_INCOMING + } else { + CallAttributesCompat.DIRECTION_OUTGOING + }, + callType = + if (isVideo) { + CallAttributesCompat.CALL_TYPE_VIDEO_CALL + } else { + CallAttributesCompat.CALL_TYPE_AUDIO_CALL + }, + callCapabilities = + CallAttributesCompat.SUPPORTS_SET_INACTIVE or + CallAttributesCompat.SUPPORTS_STREAM or + CallAttributesCompat.SUPPORTS_TRANSFER, + ) + } +} diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallRepository.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt similarity index 92% rename from packages/react-native-callingx/android/src/main/java/com/callingx/CallRepository.kt rename to packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt index 029ae91fa3..b2e4405464 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallRepository.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt @@ -1,20 +1,4 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.callingx +package com.callingx.repo import android.content.Context import android.net.Uri @@ -44,6 +28,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import com.callingx.model.Call import com.callingx.model.CallAction +import CallRepository /** * The central repository that keeps track of the current call and allows to register new calls. @@ -53,24 +38,13 @@ import com.callingx.model.CallAction * @see registerCall */ @RequiresApi(Build.VERSION_CODES.O) -class CallRepository(context: Context) { - - interface Listener { - fun onCallStateChanged(call: Call) - fun onIsCallAnswered(callId: String) - fun onIsCallDisconnected(cause: DisconnectCause) - fun onIsCallInactive(callId: String) - fun onIsCallActive(callId: String) - fun onCallRegistered(callId: String) - fun onMuteCallChanged(callId: String, isMuted: Boolean) - fun onCallEndpointChanged(callId: String, endpoint: String) - } +class TelecomCallRepository(private val context: Context) : CallRepository { companion object { - private const val TAG = "[Callingx] CallRepository" + private const val TAG = "[Callingx] TelecomCallRepository" } - private var listener: Listener? = null + private var listener: CallRepository.Listener? = null private var observeCallStateJob: Job? = null private val callsManager: CallsManager @@ -78,7 +52,7 @@ class CallRepository(context: Context) { // Keeps track of the current TelecomCall state private val _currentCall: MutableStateFlow = MutableStateFlow(Call.None) - val currentCall = _currentCall.asStateFlow() + override val currentCall = _currentCall.asStateFlow() init { val capabilities = @@ -91,14 +65,14 @@ class CallRepository(context: Context) { Log.d(TAG, "[repository] init: CallsManager created and registered") } - fun setListener(listener: Listener) { + override fun setListener(listener: CallRepository.Listener) { this.listener = listener observeCallStateJob?.cancel() observeCallStateJob = observeCallState() } - fun release() { + override fun release() { val currentCall = currentCall.value if (currentCall is Call.Registered) { currentCall.processAction( @@ -118,7 +92,7 @@ class CallRepository(context: Context) { * Register a new call with the provided attributes. Use the [currentCall] StateFlow to receive * status updates and process call related actions. */ - suspend fun registerCall( + override suspend fun registerCall( callId: String, displayName: String, address: Uri, @@ -140,9 +114,7 @@ class CallRepository(context: Context) { "[repository] registerCall: No existing call found, proceeding with registration" ) - // Create the call attributes val attributes = createCallAttributes(displayName, address, isIncoming, isVideo) - // Creates a channel to send actions to the call scope. val actionSource = Channel() // Register the call and handle actions in the scope From 3c345482766b11d870eb5544cc4e8d353b399d26 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Fri, 5 Dec 2025 16:09:37 +0100 Subject: [PATCH 06/27] feat: added registered call check on turbomodule side --- .../src/main/java/com/callingx/CallService.kt | 22 ++------ .../main/java/com/callingx/CallingxModule.kt | 16 ++++-- .../notifications/NotificationsConfig.kt | 2 +- .../react-native-callingx/ios/Callingx.mm | 16 ++++++ .../src/CallingxModule.ts | 4 ++ .../src/spec/NativeCallingx.ts | 2 + packages/react-native-callingx/src/types.ts | 2 + .../useCallingExpWithCallingStateEffect.ts | 54 ++++++------------- .../src/utils/push/android.ts | 16 ++---- .../src/utils/push/internal/ios.ts | 8 ++- .../src/utils/push/internal/rxSubjects.ts | 7 --- 11 files changed, 64 insertions(+), 85 deletions(-) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt index c88c2506b0..3d8b6db6eb 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt @@ -1,19 +1,3 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package com.callingx import CallRepository @@ -26,7 +10,6 @@ import android.os.Bundle import android.os.IBinder import android.telecom.DisconnectCause import android.util.Log -import androidx.annotation.RequiresApi import com.callingx.model.Call import com.callingx.model.CallAction import com.callingx.notifications.CallNotificationManager @@ -270,6 +253,11 @@ class CallService : Service(), CallRepository.Listener { } } + fun isCallRegistered(callId: String): Boolean { + val currentCall = callRepository.currentCall.value + return currentCall is Call.Registered && currentCall.id == callId + } + public fun processAction(action: CallAction) { Log.d(TAG, "[service] processAction: Processing action: ${action::class.simpleName}") val currentCall = callRepository.currentCall.value diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt index 27efb903ee..885aeabf22 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt @@ -12,6 +12,7 @@ import android.os.IBinder import android.telecom.DisconnectCause import android.util.Log import androidx.core.content.ContextCompat +import androidx.core.net.toUri import com.callingx.model.CallAction import com.callingx.notifications.NotificationChannelsManager import com.callingx.notifications.NotificationsConfig @@ -25,7 +26,6 @@ import com.facebook.react.bridge.WritableMap import com.facebook.react.bridge.WritableNativeArray import com.facebook.react.module.annotations.ReactModule import com.facebook.react.modules.core.DeviceEventManagerModule -import androidx.core.net.toUri @ReactModule(name = CallingxModule.NAME) class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec(reactContext) { @@ -252,6 +252,11 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec executeServiceAction(action, promise) } + override fun isCallRegistered(callId: String): Boolean { + Log.d(TAG, "[module] isCallRegistered: Checking if call is registered: $callId") + return callService?.isCallRegistered(callId) ?: false + } + override fun setMutedCall(callId: String, isMuted: Boolean, promise: Promise) { Log.d(TAG, "[module] setMutedCall: Setting muted call: $callId, $isMuted") val action = CallAction.ToggleMute(isMuted) @@ -475,6 +480,11 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec val action = intent.action val callId = intent.getStringExtra(EXTRA_CALL_ID) + Log.d( + TAG, + "[module] onReceive: Received intent: $action callId: $callId callService: ${callService != null}" + ) + if (action == SERVICE_READY_ACTION) { Log.d(TAG, "[module] onReceive: Service is ready, initiating binding") bindToServiceIfNeeded() @@ -486,10 +496,6 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec return } - Log.d( - TAG, - "[module] onReceive: Received intent: $action callId: $callId callService: ${callService != null}" - ) val params = Arguments.createMap() if (callId != null) { params.putString("callId", callId) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt index 4eb3390c8f..7e7b7ca6f1 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt @@ -118,7 +118,7 @@ object NotificationsConfig { id = channel.getString("id") ?: "", name = channel.getString("name") ?: "", sound = channel.getString("sound"), - vibration = channel.getBoolean("vibration"), + vibration = channel.hasKey("vibration") && channel.getBoolean("vibration") ?: false, importance = importance, ) } diff --git a/packages/react-native-callingx/ios/Callingx.mm b/packages/react-native-callingx/ios/Callingx.mm index 69c396490e..24594f27b3 100644 --- a/packages/react-native-callingx/ios/Callingx.mm +++ b/packages/react-native-callingx/ios/Callingx.mm @@ -355,6 +355,8 @@ + (void)endCall:(NSString *)callId reason:(int)reason { default: break; } + + [uuidStorage removeCid:callId]; } + (BOOL)requiresMainQueueSetup { @@ -744,6 +746,20 @@ - (void)endCall:(nonnull NSString *)callId resolve(@YES); } +- (NSNumber *)isCallRegistered:(nonnull NSString *)callId { + NSUUID *uuid = [uuidStorage getUUIDForCid:callId]; + if (uuid == nil) return @NO; + + CXCallObserver *observer = [[CXCallObserver alloc] init]; + for (CXCall *call in observer.calls) { + if ([call.UUID isEqual:uuid]) { + return @YES; + } + } + + return @NO; +} + - (void)setCurrentCallActive:(nonnull NSString *)callId resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { diff --git a/packages/react-native-callingx/src/CallingxModule.ts b/packages/react-native-callingx/src/CallingxModule.ts index 7e5fa35380..1fd51f23ea 100644 --- a/packages/react-native-callingx/src/CallingxModule.ts +++ b/packages/react-native-callingx/src/CallingxModule.ts @@ -184,6 +184,10 @@ class CallingxModule implements ICallingxModule { return NativeCallingModule.endCallWithReason(callId, reasons[reason]); } + isCallRegistered(callId: string): boolean { + return NativeCallingModule.isCallRegistered(callId); + } + setMutedCall(callId: string, isMuted: boolean): Promise { return NativeCallingModule.setMutedCall(callId, isMuted); } diff --git a/packages/react-native-callingx/src/spec/NativeCallingx.ts b/packages/react-native-callingx/src/spec/NativeCallingx.ts index 9a9780fd83..9f1d1a035d 100644 --- a/packages/react-native-callingx/src/spec/NativeCallingx.ts +++ b/packages/react-native-callingx/src/spec/NativeCallingx.ts @@ -76,6 +76,8 @@ export interface Spec extends TurboModule { } ): Promise; + isCallRegistered(callId: string): boolean; + endCallWithReason(callId: string, reason: number): Promise; endCall(callId: string): Promise; diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index 6683bb985b..46cac7a0e1 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -39,6 +39,8 @@ export interface ICallingxModule { displayOptions?: InfoDisplayOptions ): Promise; + isCallRegistered(callId: string): boolean; + endCallWithReason(callId: string, reason: EndCallReason): Promise; setMutedCall(callId: string, isMuted: boolean): Promise; diff --git a/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts index 84c5c295ce..50323553ca 100644 --- a/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts +++ b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts @@ -5,7 +5,6 @@ import { } from '@stream-io/video-client'; import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings'; import { useEffect, useRef } from 'react'; -import { voipPushNotificationCallCId$ } from '../../utils/push/internal/rxSubjects'; import { getCallingxLibIfAvailable } from '../../utils/push/libs/callingx'; //calling state methods are not exhaustive, so we need to add more methods to cover all the cases @@ -40,53 +39,40 @@ export const useCallingExpWithCallingStateEffect = () => { useEffect(() => { return () => { const callingx = getCallingxLibIfAvailable(); - if (!callingx) { + if (!callingx || !activeCallCid) { return; } - //as an alternative think about method which will return CallKit/Telecom state for given cid - const incomingCallCid = RxUtils.getCurrentValue( - voipPushNotificationCallCId$, - ); - if ( - !activeCallCid || - !incomingCallCid || - incomingCallCid !== activeCallCid - ) { + + const isCallRegistered = callingx.isCallRegistered(activeCallCid); + if (!isCallRegistered) { logger.debug( - `No active call cid to end in calling exp: ${activeCallCid} incomingCallCid: ${incomingCallCid}`, + `No active call cid to end in calling exp: ${activeCallCid} isCallRegistered: ${isCallRegistered}`, ); return; } //if incoming stream call was unmounted, we need to end the call in CallKit/Telecom //TODO: think about sending appropriate reason for end call + logger.debug(`Ending call in calling exp: ${activeCallCid}`); callingx.endCallWithReason(activeCallCid, 'local'); - voipPushNotificationCallCId$.next(undefined); }; }, [activeCallCid]); useEffect(() => { const callingx = getCallingxLibIfAvailable(); - if (!callingx) { + if (!callingx || !activeCallCid) { return; } - //as an alternative think about method which will return CallKit/Telecom state for given cid - const incomingCallCid = RxUtils.getCurrentValue( - voipPushNotificationCallCId$, - ); - if ( - !incomingCallCid || - !activeCallCid || - incomingCallCid !== activeCallCid - ) { + const isCallRegistered = callingx.isCallRegistered(activeCallCid); + if (!isCallRegistered) { logger.debug( - `No active call cid to end in calling exp: ${activeCallCid} incomingCallCid: ${incomingCallCid}`, + `No active call cid to end in calling exp: ${activeCallCid} isCallRegistered: ${isCallRegistered}`, ); return; } logger.debug( - `useEffect: ${activeCallCid} incomingCallCid: ${incomingCallCid}`, + `useEffect: ${activeCallCid} isCallRegistered: ${isCallRegistered}`, ); logger.debug( `prevState.current: ${prevState.current}, current callingState: ${callingState}`, @@ -99,7 +85,7 @@ export const useCallingExpWithCallingStateEffect = () => { ) { //in case call was registered as incoming and state changed to joined, we need to answer the call logger.debug( - `Should accept call in callkeep: ${activeCallCid} callCid: ${incomingCallCid}`, + `Should accept call in callkeep: ${activeCallCid} isCallRegistered: ${isCallRegistered}`, ); callingx.answerIncomingCall(activeCallCid); } else if ( @@ -110,7 +96,6 @@ export const useCallingExpWithCallingStateEffect = () => { logger.debug(`Should end call in callkeep: ${activeCallCid}`); //TODO: think about sending appropriate reason for end call callingx.endCallWithReason(activeCallCid, 'local'); - voipPushNotificationCallCId$.next(undefined); } } @@ -119,21 +104,14 @@ export const useCallingExpWithCallingStateEffect = () => { useEffect(() => { const callingx = getCallingxLibIfAvailable(); - if (!callingx) { + if (!callingx || !activeCallCid) { return; } - //for now supports only incoming calls - const incomingCallCid = RxUtils.getCurrentValue( - voipPushNotificationCallCId$, - ); - if ( - !incomingCallCid || - !activeCallCid || - incomingCallCid !== activeCallCid - ) { + const isCallRegistered = callingx.isCallRegistered(activeCallCid); + if (!isCallRegistered) { logger.debug( - `No active call cid to set muted in calling exp: ${activeCallCid} incomingCallCid: ${incomingCallCid}`, + `No active call cid to set muted in calling exp: ${activeCallCid} isCallRegistered: ${isCallRegistered}`, ); return; } diff --git a/packages/react-native-sdk/src/utils/push/android.ts b/packages/react-native-sdk/src/utils/push/android.ts index 5fd1e5ffb7..1722376907 100644 --- a/packages/react-native-sdk/src/utils/push/android.ts +++ b/packages/react-native-sdk/src/utils/push/android.ts @@ -1,6 +1,5 @@ import { CallingState, - RxUtils, StreamVideoClient, videoLoggerSystem, } from '@stream-io/video-client'; @@ -18,10 +17,7 @@ import { getNotifeeLibThrowIfNotInstalledForPush, type NotifeeLib, } from './libs'; -import { - pushNonRingingCallData$, - voipPushNotificationCallCId$, -} from './internal/rxSubjects'; +import { pushNonRingingCallData$ } from './internal/rxSubjects'; import { pushUnsubscriptionCallbacks } from './internal/constants'; import { canListenToWS, shouldCallBeClosed } from './internal/utils'; import { setPushLogoutCallback } from '../internal/pushLogoutCallback'; @@ -151,14 +147,15 @@ export const firebaseDataHandler = async ( return; } - if (RxUtils.getCurrentValue(voipPushNotificationCallCId$)) { + const callingx = getCallingxLib(); + + if (callingx.isCallRegistered(call_cid)) { logger.debug( `call.ring notification already processed, skipping the call.ring notification`, ); return; } - const callingx = getCallingxLib(); await callingx.checkPermissions(); if (!callingx.canPostNotifications()) { logger.debug( @@ -190,7 +187,6 @@ export const firebaseDataHandler = async ( logger.debug( `Displaying incoming call notification with callCid: ${call_cid} asForegroundService: ${asForegroundService}`, ); - voipPushNotificationCallCId$.next(call_cid); if (asForegroundService) { // Listen to call events from WS through fg service @@ -223,7 +219,6 @@ export const firebaseDataHandler = async ( `Ending call with callCid: ${call_cid} shouldCallBeClosed`, 'debug', ); - voipPushNotificationCallCId$.next(undefined); //TODO: think about sending appropriate reason for end call callingx.endCallWithReason(call_cid, 'remote'); return; @@ -251,7 +246,6 @@ export const firebaseDataHandler = async ( ); unsubscribeFunctions.forEach((fn) => fn()); - voipPushNotificationCallCId$.next(undefined); finishBackgroundTask(); //TODO: think about sending appropriate reason for end call callingx.endCallWithReason(call_cid, 'rejected'); @@ -275,7 +269,6 @@ export const firebaseDataHandler = async ( `Ending call with callCid: ${call_cid} callingState: ${callingState}`, 'debug', ); - voipPushNotificationCallCId$.next(undefined); finishBackgroundTask(); callingx.endCallWithReason(call_cid, 'remote'); } @@ -297,7 +290,6 @@ export const firebaseDataHandler = async ( `Ending call with callCid: ${call_cid} callId: ${callId}`, 'debug', ); - voipPushNotificationCallCId$.next(undefined); finishBackgroundTask(); callingx.endCallWithReason(callId, 'rejected'); } diff --git a/packages/react-native-sdk/src/utils/push/internal/ios.ts b/packages/react-native-sdk/src/utils/push/internal/ios.ts index 613f3b04de..a3e7cc7913 100644 --- a/packages/react-native-sdk/src/utils/push/internal/ios.ts +++ b/packages/react-native-sdk/src/utils/push/internal/ios.ts @@ -1,10 +1,9 @@ import { NativeModules, Platform } from 'react-native'; import { getVoipPushNotificationLib } from '../libs'; -import { voipPushNotificationCallCId$ } from './rxSubjects'; import { pushUnsubscriptionCallbacks } from './constants'; import { canListenToWS, shouldCallBeClosed } from './utils'; import { StreamVideoConfig } from '../../StreamVideoRN/types'; -import { RxUtils, videoLoggerSystem } from '@stream-io/video-client'; +import { videoLoggerSystem } from '@stream-io/video-client'; import { getCallingxLib } from '../libs/callingx'; export const onVoipNotificationReceived = async ( @@ -46,7 +45,8 @@ export const onVoipNotificationReceived = async ( return; } - if (RxUtils.getCurrentValue(voipPushNotificationCallCId$)) { + const callingx = getCallingxLib(); + if (callingx.isCallRegistered(call_cid)) { logger.debug( `call.ring notification already processed, skipping the call.ring notification`, ); @@ -74,7 +74,6 @@ export const onVoipNotificationReceived = async ( if (mustEndCall) { logger.debug(`callkeep.reportEndCallWithUUID for call_cid: ${call_cid}`); //TODO: think about sending appropriate reason for end call - const callingx = getCallingxLib(); callingx.endCallWithReason(call_cid, 'remote'); const voipPushNotification = getVoipPushNotificationLib(); @@ -115,5 +114,4 @@ export const onVoipNotificationReceived = async ( logger.debug( `call_cid:${call_cid} received and processed from call.ring push notification`, ); - voipPushNotificationCallCId$.next(call_cid); }; diff --git a/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts b/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts index 67b4b3eeab..0e10e40b71 100644 --- a/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts +++ b/packages/react-native-sdk/src/utils/push/internal/rxSubjects.ts @@ -40,10 +40,3 @@ export const pushAndroidBackgroundDeliveredIncomingCallCId$ = export const pushRejectedIncomingCallCId$ = new BehaviorSubject< string | undefined >(undefined); - -/** - * This rxjs subject is used to store the call cid of the incoming call from voip notification - */ -export const voipPushNotificationCallCId$ = new BehaviorSubject< - string | undefined ->(undefined); From 6a12d555a68ac902146bfde8f00c186ff47fab1c Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Tue, 9 Dec 2025 09:29:00 +0100 Subject: [PATCH 07/27] chore: removed unused param --- packages/react-native-sdk/src/utils/push/ios.ts | 7 +------ .../react-native/dogfood/src/utils/setPushConfig.ts | 6 +++--- .../react-native/expo-video-sample/utils/setPushConfig.ts | 6 +++--- .../ringing-tutorial/utils/setFirebaseListeners.android.ts | 4 ++-- 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/packages/react-native-sdk/src/utils/push/ios.ts b/packages/react-native-sdk/src/utils/push/ios.ts index 077a6c8c58..f78d7a3941 100644 --- a/packages/react-native-sdk/src/utils/push/ios.ts +++ b/packages/react-native-sdk/src/utils/push/ios.ts @@ -62,12 +62,7 @@ export const oniOSExpoNotificationEvent = (event: ExpoNotification) => { } }; -export const oniOSNotifeeEvent = ({ - event, -}: { - event: Event; - isBackground: boolean; -}) => { +export const oniOSNotifeeEvent = ({ event }: { event: Event }) => { if (Platform.OS !== 'ios') return; const pushConfig = StreamVideoRN.getConfig().push; const { type, detail } = event; diff --git a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts index cb5aeb2dd3..08f1c9776f 100644 --- a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts +++ b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts @@ -108,13 +108,13 @@ export function setPushConfig() { // on press handlers of background notifications notifee.onBackgroundEvent(async (event) => { if (isNotifeeStreamVideoEvent(event)) { - await onAndroidNotifeeEvent({ event, isBackground: true }); + await onAndroidNotifeeEvent({ event }); } }); // on press handlers of foreground notifications notifee.onForegroundEvent((event) => { if (isNotifeeStreamVideoEvent(event)) { - onAndroidNotifeeEvent({ event, isBackground: false }); + onAndroidNotifeeEvent({ event }); } }); } @@ -123,7 +123,7 @@ export function setPushConfig() { // note: used only for non-ringing notifications notifee.onForegroundEvent((event) => { if (isNotifeeStreamVideoEvent(event)) { - oniOSNotifeeEvent({ event, isBackground: false }); + oniOSNotifeeEvent({ event }); } }); } diff --git a/sample-apps/react-native/expo-video-sample/utils/setPushConfig.ts b/sample-apps/react-native/expo-video-sample/utils/setPushConfig.ts index 3a8b651dd3..117a453a02 100644 --- a/sample-apps/react-native/expo-video-sample/utils/setPushConfig.ts +++ b/sample-apps/react-native/expo-video-sample/utils/setPushConfig.ts @@ -68,13 +68,13 @@ export function setPushConfig() { // on press handlers of background notifications notifee.onBackgroundEvent(async (event) => { if (isNotifeeStreamVideoEvent(event)) { - await onAndroidNotifeeEvent({ event, isBackground: true }); + await onAndroidNotifeeEvent({ event }); } }); // on press handlers of foreground notifications notifee.onForegroundEvent((event) => { if (isNotifeeStreamVideoEvent(event)) { - onAndroidNotifeeEvent({ event, isBackground: false }); + onAndroidNotifeeEvent({ event }); } }); } @@ -96,7 +96,7 @@ export function setPushConfig() { // note: used only for non-ringing notifications notifee.onForegroundEvent((event) => { if (isNotifeeStreamVideoEvent(event)) { - oniOSNotifeeEvent({ event, isBackground: false }); + oniOSNotifeeEvent({ event }); } }); } diff --git a/sample-apps/react-native/ringing-tutorial/utils/setFirebaseListeners.android.ts b/sample-apps/react-native/ringing-tutorial/utils/setFirebaseListeners.android.ts index 44486ccde0..a1123779c2 100644 --- a/sample-apps/react-native/ringing-tutorial/utils/setFirebaseListeners.android.ts +++ b/sample-apps/react-native/ringing-tutorial/utils/setFirebaseListeners.android.ts @@ -16,7 +16,7 @@ export const setFirebaseListeners = () => { }); notifee.onBackgroundEvent(async (event) => { if (isNotifeeStreamVideoEvent(event)) { - await onAndroidNotifeeEvent({ event, isBackground: true }); + await onAndroidNotifeeEvent({ event }); } }); // Set up the foreground message handlers @@ -27,7 +27,7 @@ export const setFirebaseListeners = () => { }); notifee.onForegroundEvent((event) => { if (isNotifeeStreamVideoEvent(event)) { - onAndroidNotifeeEvent({ event, isBackground: false }); + onAndroidNotifeeEvent({ event }); } }); }; From a9205074f2fed129e704552b0536750517c87d2c Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Tue, 9 Dec 2025 15:29:43 +0100 Subject: [PATCH 08/27] feat: added android notification timer --- .../callingx/notifications/CallNotificationManager.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt index 3cd314ea64..78ef300afd 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt @@ -39,6 +39,8 @@ class CallNotificationManager( private var ringtone: Ringtone? = null + private var hasBecameActive = false + fun createNotification(call: Call.Registered): Notification { Log.d(TAG, "createNotification: Creating notification for call ID: ${call.id}") @@ -61,6 +63,14 @@ class CallNotificationManager( .setPriority(NotificationCompat.PRIORITY_MAX) .setOngoing(true) + if (!hasBecameActive && call.isActive) { + Log.d(TAG, "createNotification: Setting when to current time") + builder.setWhen(System.currentTimeMillis()) + builder.setUsesChronometer(true) + builder.setShowWhen(true) + hasBecameActive = true + } + call.displayOptions?.let { if (it.containsKey(CallService.EXTRA_DISPLAY_SUBTITLE)) { builder.setContentText(it.getString(CallService.EXTRA_DISPLAY_SUBTITLE)) @@ -86,6 +96,7 @@ class CallNotificationManager( fun cancelNotifications() { notificationManager.cancel(NOTIFICATION_ID) + hasBecameActive = false } fun startRingtone() { From d8e53bce7715b19dfc941e90887e7fd3f11593c6 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Tue, 9 Dec 2025 15:38:48 +0100 Subject: [PATCH 09/27] feat: added outcoming call support --- .../callingx/repo/TelecomCallRepository.kt | 23 +-- .../react-native-callingx/ios/Callingx.mm | 1 + .../src/CallingxModule.ts | 12 +- packages/react-native-callingx/src/types.ts | 4 +- .../useCallingExpWithCallingStateEffect.ts | 161 ++++++++++++++---- .../src/utils/push/android.ts | 2 +- .../dogfood/src/utils/setPushConfig.ts | 11 +- 7 files changed, 157 insertions(+), 57 deletions(-) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt index b2e4405464..61d3ab7fdd 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt @@ -1,5 +1,6 @@ package com.callingx.repo +import CallRepository import android.content.Context import android.net.Uri import android.os.Build @@ -11,6 +12,8 @@ import androidx.core.telecom.CallAttributesCompat import androidx.core.telecom.CallControlResult import androidx.core.telecom.CallControlScope import androidx.core.telecom.CallsManager +import com.callingx.model.Call +import com.callingx.model.CallAction import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -26,9 +29,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.scan import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import com.callingx.model.Call -import com.callingx.model.CallAction -import CallRepository /** * The central repository that keeps track of the current call and allows to register new calls. @@ -75,9 +75,7 @@ class TelecomCallRepository(private val context: Context) : CallRepository { override fun release() { val currentCall = currentCall.value if (currentCall is Call.Registered) { - currentCall.processAction( - CallAction.Disconnect(DisconnectCause(DisconnectCause.LOCAL)) - ) + currentCall.processAction(CallAction.Disconnect(DisconnectCause(DisconnectCause.LOCAL))) } _currentCall.value = Call.None @@ -182,11 +180,16 @@ class TelecomCallRepository(private val context: Context) : CallRepository { .onEach { (previous, current) -> when { previous is Call.None && current is Call.Registered -> { - listener?.onCallRegistered(current.id) + if (!(current as Call.Registered).isIncoming()) { + listener?.onCallRegistered(current.id) + } } previous is Call.Registered && current is Call.Registered -> { if (previous.isMuted != current.isMuted) { - Log.d(TAG, "[repository] observeCallState: Mute changed: ${current.isMuted}") + Log.d( + TAG, + "[repository] observeCallState: Mute changed: ${current.isMuted}" + ) listener?.onMuteCallChanged(current.id, current.isMuted) } if (previous.currentCallEndpoint != current.currentCallEndpoint) { @@ -306,9 +309,7 @@ class TelecomCallRepository(private val context: Context) : CallRepository { } } - private suspend fun CallControlScope.doSwitchEndpoint( - action: CallAction.SwitchAudioEndpoint - ) { + private suspend fun CallControlScope.doSwitchEndpoint(action: CallAction.SwitchAudioEndpoint) { Log.d(TAG, "[repository] doSwitchEndpoint: Switching to endpoint: ${action.endpointId}") // TODO once availableCallEndpoints is a state flow we can just get the value val endpoints = (_currentCall.value as Call.Registered).availableCallEndpoints diff --git a/packages/react-native-callingx/ios/Callingx.mm b/packages/react-native-callingx/ios/Callingx.mm index 24594f27b3..205cef603e 100644 --- a/packages/react-native-callingx/ios/Callingx.mm +++ b/packages/react-native-callingx/ios/Callingx.mm @@ -772,6 +772,7 @@ - (void)setCurrentCallActive:(nonnull NSString *)callId } [self.callKeepProvider reportOutgoingCallWithUUID:uuid startedConnectingAtDate:[NSDate date]]; + [self.callKeepProvider reportOutgoingCallWithUUID:uuid connectedAtDate:[NSDate date]]; resolve(@YES); } diff --git a/packages/react-native-callingx/src/CallingxModule.ts b/packages/react-native-callingx/src/CallingxModule.ts index 1fd51f23ea..4f119c0c84 100644 --- a/packages/react-native-callingx/src/CallingxModule.ts +++ b/packages/react-native-callingx/src/CallingxModule.ts @@ -120,8 +120,8 @@ class CallingxModule implements ICallingxModule { hasVideo: boolean ): Promise { const displayOptions: InfoDisplayOptions = { - displayTitle: this.titleTransformer(callerName), - displaySubtitle: this.subtitleTransformer?.(phoneNumber), + displayTitle: this.titleTransformer(callerName, true), + displaySubtitle: this.subtitleTransformer?.(phoneNumber, true), }; return NativeCallingModule.displayIncomingCall( callId, @@ -144,8 +144,8 @@ class CallingxModule implements ICallingxModule { hasVideo: boolean ): Promise { const displayOptions: InfoDisplayOptions = { - displayTitle: this.titleTransformer(callerName), - displaySubtitle: this.subtitleTransformer?.(phoneNumber), + displayTitle: this.titleTransformer(callerName, false), + displaySubtitle: this.subtitleTransformer?.(phoneNumber, false), }; return NativeCallingModule.startCall( callId, @@ -162,8 +162,8 @@ class CallingxModule implements ICallingxModule { callerName: string ): Promise { const displayOptions: InfoDisplayOptions = { - displayTitle: this.titleTransformer(callerName), - displaySubtitle: this.subtitleTransformer?.(phoneNumber), + displayTitle: this.titleTransformer(callerName, false), + displaySubtitle: this.subtitleTransformer?.(phoneNumber, false), }; return NativeCallingModule.updateDisplay( callId, diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index 46cac7a0e1..afda14edce 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -30,7 +30,7 @@ export interface ICallingxModule { callerName: string, hasVideo: boolean, displayOptions?: InfoDisplayOptions - ): void; + ): Promise; updateDisplay( callId: string, @@ -80,7 +80,7 @@ export type AndroidOptions = { }; }; -export type TextTransformer = (text: string) => string; +export type TextTransformer = (text: string, incoming: boolean) => string; export type NotificationTransformers = { titleTransformer: TextTransformer; subtitleTransformer?: TextTransformer; diff --git a/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts index 50323553ca..47684abefa 100644 --- a/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts +++ b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts @@ -1,10 +1,11 @@ import { CallingState, + MemberResponse, RxUtils, videoLoggerSystem, } from '@stream-io/video-client'; import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings'; -import { useEffect, useRef } from 'react'; +import { useEffect, useMemo, useRef } from 'react'; import { getCallingxLibIfAvailable } from '../../utils/push/libs/callingx'; //calling state methods are not exhaustive, so we need to add more methods to cover all the cases @@ -21,8 +22,24 @@ const logger = videoLoggerSystem.getLogger( 'useCallingExpWithCallingStateEffect', ); +function getOutcomingDisplayName( + members: MemberResponse[] | undefined, + currentUserId: string | undefined, +) { + if (!members || !currentUserId) { + return 'Unknown'; + } + + const names = members + .filter((member) => member.user_id !== currentUserId) + .map((member) => member.user.name) + .filter(Boolean); + + return names.length > 0 ? names.join(', ') : 'Unknown'; +} + /** - * This hook is used to inform sync call state with CallKit/Telecom (e.i. start call, end call, mute/unmute call). + * This hook is used to inform sync call state with CallKit/Telecom (i.e. start call, end call, mute/unmute call). */ export const useCallingExpWithCallingStateEffect = () => { const { useCallCallingState, useMicrophoneState } = useCallStateHooks(); @@ -34,7 +51,14 @@ export const useCallingExpWithCallingStateEffect = () => { const prevState = useRef(undefined); const activeCallCid = activeCall?.cid; - // const isOutcomingCall = activeCall?.isCreatedByMe; //can be used to trigger CallKit/Telecom start call for outcoming calls + const isOutcomingCall = activeCall?.isCreatedByMe && activeCall?.ringing; //is this reliable?? + const currentUserId = activeCall?.currentUserId; + const isVideoCall = activeCall?.state.settings?.video?.enabled ?? false; + + const outcomingDisplayName = useMemo( + () => getOutcomingDisplayName(activeCall?.state.members, currentUserId), + [activeCall?.state.members, currentUserId], + ); useEffect(() => { return () => { @@ -53,54 +77,116 @@ export const useCallingExpWithCallingStateEffect = () => { //if incoming stream call was unmounted, we need to end the call in CallKit/Telecom //TODO: think about sending appropriate reason for end call logger.debug(`Ending call in calling exp: ${activeCallCid}`); - callingx.endCallWithReason(activeCallCid, 'local'); + callingx + .endCallWithReason(activeCallCid, 'remote') + .catch((error: unknown) => { + logger.error( + `Error ending call in calling exp: ${activeCallCid}`, + error, + ); + }); }; }, [activeCallCid]); useEffect(() => { const callingx = getCallingxLibIfAvailable(); - if (!callingx || !activeCallCid) { + if (!callingx || !activeCallCid || prevState.current === callingState) { return; } + //tells if call is registered in CallKit/Telecom const isCallRegistered = callingx.isCallRegistered(activeCallCid); - if (!isCallRegistered) { - logger.debug( - `No active call cid to end in calling exp: ${activeCallCid} isCallRegistered: ${isCallRegistered}`, - ); - return; - } - logger.debug( - `useEffect: ${activeCallCid} isCallRegistered: ${isCallRegistered}`, + `useEffect: ${activeCallCid} isCallRegistered: ${isCallRegistered} isOutcomingCall: ${isOutcomingCall}`, ); logger.debug( `prevState.current: ${prevState.current}, current callingState: ${callingState}`, ); - if (prevState.current !== callingState) { - if ( - !isAcceptedCallingState(prevState.current) && - isAcceptedCallingState(callingState) - ) { - //in case call was registered as incoming and state changed to joined, we need to answer the call + if ( + !isAcceptedCallingState(prevState.current) && + isAcceptedCallingState(callingState) + ) { + if (isOutcomingCall && !isCallRegistered) { + //we request start call action from CallKit/Telecom, next step is to make call active when we receive call started event + logger.debug(`Should start call in callkeep: ${activeCallCid}`); + callingx + .startCall( + activeCallCid, + activeCallCid, + outcomingDisplayName, + isVideoCall, + ) + .catch((error: unknown) => { + logger.error( + `Error starting call in calling exp: ${activeCallCid}`, + error, + ); + }); + } else if (isCallRegistered) { logger.debug( `Should accept call in callkeep: ${activeCallCid} isCallRegistered: ${isCallRegistered}`, ); - callingx.answerIncomingCall(activeCallCid); - } else if ( - isAcceptedCallingState(prevState.current) && - !isAcceptedCallingState(callingState) - ) { - //in case call was registered as incoming and state changed to "not joined", we need to end the call and clear rxjs subject - logger.debug(`Should end call in callkeep: ${activeCallCid}`); - //TODO: think about sending appropriate reason for end call - callingx.endCallWithReason(activeCallCid, 'local'); + callingx.answerIncomingCall(activeCallCid).catch((error: unknown) => { + logger.error( + `Error answering call in calling exp: ${activeCallCid}`, + error, + ); + }); } + } else if ( + isAcceptedCallingState(prevState.current) && + !isAcceptedCallingState(callingState) && + isCallRegistered + ) { + //in case call was registered as incoming and state changed to "not joined", we need to end the call and clear rxjs subject + logger.debug(`Should end call in callkeep: ${activeCallCid}`); + //TODO: think about sending appropriate reason for end call + callingx + .endCallWithReason(activeCallCid, 'remote') + .catch((error: unknown) => { + logger.error( + `Error ending call in calling exp: ${activeCallCid}`, + error, + ); + }); } prevState.current = callingState; - }, [activeCallCid, callingState]); + }, [ + activeCallCid, + callingState, + isOutcomingCall, + outcomingDisplayName, + isVideoCall, + ]); + + useEffect(() => { + const callingx = getCallingxLibIfAvailable(); + if (!callingx || !activeCallCid) { + return; + } + + //listen to start call action from CallKit/Telecom and set the current call active + const subscription = callingx.addEventListener( + 'didReceiveStartCallAction', + ({ callId }: { callId: string }) => { + if (callId === activeCallCid) { + logger.debug(`Received start call action for call: ${activeCallCid}`); + callingx.answerIncomingCall(activeCallCid).catch((error: unknown) => { + logger.error( + `Error answering call in calling exp: ${activeCallCid}`, + error, + ); + }); + } + }, + ); + + return () => { + subscription.remove(); + }; + }, [activeCallCid]); useEffect(() => { const callingx = getCallingxLibIfAvailable(); @@ -128,7 +214,7 @@ export const useCallingExpWithCallingStateEffect = () => { //listen to mic toggle events from CallKit/Telecom and update stream call microphone state const subscription = callingx.addEventListener( 'didPerformSetMutedCallAction', - async (event) => { + async (event: { callId: string; muted: boolean }) => { const { callId, muted } = event; if (callId === activeCallCid) { @@ -142,10 +228,17 @@ export const useCallingExpWithCallingStateEffect = () => { return; } - if (muted) { - await microphone.disable(); - } else { - await microphone.enable(); + try { + if (muted) { + await microphone.disable(); + } else { + await microphone.enable(); + } + } catch (error: unknown) { + logger.error( + `Error toggling mic in calling exp: ${activeCallCid}`, + error, + ); } } }, diff --git a/packages/react-native-sdk/src/utils/push/android.ts b/packages/react-native-sdk/src/utils/push/android.ts index 1722376907..7b1391837d 100644 --- a/packages/react-native-sdk/src/utils/push/android.ts +++ b/packages/react-native-sdk/src/utils/push/android.ts @@ -175,7 +175,7 @@ export const firebaseDataHandler = async ( const asForegroundService = canListenToWS(); const phoneNumber = data.created_by_display_name as string; - const callerName = data.call_display_name as string; + const callerName = data.created_by_display_name as string; const hasVideo = data.video === 'true'; await callingx.displayIncomingCall( diff --git a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts index 08f1c9776f..ef416749bb 100644 --- a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts +++ b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts @@ -97,9 +97,14 @@ export function setPushConfig() { id: 'stream_outgoing_call_channel_update2', name: 'Outgoing call notifications', }, - titleTransformer: (callerName: string) => - `Incoming call from ${callerName}`, - subtitleTransformer: (phoneNumber: string) => `Tap to open the call`, + titleTransformer: (callerName: string, incoming: boolean) => + incoming + ? `Incoming call from ${callerName}` + : `Outgoing call to ${callerName}`, + subtitleTransformer: (phoneNumber: string, incoming: boolean) => + incoming + ? `Tap to open the call: ${phoneNumber}` + : `Tap to open the call: ${phoneNumber}`, }, }); From da0901e5048057af152e40ed4e0382f576e9b8f1 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Tue, 9 Dec 2025 15:47:23 +0100 Subject: [PATCH 10/27] chore: code cleanup --- packages/react-native-callingx/ios/Callingx.h | 27 +-- .../react-native-callingx/ios/Callingx.mm | 203 +++++------------- .../ios/CallingxPublic.h | 27 +-- packages/react-native-callingx/ios/Settings.h | 16 ++ .../react-native-callingx/ios/Settings.mm | 87 ++++++++ .../react-native-callingx/ios/UUIDStorage.mm | 19 +- .../getstream/rnvideosample/MainActivity.kt | 1 - 7 files changed, 185 insertions(+), 195 deletions(-) create mode 100644 packages/react-native-callingx/ios/Settings.h create mode 100644 packages/react-native-callingx/ios/Settings.mm diff --git a/packages/react-native-callingx/ios/Callingx.h b/packages/react-native-callingx/ios/Callingx.h index 90b635a903..93754f11bc 100644 --- a/packages/react-native-callingx/ios/Callingx.h +++ b/packages/react-native-callingx/ios/Callingx.h @@ -1,10 +1,3 @@ -// -// CallingModule.h -// POCCallingX -// -// Created by Artem Grintsevich on 17/11/2025. -// - #import #import #import @@ -17,13 +10,13 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong) CXCallController *callKeepCallController; @property (nonatomic, strong) CXProvider *callKeepProvider; -+ (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - options:(NSDictionary *)options NS_AVAILABLE_IOS(9_0); - -+ (BOOL)application:(UIApplication *)application -continueUserActivity:(NSUserActivity *)userActivity - restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler; +//+ (BOOL)application:(UIApplication *)application +// openURL:(NSURL *)url +// options:(NSDictionary *)options NS_AVAILABLE_IOS(9_0); +// +//+ (BOOL)application:(UIApplication *)application +//continueUserActivity:(NSUserActivity *)userActivity +// restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler; + (void)reportNewIncomingCall:(NSString *)uuidString handle:(NSString *)handle @@ -38,13 +31,9 @@ continueUserActivity:(NSUserActivity *)userActivity payload:(NSDictionary * _Nullable)payload withCompletionHandler:(void (^_Nullable)(void))completion; -+ (void)endCallWithUUID:(NSString *)uuidString ++ (void)endCall:(NSString *)callId reason:(int)reason; -+ (BOOL)isCallActive:(NSString *)uuidString; - -+ (void)setup:(NSDictionary *)options; - @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native-callingx/ios/Callingx.mm b/packages/react-native-callingx/ios/Callingx.mm index 205cef603e..d81e2661aa 100644 --- a/packages/react-native-callingx/ios/Callingx.mm +++ b/packages/react-native-callingx/ios/Callingx.mm @@ -1,10 +1,3 @@ -// -// Callingx.m -// POCCallingX -// -// Created by Artem Grintsevich on 17/11/2025. -// - #import "Callingx.h" #import #import @@ -12,6 +5,7 @@ #import #import #import "UUIDStorage.h" +#import "Settings.h" #ifdef DEBUG static int const OUTGOING_CALL_WAKEUP_DELAY = 10; @@ -28,12 +22,10 @@ static NSString *const CallingxDidLoadWithEvents = @"didLoadWithEvents"; static NSString *const CallingxDidDisplayIncomingCall = @"didDisplayIncomingCall"; -static NSString *const RNCallKeepHandleStartCallNotification = @"RNCallKeepHandleStartCallNotification"; -static NSString *const RNCallKeepDidActivateAudioSession = @"RNCallKeepDidActivateAudioSession"; -static NSString *const RNCallKeepDidDeactivateAudioSession = @"RNCallKeepDidDeactivateAudioSession"; -static NSString *const RNCallKeepPerformPlayDTMFCallAction = @"RNCallKeepDidPerformDTMFAction"; -static NSString *const RNCallKeepProviderReset = @"RNCallKeepProviderReset"; -static NSString *const RNCallKeepCheckReachability = @"RNCallKeepCheckReachability"; +static NSString *const CallingxDidActivateAudioSession = @"didActivateAudioSession"; +static NSString *const CallingxDidDeactivateAudioSession = @"didDeactivateAudioSession"; +static NSString *const CallingxPerformPlayDTMFCallAction = @"didPerformDTMFAction"; +static NSString *const CallingxProviderReset = @"providerReset"; @implementation Callingx { NSOperatingSystemVersion _version; @@ -42,7 +34,6 @@ @implementation Callingx { NSMutableArray *_delayedEvents; } -static bool isSetupNatively; static CXProvider *sharedProvider; static UUIDStorage *uuidStorage; @@ -57,10 +48,6 @@ + (id)allocWithZone:(NSZone *)zone { return sharedInstance; } -+ (NSDictionary *)getSettings { - return [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"CallingxSettings"]; -} - + (void)initUUIDStorage { if (uuidStorage == nil) { uuidStorage = [[UUIDStorage alloc] init]; @@ -70,46 +57,14 @@ + (void)initUUIDStorage { + (void)initCallKitProvider { if (sharedProvider == nil) { - NSDictionary *settings = [self getSettings]; + NSDictionary *settings = [Settings getSettings]; if (settings != nil) { - sharedProvider = [[CXProvider alloc] initWithConfiguration:[Callingx getProviderConfiguration:settings]]; + sharedProvider = [[CXProvider alloc] initWithConfiguration:[Settings getProviderConfiguration:settings]]; NSLog(@"[Callingx] initCallKitProvider"); } } } -+ (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary *)settings { -#ifdef DEBUG - NSLog(@"[Callingx][getProviderConfiguration]"); -#endif - CXProviderConfiguration *providerConfiguration = [[CXProviderConfiguration alloc] init]; - providerConfiguration.supportsVideo = YES; - providerConfiguration.maximumCallGroups = 3; - providerConfiguration.maximumCallsPerCallGroup = 1; - providerConfiguration.supportedHandleTypes = [Callingx getSupportedHandleTypes:settings[@"handleType"]]; - - if (settings[@"supportsVideo"]) { - providerConfiguration.supportsVideo = [settings[@"supportsVideo"] boolValue]; - } - if (settings[@"maximumCallGroups"]) { - providerConfiguration.maximumCallGroups = [settings[@"maximumCallGroups"] integerValue]; - } - if (settings[@"maximumCallsPerCallGroup"]) { - providerConfiguration.maximumCallsPerCallGroup = [settings[@"maximumCallsPerCallGroup"] integerValue]; - } - if (settings[@"imageName"]) { - providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:settings[@"imageName"]]); - } - if (settings[@"ringtoneSound"]) { - providerConfiguration.ringtoneSound = settings[@"ringtoneSound"]; - } - if (@available(iOS 11.0, *)) { - if (settings[@"includesCallsInRecents"]) { - providerConfiguration.includesCallsInRecents = [settings[@"includesCallsInRecents"] boolValue]; - } - } - return providerConfiguration; -} + (void)reportNewIncomingCall:(NSString *)callId handle:(NSString *)handle @@ -125,6 +80,8 @@ + (void)reportNewIncomingCall:(NSString *)callId withCompletionHandler:(void (^_Nullable)(void))completion { #ifdef DEBUG NSLog(@"[Callingx][reportNewIncomingCall] callId = %@", callId); + NSLog(@"[Callingx][reportNewIncomingCall] handle = %@", handle); + NSLog(@"[Callingx][reportNewIncomingCall] localizedCallerName = %@", localizedCallerName); #endif [Callingx initUUIDStorage]; @@ -135,7 +92,7 @@ + (void)reportNewIncomingCall:(NSString *)callId return; } - CXHandleType _handleType = [Callingx getHandleType:handleType]; + CXHandleType _handleType = [Settings getHandleType:handleType]; NSUUID *uuid = [uuidStorage getOrCreateUUIDForCid:callId]; CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; callUpdate.remoteHandle = [[CXHandle alloc] initWithType:_handleType value:handle]; @@ -263,39 +220,6 @@ + (void)reportNewIncomingCall:(NSString *)callId // return NO; //} -+ (NSSet *)getSupportedHandleTypes:(id)handleType { - if (handleType) { - if ([handleType isKindOfClass:[NSArray class]]) { - NSSet *types = [NSSet set]; - - for (NSString *type in handleType) { - types = [types setByAddingObject: [NSNumber numberWithInteger:[Callingx getHandleType:type]]]; - } - - return types; - } else { - CXHandleType _handleType = [Callingx getHandleType:handleType]; - - return [NSSet setWithObjects:[NSNumber numberWithInteger:_handleType], nil]; - } - } else { - return [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypePhoneNumber], nil]; - } -} - -+ (CXHandleType)getHandleType:(NSString *)handleType { - if ([handleType isEqualToString:@"generic"]) { - return CXHandleTypeGeneric; - } else if ([handleType isEqualToString:@"number"]) { - return CXHandleTypePhoneNumber; - } else if ([handleType isEqualToString:@"phone"]) { - return CXHandleTypePhoneNumber; - } else if ([handleType isEqualToString:@"email"]) { - return CXHandleTypeEmailAddress; - } else { - return CXHandleTypeGeneric; - } -} + (NSString *)getAudioOutput { @try { @@ -311,12 +235,6 @@ + (NSString *)getAudioOutput { return nil; } -+ (void)setup:(NSDictionary *)options { - Callingx *callKeep = [Callingx allocWithZone: nil]; - [callKeep setup:options]; - isSetupNatively = YES; -} - + (void)endCall:(NSString *)callId reason:(int)reason { #ifdef DEBUG NSLog(@"[Callingx][endCall] callId = %@ reason = %d", callId, reason); @@ -413,25 +331,6 @@ - (void)dealloc { _isReachable = NO; } -// Override method of RCTEventEmitter -- (NSArray *)supportedEvents { - return @[ - CallingxDidReceiveStartCallAction, - CallingxPerformAnswerCallAction, - CallingxPerformEndCallAction, - CallingxDidPerformSetMutedCallAction, - CallingxDidToggleHoldAction, - CallingxDidLoadWithEvents, - CallingxDidChangeAudioRoute, - CallingxDidDisplayIncomingCall, - RNCallKeepDidActivateAudioSession, - RNCallKeepDidDeactivateAudioSession, - RNCallKeepPerformPlayDTMFCallAction, - RNCallKeepProviderReset, - RNCallKeepCheckReachability, - ]; -} - //- (void)startObserving { // NSLog(@"[Callingx][startObserving]"); // _hasListeners = YES; @@ -465,15 +364,6 @@ - (void)dealloc { // } //} -- (void)setSettings:(NSDictionary *)options { -#ifdef DEBUG - NSLog(@"[Callingx][setSettings] options = %@", options); -#endif - NSDictionary *settings = [[NSMutableDictionary alloc] initWithDictionary:options]; - // Store settings in NSUserDefault - [[NSUserDefaults standardUserDefaults] setObject:settings forKey:@"CallingxSettings"]; - [[NSUserDefaults standardUserDefaults] synchronize]; -} - (void)configureAudioSession { #ifdef DEBUG @@ -484,7 +374,7 @@ - (void)configureAudioSession { AVAudioSessionCategoryOptionAllowBluetoothA2DP; NSString *mode = AVAudioSessionModeDefault; - NSDictionary *settings = [Callingx getSettings]; + NSDictionary *settings = [Settings getSettings]; if (settings && settings[@"audioSession"]) { if (settings[@"audioSession"][@"categoryOptions"]) { categoryOptions = [settings[@"audioSession"][@"categoryOptions"] integerValue]; @@ -599,31 +489,22 @@ - (NSString *)getIncomingCallErrorCode:(NSError *)error { } } -- (void)setup:(NSDictionary *)options { - NSLog(@"[Callingx][setup] options = %@", options); - if (isSetupNatively) { -#ifdef DEBUG - NSLog(@"[Callingx][setup] already setup"); - RCTLog(@"[Callingx][setup] already setup in native code"); -#endif - return; - } - -#ifdef DEBUG - NSLog(@"[Callingx][setup] options = %@", options); -#endif - _version = [[[NSProcessInfo alloc] init] operatingSystemVersion]; - self.callKeepCallController = [[CXCallController alloc] init]; - - [self setSettings:options]; - - [Callingx initCallKitProvider]; - [Callingx initUUIDStorage]; - - self.callKeepProvider = sharedProvider; - [self.callKeepProvider setDelegate:nil queue:nil]; - [self.callKeepProvider setDelegate:self queue:nil]; -} +//- (void)setup:(NSDictionary *)options { +//#ifdef DEBUG +// NSLog(@"[Callingx][setup] options = %@", options); +//#endif +// _version = [[[NSProcessInfo alloc] init] operatingSystemVersion]; +// self.callKeepCallController = [[CXCallController alloc] init]; +// +// [Settings setSettings:options]; +// +// [Callingx initCallKitProvider]; +// [Callingx initUUIDStorage]; +// +// self.callKeepProvider = sharedProvider; +// [self.callKeepProvider setDelegate:nil queue:nil]; +// [self.callKeepProvider setDelegate:self queue:nil]; +//} #pragma mark - Turbo module methods @@ -635,7 +516,17 @@ - (void)setupiOS:(JS::NativeCallingx::SpecSetupiOSOptions &)options { @"maximumCallGroups" : @(options.maximumCallGroups()), @"handleType" : options.handleType() }; - [self setup:optionsDict]; + + _version = [[[NSProcessInfo alloc] init] operatingSystemVersion]; + self.callKeepCallController = [[CXCallController alloc] init]; + + [Settings setSettings:optionsDict]; + [Callingx initCallKitProvider]; + [Callingx initUUIDStorage]; + + self.callKeepProvider = sharedProvider; + [self.callKeepProvider setDelegate:nil queue:nil]; + [self.callKeepProvider setDelegate:self queue:nil]; _isInitialized = YES; } @@ -696,7 +587,7 @@ - (void)displayIncomingCall:(nonnull NSString *)callId payload:nil withCompletionHandler:nil]; - NSDictionary *settings = [Callingx getSettings]; + NSDictionary *settings = [Settings getSettings]; NSNumber *timeout = settings[@"displayCallReachabilityTimeout"]; if (timeout) { @@ -763,7 +654,6 @@ - (NSNumber *)isCallRegistered:(nonnull NSString *)callId { - (void)setCurrentCallActive:(nonnull NSString *)callId resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { - //TODO: adjust implementation NSUUID *uuid = [uuidStorage getUUIDForCid:callId]; if (uuid == nil) { NSLog(@"[Callingx][setCurrentCallActive] callId not found"); @@ -831,7 +721,7 @@ - (void)startCall:(nonnull NSString *)callId #ifdef DEBUG NSLog(@"[Callingx][startCall] uuidString = %@", callId, phoneNumber); #endif - CXHandleType _handleType = [Callingx getHandleType:@"generic"]; + CXHandleType _handleType = [Settings getHandleType:@"generic"]; NSUUID *uuid = [uuidStorage getOrCreateUUIDForCid:callId]; CXHandle *callHandle = [[CXHandle alloc] initWithType:_handleType value:phoneNumber]; CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle]; @@ -912,6 +802,11 @@ - (void)stopBackgroundTask:(NSString *)taskName resolve(@YES); } +- (nonnull NSNumber *)canPostNotifications { + return @YES; +} + + #pragma mark - CXProviderDelegate - (void)providerDidReset:(CXProvider *)provider { @@ -920,7 +815,7 @@ - (void)providerDidReset:(CXProvider *)provider { #endif // this means something big changed, so tell the JS. The JS should // probably respond by hanging up all calls. - [self sendEventWithNameWrapper:RNCallKeepProviderReset body:nil]; + [self sendEventWithNameWrapper:CallingxProviderReset body:nil]; } - (void)provider:(CXProvider *)provider @@ -1030,7 +925,7 @@ - (void)provider:(CXProvider *)provider return; } - [self sendEventWithNameWrapper:RNCallKeepPerformPlayDTMFCallAction + [self sendEventWithNameWrapper:CallingxPerformPlayDTMFCallAction body:@{ @"digits" : action.digits, @"callId" : callId @@ -1082,7 +977,7 @@ - (void)provider:(CXProvider *)provider userInfo:userInfo]; [self configureAudioSession]; - [self sendEventWithNameWrapper:RNCallKeepDidActivateAudioSession body:nil]; + [self sendEventWithNameWrapper:CallingxDidActivateAudioSession body:nil]; } - (void)provider:(CXProvider *)provider @@ -1091,7 +986,7 @@ - (void)provider:(CXProvider *)provider NSLog( @"[Callingx][CXProviderDelegate][provider:didDeactivateAudioSession]"); #endif - [self sendEventWithNameWrapper:RNCallKeepDidDeactivateAudioSession body:nil]; + [self sendEventWithNameWrapper:CallingxDidDeactivateAudioSession body:nil]; } @end diff --git a/packages/react-native-callingx/ios/CallingxPublic.h b/packages/react-native-callingx/ios/CallingxPublic.h index 3e872b71b5..f5b617f1e9 100644 --- a/packages/react-native-callingx/ios/CallingxPublic.h +++ b/packages/react-native-callingx/ios/CallingxPublic.h @@ -1,10 +1,3 @@ -// -// CallingModule.h -// POCCallingX -// -// Created by Artem Grintsevich on 17/11/2025. -// - #import #import @@ -12,13 +5,13 @@ NS_ASSUME_NONNULL_BEGIN @interface Callingx : NSObject -+ (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - options:(NSDictionary *)options NS_AVAILABLE_IOS(9_0); - -+ (BOOL)application:(UIApplication *)application -continueUserActivity:(NSUserActivity *)userActivity - restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler; +//+ (BOOL)application:(UIApplication *)application +// openURL:(NSURL *)url +// options:(NSDictionary *)options NS_AVAILABLE_IOS(9_0); +// +//+ (BOOL)application:(UIApplication *)application +//continueUserActivity:(NSUserActivity *)userActivity +// restorationHandler:(void(^)(NSArray * __nullable restorableObjects))restorationHandler; + (void)reportNewIncomingCall:(NSString *)uuidString handle:(NSString *)handle @@ -33,13 +26,9 @@ continueUserActivity:(NSUserActivity *)userActivity payload:(NSDictionary * _Nullable)payload withCompletionHandler:(void (^_Nullable)(void))completion; -+ (void)endCallWithUUID:(NSString *)uuidString ++ (void)endCall:(NSString *)callId reason:(int)reason; -+ (BOOL)isCallActive:(NSString *)uuidString; - -+ (void)setup:(NSDictionary *)options; - @end NS_ASSUME_NONNULL_END diff --git a/packages/react-native-callingx/ios/Settings.h b/packages/react-native-callingx/ios/Settings.h new file mode 100644 index 0000000000..ae0770f037 --- /dev/null +++ b/packages/react-native-callingx/ios/Settings.h @@ -0,0 +1,16 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface Settings : NSObject + ++ (NSDictionary *)getSettings; ++ (void)setSettings:(NSDictionary *)options; ++ (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary *)settings; ++ (NSSet *)getSupportedHandleTypes:(id)handleType; ++ (CXHandleType)getHandleType:(NSString *)handleType; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-callingx/ios/Settings.mm b/packages/react-native-callingx/ios/Settings.mm new file mode 100644 index 0000000000..a1cbe5d200 --- /dev/null +++ b/packages/react-native-callingx/ios/Settings.mm @@ -0,0 +1,87 @@ +#import "Settings.h" +#import + +@implementation Settings + ++ (NSDictionary *)getSettings { + return [[NSUserDefaults standardUserDefaults] dictionaryForKey:@"CallingxSettings"]; +} + ++ (void)setSettings:(NSDictionary *)options { +#ifdef DEBUG + NSLog(@"[Settings][setSettings] options = %@", options); +#endif + NSDictionary *settings = [[NSMutableDictionary alloc] initWithDictionary:options]; + // Store settings in NSUserDefault + [[NSUserDefaults standardUserDefaults] setObject:settings forKey:@"CallingxSettings"]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + ++ (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary *)settings { +#ifdef DEBUG + NSLog(@"[Settings][getProviderConfiguration]"); +#endif + CXProviderConfiguration *providerConfiguration = [[CXProviderConfiguration alloc] init]; + providerConfiguration.supportsVideo = YES; + providerConfiguration.maximumCallGroups = 3; + providerConfiguration.maximumCallsPerCallGroup = 1; + providerConfiguration.supportedHandleTypes = [Settings getSupportedHandleTypes:settings[@"handleType"]]; + + if (settings[@"supportsVideo"]) { + providerConfiguration.supportsVideo = [settings[@"supportsVideo"] boolValue]; + } + if (settings[@"maximumCallGroups"]) { + providerConfiguration.maximumCallGroups = [settings[@"maximumCallGroups"] integerValue]; + } + if (settings[@"maximumCallsPerCallGroup"]) { + providerConfiguration.maximumCallsPerCallGroup = [settings[@"maximumCallsPerCallGroup"] integerValue]; + } + if (settings[@"imageName"]) { + providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:settings[@"imageName"]]); + } + if (settings[@"ringtoneSound"]) { + providerConfiguration.ringtoneSound = settings[@"ringtoneSound"]; + } + if (@available(iOS 11.0, *)) { + if (settings[@"includesCallsInRecents"]) { + providerConfiguration.includesCallsInRecents = [settings[@"includesCallsInRecents"] boolValue]; + } + } + return providerConfiguration; +} + ++ (NSSet *)getSupportedHandleTypes:(id)handleType { + if (handleType) { + if ([handleType isKindOfClass:[NSArray class]]) { + NSSet *types = [NSSet set]; + + for (NSString *type in handleType) { + types = [types setByAddingObject: [NSNumber numberWithInteger:[Settings getHandleType:type]]]; + } + + return types; + } else { + CXHandleType _handleType = [Settings getHandleType:handleType]; + + return [NSSet setWithObjects:[NSNumber numberWithInteger:_handleType], nil]; + } + } else { + return [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypePhoneNumber], nil]; + } +} + ++ (CXHandleType)getHandleType:(NSString *)handleType { + if ([handleType isEqualToString:@"generic"]) { + return CXHandleTypeGeneric; + } else if ([handleType isEqualToString:@"number"]) { + return CXHandleTypePhoneNumber; + } else if ([handleType isEqualToString:@"phone"]) { + return CXHandleTypePhoneNumber; + } else if ([handleType isEqualToString:@"email"]) { + return CXHandleTypeEmailAddress; + } else { + return CXHandleTypeGeneric; + } +} + +@end diff --git a/packages/react-native-callingx/ios/UUIDStorage.mm b/packages/react-native-callingx/ios/UUIDStorage.mm index d4c92d4628..0f066ed509 100644 --- a/packages/react-native-callingx/ios/UUIDStorage.mm +++ b/packages/react-native-callingx/ios/UUIDStorage.mm @@ -19,7 +19,9 @@ - (instancetype)init { - (NSUUID *)getOrCreateUUIDForCid:(NSString *)cid { if ([self containsCid:cid]) { NSString *existingUUID = self.uuidDict[cid]; +#ifdef DEBUG NSLog(@"[UUIDStorage] getUUIDForCid: found existing UUID %@ for cid %@", existingUUID, cid); +#endif return [[NSUUID alloc] initWithUUIDString:existingUUID]; } @@ -27,8 +29,9 @@ - (NSUUID *)getOrCreateUUIDForCid:(NSString *)cid { NSString *uuidString = [uuid.UUIDString lowercaseString]; self.uuidDict[cid] = uuidString; self.cidDict[uuidString] = cid; +#ifdef DEBUG NSLog(@"[UUIDStorage] getUUIDForCid: created new UUID %@ for cid %@", uuidString, cid); - +#endif return uuid; } @@ -43,7 +46,9 @@ - (NSUUID *)getUUIDForCid:(NSString *)cid { - (NSString *)getCidForUUID:(NSUUID *)uuid { NSString *uuidString = [uuid.UUIDString lowercaseString]; NSString *cid = self.cidDict[uuidString]; +#ifdef DEBUG NSLog(@"[UUIDStorage] getCidForUUID: UUID %@ -> cid %@", uuidString, cid ?: @"(not found)"); +#endif return cid; } @@ -53,9 +58,13 @@ - (void)removeCidForUUID:(NSUUID *)uuid { if (cid) { [self.uuidDict removeObjectForKey:cid]; [self.cidDict removeObjectForKey:uuidString]; +#ifdef DEBUG NSLog(@"[UUIDStorage] removeCidForUUID: removed cid %@ for UUID %@", cid, uuidString); +#endif } else { +#ifdef DEBUG NSLog(@"[UUIDStorage] removeCidForUUID: no cid found for UUID %@", uuidString); +#endif } } @@ -64,9 +73,13 @@ - (void)removeCid:(NSString *)cid { if (uuidString) { [self.cidDict removeObjectForKey:uuidString]; [self.uuidDict removeObjectForKey:cid]; +#ifdef DEBUG NSLog(@"[UUIDStorage] removeCid: removed cid %@ with UUID %@", cid, uuidString); +#endif } else { +#ifdef DEBUG NSLog(@"[UUIDStorage] removeCid: no UUID found for cid %@", cid); +#endif } } @@ -74,7 +87,9 @@ - (void)removeAllObjects { NSUInteger count = [self.uuidDict count]; [self.uuidDict removeAllObjects]; [self.cidDict removeAllObjects]; +#ifdef DEBUG NSLog(@"[UUIDStorage] removeAllObjects: cleared %lu entries", (unsigned long)count); +#endif } - (NSUInteger)count { @@ -93,4 +108,4 @@ - (NSString *)description { return [NSString stringWithFormat:@"UUIDStorage: %@", self.uuidDict]; } -@end \ No newline at end of file +@end diff --git a/sample-apps/react-native/dogfood/android/app/src/main/java/io/getstream/rnvideosample/MainActivity.kt b/sample-apps/react-native/dogfood/android/app/src/main/java/io/getstream/rnvideosample/MainActivity.kt index 8de5656dfd..9af3a7a2c5 100644 --- a/sample-apps/react-native/dogfood/android/app/src/main/java/io/getstream/rnvideosample/MainActivity.kt +++ b/sample-apps/react-native/dogfood/android/app/src/main/java/io/getstream/rnvideosample/MainActivity.kt @@ -12,7 +12,6 @@ import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnable import com.facebook.react.defaults.DefaultReactActivityDelegate import com.oney.WebRTCModule.WebRTCModuleOptions import com.streamvideo.reactnative.StreamVideoReactNative -import com.callingx.CallingxModule class MainActivity : ReactActivity() { override fun onCreate(savedInstanceState: Bundle?) { From 7ad9360651e77aa0d2861dc6164e44899202b0e6 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Tue, 9 Dec 2025 18:08:30 +0100 Subject: [PATCH 11/27] feat: added ringtone support to ios --- packages/react-native-callingx/ios/Callingx.mm | 3 ++- packages/react-native-callingx/src/spec/NativeCallingx.ts | 1 + packages/react-native-callingx/src/types.ts | 1 + packages/react-native-callingx/src/utils/constants.ts | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/react-native-callingx/ios/Callingx.mm b/packages/react-native-callingx/ios/Callingx.mm index d81e2661aa..22f4b6db03 100644 --- a/packages/react-native-callingx/ios/Callingx.mm +++ b/packages/react-native-callingx/ios/Callingx.mm @@ -514,7 +514,8 @@ - (void)setupiOS:(JS::NativeCallingx::SpecSetupiOSOptions &)options { @"supportsVideo" : @(options.supportsVideo()), @"maximumCallsPerCallGroup" : @(options.maximumCallsPerCallGroup()), @"maximumCallGroups" : @(options.maximumCallGroups()), - @"handleType" : options.handleType() + @"handleType" : options.handleType(), + @"ringtoneSound" : options.sound() }; _version = [[[NSProcessInfo alloc] init] operatingSystemVersion]; diff --git a/packages/react-native-callingx/src/spec/NativeCallingx.ts b/packages/react-native-callingx/src/spec/NativeCallingx.ts index 9f1d1a035d..a2d645d590 100644 --- a/packages/react-native-callingx/src/spec/NativeCallingx.ts +++ b/packages/react-native-callingx/src/spec/NativeCallingx.ts @@ -10,6 +10,7 @@ export interface Spec extends TurboModule { maximumCallsPerCallGroup: number; maximumCallGroups: number; handleType: string; + sound: string | null; }): void; setupAndroid(options: { diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index afda14edce..2d4c91ab94 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -65,6 +65,7 @@ export type iOSOptions = { maximumCallsPerCallGroup?: number; maximumCallGroups?: number; handleType?: string; //'generic' | 'number' | 'phone' | 'email'; + sound?: string | null; }; export type AndroidOptions = { diff --git a/packages/react-native-callingx/src/utils/constants.ts b/packages/react-native-callingx/src/utils/constants.ts index f6b2554579..96d5c42efc 100644 --- a/packages/react-native-callingx/src/utils/constants.ts +++ b/packages/react-native-callingx/src/utils/constants.ts @@ -13,6 +13,7 @@ export const defaultiOSOptions: Required = { maximumCallsPerCallGroup: 1, maximumCallGroups: 1, handleType: 'generic', + sound: null, }; export const defaultAndroidOptions: Required = { From d374c9b70393cbbe692f886000690fc5ee0b2ab3 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Tue, 9 Dec 2025 18:44:17 +0100 Subject: [PATCH 12/27] feat: added calls history for ios --- packages/react-native-callingx/ios/Callingx.mm | 4 ++-- packages/react-native-callingx/ios/Settings.mm | 4 ++-- packages/react-native-callingx/src/spec/NativeCallingx.ts | 2 +- packages/react-native-callingx/src/types.ts | 4 ++-- packages/react-native-callingx/src/utils/constants.ts | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/react-native-callingx/ios/Callingx.mm b/packages/react-native-callingx/ios/Callingx.mm index 22f4b6db03..cce32303bd 100644 --- a/packages/react-native-callingx/ios/Callingx.mm +++ b/packages/react-native-callingx/ios/Callingx.mm @@ -510,12 +510,12 @@ - (NSString *)getIncomingCallErrorCode:(NSError *)error { - (void)setupiOS:(JS::NativeCallingx::SpecSetupiOSOptions &)options { NSDictionary *optionsDict = @{ - @"appName" : options.appName(), @"supportsVideo" : @(options.supportsVideo()), @"maximumCallsPerCallGroup" : @(options.maximumCallsPerCallGroup()), @"maximumCallGroups" : @(options.maximumCallGroups()), @"handleType" : options.handleType(), - @"ringtoneSound" : options.sound() + @"ringtoneSound" : options.sound(), + @"includesCallsInRecents" : @(options.callsHistory()) }; _version = [[[NSProcessInfo alloc] init] operatingSystemVersion]; diff --git a/packages/react-native-callingx/ios/Settings.mm b/packages/react-native-callingx/ios/Settings.mm index a1cbe5d200..ab8bcbd48c 100644 --- a/packages/react-native-callingx/ios/Settings.mm +++ b/packages/react-native-callingx/ios/Settings.mm @@ -23,10 +23,10 @@ + (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary *)settings { #endif CXProviderConfiguration *providerConfiguration = [[CXProviderConfiguration alloc] init]; providerConfiguration.supportsVideo = YES; - providerConfiguration.maximumCallGroups = 3; + providerConfiguration.maximumCallGroups = 1; providerConfiguration.maximumCallsPerCallGroup = 1; providerConfiguration.supportedHandleTypes = [Settings getSupportedHandleTypes:settings[@"handleType"]]; - + if (settings[@"supportsVideo"]) { providerConfiguration.supportsVideo = [settings[@"supportsVideo"] boolValue]; } diff --git a/packages/react-native-callingx/src/spec/NativeCallingx.ts b/packages/react-native-callingx/src/spec/NativeCallingx.ts index a2d645d590..432f6d44e5 100644 --- a/packages/react-native-callingx/src/spec/NativeCallingx.ts +++ b/packages/react-native-callingx/src/spec/NativeCallingx.ts @@ -5,12 +5,12 @@ import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypes'; export interface Spec extends TurboModule { setupiOS(options: { - appName: string; supportsVideo: boolean; maximumCallsPerCallGroup: number; maximumCallGroups: number; handleType: string; sound: string | null; + callsHistory: boolean; }): void; setupAndroid(options: { diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index 2d4c91ab94..797f14d2f4 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -60,12 +60,12 @@ export interface ICallingxModule { } export type iOSOptions = { - appName: string; supportsVideo?: boolean; maximumCallsPerCallGroup?: number; maximumCallGroups?: number; handleType?: string; //'generic' | 'number' | 'phone' | 'email'; sound?: string | null; + callsHistory?: boolean; }; export type AndroidOptions = { @@ -88,7 +88,7 @@ export type NotificationTransformers = { }; export type Options = { - ios: iOSOptions; + ios?: iOSOptions; android: AndroidOptions & NotificationTransformers; }; diff --git a/packages/react-native-callingx/src/utils/constants.ts b/packages/react-native-callingx/src/utils/constants.ts index 96d5c42efc..c10a81d34f 100644 --- a/packages/react-native-callingx/src/utils/constants.ts +++ b/packages/react-native-callingx/src/utils/constants.ts @@ -8,12 +8,12 @@ import type { export const defaultTextTransformer: TextTransformer = (text: string) => text; export const defaultiOSOptions: Required = { - appName: 'My App', supportsVideo: true, maximumCallsPerCallGroup: 1, maximumCallGroups: 1, handleType: 'generic', sound: null, + callsHistory: false, }; export const defaultAndroidOptions: Required = { From 95b45d10d5508d6e970650e00236b0238041deb1 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Tue, 9 Dec 2025 19:00:26 +0100 Subject: [PATCH 13/27] feat: added image support for ios --- packages/react-native-callingx/ios/Callingx.mm | 1 + packages/react-native-callingx/src/CallingxModule.ts | 2 +- packages/react-native-callingx/src/spec/NativeCallingx.ts | 1 + packages/react-native-callingx/src/types.ts | 1 + packages/react-native-callingx/src/utils/constants.ts | 1 + sample-apps/react-native/dogfood/ios/AppDelegate.swift | 2 +- sample-apps/react-native/dogfood/src/utils/setPushConfig.ts | 6 ++---- 7 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/react-native-callingx/ios/Callingx.mm b/packages/react-native-callingx/ios/Callingx.mm index cce32303bd..0139826526 100644 --- a/packages/react-native-callingx/ios/Callingx.mm +++ b/packages/react-native-callingx/ios/Callingx.mm @@ -515,6 +515,7 @@ - (void)setupiOS:(JS::NativeCallingx::SpecSetupiOSOptions &)options { @"maximumCallGroups" : @(options.maximumCallGroups()), @"handleType" : options.handleType(), @"ringtoneSound" : options.sound(), + @"imageName" : options.imageName(), @"includesCallsInRecents" : @(options.callsHistory()) }; diff --git a/packages/react-native-callingx/src/CallingxModule.ts b/packages/react-native-callingx/src/CallingxModule.ts index 4f119c0c84..0f262ab1be 100644 --- a/packages/react-native-callingx/src/CallingxModule.ts +++ b/packages/react-native-callingx/src/CallingxModule.ts @@ -50,7 +50,7 @@ class CallingxModule implements ICallingxModule { } setup(options: { - ios: Partial; + ios?: Partial; android: Partial; }): void { if (Platform.OS === 'ios') { diff --git a/packages/react-native-callingx/src/spec/NativeCallingx.ts b/packages/react-native-callingx/src/spec/NativeCallingx.ts index 432f6d44e5..fb6ffe4e0c 100644 --- a/packages/react-native-callingx/src/spec/NativeCallingx.ts +++ b/packages/react-native-callingx/src/spec/NativeCallingx.ts @@ -10,6 +10,7 @@ export interface Spec extends TurboModule { maximumCallGroups: number; handleType: string; sound: string | null; + imageName: string | null; callsHistory: boolean; }): void; diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index 797f14d2f4..90a83f7264 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -65,6 +65,7 @@ export type iOSOptions = { maximumCallGroups?: number; handleType?: string; //'generic' | 'number' | 'phone' | 'email'; sound?: string | null; + imageName?: string | null; callsHistory?: boolean; }; diff --git a/packages/react-native-callingx/src/utils/constants.ts b/packages/react-native-callingx/src/utils/constants.ts index c10a81d34f..85b9917169 100644 --- a/packages/react-native-callingx/src/utils/constants.ts +++ b/packages/react-native-callingx/src/utils/constants.ts @@ -13,6 +13,7 @@ export const defaultiOSOptions: Required = { maximumCallGroups: 1, handleType: 'generic', sound: null, + imageName: null, callsHistory: false, }; diff --git a/sample-apps/react-native/dogfood/ios/AppDelegate.swift b/sample-apps/react-native/dogfood/ios/AppDelegate.swift index 7d11bd5825..afffc60e08 100644 --- a/sample-apps/react-native/dogfood/ios/AppDelegate.swift +++ b/sample-apps/react-native/dogfood/ios/AppDelegate.swift @@ -71,7 +71,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD ) { guard let stream = payload.dictionaryPayload["stream"] as? [String: Any], - let createdCallerName = stream["created_by_display_name"] as? String, + let _ = stream["created_by_display_name"] as? String, let cid = stream["call_cid"] as? String else { completion() // Ensure completion handler is called even if parsing fails return diff --git a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts index ef416749bb..1f30e828ee 100644 --- a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts +++ b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts @@ -101,10 +101,8 @@ export function setPushConfig() { incoming ? `Incoming call from ${callerName}` : `Outgoing call to ${callerName}`, - subtitleTransformer: (phoneNumber: string, incoming: boolean) => - incoming - ? `Tap to open the call: ${phoneNumber}` - : `Tap to open the call: ${phoneNumber}`, + subtitleTransformer: (callId: string) => + `Tap to open the call: ${callId}`, }, }); From 151f36ac913cab92ed47932d6afad2dea01e94c7 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Wed, 10 Dec 2025 11:15:09 +0100 Subject: [PATCH 14/27] feat: made outcoming calls support optional --- .../src/CallingxModule.ts | 25 +++++++++++-------- packages/react-native-callingx/src/types.ts | 7 ++++-- .../useCallingExpWithCallingStateEffect.ts | 9 ++++++- .../src/utils/push/android.ts | 2 +- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/packages/react-native-callingx/src/CallingxModule.ts b/packages/react-native-callingx/src/CallingxModule.ts index 0f262ab1be..29c77e363a 100644 --- a/packages/react-native-callingx/src/CallingxModule.ts +++ b/packages/react-native-callingx/src/CallingxModule.ts @@ -17,11 +17,10 @@ import { type ICallingxModule, type InfoDisplayOptions, type AndroidOptions, - type iOSOptions, type TextTransformer, - type NotificationTransformers, type EndCallReason, type EventData, + type Options, } from './types'; import { androidEndCallReasonMap, @@ -32,27 +31,31 @@ import { } from './utils/constants'; class CallingxModule implements ICallingxModule { - private isNotificationsAllowed = false; + private _isNotificationsAllowed = false; + private _isOutcomingCallsEnabled = false; private titleTransformer: TextTransformer = (text: string) => text; private subtitleTransformer: TextTransformer | undefined = undefined; private eventManager: EventManager = new EventManager(); - canPostNotifications(): boolean { + get isOutcomingCallsEnabled(): boolean { + return this._isOutcomingCallsEnabled; + } + + get isNotificationsAllowed(): boolean { if (Platform.OS !== 'android') { return true; } return ( - this.isNotificationsAllowed && NativeCallingModule.canPostNotifications() + this._isNotificationsAllowed && NativeCallingModule.canPostNotifications() ); } - setup(options: { - ios?: Partial; - android: Partial; - }): void { + setup(options: Options): void { + this._isOutcomingCallsEnabled = options.enableOutcomingCalls ?? false; + if (Platform.OS === 'ios') { NativeCallingModule.setupiOS({ ...defaultiOSOptions, ...options.ios }); } @@ -86,7 +89,7 @@ class CallingxModule implements ICallingxModule { postNotifications: boolean; } = await requestCallPermissions(); - this.isNotificationsAllowed = result.postNotifications; + this._isNotificationsAllowed = result.postNotifications; return result; } @@ -96,7 +99,7 @@ class CallingxModule implements ICallingxModule { postNotifications: boolean; } = await checkCallPermissions(); - this.isNotificationsAllowed = result.postNotifications; + this._isNotificationsAllowed = result.postNotifications; return result; } diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index 90a83f7264..1f51b49c1f 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -3,14 +3,16 @@ import type { ManagableTask } from './utils/headlessTask'; import type { PermissionsResult } from './utils/permissions'; export interface ICallingxModule { + get isOutcomingCallsEnabled(): boolean; + + get isNotificationsAllowed(): boolean; + setup(options: Options): void; checkPermissions(): Promise; requestPermissions(): Promise; - canPostNotifications(): boolean; - setCurrentCallActive(callId: string): Promise; getInitialEvents(): EventData[]; @@ -91,6 +93,7 @@ export type NotificationTransformers = { export type Options = { ios?: iOSOptions; android: AndroidOptions & NotificationTransformers; + enableOutcomingCalls?: boolean; }; export type InfoDisplayOptions = { diff --git a/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts index 47684abefa..ed856d75a8 100644 --- a/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts +++ b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts @@ -102,12 +102,19 @@ export const useCallingExpWithCallingStateEffect = () => { logger.debug( `prevState.current: ${prevState.current}, current callingState: ${callingState}`, ); + logger.debug( + `isOutcomingCallsEnabled: ${callingx.isOutcomingCallsEnabled}`, + ); if ( !isAcceptedCallingState(prevState.current) && isAcceptedCallingState(callingState) ) { - if (isOutcomingCall && !isCallRegistered) { + if ( + isOutcomingCall && + !isCallRegistered && + callingx.isOutcomingCallsEnabled + ) { //we request start call action from CallKit/Telecom, next step is to make call active when we receive call started event logger.debug(`Should start call in callkeep: ${activeCallCid}`); callingx diff --git a/packages/react-native-sdk/src/utils/push/android.ts b/packages/react-native-sdk/src/utils/push/android.ts index 7b1391837d..2090036396 100644 --- a/packages/react-native-sdk/src/utils/push/android.ts +++ b/packages/react-native-sdk/src/utils/push/android.ts @@ -157,7 +157,7 @@ export const firebaseDataHandler = async ( } await callingx.checkPermissions(); - if (!callingx.canPostNotifications()) { + if (!callingx.isNotificationsAllowed) { logger.debug( `Notification permission not granted, unable to post ${data.type} notifications`, ); From ad13d9020ac595e72ca0deebf5b59997a214b5e2 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Wed, 10 Dec 2025 13:42:22 +0100 Subject: [PATCH 15/27] chore: code cleanup --- packages/react-native-callingx/ios/Callingx.mm | 17 ----------------- .../react-native-callingx/src/EventManager.ts | 6 ------ .../src/utils/permissions.ts | 10 ---------- .../src/utils/StreamVideoRN/index.ts | 1 - 4 files changed, 34 deletions(-) diff --git a/packages/react-native-callingx/ios/Callingx.mm b/packages/react-native-callingx/ios/Callingx.mm index 0139826526..318acf0528 100644 --- a/packages/react-native-callingx/ios/Callingx.mm +++ b/packages/react-native-callingx/ios/Callingx.mm @@ -489,23 +489,6 @@ - (NSString *)getIncomingCallErrorCode:(NSError *)error { } } -//- (void)setup:(NSDictionary *)options { -//#ifdef DEBUG -// NSLog(@"[Callingx][setup] options = %@", options); -//#endif -// _version = [[[NSProcessInfo alloc] init] operatingSystemVersion]; -// self.callKeepCallController = [[CXCallController alloc] init]; -// -// [Settings setSettings:options]; -// -// [Callingx initCallKitProvider]; -// [Callingx initUUIDStorage]; -// -// self.callKeepProvider = sharedProvider; -// [self.callKeepProvider setDelegate:nil queue:nil]; -// [self.callKeepProvider setDelegate:self queue:nil]; -//} - #pragma mark - Turbo module methods - (void)setupiOS:(JS::NativeCallingx::SpecSetupiOSOptions &)options { diff --git a/packages/react-native-callingx/src/EventManager.ts b/packages/react-native-callingx/src/EventManager.ts index 82c5cde2c4..73fdac17f8 100644 --- a/packages/react-native-callingx/src/EventManager.ts +++ b/packages/react-native-callingx/src/EventManager.ts @@ -50,12 +50,6 @@ class EventManager { this.subscription?.remove(); this.subscription = null; } - - console.log( - '!!! remove listener', - this.listenersCount, - this.subscription === null ? 'null' : 'not null' - ); } } diff --git a/packages/react-native-callingx/src/utils/permissions.ts b/packages/react-native-callingx/src/utils/permissions.ts index 1f7cbf5df8..ef1dbbadd5 100644 --- a/packages/react-native-callingx/src/utils/permissions.ts +++ b/packages/react-native-callingx/src/utils/permissions.ts @@ -117,13 +117,3 @@ export const requestPostNotificationPermissions = results === PermissionsAndroid.RESULTS.GRANTED || allowedPostNotifications ); }; - -export const canPostNotifications = async (): Promise => { - if (Platform.OS !== 'android') { - return true; - } - - return PermissionsAndroid.check( - PermissionsAndroid.PERMISSIONS.POST_NOTIFICATIONS - ); -}; diff --git a/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts b/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts index f9d1d8fca6..b9c08f3433 100644 --- a/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts +++ b/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts @@ -131,7 +131,6 @@ export class StreamVideoRN { } static setupCallingExp(options: CallingxOptions) { - //check if calling exp pacakge is installed const callingx = getCallingxLib(); callingx.setup(options); callingx.checkPermissions().catch((error) => { From 7906d795cd6706c6d169f9275a78c2c8074630cc Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Wed, 10 Dec 2025 16:02:08 +0100 Subject: [PATCH 16/27] feat: added optional default audio session creation --- .../ios/AudioSessionManager.h | 12 +++++ .../ios/AudioSessionManager.mm | 50 +++++++++++++++++++ .../react-native-callingx/ios/Callingx.mm | 45 ++--------------- packages/react-native-callingx/ios/Settings.h | 1 + .../react-native-callingx/ios/Settings.mm | 8 +++ .../src/spec/NativeCallingx.ts | 1 + packages/react-native-callingx/src/types.ts | 7 ++- .../src/utils/constants.ts | 1 + .../src/utils/push/setupCallingExpEvents.ts | 26 ++++++++-- 9 files changed, 106 insertions(+), 45 deletions(-) create mode 100644 packages/react-native-callingx/ios/AudioSessionManager.h create mode 100644 packages/react-native-callingx/ios/AudioSessionManager.mm diff --git a/packages/react-native-callingx/ios/AudioSessionManager.h b/packages/react-native-callingx/ios/AudioSessionManager.h new file mode 100644 index 0000000000..410ca184bc --- /dev/null +++ b/packages/react-native-callingx/ios/AudioSessionManager.h @@ -0,0 +1,12 @@ +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface AudioSessionManager : NSObject + ++ (void)createAudioSessionIfNeeded; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/react-native-callingx/ios/AudioSessionManager.mm b/packages/react-native-callingx/ios/AudioSessionManager.mm new file mode 100644 index 0000000000..fc61e7cb89 --- /dev/null +++ b/packages/react-native-callingx/ios/AudioSessionManager.mm @@ -0,0 +1,50 @@ +#import "AudioSessionManager.h" +#import "Settings.h" +#import + +@implementation AudioSessionManager + ++ (void)createAudioSessionIfNeeded { + BOOL autoConfigureAudioSession = [Settings getAutoConfigureAudioSession]; + if (!autoConfigureAudioSession) { +#ifdef DEBUG + NSLog(@"[Callingx][createAudioSessionIfNeeded] Auto-configuration disabled, user handles audio session"); +#endif + return; + } + +#ifdef DEBUG + NSLog(@"[Callingx][createAudioSessionIfNeeded] Activating audio session"); +#endif + + NSUInteger categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth | + AVAudioSessionCategoryOptionAllowBluetoothA2DP; + NSString *mode = AVAudioSessionModeDefault; + + NSDictionary *settings = [Settings getSettings]; + if (settings && settings[@"audioSession"]) { + if (settings[@"audioSession"][@"categoryOptions"]) { + categoryOptions = [settings[@"audioSession"][@"categoryOptions"] integerValue]; + } + + if (settings[@"audioSession"][@"mode"]) { + mode = settings[@"audioSession"][@"mode"]; + } + } + + AVAudioSession *audioSession = [AVAudioSession sharedInstance]; + [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord + withOptions:categoryOptions + error:nil]; + + [audioSession setMode:mode error:nil]; + + double sampleRate = 44100.0; + [audioSession setPreferredSampleRate:sampleRate error:nil]; + + NSTimeInterval bufferDuration = .005; + [audioSession setPreferredIOBufferDuration:bufferDuration error:nil]; + [audioSession setActive:TRUE error:nil]; +} + +@end diff --git a/packages/react-native-callingx/ios/Callingx.mm b/packages/react-native-callingx/ios/Callingx.mm index 318acf0528..3b6f8e0a4a 100644 --- a/packages/react-native-callingx/ios/Callingx.mm +++ b/packages/react-native-callingx/ios/Callingx.mm @@ -6,6 +6,7 @@ #import #import "UUIDStorage.h" #import "Settings.h" +#import "AudioSessionManager.h" #ifdef DEBUG static int const OUTGOING_CALL_WAKEUP_DELAY = 10; @@ -364,42 +365,6 @@ - (void)dealloc { // } //} - -- (void)configureAudioSession { -#ifdef DEBUG - NSLog(@"[Callingx][configureAudioSession] Activating audio session"); -#endif - - NSUInteger categoryOptions = AVAudioSessionCategoryOptionAllowBluetooth | - AVAudioSessionCategoryOptionAllowBluetoothA2DP; - NSString *mode = AVAudioSessionModeDefault; - - NSDictionary *settings = [Settings getSettings]; - if (settings && settings[@"audioSession"]) { - if (settings[@"audioSession"][@"categoryOptions"]) { - categoryOptions = [settings[@"audioSession"][@"categoryOptions"] integerValue]; - } - - if (settings[@"audioSession"][@"mode"]) { - mode = settings[@"audioSession"][@"mode"]; - } - } - - AVAudioSession *audioSession = [AVAudioSession sharedInstance]; - [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord - withOptions:categoryOptions - error:nil]; - - [audioSession setMode:mode error:nil]; - - double sampleRate = 44100.0; - [audioSession setPreferredSampleRate:sampleRate error:nil]; - - NSTimeInterval bufferDuration = .005; - [audioSession setPreferredIOBufferDuration:bufferDuration error:nil]; - [audioSession setActive:TRUE error:nil]; -} - - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared(params); @@ -499,7 +464,8 @@ - (void)setupiOS:(JS::NativeCallingx::SpecSetupiOSOptions &)options { @"handleType" : options.handleType(), @"ringtoneSound" : options.sound(), @"imageName" : options.imageName(), - @"includesCallsInRecents" : @(options.callsHistory()) + @"includesCallsInRecents" : @(options.callsHistory()), + @"autoConfigureAudioSession" : @(options.setupAudioSession()) }; _version = [[[NSProcessInfo alloc] init] operatingSystemVersion]; @@ -814,7 +780,7 @@ - (void)provider:(CXProvider *)provider return; } // do this first, audio sessions are flakey - [self configureAudioSession]; + [AudioSessionManager createAudioSessionIfNeeded]; // tell the JS to actually make the call [self sendEventWithNameWrapper:CallingxDidReceiveStartCallAction body:@{ @@ -850,7 +816,7 @@ - (void)provider:(CXProvider *)provider return; } - [self configureAudioSession]; + [AudioSessionManager createAudioSessionIfNeeded]; [self sendEventWithNameWrapper:CallingxPerformAnswerCallAction body:@{ @"callId" : callId @@ -961,7 +927,6 @@ - (void)provider:(CXProvider *)provider object:nil userInfo:userInfo]; - [self configureAudioSession]; [self sendEventWithNameWrapper:CallingxDidActivateAudioSession body:nil]; } diff --git a/packages/react-native-callingx/ios/Settings.h b/packages/react-native-callingx/ios/Settings.h index ae0770f037..be642e6d8c 100644 --- a/packages/react-native-callingx/ios/Settings.h +++ b/packages/react-native-callingx/ios/Settings.h @@ -7,6 +7,7 @@ NS_ASSUME_NONNULL_BEGIN + (NSDictionary *)getSettings; + (void)setSettings:(NSDictionary *)options; ++ (BOOL)getAutoConfigureAudioSession; + (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary *)settings; + (NSSet *)getSupportedHandleTypes:(id)handleType; + (CXHandleType)getHandleType:(NSString *)handleType; diff --git a/packages/react-native-callingx/ios/Settings.mm b/packages/react-native-callingx/ios/Settings.mm index ab8bcbd48c..45159e8b4a 100644 --- a/packages/react-native-callingx/ios/Settings.mm +++ b/packages/react-native-callingx/ios/Settings.mm @@ -17,6 +17,14 @@ + (void)setSettings:(NSDictionary *)options { [[NSUserDefaults standardUserDefaults] synchronize]; } ++ (BOOL)getAutoConfigureAudioSession { + NSDictionary *settings = [Settings getSettings]; + if (settings && settings[@"autoConfigureAudioSession"]) { + return [settings[@"autoConfigureAudioSession"] boolValue]; + } + return NO; +} + + (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary *)settings { #ifdef DEBUG NSLog(@"[Settings][getProviderConfiguration]"); diff --git a/packages/react-native-callingx/src/spec/NativeCallingx.ts b/packages/react-native-callingx/src/spec/NativeCallingx.ts index fb6ffe4e0c..9957606b6e 100644 --- a/packages/react-native-callingx/src/spec/NativeCallingx.ts +++ b/packages/react-native-callingx/src/spec/NativeCallingx.ts @@ -12,6 +12,7 @@ export interface Spec extends TurboModule { sound: string | null; imageName: string | null; callsHistory: boolean; + setupAudioSession: boolean; }): void; setupAndroid(options: { diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index 1f51b49c1f..a3a3286c7a 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -69,6 +69,7 @@ export type iOSOptions = { sound?: string | null; imageName?: string | null; callsHistory?: boolean; + setupAudioSession?: boolean; }; export type AndroidOptions = { @@ -113,7 +114,9 @@ export type EventName = | 'didToggleHoldCallAction' | 'didChangeAudioRoute' | 'didReceiveStartCallAction' - | 'didPerformSetMutedCallAction'; + | 'didPerformSetMutedCallAction' + | 'didActivateAudioSession' + | 'didDeactivateAudioSession'; export type EventParams = { answerCall: { @@ -141,6 +144,8 @@ export type EventParams = { didReceiveStartCallAction: { callId: string; }; + didActivateAudioSession: undefined; + didDeactivateAudioSession: undefined; }; export type EndCallReason = diff --git a/packages/react-native-callingx/src/utils/constants.ts b/packages/react-native-callingx/src/utils/constants.ts index 85b9917169..be0a75b2bb 100644 --- a/packages/react-native-callingx/src/utils/constants.ts +++ b/packages/react-native-callingx/src/utils/constants.ts @@ -15,6 +15,7 @@ export const defaultiOSOptions: Required = { sound: null, imageName: null, callsHistory: false, + setupAudioSession: true, }; export const defaultAndroidOptions: Required = { diff --git a/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts b/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts index fd806ad1a3..630f1a4237 100644 --- a/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts +++ b/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts @@ -26,11 +26,19 @@ export function setupCallingExpEvents(pushConfig: NonNullable) { const { remove: removeAnswerCall } = callingx.addEventListener( 'answerCall', - callingExpAcceptCall, + onAcceptCall, ); const { remove: removeEndCall } = callingx.addEventListener( 'endCall', - callingExpRejectCall(pushConfig), + onEndCall(pushConfig), + ); + const { remove: removeDidActivateAudioSession } = callingx.addEventListener( + 'didActivateAudioSession', + onDidActivateAudioSession, + ); + const { remove: removeDidDeactivateAudioSession } = callingx.addEventListener( + 'didDeactivateAudioSession', + onDidDeactivateAudioSession, ); //TODO: need to find cases where delayed events can appear @@ -51,10 +59,12 @@ export function setupCallingExpEvents(pushConfig: NonNullable) { setPushLogoutCallback(async () => { removeAnswerCall(); removeEndCall(); + removeDidActivateAudioSession(); + removeDidDeactivateAudioSession(); }); } -const callingExpAcceptCall = ({ callId: call_cid }: { callId: string }) => { +const onAcceptCall = ({ callId: call_cid }: { callId: string }) => { videoLoggerSystem .getLogger('callingExpAcceptCall') .debug(`callingExpAcceptCall event callId: ${call_cid}`); @@ -72,7 +82,7 @@ const callingExpAcceptCall = ({ callId: call_cid }: { callId: string }) => { pushAcceptedIncomingCallCId$.next(call_cid); }; -const callingExpRejectCall = +const onEndCall = (pushConfig: PushConfig) => async ({ callId: call_cid }: { callId: string }) => { getCallingxLibIfAvailable()?.log( @@ -98,3 +108,11 @@ const callingExpRejectCall = .debug(`ending call with call_cid: ${call_cid}`); await processCallFromPushInBackground(pushConfig, call_cid, 'decline'); }; + +const onDidActivateAudioSession = () => { + //TODO: start audio session here +}; + +const onDidDeactivateAudioSession = () => { + //TODO: end audio session here +}; From 53f20364a055f67f5388c1c19a6f05508f177d6f Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Wed, 10 Dec 2025 17:03:00 +0100 Subject: [PATCH 17/27] feat: made Android permissions adjustable --- .../src/main/java/com/callingx/ResourceUtils.kt | 2 ++ .../callingx/notifications/CallNotificationManager.kt | 1 + packages/react-native-callingx/ios/Settings.mm | 10 ++++++---- packages/react-native-callingx/src/CallingxModule.ts | 11 +++++++++++ packages/react-native-callingx/src/types.ts | 5 +++-- packages/react-native-callingx/src/utils/constants.ts | 4 ++-- .../react-native-sdk/src/utils/StreamVideoRN/index.ts | 5 ----- 7 files changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/ResourceUtils.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/ResourceUtils.kt index 9f13b5009f..c0d01f7e89 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/ResourceUtils.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/ResourceUtils.kt @@ -1,3 +1,5 @@ +package com.callingx + import android.content.Context import android.media.RingtoneManager import android.net.Uri diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt index 78ef300afd..1c9486e30e 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt @@ -14,6 +14,7 @@ import androidx.core.graphics.drawable.IconCompat import com.callingx.CallService import com.callingx.CallingxModule import com.callingx.R +import com.callingx.ResourceUtils import com.callingx.getDisconnectCauseString import com.callingx.model.Call diff --git a/packages/react-native-callingx/ios/Settings.mm b/packages/react-native-callingx/ios/Settings.mm index 45159e8b4a..b573abae09 100644 --- a/packages/react-native-callingx/ios/Settings.mm +++ b/packages/react-native-callingx/ios/Settings.mm @@ -44,11 +44,13 @@ + (CXProviderConfiguration *)getProviderConfiguration:(NSDictionary *)settings { if (settings[@"maximumCallsPerCallGroup"]) { providerConfiguration.maximumCallsPerCallGroup = [settings[@"maximumCallsPerCallGroup"] integerValue]; } - if (settings[@"imageName"]) { - providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:settings[@"imageName"]]); + NSString *imageName = settings[@"imageName"]; + if ([imageName isKindOfClass:[NSString class]] && imageName.length > 0) { + providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:imageName]); } - if (settings[@"ringtoneSound"]) { - providerConfiguration.ringtoneSound = settings[@"ringtoneSound"]; + NSString *ringtoneSound = settings[@"ringtoneSound"]; + if ([ringtoneSound isKindOfClass:[NSString class]] && ringtoneSound.length > 0) { + providerConfiguration.ringtoneSound = ringtoneSound; } if (@available(iOS 11.0, *)) { if (settings[@"includesCallsInRecents"]) { diff --git a/packages/react-native-callingx/src/CallingxModule.ts b/packages/react-native-callingx/src/CallingxModule.ts index 29c77e363a..b91a18e6d3 100644 --- a/packages/react-native-callingx/src/CallingxModule.ts +++ b/packages/react-native-callingx/src/CallingxModule.ts @@ -81,6 +81,17 @@ class CallingxModule implements ICallingxModule { registerHeadlessTask(); } + + //by default we will request permissions on setup call + if (options.enableAutoPermissions ?? true) { + this.requestPermissions() + .then((result) => { + console.log('Permissions result:', result); + }) + .catch((error) => { + console.error('Error requesting permissions:', error); + }); + } } async requestPermissions(): Promise { diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index a3a3286c7a..2aee59eb53 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -66,8 +66,8 @@ export type iOSOptions = { maximumCallsPerCallGroup?: number; maximumCallGroups?: number; handleType?: string; //'generic' | 'number' | 'phone' | 'email'; - sound?: string | null; - imageName?: string | null; + sound?: string; + imageName?: string; callsHistory?: boolean; setupAudioSession?: boolean; }; @@ -95,6 +95,7 @@ export type Options = { ios?: iOSOptions; android: AndroidOptions & NotificationTransformers; enableOutcomingCalls?: boolean; + enableAutoPermissions?: boolean; }; export type InfoDisplayOptions = { diff --git a/packages/react-native-callingx/src/utils/constants.ts b/packages/react-native-callingx/src/utils/constants.ts index be0a75b2bb..cd2f05c905 100644 --- a/packages/react-native-callingx/src/utils/constants.ts +++ b/packages/react-native-callingx/src/utils/constants.ts @@ -12,8 +12,8 @@ export const defaultiOSOptions: Required = { maximumCallsPerCallGroup: 1, maximumCallGroups: 1, handleType: 'generic', - sound: null, - imageName: null, + sound: '', + imageName: '', callsHistory: false, setupAudioSession: true, }; diff --git a/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts b/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts index b9c08f3433..3e5b1e5706 100644 --- a/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts +++ b/packages/react-native-sdk/src/utils/StreamVideoRN/index.ts @@ -133,11 +133,6 @@ export class StreamVideoRN { static setupCallingExp(options: CallingxOptions) { const callingx = getCallingxLib(); callingx.setup(options); - callingx.checkPermissions().catch((error) => { - videoLoggerSystem - .getLogger('setupCallingExp') - .error('failed to check permissions', error); - }); } static getConfig() { From fa6729bf5d7b6dba7760c88b5d249c9e730ec02d Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Thu, 11 Dec 2025 11:12:47 +0100 Subject: [PATCH 18/27] feat: adjusted android endCall behavior --- .../src/main/java/com/callingx/CallService.kt | 5 ++++- .../main/java/com/callingx/CallingxModule.kt | 8 ++++---- .../java/com/callingx/repo/CallRepository.kt | 2 +- .../com/callingx/repo/LegacyCallRepository.kt | 2 +- .../com/callingx/repo/TelecomCallRepository.kt | 11 +++++++++-- .../push/useCallingExpWithCallingStateEffect.ts | 1 - .../src/utils/push/setupCallingExpEvents.ts | 17 +++++------------ 7 files changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt index 3d8b6db6eb..0212168ed2 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt @@ -210,10 +210,13 @@ class CallService : Service(), CallRepository.Listener { } } - override fun onIsCallDisconnected(cause: DisconnectCause) { + override fun onIsCallDisconnected(callId: String?, cause: DisconnectCause) { // we're not passing the callId here to prevent infinite loops // callEnd event with callId will sent only when after interaction with notification buttons sendBroadcastEvent(CallingxModule.CALL_END_ACTION) { + if (callId != null) { + putExtra(CallingxModule.EXTRA_CALL_ID, callId) + } putExtra(CallingxModule.EXTRA_DISCONNECT_CAUSE, getDisconnectCauseString(cause)) } } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt index 885aeabf22..04a0f3e334 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt @@ -509,12 +509,12 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec sendJSEvent("answerCall", params) } CALL_END_ACTION -> { - params.putString("cause", intent.getStringExtra(EXTRA_DISCONNECT_CAUSE)) - sendJSEvent("endCall", params) - if (callId == null) { - // callId null means the call was disconnected, service should be unbound + // means the call was disconnected, we're ready to unbind the service unbindServiceSafely() + } else { + params.putString("cause", intent.getStringExtra(EXTRA_DISCONNECT_CAUSE)) + sendJSEvent("endCall", params) } } CALL_INACTIVE_ACTION -> { diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/CallRepository.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/CallRepository.kt index 4f1be649f3..18329ed06f 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/CallRepository.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/CallRepository.kt @@ -12,7 +12,7 @@ interface CallRepository { interface Listener { fun onCallStateChanged(call: Call) fun onIsCallAnswered(callId: String) - fun onIsCallDisconnected(cause: DisconnectCause) + fun onIsCallDisconnected(callId: String?, cause: DisconnectCause) fun onIsCallInactive(callId: String) fun onIsCallActive(callId: String) fun onCallRegistered(callId: String) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/LegacyCallRepository.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/LegacyCallRepository.kt index f481e70b43..50b64edbac 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/LegacyCallRepository.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/LegacyCallRepository.kt @@ -87,7 +87,7 @@ class LegacyCallRepository(private val context: Context) : CallRepository { if (call != null) { _currentCall.value = Call.Unregistered(call.id, call.callAttributes, action.cause) - listener?.onIsCallDisconnected(action.cause) + listener?.onIsCallDisconnected(call.id, action.cause) } } is CallAction.ToggleMute -> { diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt index 61d3ab7fdd..6379a340b9 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt @@ -49,6 +49,7 @@ class TelecomCallRepository(private val context: Context) : CallRepository { private val callsManager: CallsManager private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private var isSelfDisconnected = false // Keeps track of the current TelecomCall state private val _currentCall: MutableStateFlow = MutableStateFlow(Call.None) @@ -331,6 +332,7 @@ class TelecomCallRepository(private val context: Context) : CallRepository { } private suspend fun CallControlScope.doDisconnect(action: CallAction.Disconnect) { + isSelfDisconnected = true Log.d(TAG, "[repository] doDisconnect: Disconnecting call with cause: ${action.cause}") disconnect(action.cause) Log.d(TAG, "[repository] doDisconnect: Disconnect called, triggering onIsCallDisconnected") @@ -383,9 +385,14 @@ class TelecomCallRepository(private val context: Context) : CallRepository { TAG, "[repository] onIsCallDisconnected: Call disconnected, cause: ${it.reason}, description: ${it.description}" ) - updateCurrentCall { Call.Unregistered(id, callAttributes, it) } + var callId: String? = null + if (!isSelfDisconnected && _currentCall.value is Call.Registered) { + callId = (_currentCall.value as Call.Registered).id + } - listener?.onIsCallDisconnected(it) + updateCurrentCall { Call.Unregistered(id, callAttributes, it) } + listener?.onIsCallDisconnected(callId, it) + isSelfDisconnected = false Log.d(TAG, "[repository] onIsCallDisconnected: Call state updated to Unregistered") } diff --git a/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts index ed856d75a8..32088a3271 100644 --- a/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts +++ b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts @@ -75,7 +75,6 @@ export const useCallingExpWithCallingStateEffect = () => { return; } //if incoming stream call was unmounted, we need to end the call in CallKit/Telecom - //TODO: think about sending appropriate reason for end call logger.debug(`Ending call in calling exp: ${activeCallCid}`); callingx .endCallWithReason(activeCallCid, 'remote') diff --git a/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts b/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts index 630f1a4237..4e4aa1fbed 100644 --- a/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts +++ b/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts @@ -85,27 +85,20 @@ const onAcceptCall = ({ callId: call_cid }: { callId: string }) => { const onEndCall = (pushConfig: PushConfig) => async ({ callId: call_cid }: { callId: string }) => { - getCallingxLibIfAvailable()?.log( - `callingExpRejectCall call_cid: ${call_cid}`, - 'debug', - ); videoLoggerSystem .getLogger('callingExpRejectCall') - .debug(`call_cid: ${call_cid}`); + .debug(`callingExpRejectCall event callId: ${call_cid}`); if (!call_cid) { - videoLoggerSystem - .getLogger('callingExpRejectCall') - .debug('call_cid is undefined, so returning early'); + getCallingxLibIfAvailable()?.log( + `call_cid is undefined, so returning early`, + 'debug', + ); return; } clearPushWSEventSubscriptions(call_cid); - // remove the references if the call_cid matches - videoLoggerSystem - .getLogger('callingExpRejectCall') - .debug(`ending call with call_cid: ${call_cid}`); await processCallFromPushInBackground(pushConfig, call_cid, 'decline'); }; From 0a2ca4ea76b224c6ffb30c6d34b31e974500c172 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Thu, 11 Dec 2025 11:58:33 +0100 Subject: [PATCH 19/27] chore: adjusted android setup params --- .../notifications/CallNotificationManager.kt | 17 ++++++++++------- .../notifications/NotificationsConfig.kt | 17 ++++------------- .../react-native-callingx/src/CallingxModule.ts | 15 +++++++++------ .../src/spec/NativeCallingx.ts | 10 ++++------ packages/react-native-callingx/src/types.ts | 10 +++++----- .../src/utils/constants.ts | 7 ++++--- .../react-native-callingx/src/utils/types.ts | 5 +++++ 7 files changed, 41 insertions(+), 40 deletions(-) create mode 100644 packages/react-native-callingx/src/utils/types.ts diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt index 1c9486e30e..8e8689bda7 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt @@ -43,7 +43,7 @@ class CallNotificationManager( private var hasBecameActive = false fun createNotification(call: Call.Registered): Notification { - Log.d(TAG, "createNotification: Creating notification for call ID: ${call.id}") + Log.d(TAG, "[notifications] createNotification: Creating notification for call ID: ${call.id}") val contentIntent = NotificationIntentFactory.getLaunchActivityIntent( @@ -53,6 +53,7 @@ class CallNotificationManager( ) val callStyle = createCallStyle(call) val channelId = getChannelId(call) + Log.d(TAG, "[notifications] createNotification: Channel ID: $channelId") val builder = NotificationCompat.Builder(context, channelId) @@ -65,7 +66,7 @@ class CallNotificationManager( .setOngoing(true) if (!hasBecameActive && call.isActive) { - Log.d(TAG, "createNotification: Setting when to current time") + Log.d(TAG, "[notifications] createNotification: Setting when to current time") builder.setWhen(System.currentTimeMillis()) builder.setUsesChronometer(true) builder.setShowWhen(true) @@ -84,13 +85,13 @@ class CallNotificationManager( fun updateCallNotification(call: Call) { when (call) { Call.None, is Call.Unregistered -> { - Log.d(TAG, "Dismissing notification (call is None or Unregistered)") + Log.d(TAG, "[notifications] updateCallNotification: Dismissing notification (call is None or Unregistered)") notificationManager.cancel(NOTIFICATION_ID) } is Call.Registered -> { val notification = createNotification(call) notificationManager.notify(NOTIFICATION_ID, notification) - Log.d(TAG, "updateCallNotification: Notification posted successfully") + Log.d(TAG, "[notifications] updateCallNotification: Notification posted successfully") } } } @@ -102,7 +103,7 @@ class CallNotificationManager( fun startRingtone() { if (ringtone?.isPlaying == true) { - Log.d(TAG, "startRingtone: Ringtone already playing") + Log.d(TAG, "[notifications] startRingtone: Ringtone already playing") return } @@ -111,17 +112,19 @@ class CallNotificationManager( ?: RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE) ringtone = RingtoneManager.getRingtone(context, soundUri) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { ringtone?.isLooping = true } + ringtone?.play() - Log.d(TAG, "startRingtone: Ringtone started") + Log.d(TAG, "[notifications] startRingtone: Ringtone started") } fun stopRingtone() { if (ringtone?.isPlaying == true) { ringtone?.stop() - Log.d(TAG, "stopRingtone: Ringtone stopped") + Log.d(TAG, "[notifications] stopRingtone: Ringtone stopped") } ringtone = null } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt index 7e7b7ca6f1..700923c00c 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationsConfig.kt @@ -16,11 +16,6 @@ object NotificationsConfig { private const val KEY_SOUND = "sound" private const val KEY_VIBRATION = "vibration" - private const val DEFAULT_INCOMING_CHANNEL_ID = "incoming_channel" - private const val DEFAULT_INCOMING_CHANNEL_NAME = "Incoming calls" - private const val DEFAULT_ONGOING_CHANNEL_ID = "ongoing_channel" - private const val DEFAULT_ONGOING_CHANNEL_NAME = "Ongoing calls" - data class ChannelParams( val id: String, val name: String, @@ -67,20 +62,16 @@ object NotificationsConfig { return Channels( incomingChannel = ChannelParams( - id = prefs.getString(PREFIX_IN + KEY_ID, "") - ?: DEFAULT_INCOMING_CHANNEL_ID, - name = prefs.getString(PREFIX_IN + KEY_NAME, "") - ?: DEFAULT_INCOMING_CHANNEL_NAME, + id = prefs.getString(PREFIX_IN + KEY_ID, "") ?: "", + name = prefs.getString(PREFIX_IN + KEY_NAME, "") ?: "", sound = prefs.getString(PREFIX_IN + KEY_SOUND, "") ?: "", vibration = prefs.getBoolean(PREFIX_IN + KEY_VIBRATION, false), importance = NotificationManagerCompat.IMPORTANCE_MAX, ), outgoingChannel = ChannelParams( - id = prefs.getString(PREFIX_OUT + KEY_ID, "") - ?: DEFAULT_ONGOING_CHANNEL_ID, - name = prefs.getString(PREFIX_OUT + KEY_NAME, "") - ?: DEFAULT_ONGOING_CHANNEL_NAME, + id = prefs.getString(PREFIX_OUT + KEY_ID, "") ?: "", + name = prefs.getString(PREFIX_OUT + KEY_NAME, "") ?: "", importance = NotificationManagerCompat.IMPORTANCE_DEFAULT, vibration = false, sound = null, diff --git a/packages/react-native-callingx/src/CallingxModule.ts b/packages/react-native-callingx/src/CallingxModule.ts index b91a18e6d3..ed08696299 100644 --- a/packages/react-native-callingx/src/CallingxModule.ts +++ b/packages/react-native-callingx/src/CallingxModule.ts @@ -16,7 +16,6 @@ import type { EventListener, EventName, EventParams } from './EventManager'; import { type ICallingxModule, type InfoDisplayOptions, - type AndroidOptions, type TextTransformer, type EndCallReason, type EventData, @@ -61,20 +60,24 @@ class CallingxModule implements ICallingxModule { } if (Platform.OS === 'android') { - const { titleTransformer, subtitleTransformer, ...rest } = - options.android; + const { + titleTransformer, + subtitleTransformer, + incomingChannel, + outgoingChannel, + } = options.android ?? {}; this.titleTransformer = titleTransformer ?? defaultTextTransformer; this.subtitleTransformer = subtitleTransformer; - const notificationsConfig: Required = { + const notificationsConfig = { incomingChannel: { ...defaultAndroidOptions.incomingChannel, - ...(rest.incomingChannel ?? {}), + ...(incomingChannel ?? {}), }, outgoingChannel: { ...defaultAndroidOptions.outgoingChannel, - ...(rest.outgoingChannel ?? {}), + ...(outgoingChannel ?? {}), }, }; NativeCallingModule.setupAndroid(notificationsConfig); diff --git a/packages/react-native-callingx/src/spec/NativeCallingx.ts b/packages/react-native-callingx/src/spec/NativeCallingx.ts index 9957606b6e..dbd1604eaa 100644 --- a/packages/react-native-callingx/src/spec/NativeCallingx.ts +++ b/packages/react-native-callingx/src/spec/NativeCallingx.ts @@ -16,17 +16,15 @@ export interface Spec extends TurboModule { }): void; setupAndroid(options: { - incomingChannel?: { + incomingChannel: { id: string; name: string; - sound?: string; - vibration?: boolean; + sound: string; + vibration: boolean; }; - outgoingChannel?: { + outgoingChannel: { id: string; name: string; - sound?: string; - vibration?: boolean; }; }): void; diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index 2aee59eb53..68663d389c 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -74,14 +74,14 @@ export type iOSOptions = { export type AndroidOptions = { incomingChannel?: { - id: string; - name: string; + id?: string; + name?: string; sound?: string; vibration?: boolean; }; outgoingChannel?: { - id: string; - name: string; + id?: string; + name?: string; }; }; @@ -93,7 +93,7 @@ export type NotificationTransformers = { export type Options = { ios?: iOSOptions; - android: AndroidOptions & NotificationTransformers; + android?: AndroidOptions & NotificationTransformers; enableOutcomingCalls?: boolean; enableAutoPermissions?: boolean; }; diff --git a/packages/react-native-callingx/src/utils/constants.ts b/packages/react-native-callingx/src/utils/constants.ts index cd2f05c905..a31ce11830 100644 --- a/packages/react-native-callingx/src/utils/constants.ts +++ b/packages/react-native-callingx/src/utils/constants.ts @@ -4,6 +4,7 @@ import type { EndCallReason, TextTransformer, } from '../types'; +import type { DeepRequired } from './types'; export const defaultTextTransformer: TextTransformer = (text: string) => text; @@ -18,15 +19,15 @@ export const defaultiOSOptions: Required = { setupAudioSession: true, }; -export const defaultAndroidOptions: Required = { +export const defaultAndroidOptions: DeepRequired = { incomingChannel: { - id: 'telecom_incoming_channel', + id: 'incoming_calls_channel', name: 'Incoming calls', sound: '', vibration: false, }, outgoingChannel: { - id: 'telecom_ongoing_channel', + id: 'ongoing_calls_channel', name: 'Ongoing calls', }, }; diff --git a/packages/react-native-callingx/src/utils/types.ts b/packages/react-native-callingx/src/utils/types.ts new file mode 100644 index 0000000000..7dd8dee102 --- /dev/null +++ b/packages/react-native-callingx/src/utils/types.ts @@ -0,0 +1,5 @@ +export type DeepRequired = { + [K in keyof T]-?: NonNullable extends object + ? DeepRequired> + : NonNullable; +}; From 2952fc12cb9963ef18cc12b6aa7a0743efcdbab8 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Thu, 11 Dec 2025 13:35:14 +0100 Subject: [PATCH 20/27] feat: updated expo plugins --- .../__tests__/withAndroidManifest.test.ts | 4 +- .../__tests__/withAndroidPermissions.test.ts | 2 +- .../__tests__/withAppDelegate.test.ts | 30 +-- .../__tests__/withMainActivity.test.ts | 4 +- .../__tests__/withiOSInfoPlist.test.ts | 5 +- .../expo-config-plugin/src/common/types.ts | 8 +- .../src/withAndroidManifest.ts | 2 +- .../src/withAndroidPermissions.ts | 6 +- .../expo-config-plugin/src/withAppDelegate.ts | 224 ++---------------- .../src/withMainActivity.ts | 2 +- .../src/withiOSInfoPlist.ts | 7 +- .../react-native/expo-video-sample/app.json | 6 +- .../expo-video-sample/package.json | 2 +- .../react-native/ringing-tutorial/app.json | 6 +- .../ringing-tutorial/package.json | 2 +- 15 files changed, 36 insertions(+), 274 deletions(-) diff --git a/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidManifest.test.ts b/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidManifest.test.ts index 9b68104ab2..15cdea767a 100644 --- a/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidManifest.test.ts +++ b/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidManifest.test.ts @@ -35,9 +35,7 @@ const getMainActivityOrThrow = AndroidConfig.Manifest.getMainActivityOrThrow; const sampleManifestPath = getFixturePath('AndroidManifest.xml'); const props: ConfigProps = { - ringingPushNotifications: { - disableVideoIos: false, - }, + ringing: true, androidPictureInPicture: true, androidKeepCallAlive: true, }; diff --git a/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidPermissions.test.ts b/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidPermissions.test.ts index 07b9f740bd..6d6537a9c3 100644 --- a/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidPermissions.test.ts +++ b/packages/react-native-sdk/expo-config-plugin/__tests__/withAndroidPermissions.test.ts @@ -14,7 +14,7 @@ describe('withStreamVideoReactNativeSDKAndroidPermissions', () => { }; const props: ConfigProps = { enableScreenshare: true, - ringingPushNotifications: { disableVideoIos: false }, + ringing: true, androidKeepCallAlive: true, }; diff --git a/packages/react-native-sdk/expo-config-plugin/__tests__/withAppDelegate.test.ts b/packages/react-native-sdk/expo-config-plugin/__tests__/withAppDelegate.test.ts index 92b368eba9..06d9a54cb6 100644 --- a/packages/react-native-sdk/expo-config-plugin/__tests__/withAppDelegate.test.ts +++ b/packages/react-native-sdk/expo-config-plugin/__tests__/withAppDelegate.test.ts @@ -111,10 +111,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { const props: ConfigProps = { iOSEnableMultitaskingCameraAccess: true, addNoiseCancellation: true, - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: true, - }, + ringing: true, }; const updatedConfig = withAppDelegate(config, props) as CustomExpoConfig; @@ -183,10 +180,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { const props: ConfigProps = { iOSEnableMultitaskingCameraAccess: true, addNoiseCancellation: true, - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: true, - }, + ringing: true, }; const updatedConfig = withAppDelegate(config, props) as CustomExpoConfig; @@ -236,10 +230,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { it('objc - should not modify config if already added', () => { const props: ConfigProps = { iOSEnableMultitaskingCameraAccess: true, - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: true, - }, + ringing: true, }; const updatedConfig = withAppDelegate( @@ -256,10 +247,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { it('swift - should not modify config if already added', () => { const props: ConfigProps = { iOSEnableMultitaskingCameraAccess: true, - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: true, - }, + ringing: true, }; const updatedConfig = withAppDelegate( @@ -288,10 +276,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { }, }; const props: ConfigProps = { - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: false, - }, + ringing: true, }; expect(() => withAppDelegate(config, props)).toThrow(); }); @@ -311,10 +296,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { }, }; const props: ConfigProps = { - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: false, - }, + ringing: true, }; expect(() => withAppDelegate(config, props)).toThrow(); }); diff --git a/packages/react-native-sdk/expo-config-plugin/__tests__/withMainActivity.test.ts b/packages/react-native-sdk/expo-config-plugin/__tests__/withMainActivity.test.ts index 97281f1049..ef0fcf5abf 100644 --- a/packages/react-native-sdk/expo-config-plugin/__tests__/withMainActivity.test.ts +++ b/packages/react-native-sdk/expo-config-plugin/__tests__/withMainActivity.test.ts @@ -43,9 +43,7 @@ describe('withStreamVideoReactNativeSDKAppDelegate', () => { const props: ConfigProps = { androidPictureInPicture: true, enableScreenshare: true, - ringingPushNotifications: { - showWhenLockedAndroid: true, - }, + ringing: true, }; const updatedConfig = withMainActivity(config, props) as CustomExpoConfig; diff --git a/packages/react-native-sdk/expo-config-plugin/__tests__/withiOSInfoPlist.test.ts b/packages/react-native-sdk/expo-config-plugin/__tests__/withiOSInfoPlist.test.ts index 1fbcda78b5..ee13e91e67 100644 --- a/packages/react-native-sdk/expo-config-plugin/__tests__/withiOSInfoPlist.test.ts +++ b/packages/react-native-sdk/expo-config-plugin/__tests__/withiOSInfoPlist.test.ts @@ -129,10 +129,7 @@ describe('withStreamVideoReactNativeSDKiOSInfoPList', () => { }, }; const props: ConfigProps = { - ringingPushNotifications: { - disableVideoIos: true, - includesCallsInRecentsIos: true, - }, + ringing: true, }; const modifiedConfig = withStreamVideoReactNativeSDKiOSInfoPList( config, diff --git a/packages/react-native-sdk/expo-config-plugin/src/common/types.ts b/packages/react-native-sdk/expo-config-plugin/src/common/types.ts index b6822b0192..b4a4c2b3e1 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/common/types.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/common/types.ts @@ -1,12 +1,6 @@ -export type RingingPushNotifications = { - disableVideoIos?: boolean; - includesCallsInRecentsIos?: boolean; - showWhenLockedAndroid?: boolean; -}; - export type ConfigProps = | { - ringingPushNotifications?: RingingPushNotifications; + ringing?: boolean; enableNonRingingPushNotifications?: boolean; androidPictureInPicture?: boolean; androidKeepCallAlive?: boolean; diff --git a/packages/react-native-sdk/expo-config-plugin/src/withAndroidManifest.ts b/packages/react-native-sdk/expo-config-plugin/src/withAndroidManifest.ts index 604f71106a..7c457abd8c 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/withAndroidManifest.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/withAndroidManifest.ts @@ -50,7 +50,7 @@ const withStreamVideoReactNativeSDKManifest: ConfigPlugin = ( return withAndroidManifest(configuration, (config) => { const androidManifest = config.modResults; const mainApplication = getMainApplicationOrThrow(androidManifest); - if (props?.ringingPushNotifications || props?.androidKeepCallAlive) { + if (props?.ringing || props?.androidKeepCallAlive) { ensureToolsAvailable(androidManifest); /* Add the notifee foreground Service */ let services = mainApplication.service ?? []; diff --git a/packages/react-native-sdk/expo-config-plugin/src/withAndroidPermissions.ts b/packages/react-native-sdk/expo-config-plugin/src/withAndroidPermissions.ts index 1edd3462a0..1dfe642b6c 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/withAndroidPermissions.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/withAndroidPermissions.ts @@ -12,7 +12,7 @@ const withStreamVideoReactNativeSDKAndroidPermissions: ConfigPlugin< ]; if ( props?.androidKeepCallAlive || - props?.ringingPushNotifications || + props?.ringing || props?.enableScreenshare ) { permissions.push( @@ -25,7 +25,7 @@ const withStreamVideoReactNativeSDKAndroidPermissions: ConfigPlugin< ); } } - if (props?.androidKeepCallAlive || props?.ringingPushNotifications) { + if (props?.androidKeepCallAlive || props?.ringing) { permissions.push( 'android.permission.FOREGROUND_SERVICE_CAMERA', 'android.permission.FOREGROUND_SERVICE_MICROPHONE', @@ -33,7 +33,7 @@ const withStreamVideoReactNativeSDKAndroidPermissions: ConfigPlugin< 'android.permission.FOREGROUND_SERVICE_DATA_SYNC', ); } - if (props?.ringingPushNotifications?.showWhenLockedAndroid) { + if (props?.ringing) { permissions.push('android.permission.USE_FULL_SCREEN_INTENT'); } const config = AndroidConfig.Permissions.withPermissions( diff --git a/packages/react-native-sdk/expo-config-plugin/src/withAppDelegate.ts b/packages/react-native-sdk/expo-config-plugin/src/withAppDelegate.ts index d12c43dad5..ca08ab9652 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/withAppDelegate.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/withAppDelegate.ts @@ -12,17 +12,14 @@ import { insertContentsInsideObjcFunctionBlock, } from '@expo/config-plugins/build/ios/codeMod'; -import { - type ConfigProps, - type RingingPushNotifications, -} from './common/types'; +import { type ConfigProps } from './common/types'; import addNewLinesToAppDelegateObjc from './common/addNewLinesToAppDelegateObjc'; import { addToSwiftBridgingHeaderFile } from './common/addToSwiftBridgingHeaderFile'; const withAppDelegate: ConfigPlugin = (configuration, props) => { return withAppDelegateUtil(configuration, (config) => { if ( - !props?.ringingPushNotifications && + !props?.ringing && !props?.iOSEnableMultitaskingCameraAccess && !props?.addNoiseCancellation ) { @@ -42,22 +39,19 @@ const withAppDelegate: ConfigPlugin = (configuration, props) => { props.iOSEnableMultitaskingCameraAccess, props.addNoiseCancellation, ); - if (props?.ringingPushNotifications) { + if (props?.ringing) { config.modResults.contents = addObjcImports( config.modResults.contents, [ - '"RNCallKeep.h"', '', '"RNVoipPushNotificationManager.h"', '"StreamVideoReactNative.h"', - '', ], ); config.modResults.contents = addDidFinishLaunchingWithOptionsRingingObjc( config.modResults.contents, - props.ringingPushNotifications, ); config.modResults.contents = addDidUpdatePushCredentialsObjc( @@ -67,10 +61,6 @@ const withAppDelegate: ConfigPlugin = (configuration, props) => { config.modResults.contents = addDidReceiveIncomingPushCallbackObjc( config.modResults.contents, ); - - config.modResults.contents = addAudioSessionMethodsObjc( - config.modResults.contents, - ); } return config; } catch (error: any) { @@ -80,7 +70,7 @@ const withAppDelegate: ConfigPlugin = (configuration, props) => { } } else { try { - if (props?.ringingPushNotifications) { + if (props?.ringing) { // make it public class AppDelegate: ExpoAppDelegate, PKPushRegistryDelegate { const regex = /(class\s+AppDelegate[^{]*)(\s*\{)/; config.modResults.contents = config.modResults.contents.replace( @@ -130,15 +120,14 @@ const withAppDelegate: ConfigPlugin = (configuration, props) => { props.iOSEnableMultitaskingCameraAccess, props.addNoiseCancellation, ); - if (props?.ringingPushNotifications) { + if (props?.ringing) { config.modResults.contents = addSwiftImports( config.modResults.contents, - ['RNCallKeep', 'PushKit', 'RNVoipPushNotification'], + ['PushKit', 'RNVoipPushNotification'], ); config.modResults.contents = addDidFinishLaunchingWithOptionsRingingSwift( config.modResults.contents, - props.ringingPushNotifications, ); config.modResults.contents = addDidUpdatePushCredentialsSwift( @@ -148,10 +137,6 @@ const withAppDelegate: ConfigPlugin = (configuration, props) => { config.modResults.contents = addDidReceiveIncomingPushCallbackSwift( config.modResults.contents, ); - - config.modResults.contents = addAudioSessionMethodsSwift( - config.modResults.contents, - ); } return config; } catch (error: any) { @@ -231,31 +216,8 @@ function addDidFinishLaunchingWithOptionsObjc( return contents; } -function addDidFinishLaunchingWithOptionsRingingSwift( - contents: string, - ringingPushNotifications: RingingPushNotifications, -) { +function addDidFinishLaunchingWithOptionsRingingSwift(contents: string) { const functionSelector = 'application(_:didFinishLaunchingWithOptions:)'; - const supportsVideoString = ringingPushNotifications.disableVideoIos - ? 'false' - : 'true'; - const includesCallsInRecents = - ringingPushNotifications.includesCallsInRecentsIos ? 'false' : 'true'; - const setupCallKeep = ` let localizedAppName = Bundle.main.localizedInfoDictionary?["CFBundleDisplayName"] as? String - let appName = Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String - RNCallKeep.setup([ - "appName": localizedAppName != nil ? localizedAppName! : appName as Any, - "supportsVideo": ${supportsVideoString}, - "includesCallsInRecents": ${includesCallsInRecents}, - ])`; - if (!contents.includes('RNCallKeep.setup')) { - contents = insertContentsInsideSwiftFunctionBlock( - contents, - functionSelector, - setupCallKeep, - { position: 'head' }, - ); - } // call the setup of voip push notification const voipSetupMethod = 'RNVoipPushNotificationManager.voipRegistration()'; if (!contents.includes(voipSetupMethod)) { @@ -269,32 +231,8 @@ function addDidFinishLaunchingWithOptionsRingingSwift( return contents; } -function addDidFinishLaunchingWithOptionsRingingObjc( - contents: string, - ringingPushNotifications: RingingPushNotifications, -) { +function addDidFinishLaunchingWithOptionsRingingObjc(contents: string) { const functionSelector = 'application:didFinishLaunchingWithOptions:'; - // call the setup RNCallKeep - const supportsVideoString = ringingPushNotifications.disableVideoIos - ? '@NO' - : '@YES'; - const includesCallsInRecents = - ringingPushNotifications.includesCallsInRecentsIos ? '@YES' : '@NO'; - const setupCallKeep = `NSString *localizedAppName = [[[NSBundle mainBundle] localizedInfoDictionary] objectForKey:@"CFBundleDisplayName"]; - NSString *appName = [[[NSBundle mainBundle] infoDictionary]objectForKey :@"CFBundleDisplayName"]; - [RNCallKeep setup:@{ - @"appName": localizedAppName != nil ? localizedAppName : appName, - @"supportsVideo": ${supportsVideoString}, - @"includesCallsInRecents": ${includesCallsInRecents}, - }];`; - if (!contents.includes('[RNCallKeep setup:@')) { - contents = insertContentsInsideObjcFunctionBlock( - contents, - functionSelector, - setupCallKeep, - { position: 'head' }, - ); - } // call the setup of voip push notification const voipSetupMethod = '[RNVoipPushNotificationManager voipRegistration];'; if (!contents.includes(voipSetupMethod)) { @@ -366,108 +304,10 @@ function addDidUpdatePushCredentialsObjc(contents: string) { return contents; } -function addAudioSessionMethodsSwift(contents: string) { - const audioSessionDidActivateMethod = - 'RTCAudioSession.sharedInstance().audioSessionDidActivate(AVAudioSession.sharedInstance())'; - if (!contents.includes(audioSessionDidActivateMethod)) { - const functionSelector = 'provider(_:didActivate:)'; - if (!contents.includes('didActivateAudioSession')) { - contents = insertContentsInsideSwiftClassBlock( - contents, - 'class AppDelegate', - ` - func provider(_ provider: CXProvider, didActivateAudioSession audioSession: AVAudioSession) { - ${audioSessionDidActivateMethod} - } - `, - { position: 'tail' }, - ); - } else { - contents = insertContentsInsideSwiftFunctionBlock( - contents, - functionSelector, - audioSessionDidActivateMethod, - { position: 'tail' }, - ); - } - } - const audioSessionDidDeactivateMethod = - 'RTCAudioSession.sharedInstance().audioSessionDidDeactivate(AVAudioSession.sharedInstance())'; - - if (!contents.includes(audioSessionDidDeactivateMethod)) { - const functionSelector = 'provider(_:didDeactivate:)'; - if (!contents.includes('didDeactivateAudioSession')) { - contents = insertContentsInsideSwiftClassBlock( - contents, - 'class AppDelegate', - ` - func provider(_ provider: CXProvider, didDeactivateAudioSession audioSession: AVAudioSession) { - ${audioSessionDidDeactivateMethod} - } - `, - { position: 'tail' }, - ); - } else { - contents = insertContentsInsideSwiftFunctionBlock( - contents, - functionSelector, - audioSessionDidDeactivateMethod, - { position: 'tail' }, - ); - } - } - return contents; -} - -function addAudioSessionMethodsObjc(contents: string) { - const audioSessionDidActivateMethod = - '[[RTCAudioSession sharedInstance] audioSessionDidActivate:[AVAudioSession sharedInstance]];'; - if (!contents.includes(audioSessionDidActivateMethod)) { - const functionSelector = 'provider:didActivateAudioSession:audioSession:'; - const codeblock = findObjcFunctionCodeBlock(contents, functionSelector); - if (!codeblock) { - contents = addNewLinesToAppDelegateObjc(contents, [ - '- (void) provider:(CXProvider *) provider didActivateAudioSession:(AVAudioSession *) audioSession {', - ' ' /* indentation */ + audioSessionDidActivateMethod, - '}', - ]); - } else { - contents = insertContentsInsideObjcFunctionBlock( - contents, - functionSelector, - audioSessionDidActivateMethod, - { position: 'tail' }, - ); - } - } - const audioSessionDidDeactivateMethod = - '[[RTCAudioSession sharedInstance] audioSessionDidDeactivate:[AVAudioSession sharedInstance]];'; - - if (!contents.includes(audioSessionDidDeactivateMethod)) { - const functionSelector = 'provider:didDeactivateAudioSession:audioSession:'; - const codeblock = findObjcFunctionCodeBlock(contents, functionSelector); - if (!codeblock) { - contents = addNewLinesToAppDelegateObjc(contents, [ - '- (void) provider:(CXProvider *) provider didDeactivateAudioSession:(AVAudioSession *) audioSession {', - ' ' /* indentation */ + audioSessionDidDeactivateMethod, - '}', - ]); - } else { - contents = insertContentsInsideObjcFunctionBlock( - contents, - functionSelector, - audioSessionDidDeactivateMethod, - { position: 'tail' }, - ); - } - } - return contents; -} - function addDidReceiveIncomingPushCallbackSwift(contents: string) { const onIncomingPush = ` guard let stream = payload.dictionaryPayload["stream"] as? [String: Any], - let createdCallerName = stream["created_by_display_name"] as? String, + let _ = stream["created_by_display_name"] as? String, let cid = stream["call_cid"] as? String else { completion() return @@ -483,28 +323,12 @@ function addDidReceiveIncomingPushCallbackSwift(contents: string) { return } - let uuid = UUID().uuidString - let videoIncluded = stream["video"] as? String - let hasVideo = videoIncluded == "false" ? false : true - - StreamVideoReactNative.registerIncomingCall(cid, uuid: uuid) - - RNVoipPushNotificationManager.addCompletionHandler(uuid, completionHandler: completion) + RNVoipPushNotificationManager.addCompletionHandler(cid, completionHandler: completion) + // Process the received push // fire 'notification' event to JS RNVoipPushNotificationManager.didReceiveIncomingPush(with: payload, forType: type.rawValue) - RNCallKeep.reportNewIncomingCall(uuid, - handle: createdCallerName, - handleType: "generic", - hasVideo: hasVideo, - localizedCallerName: createdCallerName, - supportsHolding: false, - supportsDTMF: false, - supportsGrouping: false, - supportsUngrouping: false, - fromPushKit: true, - payload: stream, - withCompletionHandler: nil)`; + StreamVideoReactNative.didReceiveIncomingPush(payload, completionHandler: nil)`; if ( !contents.includes('RNVoipPushNotificationManager.didReceiveIncomingPush') ) { @@ -543,8 +367,6 @@ function addDidReceiveIncomingPushCallbackObjc(contents: string) { const onIncomingPush = ` // process the payload and store it in the native module's cache NSDictionary *stream = payload.dictionaryPayload[@"stream"]; - NSString *uuid = [[NSUUID UUID] UUIDString]; - NSString *createdCallerName = stream[@"created_by_display_name"]; NSString *cid = stream[@"call_cid"]; // Check if user is busy BEFORE registering the call @@ -557,31 +379,13 @@ function addDidReceiveIncomingPushCallbackObjc(contents: string) { return; } - NSString *videoIncluded = stream[@"video"]; - BOOL hasVideo = [videoIncluded isEqualToString:@"false"] ? NO : YES; - - // store the call cid and uuid in the native module's cache - [StreamVideoReactNative registerIncomingCall:cid uuid:uuid]; - - // set the completion handler - this one is called by the JS SDK - [RNVoipPushNotificationManager addCompletionHandler:uuid completionHandler:completion]; + [RNVoipPushNotificationManager addCompletionHandler:cid completionHandler:completion]; // send event to JS - the JS SDK will handle the rest and call the 'completionHandler' [RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type]; // display the incoming call notification - [RNCallKeep reportNewIncomingCall: uuid - handle: createdCallerName - handleType: @"generic" - hasVideo: hasVideo - localizedCallerName: createdCallerName - supportsHolding: NO - supportsDTMF: NO - supportsGrouping: NO - supportsUngrouping: NO - fromPushKit: YES - payload: stream - withCompletionHandler: nil]; + [StreamVideoReactNative didReceiveIncomingPush:payload completionHandler:nil]; `; if ( !contents.includes( diff --git a/packages/react-native-sdk/expo-config-plugin/src/withMainActivity.ts b/packages/react-native-sdk/expo-config-plugin/src/withMainActivity.ts index d6e14e353f..ff44e5a2e3 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/withMainActivity.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/withMainActivity.ts @@ -41,7 +41,7 @@ const withStreamVideoReactNativeSDKMainActivity: ConfigPlugin = ( ); } - if (props?.ringingPushNotifications?.showWhenLockedAndroid) { + if (props?.ringing) { config.modResults.contents = addInsideOnCreateLockscreen( config.modResults.contents, isMainActivityJava, diff --git a/packages/react-native-sdk/expo-config-plugin/src/withiOSInfoPlist.ts b/packages/react-native-sdk/expo-config-plugin/src/withiOSInfoPlist.ts index bfbd55a2b9..b2ee1e4728 100644 --- a/packages/react-native-sdk/expo-config-plugin/src/withiOSInfoPlist.ts +++ b/packages/react-native-sdk/expo-config-plugin/src/withiOSInfoPlist.ts @@ -15,7 +15,7 @@ const withStreamVideoReactNativeSDKiOSInfoPList: ConfigPlugin = ( } } addBackgroundMode('audio'); - if (props?.ringingPushNotifications) { + if (props?.ringing) { addBackgroundMode('voip'); addBackgroundMode('fetch'); addBackgroundMode('processing'); @@ -23,10 +23,7 @@ const withStreamVideoReactNativeSDKiOSInfoPList: ConfigPlugin = ( '$(PRODUCT_BUNDLE_IDENTIFIER)', ]; } - if ( - props?.enableNonRingingPushNotifications || - props?.ringingPushNotifications - ) { + if (props?.enableNonRingingPushNotifications || props?.ringing) { addBackgroundMode('remote-notification'); } return config; diff --git a/sample-apps/react-native/expo-video-sample/app.json b/sample-apps/react-native/expo-video-sample/app.json index a6c1ceed66..e75a35e4b3 100644 --- a/sample-apps/react-native/expo-video-sample/app.json +++ b/sample-apps/react-native/expo-video-sample/app.json @@ -74,11 +74,7 @@ "addNoiseCancellation": true, "androidKeepCallAlive": true, "appleTeamId": "EHV7XZLAHA", - "ringingPushNotifications": { - "disableVideoIos": false, - "includesCallsInRecentsIos": false, - "showWhenLockedAndroid": true - }, + "ringing": true, "enableNonRingingPushNotifications": true, "androidPictureInPicture": true, "iOSEnableMultitaskingCameraAccess": true diff --git a/sample-apps/react-native/expo-video-sample/package.json b/sample-apps/react-native/expo-video-sample/package.json index 60cfd0ff81..9707ed13ff 100644 --- a/sample-apps/react-native/expo-video-sample/package.json +++ b/sample-apps/react-native/expo-video-sample/package.json @@ -36,7 +36,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-native": "^0.81.5", - "react-native-callkeep": "^4.3.16", + "react-native-callingx": "workspace:^", "react-native-gesture-handler": "^2.28.0", "react-native-reanimated": "~4.1.2", "react-native-safe-area-context": "~5.6.1", diff --git a/sample-apps/react-native/ringing-tutorial/app.json b/sample-apps/react-native/ringing-tutorial/app.json index e0f5055000..fa89effa5a 100644 --- a/sample-apps/react-native/ringing-tutorial/app.json +++ b/sample-apps/react-native/ringing-tutorial/app.json @@ -80,11 +80,7 @@ [ "@stream-io/video-react-native-sdk", { - "ringingPushNotifications": { - "disableVideoIos": false, - "includesCallsInRecentsIos": false, - "showWhenLockedAndroid": true - }, + "ringing": true, "androidKeepCallAlive": true } ], diff --git a/sample-apps/react-native/ringing-tutorial/package.json b/sample-apps/react-native/ringing-tutorial/package.json index ee119f1116..1c6c067967 100644 --- a/sample-apps/react-native/ringing-tutorial/package.json +++ b/sample-apps/react-native/ringing-tutorial/package.json @@ -42,7 +42,7 @@ "react": "19.1.0", "react-dom": "19.1.0", "react-native": "^0.81.5", - "react-native-callkeep": "^4.3.16", + "react-native-callingx": "workspace:^", "react-native-gesture-handler": "^2.28.0", "react-native-reanimated": "~4.1.2", "react-native-safe-area-context": "~5.6.1", From 2f2709b5b7564d004bccd9a8e412bc4e7e5a7380 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Thu, 11 Dec 2025 16:38:31 +0100 Subject: [PATCH 21/27] feat: adjusted answer call events behavior --- .../src/main/java/com/callingx/CallService.kt | 9 ++++++- .../main/java/com/callingx/CallingxModule.kt | 4 +-- .../callingx/repo/TelecomCallRepository.kt | 8 ++++-- .../react-native-callingx/ios/Callingx.mm | 27 +++++++++++++++---- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt index 0212168ed2..bd34d815a9 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt @@ -120,6 +120,13 @@ class CallService : Service(), CallRepository.Listener { registerCall(intent, false) } ACTION_START_BACKGROUND_TASK -> { + if (!isInForeground) { + Log.d(TAG, "[service] onStartCommand: Starting foreground for background task") + //for now bg task is intended to be used after a call registered and notification is shown, so we don't need to show a separate notification for bg task + // startForeground(CallNotificationManager.NOTIFICATION_ID, notification) + isInForeground = true + } + val taskName = intent.getStringExtra(EXTRA_TASK_NAME)!! val taskData = intent.getBundleExtra(EXTRA_TASK_DATA)!! val taskTimeout = intent.getLongExtra(EXTRA_TASK_TIMEOUT, 0) @@ -256,7 +263,7 @@ class CallService : Service(), CallRepository.Listener { } } - fun isCallRegistered(callId: String): Boolean { + public fun isCallRegistered(callId: String): Boolean { val currentCall = callRepository.currentCall.value return currentCall is Call.Registered && currentCall.id == callId } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt index 04a0f3e334..70f678d945 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt @@ -279,7 +279,7 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec putExtra(CallService.EXTRA_TASK_DATA, Bundle()) putExtra(CallService.EXTRA_TASK_TIMEOUT, timeout.toLong()) } - .also { reactApplicationContext.startService(it) } + .also { ContextCompat.startForegroundService(reactApplicationContext, it) } promise.resolve(true) } @@ -292,7 +292,7 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec this.action = CallService.ACTION_STOP_BACKGROUND_TASK putExtra(CallService.EXTRA_TASK_NAME, taskName) } - .also { reactApplicationContext.startService(it) } + .also { ContextCompat.startForegroundService(reactApplicationContext, it) } promise.resolve(true) } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt index 6379a340b9..6a676498cd 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt @@ -49,6 +49,7 @@ class TelecomCallRepository(private val context: Context) : CallRepository { private val callsManager: CallsManager private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private var isSelfAnswered = false private var isSelfDisconnected = false // Keeps track of the current TelecomCall state @@ -340,6 +341,7 @@ class TelecomCallRepository(private val context: Context) : CallRepository { } private suspend fun CallControlScope.doAnswer(isAudioCall: Boolean) { + isSelfAnswered = true val callType = if (isAudioCall) CallAttributesCompat.CALL_TYPE_AUDIO_CALL else CallAttributesCompat.CALL_TYPE_VIDEO_CALL @@ -353,6 +355,7 @@ class TelecomCallRepository(private val context: Context) : CallRepository { TAG, "[repository] doAnswer: Answer failed with error code: ${result.errorCode}" ) + isSelfAnswered = false updateCurrentCall { Call.Unregistered( id = id, @@ -369,13 +372,14 @@ class TelecomCallRepository(private val context: Context) : CallRepository { * if we can answer a call Example you may need to wait for another call to hold. */ val onIsCallAnswered: suspend (type: Int) -> Unit = { - Log.d(TAG, "[repository] onIsCallAnswered: Call answered, type: $it") + Log.d(TAG, "[repository] onIsCallAnswered: Call answered, type: $it, isSelfAnswered: $isSelfAnswered") updateCurrentCall { copy(isActive = true, isOnHold = false) } val call = _currentCall.value - if (call is Call.Registered) { + if (!isSelfAnswered && call is Call.Registered) { listener?.onIsCallAnswered(call.id) } + isSelfAnswered = false Log.d(TAG, "[repository] onIsCallAnswered: Call state updated to active") } diff --git a/packages/react-native-callingx/ios/Callingx.mm b/packages/react-native-callingx/ios/Callingx.mm index 3b6f8e0a4a..7e701ba19b 100644 --- a/packages/react-native-callingx/ios/Callingx.mm +++ b/packages/react-native-callingx/ios/Callingx.mm @@ -32,6 +32,7 @@ @implementation Callingx { NSOperatingSystemVersion _version; bool _isReachable; bool _isInitialized; + bool _isSelfAnswered; NSMutableArray *_delayedEvents; } @@ -508,6 +509,17 @@ - (void)answerIncomingCall:(nonnull NSString *)callId return; } + // Check if call was already answered + CXCallObserver *observer = [[CXCallObserver alloc] init]; + for (CXCall *call in observer.calls) { + if ([call.UUID isEqual:uuid] && call.hasConnected) { + NSLog(@"[Callingx][answerIncomingCall] call already answered, skipping"); + resolve(@YES); + return; + } + } + + _isSelfAnswered = true; CXAnswerCallAction *answerCallAction = [[CXAnswerCallAction alloc] initWithCallUUID:uuid]; CXTransaction *transaction = [[CXTransaction alloc] init]; [transaction addAction:answerCallAction]; @@ -807,7 +819,7 @@ - (void)provider:(CXProvider *)provider - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action { #ifdef DEBUG - NSLog(@"[Callingx][CXProviderDelegate][provider:performAnswerCallAction]"); + NSLog(@"[Callingx][CXProviderDelegate][provider:performAnswerCallAction] isSelfAnswered: %d", _isSelfAnswered); #endif NSString *callId = [uuidStorage getCidForUUID:action.callUUID]; if (callId == nil) { @@ -817,10 +829,15 @@ - (void)provider:(CXProvider *)provider } [AudioSessionManager createAudioSessionIfNeeded]; - [self sendEventWithNameWrapper:CallingxPerformAnswerCallAction - body:@{ - @"callId" : callId - }]; + + // Only send event if the answer was triggered by the system UI, not programmatically + if (!_isSelfAnswered) { + [self sendEventWithNameWrapper:CallingxPerformAnswerCallAction + body:@{ + @"callId" : callId + }]; + } + _isSelfAnswered = false; [action fulfill]; } From b0ffbab0dea41934b6a9a72cfbf39b3e8fa76cce Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Thu, 11 Dec 2025 20:11:16 +0100 Subject: [PATCH 22/27] feat: answer/end callbacks tweaks --- .../src/main/java/com/callingx/CallService.kt | 6 ++-- .../main/java/com/callingx/CallingxModule.kt | 13 ++++++-- .../notifications/CallNotificationManager.kt | 5 ++- .../NotificationIntentFactory.kt | 16 ++++++---- .../NotificationReceiverActivity.kt | 2 ++ .../NotificationReceiverService.kt | 22 +++++++------ .../java/com/callingx/repo/CallRepository.kt | 8 +++-- .../com/callingx/repo/LegacyCallRepository.kt | 8 +++-- .../callingx/repo/TelecomCallRepository.kt | 10 +++--- .../react-native-callingx/ios/Callingx.mm | 32 ++++++++----------- .../src/CallingxModule.ts | 2 +- .../src/spec/NativeCallingx.ts | 3 ++ packages/react-native-callingx/src/types.ts | 6 ++-- .../useCallingExpWithCallingStateEffect.ts | 20 ++++++------ .../src/utils/push/internal/ios.ts | 2 +- .../src/utils/push/libs/callingx.ts | 1 + .../src/utils/push/setupCallingExpEvents.ts | 30 ++++++++--------- .../dogfood/src/utils/setPushConfig.ts | 20 +----------- 18 files changed, 109 insertions(+), 97 deletions(-) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt index bd34d815a9..0f4221c912 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt @@ -211,13 +211,14 @@ class CallService : Service(), CallRepository.Listener { } } - override fun onIsCallAnswered(callId: String) { + override fun onIsCallAnswered(callId: String, source: CallRepository.EventSource) { sendBroadcastEvent(CallingxModule.CALL_ANSWERED_ACTION) { putExtra(CallingxModule.EXTRA_CALL_ID, callId) + putExtra(CallingxModule.EXTRA_SOURCE, source.name.lowercase()) } } - override fun onIsCallDisconnected(callId: String?, cause: DisconnectCause) { + override fun onIsCallDisconnected(callId: String?, cause: DisconnectCause, source: CallRepository.EventSource) { // we're not passing the callId here to prevent infinite loops // callEnd event with callId will sent only when after interaction with notification buttons sendBroadcastEvent(CallingxModule.CALL_END_ACTION) { @@ -225,6 +226,7 @@ class CallService : Service(), CallRepository.Listener { putExtra(CallingxModule.EXTRA_CALL_ID, callId) } putExtra(CallingxModule.EXTRA_DISCONNECT_CAUSE, getDisconnectCauseString(cause)) + putExtra(CallingxModule.EXTRA_SOURCE, source.name.lowercase()) } } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt index 70f678d945..72499719ff 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt @@ -39,6 +39,7 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec const val EXTRA_ON_HOLD = "hold" const val EXTRA_DISCONNECT_CAUSE = "disconnect_cause" const val EXTRA_AUDIO_ENDPOINT = "audio_endpoint" + const val EXTRA_SOURCE = "source" const val CALL_REGISTERED_ACTION = "call_registered" const val CALL_ANSWERED_ACTION = "call_answered" @@ -506,16 +507,22 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec sendJSEvent("didReceiveStartCallAction", params) } CALL_ANSWERED_ACTION -> { + if (intent.hasExtra(EXTRA_SOURCE)) { + params.putString("source", intent.getStringExtra(EXTRA_SOURCE)) + } sendJSEvent("answerCall", params) } CALL_END_ACTION -> { if (callId == null) { // means the call was disconnected, we're ready to unbind the service unbindServiceSafely() - } else { - params.putString("cause", intent.getStringExtra(EXTRA_DISCONNECT_CAUSE)) - sendJSEvent("endCall", params) } + + params.putString("cause", intent.getStringExtra(EXTRA_DISCONNECT_CAUSE)) + if (intent.hasExtra(EXTRA_SOURCE)) { + params.putString("source", intent.getStringExtra(EXTRA_SOURCE)) + } + sendJSEvent("endCall", params) } CALL_INACTIVE_ACTION -> { params.putBoolean("hold", true) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt index 8e8689bda7..2f91e3885e 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/CallNotificationManager.kt @@ -152,11 +152,13 @@ class CallNotificationManager( CallingxModule.EXTRA_DISCONNECT_CAUSE, getDisconnectCauseString(DisconnectCause(DisconnectCause.REJECTED)) ) + putExtra(CallingxModule.EXTRA_SOURCE, CallRepository.EventSource.SYS.name.lowercase()) }, NotificationIntentFactory.getPendingNotificationIntent( context, CallingxModule.CALL_ANSWERED_ACTION, - call.id + call.id, + CallRepository.EventSource.SYS.name.lowercase() ) ) } @@ -172,6 +174,7 @@ class CallNotificationManager( CallingxModule.EXTRA_DISCONNECT_CAUSE, getDisconnectCauseString(DisconnectCause(DisconnectCause.LOCAL)) ) + putExtra(CallingxModule.EXTRA_SOURCE, CallRepository.EventSource.SYS.name.lowercase()) }, ) } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationIntentFactory.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationIntentFactory.kt index 87852a3145..48d06267ca 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationIntentFactory.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationIntentFactory.kt @@ -15,20 +15,22 @@ object NotificationIntentFactory { fun getPendingNotificationIntent( context: Context, action: String, - callId: String + callId: String, + source: String ): PendingIntent { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - getReceiverActivityIntent(context, action, callId) + getReceiverActivityIntent(context, action, callId, source) } else { - getPendingServiceIntent(context, action, callId) + getPendingServiceIntent(context, action, callId, source) } } - fun getPendingServiceIntent(context: Context, action: String, callId: String): PendingIntent { + fun getPendingServiceIntent(context: Context, action: String, callId: String, source: String): PendingIntent { val intent = Intent(context, NotificationReceiverService::class.java).apply { this.action = action putExtra(CallingxModule.EXTRA_CALL_ID, callId) + putExtra(CallingxModule.EXTRA_SOURCE, source) } return PendingIntent.getService( @@ -39,11 +41,12 @@ object NotificationIntentFactory { ) } - fun getReceiverActivityIntent(context: Context, action: String, callId: String): PendingIntent { + fun getReceiverActivityIntent(context: Context, action: String, callId: String, source: String): PendingIntent { val receiverIntent = Intent(context, NotificationReceiverActivity::class.java).apply { this.action = action putExtra(CallingxModule.EXTRA_CALL_ID, callId) + putExtra(CallingxModule.EXTRA_SOURCE, source) } val launchActivity = context.packageManager.getLaunchIntentForPackage(context.packageName) @@ -60,12 +63,13 @@ object NotificationIntentFactory { ) } - fun getLaunchActivityIntent(context: Context, action: String, callId: String): PendingIntent { + fun getLaunchActivityIntent(context: Context, action: String, callId: String, source: String? = null): PendingIntent { val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) val callIntent = Intent(launchIntent).apply { this.action = action putExtra(CallingxModule.EXTRA_CALL_ID, callId) + source?.let { putExtra(CallingxModule.EXTRA_SOURCE, it) } addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP) } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverActivity.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverActivity.kt index c91b7e6410..1d66ce08af 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverActivity.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverActivity.kt @@ -29,10 +29,12 @@ class NotificationReceiverActivity : Activity() { //we need it only for answered call event, as for cold start case we need to send broadcast event and to launch the app if (intent.action == CallingxModule.CALL_ANSWERED_ACTION) { val callId = intent.getStringExtra(CallingxModule.EXTRA_CALL_ID) + val source = intent.getStringExtra(CallingxModule.EXTRA_SOURCE) Intent(CallingxModule.CALL_ANSWERED_ACTION) .apply { setPackage(packageName) putExtra(CallingxModule.EXTRA_CALL_ID, callId) + putExtra(CallingxModule.EXTRA_SOURCE, source) } .also { sendBroadcast(it) } } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverService.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverService.kt index 996dd9a0a1..13afa95974 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverService.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/notifications/NotificationReceiverService.kt @@ -26,20 +26,22 @@ class NotificationReceiverService : Service() { private fun onCallAnswered(intent: Intent) { val callId = intent.getStringExtra(CallingxModule.EXTRA_CALL_ID) + val source = intent.getStringExtra(CallingxModule.EXTRA_SOURCE) callId?.let { NotificationIntentFactory.getPendingBroadcastIntent( - applicationContext, - CallingxModule.CALL_ANSWERED_ACTION, - it - ) - .send() + applicationContext, + CallingxModule.CALL_ANSWERED_ACTION, + it + ) { putExtra(CallingxModule.EXTRA_SOURCE, source) } + .send() NotificationIntentFactory.getLaunchActivityIntent( - applicationContext, - CallingxModule.CALL_ANSWERED_ACTION, - it - ) - .send() + applicationContext, + CallingxModule.CALL_ANSWERED_ACTION, + it, + source + ) + .send() } } } diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/CallRepository.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/CallRepository.kt index 18329ed06f..7d2c380763 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/CallRepository.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/CallRepository.kt @@ -9,10 +9,14 @@ import kotlinx.coroutines.flow.StateFlow interface CallRepository { + enum class EventSource { + APP, SYS + } + interface Listener { fun onCallStateChanged(call: Call) - fun onIsCallAnswered(callId: String) - fun onIsCallDisconnected(callId: String?, cause: DisconnectCause) + fun onIsCallAnswered(callId: String, source: EventSource) + fun onIsCallDisconnected(callId: String?, cause: DisconnectCause, source: EventSource) fun onIsCallInactive(callId: String) fun onIsCallActive(callId: String) fun onCallRegistered(callId: String) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/LegacyCallRepository.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/LegacyCallRepository.kt index 50b64edbac..d27a896c8b 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/LegacyCallRepository.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/LegacyCallRepository.kt @@ -80,14 +80,18 @@ class LegacyCallRepository(private val context: Context) : CallRepository { when (action) { is CallAction.Answer -> { updateCurrentCall { copy(isActive = true, isOnHold = false) } - (currentCall.value as? Call.Registered)?.let { listener?.onIsCallAnswered(it.id) } + // In legacy mode, all actions are initiated from the app + (currentCall.value as? Call.Registered)?.let { + listener?.onIsCallAnswered(it.id, CallRepository.EventSource.APP) + } } is CallAction.Disconnect -> { val call = currentCall.value as? Call.Registered if (call != null) { _currentCall.value = Call.Unregistered(call.id, call.callAttributes, action.cause) - listener?.onIsCallDisconnected(call.id, action.cause) + // In legacy mode, all actions are initiated from the app + listener?.onIsCallDisconnected(call.id, action.cause, CallRepository.EventSource.APP) } } is CallAction.ToggleMute -> { diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt index 6a676498cd..2efeb1e226 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/repo/TelecomCallRepository.kt @@ -376,8 +376,9 @@ class TelecomCallRepository(private val context: Context) : CallRepository { updateCurrentCall { copy(isActive = true, isOnHold = false) } val call = _currentCall.value - if (!isSelfAnswered && call is Call.Registered) { - listener?.onIsCallAnswered(call.id) + val source = if (isSelfAnswered) CallRepository.EventSource.APP else CallRepository.EventSource.SYS + if (call is Call.Registered) { + listener?.onIsCallAnswered(call.id, source) } isSelfAnswered = false Log.d(TAG, "[repository] onIsCallAnswered: Call state updated to active") @@ -389,13 +390,14 @@ class TelecomCallRepository(private val context: Context) : CallRepository { TAG, "[repository] onIsCallDisconnected: Call disconnected, cause: ${it.reason}, description: ${it.description}" ) + val source = if (isSelfDisconnected) CallRepository.EventSource.APP else CallRepository.EventSource.SYS var callId: String? = null - if (!isSelfDisconnected && _currentCall.value is Call.Registered) { + if (_currentCall.value is Call.Registered) { callId = (_currentCall.value as Call.Registered).id } updateCurrentCall { Call.Unregistered(id, callAttributes, it) } - listener?.onIsCallDisconnected(callId, it) + listener?.onIsCallDisconnected(callId, it, source) isSelfDisconnected = false Log.d(TAG, "[repository] onIsCallDisconnected: Call state updated to Unregistered") } diff --git a/packages/react-native-callingx/ios/Callingx.mm b/packages/react-native-callingx/ios/Callingx.mm index 7e701ba19b..5178e8e2d5 100644 --- a/packages/react-native-callingx/ios/Callingx.mm +++ b/packages/react-native-callingx/ios/Callingx.mm @@ -33,6 +33,7 @@ @implementation Callingx { bool _isReachable; bool _isInitialized; bool _isSelfAnswered; + bool _isSelfEnded; NSMutableArray *_delayedEvents; } @@ -509,16 +510,6 @@ - (void)answerIncomingCall:(nonnull NSString *)callId return; } - // Check if call was already answered - CXCallObserver *observer = [[CXCallObserver alloc] init]; - for (CXCall *call in observer.calls) { - if ([call.UUID isEqual:uuid] && call.hasConnected) { - NSLog(@"[Callingx][answerIncomingCall] call already answered, skipping"); - resolve(@YES); - return; - } - } - _isSelfAnswered = true; CXAnswerCallAction *answerCallAction = [[CXAnswerCallAction alloc] initWithCallUUID:uuid]; CXTransaction *transaction = [[CXTransaction alloc] init]; @@ -593,6 +584,7 @@ - (void)endCall:(nonnull NSString *)callId return; } + _isSelfEnded = true; CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:uuid]; CXTransaction *transaction = [[CXTransaction alloc] initWithAction:endCallAction]; @@ -830,13 +822,12 @@ - (void)provider:(CXProvider *)provider [AudioSessionManager createAudioSessionIfNeeded]; - // Only send event if the answer was triggered by the system UI, not programmatically - if (!_isSelfAnswered) { - [self sendEventWithNameWrapper:CallingxPerformAnswerCallAction - body:@{ - @"callId" : callId - }]; - } + NSString *source = _isSelfAnswered ? @"app" : @"sys"; + [self sendEventWithNameWrapper:CallingxPerformAnswerCallAction + body:@{ + @"callId" : callId, + @"source" : source + }]; _isSelfAnswered = false; [action fulfill]; } @@ -844,7 +835,7 @@ - (void)provider:(CXProvider *)provider - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action { #ifdef DEBUG - NSLog(@"[Callingx][CXProviderDelegate][provider:performEndCallAction]"); + NSLog(@"[Callingx][CXProviderDelegate][provider:performEndCallAction] isSelfEnded: %d", _isSelfEnded); #endif NSString *callId = [uuidStorage getCidForUUID:action.callUUID]; if (callId == nil) { @@ -853,10 +844,13 @@ - (void)provider:(CXProvider *)provider return; } + NSString *source = _isSelfEnded ? @"app" : @"sys"; [self sendEventWithNameWrapper:CallingxPerformEndCallAction body:@{ - @"callId" : callId + @"callId" : callId, + @"source" : source }]; + _isSelfEnded = false; [uuidStorage removeCid:callId]; [action fulfill]; } diff --git a/packages/react-native-callingx/src/CallingxModule.ts b/packages/react-native-callingx/src/CallingxModule.ts index ed08696299..cb3cfb26cd 100644 --- a/packages/react-native-callingx/src/CallingxModule.ts +++ b/packages/react-native-callingx/src/CallingxModule.ts @@ -118,7 +118,7 @@ class CallingxModule implements ICallingxModule { } getInitialEvents(): EventData[] { - return NativeCallingModule.getInitialEvents(); + return NativeCallingModule.getInitialEvents() as EventData[]; } clearInitialEvents(): Promise { diff --git a/packages/react-native-callingx/src/spec/NativeCallingx.ts b/packages/react-native-callingx/src/spec/NativeCallingx.ts index dbd1604eaa..89d2afa722 100644 --- a/packages/react-native-callingx/src/spec/NativeCallingx.ts +++ b/packages/react-native-callingx/src/spec/NativeCallingx.ts @@ -35,6 +35,9 @@ export interface Spec extends TurboModule { params: { callId: string; cause?: string; + muted?: boolean; + hold?: boolean; + source?: string; }; }>; diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index 68663d389c..8245cd1c55 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -104,8 +104,8 @@ export type InfoDisplayOptions = { }; export type EventData = { - eventName: string; - params: { callId: string; cause?: string }; + eventName: EventName; + params: EventParams[EventName]; }; export type EventName = @@ -122,10 +122,12 @@ export type EventName = export type EventParams = { answerCall: { callId: string; + source: 'app' | 'sys'; }; endCall: { callId: string; cause: string; + source: 'app' | 'sys'; }; didDisplayIncomingCall: { callId: string; diff --git a/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts index 32088a3271..6160eae289 100644 --- a/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts +++ b/packages/react-native-sdk/src/hooks/push/useCallingExpWithCallingStateEffect.ts @@ -77,7 +77,7 @@ export const useCallingExpWithCallingStateEffect = () => { //if incoming stream call was unmounted, we need to end the call in CallKit/Telecom logger.debug(`Ending call in calling exp: ${activeCallCid}`); callingx - .endCallWithReason(activeCallCid, 'remote') + .endCallWithReason(activeCallCid, 'local') .catch((error: unknown) => { logger.error( `Error ending call in calling exp: ${activeCallCid}`, @@ -129,7 +129,7 @@ export const useCallingExpWithCallingStateEffect = () => { error, ); }); - } else if (isCallRegistered) { + } else if (!isOutcomingCall && isCallRegistered) { logger.debug( `Should accept call in callkeep: ${activeCallCid} isCallRegistered: ${isCallRegistered}`, ); @@ -149,7 +149,7 @@ export const useCallingExpWithCallingStateEffect = () => { logger.debug(`Should end call in callkeep: ${activeCallCid}`); //TODO: think about sending appropriate reason for end call callingx - .endCallWithReason(activeCallCid, 'remote') + .endCallWithReason(activeCallCid, 'local') .catch((error: unknown) => { logger.error( `Error ending call in calling exp: ${activeCallCid}`, @@ -179,12 +179,14 @@ export const useCallingExpWithCallingStateEffect = () => { ({ callId }: { callId: string }) => { if (callId === activeCallCid) { logger.debug(`Received start call action for call: ${activeCallCid}`); - callingx.answerIncomingCall(activeCallCid).catch((error: unknown) => { - logger.error( - `Error answering call in calling exp: ${activeCallCid}`, - error, - ); - }); + callingx + .setCurrentCallActive(activeCallCid) + .catch((error: unknown) => { + logger.error( + `Error answering call in calling exp: ${activeCallCid}`, + error, + ); + }); } }, ); diff --git a/packages/react-native-sdk/src/utils/push/internal/ios.ts b/packages/react-native-sdk/src/utils/push/internal/ios.ts index a3e7cc7913..fa27f500d0 100644 --- a/packages/react-native-sdk/src/utils/push/internal/ios.ts +++ b/packages/react-native-sdk/src/utils/push/internal/ios.ts @@ -74,7 +74,7 @@ export const onVoipNotificationReceived = async ( if (mustEndCall) { logger.debug(`callkeep.reportEndCallWithUUID for call_cid: ${call_cid}`); //TODO: think about sending appropriate reason for end call - callingx.endCallWithReason(call_cid, 'remote'); + callingx.endCallWithReason(call_cid, 'local'); const voipPushNotification = getVoipPushNotificationLib(); voipPushNotification.onVoipNotificationCompleted(call_cid); diff --git a/packages/react-native-sdk/src/utils/push/libs/callingx.ts b/packages/react-native-sdk/src/utils/push/libs/callingx.ts index f0a4fe1c07..7a3219057a 100644 --- a/packages/react-native-sdk/src/utils/push/libs/callingx.ts +++ b/packages/react-native-sdk/src/utils/push/libs/callingx.ts @@ -1,4 +1,5 @@ export type RNCallingxType = import('react-native-callingx').ICallingxModule; +export type EventParams = import('react-native-callingx').EventParams; let callingx: RNCallingxType | undefined; diff --git a/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts b/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts index 4e4aa1fbed..fba4c1cf2a 100644 --- a/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts +++ b/packages/react-native-sdk/src/utils/push/setupCallingExpEvents.ts @@ -6,8 +6,7 @@ import { processCallFromPushInBackground, } from './internal/utils'; import { setPushLogoutCallback } from '../internal/pushLogoutCallback'; - -import { getCallingxLib, getCallingxLibIfAvailable } from './libs/callingx'; +import { getCallingxLib, type EventParams } from './libs/callingx'; type PushConfig = NonNullable; @@ -64,16 +63,16 @@ export function setupCallingExpEvents(pushConfig: NonNullable) { }); } -const onAcceptCall = ({ callId: call_cid }: { callId: string }) => { +const onAcceptCall = ({ + callId: call_cid, + source, +}: EventParams['answerCall']) => { videoLoggerSystem .getLogger('callingExpAcceptCall') - .debug(`callingExpAcceptCall event callId: ${call_cid}`); + .debug(`callingExpAcceptCall event callId: ${call_cid} source: ${source}`); - if (!call_cid) { - getCallingxLibIfAvailable()?.log( - `call_cid is undefined, so returning early`, - 'debug', - ); + if (source === 'app' || !call_cid) { + //we only need to process the call if the call was answered from the system return; } @@ -84,16 +83,15 @@ const onAcceptCall = ({ callId: call_cid }: { callId: string }) => { const onEndCall = (pushConfig: PushConfig) => - async ({ callId: call_cid }: { callId: string }) => { + async ({ callId: call_cid, source }: EventParams['endCall']) => { videoLoggerSystem .getLogger('callingExpRejectCall') - .debug(`callingExpRejectCall event callId: ${call_cid}`); - - if (!call_cid) { - getCallingxLibIfAvailable()?.log( - `call_cid is undefined, so returning early`, - 'debug', + .debug( + `callingExpRejectCall event callId: ${call_cid} source: ${source}`, ); + + if (source === 'app' || !call_cid) { + //we only need to process the call if the call was rejected from the system return; } diff --git a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts index 1f30e828ee..7d0c2524f4 100644 --- a/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts +++ b/sample-apps/react-native/dogfood/src/utils/setPushConfig.ts @@ -85,25 +85,7 @@ export function setPushConfig() { }); StreamVideoRN.setupCallingExp({ - ios: { - appName: 'Dogfood app', - }, - android: { - incomingChannel: { - id: 'stream_incoming_call_channel_update2', - name: 'Incoming call notifications', - }, - outgoingChannel: { - id: 'stream_outgoing_call_channel_update2', - name: 'Outgoing call notifications', - }, - titleTransformer: (callerName: string, incoming: boolean) => - incoming - ? `Incoming call from ${callerName}` - : `Outgoing call to ${callerName}`, - subtitleTransformer: (callId: string) => - `Tap to open the call: ${callId}`, - }, + enableOutcomingCalls: true, }); setFirebaseListeners(); From 6b05cc1ddda37b6bc39d7855ec3b1b2a98d1d57e Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Fri, 12 Dec 2025 09:11:18 +0100 Subject: [PATCH 23/27] feat: added registered call check --- .../src/main/java/com/callingx/CallService.kt | 5 +++++ .../main/java/com/callingx/CallingxModule.kt | 11 +++++++++-- packages/react-native-callingx/ios/Callingx.mm | 17 +++++++++++++++++ .../react-native-callingx/ios/UUIDStorage.h | 1 + .../react-native-callingx/ios/UUIDStorage.mm | 8 ++++++++ .../react-native-callingx/src/CallingxModule.ts | 4 ++++ .../src/spec/NativeCallingx.ts | 2 ++ packages/react-native-callingx/src/types.ts | 2 ++ .../react-native-sdk/src/utils/push/android.ts | 4 ++-- .../src/utils/push/internal/ios.ts | 6 ++---- 10 files changed, 52 insertions(+), 8 deletions(-) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt index 0f4221c912..641cd9ae00 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt @@ -270,6 +270,11 @@ class CallService : Service(), CallRepository.Listener { return currentCall is Call.Registered && currentCall.id == callId } + public fun hasRegisteredCall(): Boolean { + val currentCall = callRepository.currentCall.value + return currentCall is Call.Registered + } + public fun processAction(action: CallAction) { Log.d(TAG, "[service] processAction: Processing action: ${action::class.simpleName}") val currentCall = callRepository.currentCall.value diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt index 72499719ff..cff64b2d61 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallingxModule.kt @@ -254,8 +254,15 @@ class CallingxModule(reactContext: ReactApplicationContext) : NativeCallingxSpec } override fun isCallRegistered(callId: String): Boolean { - Log.d(TAG, "[module] isCallRegistered: Checking if call is registered: $callId") - return callService?.isCallRegistered(callId) ?: false + val isCallRegistered = callService?.isCallRegistered(callId) ?: false + Log.d(TAG, "[module] isCallRegistered: Is call registered: $isCallRegistered") + return isCallRegistered + } + + override fun hasRegisteredCall(): Boolean { + val hasRegisteredCall = callService?.hasRegisteredCall() ?: false + Log.d(TAG, "[module] hasRegisteredCall: Has registered call: $hasRegisteredCall") + return hasRegisteredCall } override fun setMutedCall(callId: String, isMuted: Boolean, promise: Promise) { diff --git a/packages/react-native-callingx/ios/Callingx.mm b/packages/react-native-callingx/ios/Callingx.mm index 5178e8e2d5..05b1a695da 100644 --- a/packages/react-native-callingx/ios/Callingx.mm +++ b/packages/react-native-callingx/ios/Callingx.mm @@ -606,6 +606,23 @@ - (NSNumber *)isCallRegistered:(nonnull NSString *)callId { return @NO; } +- (NSNumber *)hasRegisteredCall { + NSArray *appUUIDs = [uuidStorage allUUIDs]; + if ([appUUIDs count] == 0) return @NO; + + CXCallObserver *observer = [[CXCallObserver alloc] init]; + + for (CXCall *call in observer.calls) { + for (NSUUID *uuid in appUUIDs) { + if ([call.UUID isEqual:uuid]) { + return @YES; + } + } + } + + return @NO; +} + - (void)setCurrentCallActive:(nonnull NSString *)callId resolve:(nonnull RCTPromiseResolveBlock)resolve reject:(nonnull RCTPromiseRejectBlock)reject { diff --git a/packages/react-native-callingx/ios/UUIDStorage.h b/packages/react-native-callingx/ios/UUIDStorage.h index 627ef83a5f..7b128a08f2 100644 --- a/packages/react-native-callingx/ios/UUIDStorage.h +++ b/packages/react-native-callingx/ios/UUIDStorage.h @@ -3,6 +3,7 @@ @interface UUIDStorage : NSObject - (instancetype)init; +- (NSArray *)allUUIDs; - (NSUUID *)getOrCreateUUIDForCid:(NSString *)cid; - (NSUUID *)getUUIDForCid:(NSString *)cid; - (NSString *)getCidForUUID:(NSUUID *)uuid; diff --git a/packages/react-native-callingx/ios/UUIDStorage.mm b/packages/react-native-callingx/ios/UUIDStorage.mm index 0f066ed509..cb96d6c35f 100644 --- a/packages/react-native-callingx/ios/UUIDStorage.mm +++ b/packages/react-native-callingx/ios/UUIDStorage.mm @@ -16,6 +16,14 @@ - (instancetype)init { return self; } +- (NSArray *)allUUIDs { + NSMutableArray *uuids = [NSMutableArray array]; + for (NSString *uuidString in self.uuidDict.allValues) { + [uuids addObject:[[NSUUID alloc] initWithUUIDString:uuidString.lowercaseString]]; + } + return uuids; +} + - (NSUUID *)getOrCreateUUIDForCid:(NSString *)cid { if ([self containsCid:cid]) { NSString *existingUUID = self.uuidDict[cid]; diff --git a/packages/react-native-callingx/src/CallingxModule.ts b/packages/react-native-callingx/src/CallingxModule.ts index cb3cfb26cd..e33b0fe27d 100644 --- a/packages/react-native-callingx/src/CallingxModule.ts +++ b/packages/react-native-callingx/src/CallingxModule.ts @@ -205,6 +205,10 @@ class CallingxModule implements ICallingxModule { return NativeCallingModule.isCallRegistered(callId); } + hasRegisteredCall(): boolean { + return NativeCallingModule.hasRegisteredCall(); + } + setMutedCall(callId: string, isMuted: boolean): Promise { return NativeCallingModule.setMutedCall(callId, isMuted); } diff --git a/packages/react-native-callingx/src/spec/NativeCallingx.ts b/packages/react-native-callingx/src/spec/NativeCallingx.ts index 89d2afa722..d39af3cef8 100644 --- a/packages/react-native-callingx/src/spec/NativeCallingx.ts +++ b/packages/react-native-callingx/src/spec/NativeCallingx.ts @@ -82,6 +82,8 @@ export interface Spec extends TurboModule { isCallRegistered(callId: string): boolean; + hasRegisteredCall(): boolean; + endCallWithReason(callId: string, reason: number): Promise; endCall(callId: string): Promise; diff --git a/packages/react-native-callingx/src/types.ts b/packages/react-native-callingx/src/types.ts index 8245cd1c55..14008739ef 100644 --- a/packages/react-native-callingx/src/types.ts +++ b/packages/react-native-callingx/src/types.ts @@ -43,6 +43,8 @@ export interface ICallingxModule { isCallRegistered(callId: string): boolean; + hasRegisteredCall(): boolean; + endCallWithReason(callId: string, reason: EndCallReason): Promise; setMutedCall(callId: string, isMuted: boolean): Promise; diff --git a/packages/react-native-sdk/src/utils/push/android.ts b/packages/react-native-sdk/src/utils/push/android.ts index 2090036396..3333028325 100644 --- a/packages/react-native-sdk/src/utils/push/android.ts +++ b/packages/react-native-sdk/src/utils/push/android.ts @@ -149,9 +149,9 @@ export const firebaseDataHandler = async ( const callingx = getCallingxLib(); - if (callingx.isCallRegistered(call_cid)) { + if (callingx.hasRegisteredCall()) { logger.debug( - `call.ring notification already processed, skipping the call.ring notification`, + `registered call found, skipping the call.ring notification`, ); return; } diff --git a/packages/react-native-sdk/src/utils/push/internal/ios.ts b/packages/react-native-sdk/src/utils/push/internal/ios.ts index fa27f500d0..fdf69d4f13 100644 --- a/packages/react-native-sdk/src/utils/push/internal/ios.ts +++ b/packages/react-native-sdk/src/utils/push/internal/ios.ts @@ -46,10 +46,8 @@ export const onVoipNotificationReceived = async ( } const callingx = getCallingxLib(); - if (callingx.isCallRegistered(call_cid)) { - logger.debug( - `call.ring notification already processed, skipping the call.ring notification`, - ); + if (callingx.hasRegisteredCall()) { + logger.debug(`registered call found, skipping the call.ring notification`); return; } From 2d5fbc2a54ff266205a75ef249d894f94aec4ec3 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Fri, 12 Dec 2025 12:54:26 +0100 Subject: [PATCH 24/27] chore: prevent service crash in case of multiple simultaneous calls --- .../src/main/java/com/callingx/CallService.kt | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt index 641cd9ae00..caf82c1ac3 100644 --- a/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt +++ b/packages/react-native-callingx/android/src/main/java/com/callingx/CallService.kt @@ -122,7 +122,9 @@ class CallService : Service(), CallRepository.Listener { ACTION_START_BACKGROUND_TASK -> { if (!isInForeground) { Log.d(TAG, "[service] onStartCommand: Starting foreground for background task") - //for now bg task is intended to be used after a call registered and notification is shown, so we don't need to show a separate notification for bg task + // for now bg task is intended to be used after a call registered and + // notification is shown, so we don't need to show a separate notification for + // bg task // startForeground(CallNotificationManager.NOTIFICATION_ID, notification) isInForeground = true } @@ -218,7 +220,11 @@ class CallService : Service(), CallRepository.Listener { } } - override fun onIsCallDisconnected(callId: String?, cause: DisconnectCause, source: CallRepository.EventSource) { + override fun onIsCallDisconnected( + callId: String?, + cause: DisconnectCause, + source: CallRepository.EventSource + ) { // we're not passing the callId here to prevent infinite loops // callEnd event with callId will sent only when after interaction with notification buttons sendBroadcastEvent(CallingxModule.CALL_END_ACTION) { @@ -321,14 +327,18 @@ class CallService : Service(), CallRepository.Listener { Log.d(TAG, "[service] registerCall: Call details - Name: $name, URI: $uri") scope.launch { - callRepository.registerCall( - callId, - name, - uri, - incoming, - isVideo, - displayOptions, - ) + try { + callRepository.registerCall( + callId, + name, + uri, + incoming, + isVideo, + displayOptions, + ) + } catch (e: Exception) { + Log.e(TAG, "[service] registerCall: Error registering call: ${e.message}") + } } } From c3cb7553dadb632b6fcc5c61257606ed8f1388ec Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri Date: Fri, 12 Dec 2025 13:26:27 +0100 Subject: [PATCH 25/27] removed yarnrc --- packages/react-native-callingx/.yarnrc.yml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 packages/react-native-callingx/.yarnrc.yml diff --git a/packages/react-native-callingx/.yarnrc.yml b/packages/react-native-callingx/.yarnrc.yml deleted file mode 100644 index a95225543b..0000000000 --- a/packages/react-native-callingx/.yarnrc.yml +++ /dev/null @@ -1,4 +0,0 @@ -nodeLinker: node-modules -nmHoistingLimits: workspaces - -yarnPath: .yarn/releases/yarn-4.11.0.cjs From 21c34ffc4f5181621290525081d9bf44ea840ce1 Mon Sep 17 00:00:00 2001 From: Artem Grintsevich Date: Fri, 12 Dec 2025 13:26:34 +0100 Subject: [PATCH 26/27] fix: removed local yarnrc --- packages/react-native-callingx/.yarnrc.yml | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 packages/react-native-callingx/.yarnrc.yml diff --git a/packages/react-native-callingx/.yarnrc.yml b/packages/react-native-callingx/.yarnrc.yml deleted file mode 100644 index a95225543b..0000000000 --- a/packages/react-native-callingx/.yarnrc.yml +++ /dev/null @@ -1,4 +0,0 @@ -nodeLinker: node-modules -nmHoistingLimits: workspaces - -yarnPath: .yarn/releases/yarn-4.11.0.cjs From f21c2429a66689c047fe2281e94e535355bbd012 Mon Sep 17 00:00:00 2001 From: Santhosh Vaiyapuri Date: Fri, 12 Dec 2025 13:29:04 +0100 Subject: [PATCH 27/27] add to rn deps --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f997a45d59..fc53e721cb 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "build:video-filters-react-native": "yarn workspace @stream-io/video-filters-react-native run build", "build:noise-cancellation-react-native": "yarn workspace @stream-io/noise-cancellation-react-native run build", "build:react:deps": "yarn build:client && yarn build:styling && yarn build:react:bindings && yarn build:video-filters-web && yarn build:audio-filters-web && yarn build:react:sdk", - "build:react-native:deps": "yarn build:client && yarn build:react:bindings && yarn build:video-filters-react-native && yarn build:noise-cancellation-react-native && yarn build:react-native:sdk", + "build:react-native:deps": "yarn build:client && yarn build:react:bindings && yarn build:video-filters-react-native && yarn build:noise-cancellation-react-native && yarn build:callingx && yarn build:react-native:sdk", "build:vercel": "yarn build:react:deps && yarn build:react:dogfood", "start:egress": "yarn workspace @stream-io/egress-composite start", "build:egress": "yarn workspace @stream-io/egress-composite build",