├── .circleci └── config.yml ├── .editorconfig ├── .gitattributes ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .release.json ├── .yarnrc ├── CHANGELOG.md ├── COMMON_ISSUES.md ├── CONTRIBUTING.md ├── KNOWN_ISSUES.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── android ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── twiliovoicereactnative │ │ ├── AudioSwitchManager.java │ │ ├── CallListenerProxy.java │ │ ├── CallMessageListenerProxy.java │ │ ├── CallRecordDatabase.java │ │ ├── ConfigurationProperties.java │ │ ├── Constants.java │ │ ├── JSEventEmitter.java │ │ ├── MediaPlayerManager.java │ │ ├── NotificationUtility.java │ │ ├── ReactNativeArgumentsSerializer.java │ │ ├── SDKLog.java │ │ ├── StatsListenerProxy.java │ │ ├── TwilioVoiceReactNativeModule.java │ │ ├── TwilioVoiceReactNativePackage.java │ │ ├── VoiceActivityProxy.java │ │ ├── VoiceApplicationProxy.java │ │ ├── VoiceFirebaseMessagingService.java │ │ └── VoiceService.java │ └── res │ ├── drawable │ ├── answered_call_small_icon.png │ ├── ic_launcher_round.png │ ├── ic_launcher_sdk.png │ ├── incoming_call_small_icon.png │ └── outgoing_call_small_icon.png │ ├── raw │ ├── disconnect.wav │ ├── incoming.wav │ ├── outgoing.wav │ ├── ringtone.wav │ └── silent.wav │ ├── values-night │ └── colors.xml │ └── values │ ├── colors.xml │ ├── config.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── api-extractor.json ├── api └── voice-react-native-sdk.api.md ├── babel.config.js ├── constants ├── constants.java.template ├── constants.objc.template ├── constants.src └── constants.typescript.template ├── docs ├── applications-own-pushkit-handler.md ├── customize-notifications.md ├── disable-full-screen-notifications.md ├── getting-started-android-java.md ├── getting-started-android-kotlin.md ├── getting-started-ios.md ├── migration-guide-beta.4.md ├── out-of-band-firebase-messaging-service.md └── play-outgoing-call-ringback-tone.md ├── ios ├── TwilioVoicePushRegistry.h ├── TwilioVoicePushRegistry.m ├── TwilioVoiceReactNative+CallInvite.m ├── TwilioVoiceReactNative+CallKit.m ├── TwilioVoiceReactNative+CallMessage.m ├── TwilioVoiceReactNative.h ├── TwilioVoiceReactNative.m ├── TwilioVoiceReactNative.xcodeproj │ └── project.pbxproj └── Utilities │ └── TwilioVoiceStatsReport.h ├── package.json ├── scripts ├── bootstrap.js ├── errors.js ├── generate-constants.js ├── generate-errors.js └── substitute-constants-version.js ├── src ├── AudioDevice.tsx ├── Call.tsx ├── CallInvite.tsx ├── CallMessage │ ├── CallMessage.ts │ ├── IncomingCallMessage.ts │ └── OutgoingCallMessage.ts ├── Voice.tsx ├── __mocks__ │ ├── AudioDevice.ts │ ├── Call.ts │ ├── CallInvite.ts │ ├── CallMessage.ts │ ├── Error.ts │ ├── RTCStats.ts │ ├── Voice.ts │ └── common.ts ├── __tests__ │ ├── AudioDevice.test.ts │ ├── Call.test.ts │ ├── CallInvite.test.ts │ ├── CallMessage │ │ ├── CallMessage.test.ts │ │ ├── IncomingCallMessage.test.ts │ │ └── OutgoingCallMessage.test.ts │ ├── RTCStats.test.ts │ ├── Voice.test.ts │ ├── constants.test.ts │ └── error │ │ ├── generated.test.ts │ │ ├── index.test.ts │ │ └── utility.test.ts ├── common.ts ├── error │ ├── InvalidArgumentError.ts │ ├── InvalidStateError.ts │ ├── TwilioError.ts │ ├── UnsupportedPlatformError.ts │ ├── generated.ts │ ├── index.ts │ └── utility.ts ├── index.tsx └── type │ ├── AudioDevice.ts │ ├── Call.ts │ ├── CallInvite.ts │ ├── CallKit.ts │ ├── CallMessage.ts │ ├── Error.ts │ ├── NativeModule.ts │ ├── RTCStats.ts │ ├── Voice.ts │ └── common.ts ├── test ├── app │ ├── .bundle │ │ └── config │ ├── .detoxrc.js │ ├── .eslintrc.js │ ├── .gitignore │ ├── .prettierrc.js │ ├── .watchmanconfig │ ├── Gemfile │ ├── README.md │ ├── android │ │ ├── app │ │ │ ├── build.gradle │ │ │ ├── debug.keystore │ │ │ ├── proguard-rules.pro │ │ │ └── src │ │ │ │ ├── androidTest │ │ │ │ └── java │ │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── twiliovoicereactnative │ │ │ │ │ └── DetoxTest.java │ │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ │ └── main │ │ │ │ ├── AndroidManifest.xml │ │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── twiliovoicereactnative │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ └── MainApplication.kt │ │ │ │ └── res │ │ │ │ ├── drawable │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ └── ic_launcher_round.png │ │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ └── styles.xml │ │ │ │ └── xml │ │ │ │ └── network_security_config.xml │ │ ├── build.gradle │ │ ├── gradle.properties │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ ├── gradle-wrapper.jar │ │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ └── settings.gradle │ ├── app.json │ ├── babel.config.js │ ├── e2e │ │ ├── common │ │ │ ├── logParser.ts │ │ │ ├── rtcStatsValidators.ts │ │ │ └── twilioClient.ts │ │ ├── jest.config.js │ │ ├── relay │ │ │ └── server.js │ │ └── suites │ │ │ ├── call.test.ts │ │ │ ├── callMessage.test.ts │ │ │ ├── registration.test.ts │ │ │ └── voice.test.ts │ ├── index.js │ ├── ios │ │ ├── .xcode.env │ │ ├── Podfile │ │ ├── Podfile.lock │ │ ├── TwilioVoiceExampleNewArch.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── TwilioVoiceExampleNewArch.xcscheme │ │ └── TwilioVoiceExampleNewArch │ │ │ ├── AppDelegate.swift │ │ │ ├── Images.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ │ ├── Info.plist │ │ │ ├── LaunchScreen.storyboard │ │ │ ├── PrivacyInfo.xcprivacy │ │ │ └── TwilioVoiceExampleNewArch.entitlements │ ├── jest.config.js │ ├── metro.config.js │ ├── package.json │ ├── src │ │ ├── App.tsx │ │ ├── Dialer.tsx │ │ ├── Grid.tsx │ │ ├── components │ │ │ └── CallMessage.tsx │ │ ├── hook.ts │ │ ├── tokenUtility.ts │ │ └── type.ts │ └── yarn.lock └── scripts │ ├── common.mjs │ └── gen-token.mjs ├── tsconfig.build.json ├── tsconfig.json ├── twilio-voice-react-native.podspec └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Issue 2 | 3 | ## Pre-submission Checklist 4 | - [ ] I have verified that the issue occurs with the latest release and is not marked as a known issue in the [CHANGELOG.md](https://github.com/twilio/twilio-voice-react-native/blob/main/CHANGELOG.md). 5 | - [ ] I reviewed the [Common Issues](https://github.com/twilio/twilio-voice-react-native/blob/main/COMMON_ISSUES.md) and open GitHub issues and verified that this report represents a potentially new issue. 6 | - [ ] I am not sharing any [Personally Identifiable Information (PII)](https://www.twilio.com/docs/glossary/what-is-personally-identifiable-information-pii) 7 | or sensitive account information (API keys, credentials, etc.) when reporting this issue. 8 | 9 | ## Description 10 | A clear and concise description of what the issue is. 11 | 12 | ## Reproduction Steps 13 | 1. Go to '...' 14 | 2. Click on '....' 15 | 3. Scroll down to '....' 16 | 4. See error 17 | 18 | ## Expected Behavior 19 | A clear and concise description of what you expected to happen. 20 | 21 | ## Actual Behavior 22 | What actually happens. 23 | 24 | ## Reproduction Frequency 25 | Is the reproducibility of the issue deterministic? If not, what percentage of the time does the issue occur? In how many attempts was the issue observed? 26 | 27 | ## Screenshots 28 | If applicable, add screenshots to help explain your problem. 29 | 30 | ## Software and Device Information 31 | **Please complete the following information.** 32 | - Device: [e.g. iPhone 12, Pixel 3] 33 | - OS: [e.g. iOS 16, Android API Level 31] 34 | - React version: [e.g. 18.1.0] 35 | - React Native version: [e.g. 0.70.9] 36 | - Node version: [e.g. 16.18.1] 37 | - npm or yarn version: [e.g. 8.19.2, 1.22.19] 38 | 39 | ## Additional Context 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | 32 | # Android/IJ 33 | # 34 | .idea 35 | .gradle 36 | local.properties 37 | *.iml 38 | test/app/android/app/google-services.json 39 | *.hprof 40 | 41 | # Cocoapods 42 | # 43 | test/app/ios/Pods 44 | 45 | # node.js 46 | # 47 | test/app/node_modules/ 48 | node_modules/ 49 | npm-debug.log 50 | yarn-debug.log 51 | yarn-error.log 52 | 53 | # BUCK 54 | buck-out/ 55 | \.buckd/ 56 | android/app/libs 57 | android/keystores/debug.keystore 58 | 59 | # Expo 60 | .expo/* 61 | 62 | # generated by bob 63 | lib/ 64 | 65 | ## 66 | # generated by Twilio 67 | ## 68 | 69 | # constants 70 | src/constants.ts 71 | android/src/main/java/com/twiliovoicereactnative/CommonConstants.java 72 | ios/TwilioVoiceReactNativeConstants.h 73 | 74 | # docs 75 | docs/api/ 76 | temp/ 77 | 78 | # e2e tests 79 | test/app/src/e2e-tests-token.ts 80 | test/app/src/e2e-tests-token.android.ts 81 | test/app/src/e2e-tests-token.ios.ts 82 | coverage/ 83 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .circleci 2 | .DS_STORE 3 | .github 4 | .editorconfig 5 | .gitattributes 6 | .gitignore 7 | .release.json 8 | .vscode 9 | api 10 | api-extractor.json 11 | babel.config.js 12 | constants 13 | coverage 14 | docs/api 15 | node_modules 16 | temp 17 | test/app 18 | scripts 19 | src 20 | PULL_REQUEST_TEMPLATE.md 21 | -------------------------------------------------------------------------------- /.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "JavaScript", 3 | "ci": "circleci", 4 | "slug": "twilio/twilio-voice-react-native", 5 | "env": { 6 | "GH_REF": "github.com/twilio/twilio-voice-react-native.git" 7 | }, 8 | "plans": { 9 | "release": { 10 | "env": { 11 | "GIT_USER_NAME": "twilio-vblocks-ci", 12 | "GIT_USER_EMAIL": "svc.vblocks-ci@twilio.com" 13 | }, 14 | "commands": [ 15 | "git config user.name \"${GIT_USER_NAME}\"", 16 | "git config user.email \"${GIT_USER_EMAIL}\"", 17 | "git remote set-url origin \"https://${GH_TOKEN}@${GH_REF}\"", 18 | "node ./node_modules/twilio-release-tool/release --bump ${CURRENT_VERSION} ${RELEASE_VERSION}", 19 | "node ./scripts/substitute-constants-version.js ${RELEASE_VERSION}", 20 | "git add package.json", 21 | "git add constants/constants.src", 22 | "yarn run build:docs", 23 | "git add -f docs/api lib", 24 | "git add -f src/constants.ts", 25 | "git add -f ios/TwilioVoiceReactNativeConstants.h", 26 | "git add -f android/src/main/java/com/twiliovoicereactnative/CommonConstants.java", 27 | "git commit -nm \"${RELEASE_VERSION}\"", 28 | "git tag ${RELEASE_VERSION}", 29 | "git tag -d latest", 30 | "git tag latest ${RELEASE_VERSION}^{commit}", 31 | "git rebase HEAD ${BRANCH}", 32 | "git push origin ${BRANCH} --tags --force &> /dev/null && echo \"Push to origin successful\" || (echo \"Push to origin failed\" 1>&2 && exit 1)" 33 | ] 34 | }, 35 | "development": { 36 | "commands": [ 37 | "git config user.name \"${GIT_USER_NAME}\"", 38 | "git config user.email \"${GIT_USER_EMAIL}\"", 39 | "git remote set-url origin \"https://${GH_TOKEN}@${GH_REF}\"", 40 | "node ./node_modules/twilio-release-tool/release --bump ${RELEASE_VERSION} ${DEVELOPMENT_VERSION}", 41 | "node ./scripts/substitute-constants-version.js ${DEVELOPMENT_VERSION}", 42 | "git add package.json", 43 | "git add constants/constants.src", 44 | "git rm -r docs/api lib", 45 | "git rm src/constants.ts", 46 | "git rm ios/TwilioVoiceReactNativeConstants.h", 47 | "git rm android/src/main/java/com/twiliovoicereactnative/CommonConstants.java", 48 | "git commit -nm \"${DEVELOPMENT_VERSION}\"", 49 | "git rebase HEAD ${BRANCH}", 50 | "git push origin ${BRANCH} &> /dev/null && echo \"Push to origin successful\" || (echo \"Push to origin failed\" 1>&2 && exit 1)" 51 | ] 52 | }, 53 | "publish": { 54 | "commands": [ 55 | "git checkout ${RELEASE_VERSION}", 56 | "echo \"//registry.npmjs.org/:_authToken=${NPM_AUTH_TOKEN}\" >~/.npmrc", 57 | "npm publish" 58 | ] 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | # Override Yarn command so we can automatically setup the repo on running `yarn` 2 | 3 | yarn-path "scripts/bootstrap.js" 4 | -------------------------------------------------------------------------------- /COMMON_ISSUES.md: -------------------------------------------------------------------------------- 1 | # Common Issues 2 | 3 | * Expo is not supported out of the box. Follow [our guide](https://github.com/twilio/twilio-voice-react-native/issues/496) to add support for your app. 4 | 5 | * Running the example app on Android using `yarn run android` may fail if the emulator is still starting up. When this happens, you can re-run the app once the emulator is fully started. 6 | 7 | * Please note that the Twilio Voice React Native SDK is tightly integrated with the iOS CallKit framework. This provides the best call and audio experience, and requires the application to be run on a physical device. The SDK will not work on an iOS simulator. 8 | -------------------------------------------------------------------------------- /KNOWN_ISSUES.md: -------------------------------------------------------------------------------- 1 | # Known Issues 2 | 3 | ## Using an out-of-band Firebase Messaging service 4 | If another service in your application declares itself as a Firebase Messaging 5 | service, its functionality is likely to break, or the Twilio Voice RN SDK will 6 | break. Please see this 7 | [document](/docs/out-of-band-firebase-messaging-service.md) for details on how 8 | to resolve this. 9 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Submission Checklist 2 | 3 | - [ ] Updated the `CHANGELOG.md` to reflect any **feature**, **bug fixes**, or **known issues** made in the source code 4 | - [ ] Tested code changes and observed expected behavior in the example app 5 | - [ ] Performed a visual inspection of the `Files changed` tab prior to submitting the pull request for review to ensure proper usage of the style guide 6 | 7 | > All third-party contributors acknowledge that any contributions they provide will be made under the same open-source license that the open-source project is provided under. 8 | 9 | - [ ] I acknowledge that all my contributions will be made under the project's license. 10 | 11 | ## Description 12 | 13 | [Description of the Pull Request] 14 | 15 | ## Breakdown 16 | 17 | - [Bulleted summary of changes] 18 | - [eg. Updated documentation] 19 | - [eg. Added audio device feature] 20 | 21 | ## Validation 22 | 23 | - [Bulleted summary of validation steps] 24 | - [eg. Added new test] 25 | - [eg. Manually tested with app] 26 | 27 | ## Additional Notes 28 | 29 | [Any additional comments, notes, or information relevant to reviewers.] 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twilio Voice React Native SDK 2 | [![NPM](https://img.shields.io/npm/v/%40twilio/voice-react-native-sdk.svg?color=blue)](https://www.npmjs.com/package/%40twilio/voice-react-native-sdk) [![CircleCI](https://dl.circleci.com/status-badge/img/gh/twilio/twilio-voice-react-native/tree/main.svg?style=shield)](https://dl.circleci.com/status-badge/redirect/gh/twilio/twilio-voice-react-native/tree/main) 3 | 4 | Twilio's Voice React Native SDK allows you to add real-time voice and PSTN calling to your React Native apps. 5 | 6 | - [Documentation](https://www.twilio.com/docs/voice/sdks/react-native) 7 | - [API Reference](https://github.com/twilio/twilio-voice-react-native/blob/latest/docs/api/voice-react-native-sdk.md) 8 | - [Reference App](https://github.com/twilio/twilio-voice-react-native-app) 9 | 10 | Please check out the following if you are new to Twilio's Programmable Voice or React Native. 11 | 12 | - [Programmable Voice](https://www.twilio.com/docs/voice/sdks) 13 | - [React Native](https://reactnative.dev/docs/getting-started) 14 | 15 | ## Installation 16 | The package is available through [npm](https://www.npmjs.com/package/@twilio/voice-react-native-sdk). 17 | 18 | ```sh 19 | yarn add @twilio/voice-react-native-sdk 20 | ``` 21 | 22 | Once the package has been installed to your React Native application, there are further steps that you will need to take for both iOS and Android platforms. Please see the supporting documentation below. 23 | 24 | ## Supporting Documentation 25 | 26 | ### Getting Started 27 | 28 | #### iOS 29 | Learn how to get started for the [iOS platform](/docs/getting-started-ios.md). 30 | 31 | #### Android 32 | Learn how to get started for the Android platform if you are using [Java](/docs/getting-started-android-java.md) or [Kotlin](/docs/getting-started-android-kotlin.md). 33 | 34 | ### Migration Guide 35 | If you are migrating from a version of the Twilio Voice React Native SDK `< 1.0.0.beta.4` to a version `>= 1.0.0.beta.4`, please see [this](/docs/migration-guide-beta.4.md) document. 36 | 37 | ### Customizing Notifications 38 | To customize the appearance and content of your application's notifications, please see [this](/docs/customize-notifications.md) document. 39 | 40 | ### Outgoing Call Ringback Tone 41 | To enable your application to play a ringback tone while making an outgoing call, please see [this](/docs/play-outgoing-call-ringback-tone.md) document. 42 | 43 | ### Out-of-band PushKit Handling 44 | To have your application implement or use its own `PushKit` delegate module, please see [this](/docs/applications-own-pushkit-handler.md) document. 45 | 46 | ### Out-of-band Firebase Messaging Service 47 | To have your application implement or use a different `FirebaseMessagingService` (such as OneSignal or RNFirebase), please see [this](/docs/out-of-band-firebase-messaging-service.md) document. 48 | 49 | ## Issues and Support 50 | Please check out our [common issues](/COMMON_ISSUES.md) page or file any issues you find here on Github. For general inquiries related to the Voice SDK you can file a support ticket. 51 | 52 | Please ensure that you are not sharing any [Personally Identifiable Information(PII)](https://www.twilio.com/docs/glossary/what-is-personally-identifiable-information-pii) or sensitive account information (API keys, credentials, etc.) when reporting an issue. 53 | 54 | Please check out our [known issues](/KNOWN_ISSUES.md) for known bugs and workarounds. 55 | 56 | ## Related 57 | - [Reference App](https://github.com/twilio/twilio-voice-react-native-app) 58 | - [Twilio Voice JS](https://github.com/twilio/twilio-voice.js) 59 | - [Twilio Voice iOS](https://github.com/twilio/voice-quickstart-ios) 60 | - [Twilio Voice Android](https://github.com/twilio/voice-quickstart-android) 61 | 62 | ## License 63 | See [LICENSE](/LICENSE) 64 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.versions = [ 3 | 'java' : JavaVersion.VERSION_11, 4 | 'androidGradlePlugin': '7.4.2', 5 | 'googleServices' : '4.3.10', 6 | 'voiceAndroid' : '6.7.1', 7 | 'androidxCore' : '1.12.0', 8 | 'androidxLifecycle' : '2.2.0', 9 | 'audioSwitch' : '1.1.8', 10 | 'firebaseMessaging' : '23.4.0' 11 | ] 12 | if (project == rootProject) { 13 | repositories { 14 | google() 15 | mavenCentral() 16 | } 17 | 18 | dependencies { 19 | classpath "com.android.tools.build:gradle:${versions.androidGradlePlugin}" 20 | classpath "com.google.gms:google-services:${versions.googleServices}" 21 | } 22 | } 23 | } 24 | 25 | apply plugin: 'com.android.library' 26 | 27 | def safeExtGet(prop, fallback) { 28 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 29 | } 30 | 31 | android { 32 | namespace 'com.twiliovoicereactnative' 33 | compileSdk safeExtGet('TwilioVoiceReactNative_compileSdkVersion', 34) 34 | defaultConfig { 35 | minSdkVersion safeExtGet('TwilioVoiceReactNative_minSdkVersion', 24) 36 | targetSdkVersion safeExtGet('TwilioVoiceReactNative_targetSdkVersion', 34) 37 | versionCode 1 38 | versionName "1.0" 39 | } 40 | 41 | buildTypes { 42 | release { 43 | minifyEnabled false 44 | } 45 | } 46 | lintOptions { 47 | disable 'GradleCompatible' 48 | } 49 | compileOptions { 50 | sourceCompatibility versions.java 51 | targetCompatibility versions.java 52 | } 53 | buildFeatures { 54 | // for buildconfig construction 55 | buildConfig true 56 | } 57 | } 58 | 59 | repositories { 60 | google() 61 | mavenCentral() 62 | maven { 63 | url 'https://maven.google.com/' 64 | name 'Google' 65 | } 66 | } 67 | 68 | dependencies { 69 | //noinspection GradleDynamicVersion 70 | implementation "com.facebook.react:react-native:+" // From node_modules 71 | implementation "com.twilio:voice-android:${versions.voiceAndroid}" 72 | implementation "androidx.core:core:${versions.androidxCore}" 73 | implementation "androidx.lifecycle:lifecycle-extensions:${versions.androidxLifecycle}" 74 | implementation "com.google.firebase:firebase-messaging:${versions.firebaseMessaging}" 75 | implementation "com.twilio:audioswitch:${versions.audioSwitch}" 76 | implementation 'com.google.android.material:material:1.1.0' 77 | 78 | constraints { 79 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") { 80 | because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib") 81 | } 82 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") { 83 | because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") 84 | } 85 | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.0") { 86 | because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib") 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | org.gradle.jvmargs=-Xmx4096m 20 | 21 | # AndroidX package structure to make it clearer which packages are bundled with the 22 | # Android operating system, and which are packaged with your app's APK 23 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 24 | android.useAndroidX=true 25 | # Automatically convert third-party libraries to use AndroidX 26 | android.enableJetifier=true 27 | 28 | # Version of flipper SDK to use with React Native 29 | FLIPPER_VERSION=0.99.0 30 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 19 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /android/src/main/java/com/twiliovoicereactnative/ConfigurationProperties.java: -------------------------------------------------------------------------------- 1 | package com.twiliovoicereactnative; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | class ConfigurationProperties { 7 | public static void setIncomingCallContactHandleTemplate(Context ctx, String template) { 8 | SharedPreferences sharedPreferences = ctx.getSharedPreferences(Constants.PREFERENCES_FILE, Context.MODE_PRIVATE); 9 | sharedPreferences 10 | .edit() 11 | .putString(Constants.INCOMING_CALL_CONTACT_HANDLE_TEMPLATE_PREFERENCES_KEY, template) 12 | .apply(); 13 | } 14 | 15 | public static String getIncomingCallContactHandleTemplate(Context ctx) { 16 | SharedPreferences sharedPreferences = ctx.getSharedPreferences(Constants.PREFERENCES_FILE, Context.MODE_PRIVATE); 17 | return sharedPreferences.getString(Constants.INCOMING_CALL_CONTACT_HANDLE_TEMPLATE_PREFERENCES_KEY, null); 18 | } 19 | 20 | /** 21 | * Get configuration boolean, used to determine if the built-in Firebase service should be enabled 22 | * or not. 23 | * @param context the application context 24 | * @return a boolean read from the application resources 25 | */ 26 | public static boolean isFirebaseServiceEnabled(Context context) { 27 | return context.getResources() 28 | .getBoolean(R.bool.twiliovoicereactnative_firebasemessagingservice_enabled); 29 | } 30 | 31 | /** 32 | * Get configuration boolean, used to determine if full screen notifications are enabled 33 | * or not. 34 | * @param context the application context 35 | * @return a boolean read from the application resources 36 | */ 37 | public static boolean isFullScreenNotificationEnabled(Context context) { 38 | return context.getResources() 39 | .getBoolean(R.bool.twiliovoicereactnative_fullscreennotification_enabled); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /android/src/main/java/com/twiliovoicereactnative/Constants.java: -------------------------------------------------------------------------------- 1 | package com.twiliovoicereactnative; 2 | 3 | class Constants { 4 | public static final String VOICE_CHANNEL_GROUP = "notification-group"; 5 | public static final String VOICE_CHANNEL_LOW_IMPORTANCE = "notification-channel-low-importance"; 6 | public static final String VOICE_CHANNEL_HIGH_IMPORTANCE = "notification-channel-high-importance"; 7 | public static final String VOICE_CHANNEL_DEFAULT_IMPORTANCE = "notification-channel-normal-importance"; 8 | public static final String ACTION_ACCEPT_CALL = "ACTION_ACCEPT_CALL"; 9 | public static final String ACTION_REJECT_CALL = "ACTION_REJECT_CALL"; 10 | public static final String ACTION_CANCEL_ACTIVE_CALL_NOTIFICATION = "ACTION_CANCEL_ACTIVE_CALL_NOTIFICATION"; 11 | public static final String ACTION_INCOMING_CALL = "ACTION_INCOMING_CALL"; 12 | public static final String ACTION_CANCEL_CALL = "ACTION_CANCEL_CALL"; 13 | public static final String ACTION_CALL_DISCONNECT = "ACTION_CALL_DISCONNECT"; 14 | public static final String ACTION_RAISE_OUTGOING_CALL_NOTIFICATION = "ACTION_RAISE_OUTGOING_CALL_NOTIFICATION"; 15 | public static final String ACTION_PUSH_APP_TO_FOREGROUND = "ACTION_PUSH_APP_TO_FOREGROUND"; 16 | public static final String ACTION_FOREGROUND_AND_DEPRIORITIZE_INCOMING_CALL_NOTIFICATION = "ACTION_FOREGROUND_AND_DEPRIORITIZE_INCOMING_CALL_NOTIFICATION"; 17 | public static final String MSG_KEY_UUID = "UUID"; 18 | public static final String JS_EVENT_KEY_CALL_INFO = "call"; 19 | public static final String JS_EVENT_KEY_CALL_INVITE_INFO = "callInvite"; 20 | public static final String JS_EVENT_KEY_CANCELLED_CALL_INVITE_INFO = "cancelledCallInvite"; 21 | public static final String PREFERENCES_FILE = "com.twilio.twiliovoicereactnative.preferences"; 22 | public static final String INCOMING_CALL_CONTACT_HANDLE_TEMPLATE_PREFERENCES_KEY = "incomingCallContactHandleTemplatePreferenceKey"; 23 | } 24 | -------------------------------------------------------------------------------- /android/src/main/java/com/twiliovoicereactnative/JSEventEmitter.java: -------------------------------------------------------------------------------- 1 | package com.twiliovoicereactnative; 2 | 3 | import android.util.Pair; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | 8 | import com.facebook.react.bridge.Arguments; 9 | import com.facebook.react.bridge.ReactApplicationContext; 10 | import com.facebook.react.bridge.ReadableArray; 11 | import com.facebook.react.bridge.ReadableMap; 12 | import com.facebook.react.bridge.WritableArray; 13 | import com.facebook.react.bridge.WritableMap; 14 | import com.facebook.react.modules.core.DeviceEventManagerModule; 15 | 16 | import java.lang.ref.WeakReference; 17 | 18 | class JSEventEmitter { 19 | private static final SDKLog logger = new SDKLog(JSEventEmitter.class); 20 | private WeakReference context = new WeakReference<>(null); 21 | 22 | public void setContext(ReactApplicationContext context) { 23 | this.context = new WeakReference<>(context); 24 | } 25 | public void sendEvent(String eventName, @Nullable WritableMap params) { 26 | logger.debug("sendEvent " + eventName + " params " + params); 27 | if ((null != context.get()) && 28 | context.get().hasActiveReactInstance()) { 29 | context.get() 30 | .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) 31 | .emit(eventName, params); 32 | } else { 33 | logger.warning( 34 | String.format( 35 | "attempt to sendEvent without context {%s} or Catalyst instance not active", 36 | context.get())); 37 | } 38 | } 39 | 40 | public static WritableArray constructJSArray(@NonNull Object...entries) { 41 | WritableArray params = Arguments.createArray(); 42 | for (Object entry: entries) { 43 | if ((entry instanceof String)) { 44 | params.pushString((String)entry); 45 | } else if (entry instanceof ReadableMap) { 46 | params.pushMap((ReadableMap) entry); 47 | } else if (entry instanceof ReadableArray) { 48 | params.pushArray((ReadableArray) entry); 49 | } else if (entry instanceof Boolean) { 50 | params.pushBoolean((Boolean) entry); 51 | } else if (entry instanceof Integer) { 52 | params.pushInt((Integer) entry); 53 | } else if (entry instanceof Float) { 54 | params.pushDouble((Float) entry); 55 | } else if (entry instanceof Double) { 56 | params.pushDouble((Double) entry); 57 | } else if (entry instanceof Long) { 58 | params.pushDouble((Long) entry); 59 | } else if (entry == null) { 60 | logger.debug("constructJSArray: filtering null value"); 61 | } else { 62 | logger.debug(String.format("constructJSArray: unexpected type %s", entry.getClass())); 63 | } 64 | } 65 | return params; 66 | } 67 | @SafeVarargs 68 | public static WritableMap constructJSMap(@NonNull Pair...entries) { 69 | WritableMap params = Arguments.createMap(); 70 | for (Pair entry: entries) { 71 | if ((entry.second instanceof String)) { 72 | params.putString(entry.first, (String) entry.second); 73 | } else if (entry.second instanceof ReadableMap) { 74 | params.putMap(entry.first, (ReadableMap) entry.second); 75 | } else if (entry.second instanceof ReadableArray) { 76 | params.putArray(entry.first, (ReadableArray) entry.second); 77 | } else if (entry.second instanceof Boolean) { 78 | params.putBoolean(entry.first, (Boolean) entry.second); 79 | } else if (entry.second instanceof Integer) { 80 | params.putInt(entry.first, (Integer) entry.second); 81 | } else if (entry.second instanceof Float) { 82 | params.putDouble(entry.first, (Float) entry.second); 83 | } else if (entry.second instanceof Double) { 84 | params.putDouble(entry.first, (Double) entry.second); 85 | } else if (entry.second instanceof Long) { 86 | params.putDouble(entry.first, (Long) entry.second); 87 | } else if (entry.second == null) { 88 | logger.debug("constructJSMap: filtering null value"); 89 | } else { 90 | logger.debug(String.format( 91 | "constructJSMap: unexpected type %s", 92 | entry.second.getClass() 93 | )); 94 | } 95 | } 96 | return params; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /android/src/main/java/com/twiliovoicereactnative/MediaPlayerManager.java: -------------------------------------------------------------------------------- 1 | package com.twiliovoicereactnative; 2 | 3 | import android.content.Context; 4 | import android.media.AudioAttributes; 5 | import android.media.SoundPool; 6 | 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | class MediaPlayerManager { 11 | public enum SoundTable { 12 | INCOMING, 13 | OUTGOING, 14 | DISCONNECT, 15 | RINGTONE 16 | } 17 | private final SoundPool soundPool; 18 | private final Map soundMap; 19 | private int activeStream; 20 | 21 | MediaPlayerManager(Context context) { 22 | soundPool = (new SoundPool.Builder()) 23 | .setMaxStreams(2) 24 | .setAudioAttributes( 25 | new AudioAttributes.Builder() 26 | .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 27 | .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) 28 | .build()) 29 | .build(); 30 | activeStream = 0; 31 | soundMap = new HashMap<>(); 32 | soundMap.put(SoundTable.INCOMING, soundPool.load(context, R.raw.incoming, 1)); 33 | soundMap.put(SoundTable.OUTGOING, soundPool.load(context, R.raw.outgoing, 1)); 34 | soundMap.put(SoundTable.DISCONNECT, soundPool.load(context, R.raw.disconnect, 1)); 35 | soundMap.put(SoundTable.RINGTONE, soundPool.load(context, R.raw.ringtone, 1)); 36 | } 37 | 38 | public void play(final SoundTable sound) { 39 | activeStream = soundPool.play( 40 | soundMap.get(sound), 41 | 1.f, 42 | 1.f, 43 | 1, 44 | (SoundTable.DISCONNECT== sound) ? 0 : -1, 45 | 1.f); 46 | } 47 | 48 | public void stop() { 49 | soundPool.stop(activeStream); 50 | activeStream = 0; 51 | } 52 | 53 | @Override 54 | protected void finalize() throws Throwable { 55 | soundPool.release(); 56 | super.finalize(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /android/src/main/java/com/twiliovoicereactnative/SDKLog.java: -------------------------------------------------------------------------------- 1 | package com.twiliovoicereactnative; 2 | 3 | import android.util.Log; 4 | 5 | import java.io.IOException; 6 | import java.io.OutputStream; 7 | import java.io.PrintStream; 8 | import java.nio.charset.Charset; 9 | import java.util.Vector; 10 | 11 | class SDKLog extends OutputStream { 12 | private final String logTag; 13 | private final Vector logInfoBuffer = new Vector<>(); 14 | public SDKLog(Class clazz) { 15 | logTag = clazz.getSimpleName(); 16 | } 17 | 18 | public void debug(final String message) { 19 | if (BuildConfig.DEBUG) { 20 | Log.d(logTag, message); 21 | } 22 | } 23 | 24 | public void log(final String message) { 25 | Log.i(logTag, message); 26 | } 27 | 28 | public void warning(final String message) { 29 | try { 30 | write(message.getBytes()); 31 | flush(); 32 | } catch (Exception ignore) {} 33 | } 34 | 35 | public void error(final String message) { 36 | Log.e(logTag, message); 37 | } 38 | 39 | public void warning(final Exception e, final String message) { 40 | PrintStream printStream = new PrintStream(this); 41 | printStream.println(message); 42 | e.printStackTrace(printStream); 43 | printStream.flush(); 44 | } 45 | 46 | @Override 47 | public synchronized void write(int i) throws IOException { 48 | logInfoBuffer.add((char)i); 49 | } 50 | 51 | @Override 52 | public synchronized void write(byte[] b) throws IOException { 53 | for (char c: (new String(b, Charset.defaultCharset()).toCharArray())) { 54 | logInfoBuffer.add(c); 55 | } 56 | } 57 | 58 | @Override 59 | public void write(byte[] b, int off, int len) throws IOException { 60 | for (char c: (new String(b, off, len, Charset.defaultCharset()).toCharArray())) { 61 | logInfoBuffer.add(c); 62 | } 63 | } 64 | 65 | @Override 66 | public synchronized void flush() throws IOException { 67 | char [] output = new char[logInfoBuffer.size()]; 68 | for (int i = 0; i < logInfoBuffer.size(); ++i) { 69 | output[i] = logInfoBuffer.get(i); 70 | } 71 | logInfoBuffer.clear(); 72 | Log.w(logTag, String.valueOf(output)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /android/src/main/java/com/twiliovoicereactnative/TwilioVoiceReactNativePackage.java: -------------------------------------------------------------------------------- 1 | package com.twiliovoicereactnative; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.facebook.react.ReactPackage; 6 | import com.facebook.react.bridge.NativeModule; 7 | import com.facebook.react.bridge.ReactApplicationContext; 8 | import com.facebook.react.uimanager.ViewManager; 9 | 10 | import java.util.ArrayList; 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | public class TwilioVoiceReactNativePackage implements ReactPackage { 15 | @NonNull 16 | @Override 17 | public List createNativeModules(@NonNull ReactApplicationContext reactContext) { 18 | List modules = new ArrayList<>(); 19 | modules.add(new TwilioVoiceReactNativeModule(reactContext)); 20 | return modules; 21 | } 22 | 23 | @NonNull 24 | @Override 25 | public List createViewManagers(@NonNull ReactApplicationContext reactContext) { 26 | return Collections.emptyList(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /android/src/main/java/com/twiliovoicereactnative/VoiceActivityProxy.java: -------------------------------------------------------------------------------- 1 | package com.twiliovoicereactnative; 2 | 3 | import java.util.List; 4 | import java.util.Vector; 5 | 6 | import android.Manifest; 7 | import android.app.Activity; 8 | import android.content.Intent; 9 | import android.content.pm.PackageManager; 10 | import android.os.Build; 11 | import android.os.Bundle; 12 | import android.view.Window; 13 | import android.view.WindowManager; 14 | 15 | import androidx.annotation.NonNull; 16 | import androidx.core.app.ActivityCompat; 17 | import androidx.core.content.ContextCompat; 18 | 19 | public class VoiceActivityProxy { 20 | private static final SDKLog logger = new SDKLog(VoiceActivityProxy.class); 21 | private static final int PERMISSION_REQUEST_CODE = 101; 22 | private static final String[] permissionList; 23 | private final Activity context; 24 | private final PermissionsRationaleNotifier notifier; 25 | 26 | public interface PermissionsRationaleNotifier { 27 | void displayRationale(final String permission); 28 | } 29 | 30 | public VoiceActivityProxy(@NonNull Activity activity, 31 | @NonNull PermissionsRationaleNotifier notifier) { 32 | this.context = activity; 33 | this.notifier = notifier; 34 | } 35 | public void onCreate(Bundle ignoredSavedInstanceState) { 36 | logger.debug("onCreate(): invoked"); 37 | // Ensure the microphone permission is enabled 38 | if (!checkPermissions()) { 39 | requestPermissions(); 40 | } 41 | // These flags ensure that the activity can be launched when the screen is locked. 42 | Window window = context.getWindow(); 43 | window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED 44 | | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON 45 | | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 46 | // handle any incoming intents 47 | handleIntent(context.getIntent()); 48 | } 49 | public void onDestroy() { 50 | logger.debug("onDestroy(): invoked"); 51 | } 52 | public void onNewIntent(Intent intent) { 53 | logger.debug("onNewIntent(...): invoked"); 54 | handleIntent(intent); 55 | } 56 | private void requestPermissions() { 57 | List permissionsRequestList = new Vector<>(); 58 | for (final String permission: VoiceActivityProxy.permissionList) { 59 | permissionsRequestList.add(permission); 60 | if (ActivityCompat.shouldShowRequestPermissionRationale(context, permission)) { 61 | notifier.displayRationale(permission); 62 | } 63 | } 64 | ActivityCompat.requestPermissions( 65 | context, 66 | permissionsRequestList.toArray(new String[0]), 67 | PERMISSION_REQUEST_CODE); 68 | } 69 | private boolean checkPermissions() { 70 | for (String permission: VoiceActivityProxy.permissionList) { 71 | if (PackageManager.PERMISSION_GRANTED != 72 | ContextCompat.checkSelfPermission(context, permission)) { 73 | return false; 74 | } 75 | } 76 | return true; 77 | } 78 | private void handleIntent(Intent intent) { 79 | String action = intent.getAction(); 80 | if ((null != action) && (!action.equals(Constants.ACTION_PUSH_APP_TO_FOREGROUND))) { 81 | Intent copiedIntent = new Intent(intent); 82 | copiedIntent.setClass(context.getApplicationContext(), VoiceService.class); 83 | copiedIntent.setFlags(0); 84 | context.getApplicationContext().startService(copiedIntent); 85 | } 86 | } 87 | 88 | static { 89 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2) { 90 | permissionList = new String[] { 91 | Manifest.permission.RECORD_AUDIO, 92 | Manifest.permission.BLUETOOTH_CONNECT, 93 | Manifest.permission.POST_NOTIFICATIONS }; 94 | } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) { 95 | permissionList = new String[] { 96 | Manifest.permission.RECORD_AUDIO, Manifest.permission.BLUETOOTH_CONNECT }; 97 | } else { 98 | permissionList = new String[] { Manifest.permission.RECORD_AUDIO }; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /android/src/main/java/com/twiliovoicereactnative/VoiceFirebaseMessagingService.java: -------------------------------------------------------------------------------- 1 | package com.twiliovoicereactnative; 2 | 3 | import static com.twiliovoicereactnative.VoiceApplicationProxy.getCallRecordDatabase; 4 | import static com.twiliovoicereactnative.VoiceApplicationProxy.getVoiceServiceApi; 5 | 6 | import com.twiliovoicereactnative.CallRecordDatabase.CallRecord; 7 | 8 | import android.os.PowerManager; 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | 12 | import com.google.firebase.messaging.FirebaseMessagingService; 13 | import com.google.firebase.messaging.RemoteMessage; 14 | import com.twilio.voice.CallException; 15 | import com.twilio.voice.CallInvite; 16 | import com.twilio.voice.CancelledCallInvite; 17 | import com.twilio.voice.MessageListener; 18 | import com.twilio.voice.Voice; 19 | 20 | import java.util.Objects; 21 | import java.util.UUID; 22 | 23 | public class VoiceFirebaseMessagingService extends FirebaseMessagingService { 24 | private static final SDKLog logger = new SDKLog(VoiceFirebaseMessagingService.class); 25 | 26 | public static class MessageHandler implements MessageListener { 27 | @Override 28 | public void onCallInvite(@NonNull CallInvite callInvite) { 29 | logger.log(String.format("onCallInvite %s", callInvite.getCallSid())); 30 | 31 | final CallRecord callRecord = new CallRecord(UUID.randomUUID(), callInvite); 32 | 33 | getCallRecordDatabase().add(callRecord); 34 | getVoiceServiceApi().incomingCall(callRecord); 35 | } 36 | 37 | @Override 38 | public void onCancelledCallInvite(@NonNull CancelledCallInvite cancelledCallInvite, 39 | @Nullable CallException callException) { 40 | logger.log(String.format("onCancelledCallInvite %s", cancelledCallInvite.getCallSid())); 41 | 42 | CallRecord callRecord = Objects.requireNonNull( 43 | getCallRecordDatabase().remove(new CallRecord(cancelledCallInvite.getCallSid()))); 44 | 45 | callRecord.setCancelledCallInvite(cancelledCallInvite); 46 | callRecord.setCallException(callException); 47 | getVoiceServiceApi().cancelCall(callRecord); 48 | } 49 | } 50 | 51 | @Override 52 | public void onNewToken(@NonNull String token) { 53 | logger.log("Refreshed FCM token: " + token); 54 | } 55 | 56 | /** 57 | * Called when message is received. 58 | * 59 | * @param remoteMessage Object representing the message received from Firebase Cloud Messaging. 60 | */ 61 | @Override 62 | public void onMessageReceived(RemoteMessage remoteMessage) { 63 | logger.debug("onMessageReceived remoteMessage: " + remoteMessage.toString()); 64 | logger.debug("Bundle data: " + remoteMessage.getData()); 65 | logger.debug("From: " + remoteMessage.getFrom()); 66 | 67 | PowerManager pm = (PowerManager)getSystemService(POWER_SERVICE); 68 | boolean isScreenOn = pm.isInteractive(); // check if screen is on 69 | if (!isScreenOn) { 70 | PowerManager.WakeLock wl = pm.newWakeLock( 71 | PowerManager.SCREEN_DIM_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, 72 | "VoiceFirebaseMessagingService:notificationLock"); 73 | wl.acquire(30000); //set your time in milliseconds 74 | } 75 | 76 | // Check if message contains a data payload. 77 | if (!remoteMessage.getData().isEmpty()) { 78 | if (!Voice.handleMessage( 79 | this, 80 | remoteMessage.getData(), 81 | new MessageHandler(), 82 | new CallMessageListenerProxy())) { 83 | logger.error("The message was not a valid Twilio Voice SDK payload: " + 84 | remoteMessage.getData()); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /android/src/main/res/drawable/answered_call_small_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/android/src/main/res/drawable/answered_call_small_icon.png -------------------------------------------------------------------------------- /android/src/main/res/drawable/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/android/src/main/res/drawable/ic_launcher_round.png -------------------------------------------------------------------------------- /android/src/main/res/drawable/ic_launcher_sdk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/android/src/main/res/drawable/ic_launcher_sdk.png -------------------------------------------------------------------------------- /android/src/main/res/drawable/incoming_call_small_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/android/src/main/res/drawable/incoming_call_small_icon.png -------------------------------------------------------------------------------- /android/src/main/res/drawable/outgoing_call_small_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/android/src/main/res/drawable/outgoing_call_small_icon.png -------------------------------------------------------------------------------- /android/src/main/res/raw/disconnect.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/android/src/main/res/raw/disconnect.wav -------------------------------------------------------------------------------- /android/src/main/res/raw/incoming.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/android/src/main/res/raw/incoming.wav -------------------------------------------------------------------------------- /android/src/main/res/raw/outgoing.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/android/src/main/res/raw/outgoing.wav -------------------------------------------------------------------------------- /android/src/main/res/raw/ringtone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/android/src/main/res/raw/ringtone.wav -------------------------------------------------------------------------------- /android/src/main/res/raw/silent.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/android/src/main/res/raw/silent.wav -------------------------------------------------------------------------------- /android/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #f10028 4 | #a3090e 5 | #b0bec5 6 | #4ECE23 7 | #EE3E27 8 | #000000 9 | #505050 10 | #FFFFFF 11 | 12 | -------------------------------------------------------------------------------- /android/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #f10028 4 | #a3090e 5 | #b0bec5 6 | #4ECE23 7 | #EE3E27 8 | #000000 9 | #505050 10 | #FFFFFF 11 | 12 | -------------------------------------------------------------------------------- /android/src/main/res/values/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 20dp 6 | 140dp 7 | 8 | -------------------------------------------------------------------------------- /android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Twilio Voice React Native 3 | Audio Device 4 | client identity or phone number 5 | ANSWER 6 | DECLINE 7 | Tap to return to call 8 | End Call 9 | Dial a client name or phone number. Leaving the field empty results in an automated response. 10 | A string of digits to be sent. Valid values are "0" - "9", "*", "#", and "w". Each "w" will cause a 500 ms pause between digits sent. 11 | Select Audio Device 12 | Registration Error: %d, %s 13 | Unregistration Error: %d, %s 14 | Fetching FCM registration token failed %s 15 | FCM token is "null" 16 | No such "audioDevice" object exists with UUID %s 17 | No such "call" object exists with UUID %s 18 | No such "callInvite" object exists with UUID %s 19 | Invalid notificaiton type %s 20 | Unknown 21 | ${from} 22 | ${to} 23 | ${from} 24 | Method invocation invalid 25 | 26 | -------------------------------------------------------------------------------- /android/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:metro-react-native-babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /constants/constants.java.template: -------------------------------------------------------------------------------- 1 | package com.twiliovoicereactnative; 2 | 3 | public class CommonConstants { 4 | // {{COMMENT}} 5 | public static final String {{LABEL}} = "{{LITERAL}}"; 6 | } 7 | -------------------------------------------------------------------------------- /constants/constants.objc.template: -------------------------------------------------------------------------------- 1 | // 2 | // TwilioVoiceReactNativeConstants.h 3 | // TwilioVoiceReactNative 4 | // 5 | // Copyright © 2022 Twilio, Inc. All rights reserved. 6 | // 7 | 8 | /* This file is auto-generated. Do not edit! */ 9 | 10 | // {{COMMENT}} 11 | static NSString * const kTwilioVoiceReactNative{{LABEL}} = @"{{LITERAL}}"; 12 | -------------------------------------------------------------------------------- /constants/constants.typescript.template: -------------------------------------------------------------------------------- 1 | export enum Constants { 2 | // {{COMMENT}} 3 | '{{LABEL}}' = '{{LITERAL}}', 4 | } 5 | -------------------------------------------------------------------------------- /docs/disable-full-screen-notifications.md: -------------------------------------------------------------------------------- 1 | The functionality detailed in this document was added in `1.6.0` of the 2 | `@twilio/voice-react-native-sdk`. 3 | 4 | # Using Full Screen Notifications on Android 5 | The `@twilio/voice-react-native-sdk` Starting with Android 14 (API 34), 6 | full screen intents are only available for alarm and phone calling 7 | applications and require user approval to enable. This is a departure 8 | from previous versions of Android. 9 | 10 | This document provides details on how to disable the use of full screen 11 | notifications if desired. 12 | 13 | ## Disabling Full Screen Notifications on Android 14 | To disable full screen notifications, you can add a `config.xml` file 15 | in the `src/main/res/values/` folder within your `android/app/` 16 | folder. Including the following content within this file will disable 17 | the use of full screen notifications. 18 | ``` 19 | false 20 | ``` 21 | See [this file](/android/src/main/res/values/config.xml) for more details. 22 | 23 | With full screen notifications disabled, the SDK will not ask for user 24 | permissions for enabling it and it will not use full screen 25 | notifications for incoming phone calls. 26 | -------------------------------------------------------------------------------- /docs/getting-started-ios.md: -------------------------------------------------------------------------------- 1 | # Getting Started on iOS 2 | 3 | Please check out the following if you are new to Twilio's Programmable Voice or React Native. 4 | 5 | - [Programmable Voice](https://www.twilio.com/docs/voice/sdks) 6 | - [React Native](https://reactnative.dev/docs/getting-started) 7 | 8 | When following the React Native environment setup guide, please ensure that "React Native CLI" is selected. 9 | 10 | ## iOS 11 | 12 | Please note that the Twilio Voice React Native SDK is tightly integrated with the iOS CallKit framework. This provides the best call and audio experience, and requires the application to be run on a physical device. The SDK will not work on an iOS simulator. 13 | 14 | Firstly, create a Bundle Identifier (Bundle ID) through the Apple Developer Portal. Then, create a Provisioning Profile for that Bundle ID and add physical devices to that profile. Those devices will also need to be registered to the developer account. 15 | 16 | For incoming call push notifications, create a VoIP certificate through the Apple Developer Portal. Then, use the VoIP certificate to create a Push Credential in the Twilio Console. This Push Credential will be used as part of vending access tokens, and will enable a device to receive incoming calls. 17 | 18 | For more information on Apple Push Notification Service, please see the Twilio Programmable Voice iOS Quickstart: 19 | https://github.com/twilio/voice-quickstart-ios 20 | 21 | ### Capabilities 22 | 23 | In Xcode, your application will need to define the following capabilities in order to make outgoing calls and receive incoming calls. 24 | 25 | - Background Modes 26 | - Audio, AirPlay, and Picture in Picture 27 | - Voice over IP 28 | 29 | - Push Notifications 30 | 31 | ## Wrapping Up 32 | Once the above has been implemented in your application, the Twilio Voice React Native SDK is ready for usage on iOS platforms. 33 | 34 | ### Access Tokens 35 | An Access Token is required to make outgoing calls or receive incoming calls. Please check out this [page](https://www.twilio.com/docs/iam/access-tokens#create-an-access-token-for-voice) for more details on creating Access Tokens. 36 | 37 | For more details on access tokens, please see the [iOS](https://github.com/twilio/voice-quickstart-ios) and [Android](https://github.com/twilio/voice-quickstart-android) quickstart for examples. 38 | 39 | ### Usage 40 | The following example demonstrates how to make and receive calls. You will need to implement your own `getAccessToken()` method. 41 | 42 | For more information on the Voice React Native SDK API, refer to the [API Docs](https://github.com/twilio/twilio-voice-react-native/blob/latest/docs/api/voice-react-native-sdk.md) or see our [Reference App](https://github.com/twilio/twilio-voice-react-native-app). 43 | 44 | ```ts 45 | import { Voice } from '@twilio/voice-react-native-sdk'; 46 | 47 | const token = getAccessToken(); // you will need to implement this method for your use case 48 | 49 | const voice = new Voice(); 50 | 51 | // Allow incoming calls 52 | await voice.initializePushRegistry(); // only necessary on ios 53 | await voice.register(token); 54 | 55 | // Handle incoming calls 56 | voice.on(Voice.Event.CallInvite, (callInvite) => { 57 | callInvite.accept(); 58 | }); 59 | 60 | // Make an outgoing call 61 | const call = await voice.connect(token, params); 62 | ``` 63 | -------------------------------------------------------------------------------- /docs/migration-guide-beta.4.md: -------------------------------------------------------------------------------- 1 | ## Migration Guide for Twilio Voice React Native Beta.4 2 | On Android, there have been significant improvements to the integration of the SDK at the (Java) 3 | native layer. These improvements simplify and encapsulate much of the integration complexity of 4 | the SDK and its interaction with the Android system. 5 | 6 | The biggest changes relate to three areas... 7 | * AndroidManifest.xml Simplifications 8 | * Android Permission Handling 9 | * Application & Main Activity Lifecycle/Event Handling 10 | 11 | For an example on how this is done, please look at the [reference application implementation](https://github.com/twilio/twilio-voice-react-native-app/tree/main) 12 | or refer to the [pull request](https://github.com/twilio/twilio-voice-react-native-app/pull/129) 13 | demonstrating these changes. 14 | 15 | #### AndroidManifest.xml Changes 16 | In previous releases, users of the SDK had to manually update their application's AndroidManifest.xml 17 | to reflect changes in the SDK. These changes included, additional permissions or when 18 | Services, BroadcastReceivers, or additional Activities were added to the SDK. This is no longer the 19 | case. Now users of the SDK just need to define their main application activity and any additional 20 | permissions their application may use (outside the scope of the SDK). For an example of this, please 21 | refer to the reference app's [AndroidManifest.xml](https://github.com/twilio/twilio-voice-react-native-app/blob/main/app/android/app/src/main/AndroidManifest.xml). 22 | 23 | #### Permission Management Changes 24 | Applications using the SDK no longer need to explicitly request for permissions used by the SDK. The 25 | SDK now encapsulates that logic and will request for the necessary permissions automatically. 26 | Furthermore, the application using the SDK can customize the reaction to a needed permission not 27 | being granted by the user. 28 | 29 | When constructing an `VoiceActivityProxy`, one of the constructors arguments is a 30 | `PermissionsRationaleNotifier`interface. This interface has a single method named 31 | `displayRationale(final String permission)` which will be invoked when a needed permission has not 32 | been granted by the user. For an example of how this works please refer to 33 | [MainActivity.java](https://github.com/twilio/twilio-voice-react-native-app/blob/8f0da0b95728d5bb198b26e889bf3dddbbd11776/app/android/app/src/main/java/com/twiliovoicereactnativereferenceapp/MainActivity.java#L36) 34 | in the reference application. 35 | 36 | #### Encapsulation of Application & Activity Lifecycle Management 37 | Now applications using the SDK just need to construct two objects, `VoiceActivityProxy` for the 38 | application's main activity and `VoiceApplicationProxy` for the application's 39 | `android.app.Application` events. 40 | 41 | ###### How to wire up VoiceActivityProxy to your Main Activity 42 | Add the `VoiceActivityProxy` as private member to your main activity class and construct it in 43 | either the constructor (not in the `onCreate(...)` method). Then, the following activity methods 44 | need to be overridden, `onCreate(...)`, `onDestroy(...)` and `onNewIntent(...)`. In the 45 | implementation of these methods, please call the corresponding matching methods in the 46 | `VoiceActivityProxy` member object. For a complete example, please refer to the reference 47 | application [here](https://github.com/twilio/twilio-voice-react-native-app/blob/main/app/android/app/src/main/java/com/twiliovoicereactnativereferenceapp/MainActivity.java). 48 | 49 | ###### How to wire up VoiceApplicationProxy to android.app.Application 50 | Similar to steps for wiring up the `VoiceActivityProxy`, add the `VoiceApplicationProxy` as a 51 | private member to your Application class and construct it in the constructor. Then, the following 52 | `android.app.Application` methods need to be overridden, `onCreate(...)` & `onTerminate(...)`. In 53 | the implementation of these methods, please call the corresponding matching methods in the 54 | `VoiceApplicationProxy` member object. For a complete example, please refer to the reference 55 | application [here](https://github.com/twilio/twilio-voice-react-native-app/blob/main/app/android/app/src/main/java/com/twiliovoicereactnativereferenceapp/MainApplication.java) -------------------------------------------------------------------------------- /docs/out-of-band-firebase-messaging-service.md: -------------------------------------------------------------------------------- 1 | The functionality detailed in this document was added in `1.2.1` of the 2 | `@twilio/voice-react-native-sdk`. 3 | 4 | # Using an out-of-band Firebase Messaging service 5 | The `@twilio/voice-react-native-sdk` includes a built-in Firebase Messaging 6 | service to allow users to register and listen for incoming calls. However, it 7 | is common that other libraries will have the same requirement of listening for 8 | Firebase messages. Due to restrictions placed by Android, only one service per 9 | application can listen for Firebase messages. If you use another library that 10 | declares a Firebase message listener, it is likely to break Twilio's incoming 11 | call functionality within your application. 12 | 13 | This document provides details on how to disable the SDK's built-in Firebase 14 | Messaging service, and how to retain the SDK's incoming call functionality. 15 | 16 | ## Disabling the built-in Firebase Messaging service 17 | To disable the built-in Firebase Messaging service, you can add a 18 | `config.xml` file in the `src/main/res/values/` folder within your 19 | `android/app/` folder. 20 | Including the following content within this file will disable the built-in 21 | Firebase Messaging service: 22 | ``` 23 | false 24 | ``` 25 | See [this file](/android/src/main/res/values/config.xml) for more details. 26 | 27 | With the built-in Firebase Messaging service disabled, any other Firebase 28 | Messaging service will be able to listen for Firebase messages. 29 | 30 | ## voice.handleFirebaseMessage API 31 | Firebase messages can now be passed into the Voice SDK from any other source. 32 | 33 | The following API has been implemented to facilitate this: 34 | ```ts 35 | import { Voice } from '@twilio/voice-react-native-sdk'; 36 | const voice = new Voice(); 37 | 38 | const remoteMessage = ...; // this remote message should be provided by a common firebase message service that is separate from the Twilio Voice RN SDK 39 | const didHandleMessage = await voice.handleFirebaseMessage(remoteMessage); 40 | if (didHandleMessage) { 41 | // the Twilio Voice RN SDK was able to parse and handle the message as an incoming call 42 | } 43 | ``` 44 | 45 | The most common third-party library that our team has seen used as a common 46 | Firebase messaging service is from the 47 | [React Native Firebase](https://rnfirebase.io/) team. More specifically, their 48 | Cloud Messaging library: `@react-native-firebase/messaging`. Their library will 49 | be used in further examples in this document. 50 | ```ts 51 | // preferably in your index.js/index.ts file 52 | // or, as early as possible in your application 53 | import messaging from '@react-native-firebase/messaging'; 54 | import { Voice } from '@twilio/voice-react-native-sdk'; 55 | 56 | const voice = new Voice(); 57 | 58 | messaging().onMessage(async (remoteMessage) => { 59 | voice.handleFirebaseMessage(remoteMessage.data); // important, note the `.data` here 60 | }); 61 | 62 | messaging().setBackgroundMessageHandler(async (remoteMessage) => { 63 | voice.handleFirebaseMessage(remoteMessage.data); // likewise, note the `.data` here 64 | }); 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/play-outgoing-call-ringback-tone.md: -------------------------------------------------------------------------------- 1 | ## Play outgoing call ringback tone while waiting for the callee to answer the call 2 | 3 | When making calls using the React Native Voice SDK in your Android or iOS apps, the SDK starts streaming audio bi-directionally when the call is connected. The timing of the call transitions to the `connected` state depends on the value of the [answerOnBridge flag of the TwiML verb](https://www.twilio.com/docs/voice/twiml/dial#answeronbridge). The call will immediately transition from `ringing` to `connected` once the mobile app has connected to the TwiML application and a ringback tone will start playing while waiting for the callee to answer the call. The default value of the `answerOnBridge` flag is true, which means the call will only transition to connected when the callee actually answers the call. In this case the caller won’t hear anything until the call is connected and the media connection established. 4 | 5 | Follow these steps to include the sound file to the app project, and the SDK will automatically play the ringback if the file is presented under the path or in the app bundle. 6 | 7 | ### Android 8 | 9 | Include a sound file named `ringtone.wav` and place the file under `android/app/src/main/res/raw`. 10 | 11 | ### iOS 12 | 13 | Include a sound file named `ringtone.wav` into the Xcode project of the app. Note that the path of the file does not matter as long as the file is properly added to the app bundle. 14 | -------------------------------------------------------------------------------- /ios/TwilioVoicePushRegistry.h: -------------------------------------------------------------------------------- 1 | // 2 | // TwilioVoicePushRegistry.h 3 | // TwilioVoiceReactNative 4 | // 5 | // Copyright © 2022 Twilio, Inc. All rights reserved. 6 | // 7 | 8 | FOUNDATION_EXPORT NSString * const kTwilioVoicePushRegistryNotification; 9 | FOUNDATION_EXPORT NSString * const kTwilioVoicePushRegistryEventType; 10 | FOUNDATION_EXPORT NSString * const kTwilioVoicePushRegistryNotificationDeviceTokenUpdated; 11 | FOUNDATION_EXPORT NSString * const kTwilioVoicePushRegistryNotificationDeviceToken; 12 | FOUNDATION_EXPORT NSString * const kTwilioVoicePushRegistryNotificationIncomingPushReceived; 13 | FOUNDATION_EXPORT NSString * const kTwilioVoicePushRegistryNotificationIncomingPushPayload; 14 | 15 | @interface TwilioVoicePushRegistry : NSObject 16 | 17 | - (void)updatePushRegistry; 18 | 19 | @end 20 | -------------------------------------------------------------------------------- /ios/TwilioVoicePushRegistry.m: -------------------------------------------------------------------------------- 1 | // 2 | // TwilioVoicePushRegistry.m 3 | // TwilioVoiceReactNative 4 | // 5 | // Copyright © 2022 Twilio, Inc. All rights reserved. 6 | // 7 | 8 | @import PushKit; 9 | @import Foundation; 10 | @import TwilioVoice; 11 | 12 | #import "TwilioVoicePushRegistry.h" 13 | #import "TwilioVoiceReactNative.h" 14 | #import "TwilioVoiceReactNativeConstants.h" 15 | 16 | NSString * const kTwilioVoicePushRegistryNotification = @"TwilioVoicePushRegistryNotification"; 17 | NSString * const kTwilioVoicePushRegistryEventType = @"type"; 18 | NSString * const kTwilioVoicePushRegistryNotificationDeviceTokenUpdated = @"deviceTokenUpdated"; 19 | NSString * const kTwilioVoicePushRegistryNotificationDeviceToken = @"deviceToken"; 20 | NSString * const kTwilioVoicePushRegistryNotificationIncomingPushReceived = @"incomingPushReceived"; 21 | NSString * const kTwilioVoicePushRegistryNotificationIncomingPushPayload = @"incomingPushPayload"; 22 | 23 | @interface TwilioVoicePushRegistry () 24 | 25 | @property (nonatomic, strong) PKPushRegistry *voipRegistry; 26 | @property (nonatomic, copy) NSString *callInviteUuid; 27 | 28 | @end 29 | 30 | @implementation TwilioVoicePushRegistry 31 | 32 | #pragma mark - TwilioVoicePushRegistry methods 33 | 34 | - (void)updatePushRegistry { 35 | self.voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()]; 36 | self.voipRegistry.delegate = self; 37 | self.voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP]; 38 | } 39 | 40 | #pragma mark - PKPushRegistryDelegate 41 | 42 | - (void)pushRegistry:(PKPushRegistry *)registry 43 | didUpdatePushCredentials:(PKPushCredentials *)credentials 44 | forType:(NSString *)type { 45 | if ([type isEqualToString:PKPushTypeVoIP]) { 46 | [[NSNotificationCenter defaultCenter] postNotificationName:kTwilioVoicePushRegistryNotification 47 | object:nil 48 | userInfo:@{kTwilioVoicePushRegistryEventType: kTwilioVoicePushRegistryNotificationDeviceTokenUpdated, 49 | kTwilioVoicePushRegistryNotificationDeviceToken: credentials.token}]; 50 | } 51 | } 52 | 53 | - (void)pushRegistry:(PKPushRegistry *)registry 54 | didReceiveIncomingPushWithPayload:(PKPushPayload *)payload 55 | forType:(PKPushType)type 56 | withCompletionHandler:(void (^)(void))completion { 57 | if ([type isEqualToString:PKPushTypeVoIP]) { 58 | [[NSNotificationCenter defaultCenter] postNotificationName:kTwilioVoicePushRegistryNotification 59 | object:nil 60 | userInfo:@{kTwilioVoicePushRegistryEventType: kTwilioVoicePushRegistryNotificationIncomingPushReceived, 61 | kTwilioVoicePushRegistryNotificationIncomingPushPayload: payload.dictionaryPayload}]; 62 | } 63 | 64 | completion(); 65 | } 66 | 67 | - (void)pushRegistry:(PKPushRegistry *)registry 68 | didInvalidatePushTokenForType:(NSString *)type { 69 | // TODO: notify view-controller to emit event that the push-registry has been invalidated 70 | } 71 | 72 | @end 73 | -------------------------------------------------------------------------------- /ios/TwilioVoiceReactNative+CallInvite.m: -------------------------------------------------------------------------------- 1 | // 2 | // TwilioVoiceReactNative+CallInvite.m 3 | // TwilioVoiceReactNative 4 | // 5 | // Copyright © 2023 Twilio, Inc. All rights reserved. 6 | // 7 | 8 | @import TwilioVoice; 9 | 10 | #import "TwilioVoiceReactNative.h" 11 | #import "TwilioVoiceReactNativeConstants.h" 12 | 13 | @interface TwilioVoiceReactNative (CallInvite) 14 | 15 | @end 16 | 17 | @implementation TwilioVoiceReactNative (CallInvite) 18 | 19 | - (void)callInviteReceived:(TVOCallInvite *)callInvite { 20 | self.callInviteMap[callInvite.uuid.UUIDString] = callInvite; 21 | 22 | [self reportNewIncomingCall:callInvite]; 23 | 24 | [self sendEventWithName:kTwilioVoiceReactNativeScopeVoice 25 | body:@{ 26 | kTwilioVoiceReactNativeVoiceEventType: kTwilioVoiceReactNativeVoiceEventTypeValueIncomingCallInvite, 27 | kTwilioVoiceReactNativeEventKeyCallInvite: [self callInviteInfo:callInvite]}]; 28 | } 29 | 30 | - (void)cancelledCallInviteReceived:(TVOCancelledCallInvite *)cancelledCallInvite error:(NSError *)error { 31 | NSString *uuid; 32 | for (NSString *uuidKey in [self.callInviteMap allKeys]) { 33 | TVOCallInvite *callInvite = self.callInviteMap[uuidKey]; 34 | if ([callInvite.callSid isEqualToString:cancelledCallInvite.callSid]) { 35 | uuid = uuidKey; 36 | break; 37 | } 38 | } 39 | NSAssert(uuid, @"No matching call invite"); 40 | self.cancelledCallInviteMap[uuid] = cancelledCallInvite; 41 | 42 | [self sendEventWithName:kTwilioVoiceReactNativeScopeCallInvite 43 | body:@{ 44 | kTwilioVoiceReactNativeVoiceEventType: kTwilioVoiceReactNativeCallInviteEventTypeValueCancelled, 45 | kTwilioVoiceReactNativeCallInviteEventKeyCallSid: cancelledCallInvite.callSid, 46 | kTwilioVoiceReactNativeEventKeyCancelledCallInvite: [self cancelledCallInviteInfo:cancelledCallInvite], 47 | kTwilioVoiceReactNativeVoiceErrorKeyError: @{ 48 | kTwilioVoiceReactNativeVoiceErrorKeyCode: @(error.code), 49 | kTwilioVoiceReactNativeVoiceErrorKeyMessage: [error localizedDescription]}}]; 50 | 51 | [self.callInviteMap removeObjectForKey:uuid]; 52 | 53 | [self endCallWithUuid:[[NSUUID alloc] initWithUUIDString:uuid]]; 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /ios/TwilioVoiceReactNative+CallMessage.m: -------------------------------------------------------------------------------- 1 | // 2 | // TwilioVoiceReactNative+CallMessage.m 3 | // TwilioVoiceReactNative 4 | // 5 | // Copyright © 2024 Twilio, Inc. All rights reserved. 6 | // 7 | 8 | @import TwilioVoice; 9 | 10 | #import "TwilioVoiceReactNative.h" 11 | #import "TwilioVoiceReactNativeConstants.h" 12 | 13 | @interface TwilioVoiceReactNative (CallMessage) 14 | 15 | @end 16 | 17 | @implementation TwilioVoiceReactNative (CallMessage) 18 | 19 | #pragma mark - TVOCallMessageDelegate (Call) 20 | 21 | - (void)messageReceivedForCallSid:(NSString *)callSid message:(TVOCallMessage *)callMessage { 22 | NSArray *keys = self.callMap.allKeys; 23 | for (NSString *uuid in keys) { 24 | TVOCall *call = self.callMap[uuid]; 25 | if ([call.sid isEqualToString:callSid]) { 26 | [self sendEventWithName:kTwilioVoiceReactNativeScopeCall 27 | body:@{kTwilioVoiceReactNativeEventKeyCall: [self callInfo:call], 28 | kTwilioVoiceReactNativeVoiceEventType: kTwilioVoiceReactNativeCallEventMessageReceived, 29 | kTwilioVoiceReactNativeJSEventKeyCallMessageInfo: [self callMessageInfo:callMessage]}]; 30 | return; 31 | } 32 | } 33 | 34 | keys = self.callInviteMap.allKeys; 35 | for (NSString *uuid in keys) { 36 | TVOCallInvite *callInvite = self.callInviteMap[uuid]; 37 | if ([callInvite.callSid isEqualToString:callSid]) { 38 | [self sendEventWithName:kTwilioVoiceReactNativeScopeCallInvite 39 | body:@{kTwilioVoiceReactNativeCallInviteEventKeyCallSid: callSid, 40 | kTwilioVoiceReactNativeVoiceEventType: kTwilioVoiceReactNativeCallEventMessageReceived, 41 | kTwilioVoiceReactNativeJSEventKeyCallMessageInfo: [self callMessageInfo:callMessage]}]; 42 | return; 43 | } 44 | } 45 | 46 | NSLog(@"No match call or call invite for %@", callSid); 47 | } 48 | 49 | - (void)messageSentForCallSid:(NSString *)callSid voiceEventSid:(NSString *)voiceEventSid { 50 | [self sendEventWithName:kTwilioVoiceReactNativeScopeCallMessage 51 | body:@{kTwilioVoiceReactNativeVoiceEventType: kTwilioVoiceReactNativeCallEventMessageSent, 52 | kTwilioVoiceReactNativeVoiceEventSid: voiceEventSid}]; 53 | } 54 | 55 | - (void)messageFailedForCallSid:(NSString *)callSid voiceEventSid:(NSString *)voiceEventSid error:(NSError *)error { 56 | // NOTE(mhuynh): We need a delay here to prevent race conditions where some errors are synchronously handled 57 | // by the C++ SDK. For those synchronously handled errors, the JS layer is not given enough time to construct 58 | // and bind event listeners for this event. 59 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 60 | [self sendEventWithName:kTwilioVoiceReactNativeScopeCallMessage 61 | body:@{kTwilioVoiceReactNativeVoiceEventType: kTwilioVoiceReactNativeCallEventMessageFailure, 62 | kTwilioVoiceReactNativeVoiceEventSid: voiceEventSid, 63 | kTwilioVoiceReactNativeVoiceErrorKeyError: @{kTwilioVoiceReactNativeVoiceErrorKeyCode: @(error.code), 64 | kTwilioVoiceReactNativeVoiceErrorKeyMessage: [error localizedDescription]}}]; 65 | }); 66 | } 67 | 68 | #pragma mark - Utility 69 | 70 | - (NSDictionary *)callMessageInfo:(TVOCallMessage *)callMessage { 71 | NSDictionary *callMessageInfoObject = @{ 72 | kTwilioVoiceReactNativeVoiceEventSid: callMessage.voiceEventSid, 73 | kTwilioVoiceReactNativeCallMessageContent: callMessage.content, 74 | kTwilioVoiceReactNativeCallMessageContentType: callMessage.contentType, 75 | kTwilioVoiceReactNativeCallMessageMessageType: callMessage.messageType 76 | }; 77 | 78 | return callMessageInfoObject; 79 | } 80 | 81 | @end 82 | -------------------------------------------------------------------------------- /ios/TwilioVoiceReactNative.h: -------------------------------------------------------------------------------- 1 | // 2 | // TwilioVoiceReactNative.h 3 | // TwilioVoiceReactNative 4 | // 5 | // Copyright © 2022 Twilio, Inc. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | #import 11 | #import 12 | 13 | @class CXCallController; 14 | @class CXProvider; 15 | @class TVOCall; 16 | @class TVOCallInvite; 17 | @class TVOCancelledCallInvite; 18 | @class TVODefaultAudioDevice; 19 | 20 | FOUNDATION_EXPORT NSString * const kTwilioVoiceReactNativeEventKeyCall; 21 | FOUNDATION_EXPORT NSString * const kTwilioVoiceReactNativeEventKeyCallInvite; 22 | FOUNDATION_EXPORT NSString * const kTwilioVoiceReactNativeEventKeyCancelledCallInvite; 23 | 24 | @interface TwilioVoiceReactNative : RCTEventEmitter 25 | 26 | @property (nonatomic, readonly, strong) NSMutableDictionary *callMap; 27 | @property (nonatomic, readonly, strong) NSMutableDictionary *callConnectMap; 28 | @property (nonatomic, readonly, strong) NSMutableDictionary *callInviteMap; 29 | @property (nonatomic, readonly, strong) NSMutableDictionary *cancelledCallInviteMap; 30 | 31 | @property (nonatomic, strong) CXProvider *callKitProvider; 32 | @property (nonatomic, strong) CXCallController *callKitCallController; 33 | 34 | @property (nonatomic, copy) NSString *accessToken; 35 | @property (nonatomic, copy) NSDictionary *twimlParams; 36 | @property (nonatomic, strong) void(^callKitCompletionCallback)(BOOL); 37 | @property (nonatomic, strong) RCTPromiseResolveBlock callPromiseResolver; 38 | 39 | // Indicates if the disconnect is triggered from app UI, instead of the system Call UI 40 | @property (nonatomic, assign) BOOL userInitiatedDisconnect; 41 | 42 | @property (nonatomic, strong) AVAudioPlayer *ringbackPlayer; 43 | 44 | + (TVODefaultAudioDevice *)twilioAudioDevice; 45 | 46 | @end 47 | 48 | @interface TwilioVoiceReactNative (EventEmitter) 49 | 50 | // Override so we can check the event observer before emitting events 51 | - (void)sendEventWithName:(NSString *)eventName body:(id)body; 52 | 53 | @end 54 | 55 | @interface TwilioVoiceReactNative (CallKit) 56 | 57 | - (void)initializeCallKit; 58 | - (void)initializeCallKitWithConfiguration:(NSDictionary *)configuration; 59 | - (void)makeCallWithAccessToken:(NSString *)accessToken 60 | params:(NSDictionary *)params 61 | contactHandle:(NSString *)contactHandle; 62 | - (void)reportNewIncomingCall:(TVOCallInvite *)callInvite; 63 | - (void)endCallWithUuid:(NSUUID *)uuid; 64 | /* Initiate the answering from the app UI */ 65 | - (void)answerCallInvite:(NSUUID *)uuid 66 | completion:(void(^)(BOOL success))completionHandler; 67 | - (void)updateCall:(NSString *)uuid callerHandle:(NSString *)handle; 68 | 69 | /* Utility */ 70 | - (NSDictionary *)callInfo:(TVOCall *)call; 71 | - (NSDictionary *)callInviteInfo:(TVOCallInvite *)callInvite; 72 | - (NSDictionary *)cancelledCallInviteInfo:(TVOCancelledCallInvite *)cancelledCallInvite; 73 | 74 | @end 75 | -------------------------------------------------------------------------------- /scripts/bootstrap.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const child_process = require('child_process'); 3 | 4 | const root = path.resolve(__dirname, '..'); 5 | const args = process.argv.slice(2); 6 | const options = { 7 | cwd: process.cwd(), 8 | env: process.env, 9 | stdio: 'inherit', 10 | encoding: 'utf-8', 11 | }; 12 | 13 | let result; 14 | 15 | if (process.cwd() !== root || args.length) { 16 | // We're not in the root of the project, or additional arguments were passed 17 | // In this case, forward the command to `yarn` 18 | result = child_process.spawnSync('yarn', args, options); 19 | } else { 20 | // If `yarn` is run without arguments, perform bootstrap 21 | result = child_process.spawnSync('yarn', ['bootstrap'], options); 22 | } 23 | 24 | process.exitCode = result.status; 25 | -------------------------------------------------------------------------------- /scripts/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ERRORS = [ 4 | /** 5 | * AuthorizationErrors 6 | */ 7 | 'AuthorizationErrors.AccessTokenInvalid', // 20101 8 | 'AuthorizationErrors.AccessTokenHeaderInvalid', // 20102 9 | 'AuthorizationErrors.AccessTokenIssuerInvalid', // 20103 10 | 'AuthorizationErrors.AccessTokenExpired', // 20104 11 | 'AuthorizationErrors.AccessTokenNotYetValid', // 20105 12 | 'AuthorizationErrors.AccessTokenGrantsInvalid', // 20106 13 | 'AuthorizationErrors.AccessTokenSignatureInvalid', // 20107 14 | 'AuthorizationErrors.AuthenticationFailed', // 20151 15 | 'AuthorizationErrors.ExpirationTimeExceedsMaxTimeAllowed', // 20157 16 | 17 | /** 18 | * ForbiddenErrors 19 | */ 20 | 'ForbiddenErrors.Forbidden', // 20403 21 | 22 | /** 23 | * TwiMLErrors 24 | */ 25 | 'TwiMLErrors.InvalidApplicationSid', // 21218 26 | 27 | /** 28 | * GeneralErrors 29 | */ 30 | 'GeneralErrors.ConnectionError', // 31005 31 | 'GeneralErrors.CallCancelledError', // 31008 32 | 'GeneralErrors.TransportError', // 31009 33 | 34 | /** 35 | * MalformedRequestErrors 36 | */ 37 | 'MalformedRequestErrors.MalformedRequestError', // 31100 38 | 39 | /** 40 | * AuthorizationErrors 41 | */ 42 | 'AuthorizationErrors.AuthorizationError', // 31201 43 | 'AuthorizationErrors.RateExceededError', // 31206 44 | 'AuthorizationErrors.CallMessageEventTypeInvalidError', // 31210 45 | 'AuthorizationErrors.CallMessageUnexpectedStateError', // 31211 46 | 'AuthorizationErrors.PayloadSizeExceededError', // 31212 47 | 48 | /** 49 | * RegistrationErrors 50 | */ 51 | 'RegistrationErrors.RegistrationError', // 31301 52 | 'RegistrationErrors.UnsupportedCancelMessageError', // 31302 53 | 54 | /** 55 | * ClientErrors 56 | */ 57 | 'ClientErrors.BadRequest', // 31400 58 | 'ClientErrors.Forbidden', // 31403 59 | 'ClientErrors.NotFound', // 31404 60 | 'ClientErrors.RequestTimeout', // 31408 61 | 'ClientErrors.Conflict', // 31409 62 | 'ClientErrors.UpgradeRequired', // 31426 63 | 'ClientErrors.TooManyRequests', // 31429 64 | 'ClientErrors.TemporarilyUnavailable', // 31480 65 | 'ClientErrors.CallTransactionDoesNotExist', // 31481 66 | 'ClientErrors.AddressIncomplete', // 31484 67 | 'ClientErrors.BusyHere', // 31486 68 | 'ClientErrors.RequestTerminated', // 31487 69 | 70 | /** 71 | * ServerErrors 72 | */ 73 | 'ServerErrors.InternalServerError', // 31500 74 | 'ServerErrors.BadGateway', // 31502 75 | 'ServerErrors.ServiceUnavailable', // 31503 76 | 'ServerErrors.GatewayTimeout', // 31504 77 | 'ServerErrors.DNSResolutionError', // 31530 78 | 79 | /** 80 | * SIPServerErrors 81 | */ 82 | 'SIPServerErrors.BusyEverywhere', // 31600 83 | 'SIPServerErrors.Decline', // 31603 84 | 'SIPServerErrors.DoesNotExistAnywhere', // 31604 85 | 86 | /** 87 | * AuthorizationErrors 88 | */ 89 | 'AuthorizationErrors.AccessTokenRejected', // 51007 90 | 91 | /** 92 | * SignalingErrors 93 | */ 94 | 'SignalingErrors.ConnectionDisconnected', // 53001 95 | 96 | /** 97 | * MediaErrors 98 | */ 99 | 'MediaErrors.ClientLocalDescFailed', // 53400 100 | 'MediaErrors.ServerLocalDescFailed', // 53401 101 | 'MediaErrors.ClientRemoteDescFailed', // 53402 102 | 'MediaErrors.ServerRemoteDescFailed', // 53403 103 | 'MediaErrors.NoSupportedCodec', // 53404 104 | 'MediaErrors.ConnectionError', // 53405 105 | 'MediaErrors.MediaDtlsTransportFailedError', // 53407 106 | 107 | /** 108 | * UserMediaErrors 109 | */ 110 | 'UserMediaErrors.PermissionDeniedError', // 31401 111 | ]; 112 | 113 | module.exports = { 114 | ERRORS, 115 | }; 116 | -------------------------------------------------------------------------------- /scripts/generate-errors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * This script is used to generate Typescript error classes from the 5 | * `@twilio/voice-errors` repository for usage within the SDK. 6 | */ 7 | 8 | const fs = require('fs'); 9 | const VoiceErrors = require('@twilio/voice-errors'); 10 | const { ERRORS } = require('./errors'); 11 | 12 | let output = `/** 13 | * This is a generated file. Any modifications here will be overwritten. 14 | * See scripts/errors.js. 15 | */ 16 | import { TwilioError } from './TwilioError'; 17 | \n`; 18 | 19 | const escapeQuotes = (str) => str.replace("'", "\\'"); 20 | const generateStringArray = (arr) => 21 | arr 22 | ? `[ 23 | ${arr.map((value) => `'${escapeQuotes(value)}'`).join(',\n ')}, 24 | ]` 25 | : '[]'; 26 | 27 | const generateDocstring = (content) => [ 28 | '/**', 29 | ...content.map((c) => ` * ${c}`), 30 | ' */', 31 | ]; 32 | 33 | const generateDefinition = ([code, subclassName, errorName, error]) => `\ 34 | /** 35 | * @public 36 | * ${subclassName}Errors.${errorName} error. 37 | * Error code \`${code}\`. 38 | */ 39 | export class ${errorName} extends TwilioError { 40 | ${generateDocstring(error.causes ?? ['Not applicable.']).join('\n ')} 41 | causes: string[] = ${generateStringArray(error.causes)}; 42 | ${generateDocstring([error.description]).join('\n ')} 43 | description: string = '${escapeQuotes(error.description)}'; 44 | ${generateDocstring([error.explanation]).join('\n ')} 45 | explanation: string = '${escapeQuotes(error.explanation)}'; 46 | ${generateDocstring([error.name]).join('\n ')} 47 | name: string = '${escapeQuotes(errorName)}'; 48 | ${generateDocstring(error.solutions ?? ['Not applicable.']).join('\n ')} 49 | solutions: string[] = ${generateStringArray(error.solutions)}; 50 | 51 | constructor(message: string) { 52 | super(message, ${code}); 53 | Object.setPrototypeOf(this, ${subclassName}Errors.${errorName}.prototype); 54 | 55 | const msg: string = typeof message === 'string' 56 | ? message 57 | : this.explanation; 58 | 59 | this.message = \`\${this.name} (\${this.code}): \${msg}\`; 60 | } 61 | }`; 62 | 63 | const generateNamespace = (name, contents) => `/** 64 | * @public 65 | * ${name} errors. 66 | */ 67 | export namespace ${name}Errors { 68 | ${contents} 69 | }\n\n`; 70 | 71 | const generateMapEntry = ([code, fullName]) => `[${code}, ${fullName}]`; 72 | 73 | const sorter = ([codeA], [codeB]) => codeA - codeB; 74 | 75 | const mapEntries = []; 76 | const namespaceDefinitions = new Map(); 77 | 78 | for (const topClass of VoiceErrors) { 79 | for (const subclass of topClass.subclasses) { 80 | const subclassName = subclass.class.replace(' ', ''); 81 | 82 | if (!namespaceDefinitions.has(subclassName)) { 83 | namespaceDefinitions.set(subclassName, []); 84 | } 85 | 86 | const definitions = namespaceDefinitions.get(subclassName); 87 | for (const error of subclass.errors) { 88 | const code = 89 | topClass.code * 1000 + (subclass.code || 0) * 100 + error.code; 90 | const errorName = error.name.replace(' ', ''); 91 | 92 | const fullName = `${subclassName}Errors.${errorName}`; 93 | if (ERRORS.includes(fullName)) { 94 | definitions.push([code, subclassName, errorName, error]); 95 | mapEntries.push([code, fullName]); 96 | } 97 | } 98 | } 99 | } 100 | 101 | for (const [subclassName, definitions] of namespaceDefinitions.entries()) { 102 | if (definitions.length) { 103 | output += generateNamespace( 104 | subclassName, 105 | definitions.sort(sorter).map(generateDefinition).join('\n\n') 106 | ); 107 | } 108 | } 109 | 110 | output += `/** 111 | * @internal 112 | */ 113 | export const errorsByCode: ReadonlyMap = new Map([ 114 | ${mapEntries.sort(sorter).map(generateMapEntry).join(',\n ')}, 115 | ]); 116 | 117 | Object.freeze(errorsByCode);\n`; 118 | 119 | fs.writeFileSync('./src/error/generated.ts', output, 'utf8'); 120 | -------------------------------------------------------------------------------- /scripts/substitute-constants-version.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { readFile, writeFile } = require('fs').promises; 4 | 5 | async function substituteVersion(constantsPath, version) { 6 | const constantsInput = (await readFile(constantsPath)).toString('utf-8'); 7 | 8 | /** 9 | * NOTE(afalls): revisit VBLOCKS-2285 10 | */ 11 | const constantsOutput = constantsInput.replace( 12 | /(ReactNativeVoiceSDKVer\s*=\s*)(.*)/g, 13 | '$1' + version 14 | ); 15 | 16 | await writeFile(constantsPath, constantsOutput); 17 | } 18 | 19 | function parseCommandLineArgs() { 20 | const args = process.argv.slice(2); 21 | 22 | if (args.length !== 1) { 23 | throw 'No version argument.'; 24 | } 25 | 26 | const version = args[0]; 27 | 28 | if (!(version.length > 0)) { 29 | throw 'Version argument invalid.'; 30 | } 31 | 32 | return { version }; 33 | } 34 | 35 | async function main() { 36 | const args = parseCommandLineArgs(); 37 | 38 | const constantsPath = './constants/constants.src'; 39 | 40 | // Substitute version 41 | await substituteVersion(constantsPath, args.version); 42 | } 43 | 44 | main().catch((error) => { 45 | console.error(error); 46 | process.exitCode = 1; 47 | }); 48 | -------------------------------------------------------------------------------- /src/AudioDevice.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2022 Twilio, Inc. All rights reserved. Licensed under the Twilio 3 | * license. 4 | * 5 | * See LICENSE in the project root for license information. 6 | */ 7 | 8 | import { NativeModule } from './common'; 9 | import type { NativeAudioDeviceInfo } from './type/AudioDevice'; 10 | import type { Uuid } from './type/common'; 11 | 12 | /** 13 | * Describes audio devices as reported by the native layer and allows the 14 | * native selection of the described audio device. 15 | * 16 | * @remarks 17 | * To fetch a list of available audio devices and the currently selected audio 18 | * device, see {@link (Voice:class).getAudioDevices}. 19 | * 20 | * - See also the {@link (AudioDevice:namespace) | AudioDevice namespace} for 21 | * types used by this class. 22 | * 23 | * @public 24 | */ 25 | export class AudioDevice { 26 | /** 27 | * The native-UUID of this object. This is generated by the native layer and 28 | * is used to associate functionality between the JS and native layers. 29 | * 30 | * @internal 31 | */ 32 | uuid: Uuid; 33 | 34 | /** 35 | * The type of the audio device. 36 | */ 37 | type: AudioDevice.Type; 38 | 39 | /** 40 | * The name of the audio device. 41 | */ 42 | name: string; 43 | 44 | /** 45 | * Audio device class constructor. 46 | * @param audioDeviceInformation - A record describing an audio device. 47 | * 48 | * @internal 49 | */ 50 | constructor({ uuid, type, name }: NativeAudioDeviceInfo) { 51 | this.uuid = uuid; 52 | this.type = type; 53 | this.name = name; 54 | } 55 | 56 | /** 57 | * Calling this method will select this audio device as the active audio 58 | * device. 59 | * @returns 60 | * A `Promise` that 61 | * - Resolves with `void` when the audio device has been successfully 62 | * selected as the active audio device. 63 | * - Rejects if the audio device cannot be selected. 64 | */ 65 | select(): Promise { 66 | return NativeModule.voice_selectAudioDevice(this.uuid); 67 | } 68 | } 69 | 70 | /** 71 | * Contains interfaces and enumerations associated with audio devices. 72 | * 73 | * @remarks 74 | * - See also the {@link (AudioDevice:class) | AudioDevice class}. 75 | * 76 | * @public 77 | */ 78 | export namespace AudioDevice { 79 | /** 80 | * Audio device type enumeration. Describes all possible audio device types as 81 | * reportable by the native layer. 82 | */ 83 | export enum Type { 84 | Earpiece = 'earpiece', 85 | Speaker = 'speaker', 86 | Bluetooth = 'bluetooth', 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/CallMessage/CallMessage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2022 Twilio, Inc. All rights reserved. Licensed under the Twilio 3 | * license. 4 | * 5 | * See LICENSE in the project root for license information. 6 | */ 7 | 8 | import { InvalidArgumentError } from '../error/InvalidArgumentError'; 9 | 10 | /** 11 | * The constituent values of a Call Message. 12 | * 13 | * @public 14 | */ 15 | export interface CallMessage { 16 | /** 17 | * The content of the message. This value should match the content type 18 | * parameter. 19 | * 20 | * See {@link CallMessage.contentType} for more information. 21 | */ 22 | content: any; 23 | 24 | /** 25 | * The content type of the message. This value should accurately describe 26 | * the content of the message. The following values are accepted: 27 | * 28 | * - "application/json" 29 | * 30 | * If no value is defined, then the default value of "application/json" will 31 | * be used. 32 | * 33 | * If the `contentType` of the message is "application/json", the content 34 | * of the message may be a JS object. 35 | */ 36 | contentType?: string; 37 | 38 | /** 39 | * The message type. The following values are accepted: 40 | * 41 | * - "user-defined-message" 42 | */ 43 | messageType: string; 44 | } 45 | 46 | /** 47 | * Parse CallMessage values. Used when constructing a CallMessage from the 48 | * native layer, or by the Call and CallInvite classes when sending a 49 | * CallMessage. 50 | * 51 | * @param message the CallMessage details. 52 | * 53 | * @internal 54 | */ 55 | export function validateCallMessage(message: CallMessage) { 56 | const content = message.content; 57 | const messageType = message.messageType; 58 | 59 | let contentType = message.contentType; 60 | 61 | if (typeof contentType === 'undefined') { 62 | contentType = 'application/json'; 63 | } 64 | 65 | if (typeof contentType !== 'string') { 66 | throw new InvalidArgumentError( 67 | 'If "contentType" is present, it must be of type "string".' 68 | ); 69 | } 70 | 71 | if (typeof messageType !== 'string') { 72 | throw new InvalidArgumentError('"messageType" must be of type "string".'); 73 | } 74 | 75 | if (typeof content === 'undefined' || content === null) { 76 | throw new InvalidArgumentError('"content" must be defined and not "null".'); 77 | } 78 | 79 | const contentStr = 80 | typeof content === 'string' ? content : JSON.stringify(content); 81 | 82 | return { content: contentStr, contentType, messageType }; 83 | } 84 | -------------------------------------------------------------------------------- /src/CallMessage/IncomingCallMessage.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2022 Twilio, Inc. All rights reserved. Licensed under the Twilio 3 | * license. 4 | * 5 | * See LICENSE in the project root for license information. 6 | */ 7 | 8 | import { EventEmitter } from 'eventemitter3'; 9 | import type { NativeCallMessageInfo } from '../type/CallMessage'; 10 | import { validateCallMessage } from './CallMessage'; 11 | 12 | /** 13 | * CallMessage API is in beta. 14 | * 15 | * Provides access to information about a CallMessage, including the call 16 | * message content, content type, message type, and voice event SID. 17 | * 18 | * @public 19 | */ 20 | export class IncomingCallMessage extends EventEmitter { 21 | /** 22 | * The content of the message which should match the contentType parameter. 23 | */ 24 | private _content: any; 25 | 26 | /** 27 | * The MIME type of the content. 28 | */ 29 | private _contentType: string; 30 | 31 | /** 32 | * Message type 33 | */ 34 | private _messageType: string; 35 | 36 | /** 37 | * An autogenerated ID that uniquely identifies this message. 38 | * This is not required when sending a message from the SDK as this is 39 | * autogenerated. 40 | * The ID will be available after the message is sent, or immediately when a 41 | * message is received. 42 | */ 43 | private _voiceEventSid?: string; 44 | 45 | /** 46 | * Constructor for the {@link IncomingCallMessage} class. This should not be 47 | * invoked by third-party code. 48 | * 49 | * @param NativeCallMessageInfo - An object containing all of the data from 50 | * the native layer necessary to fully describe a call message, as well as 51 | * invoke native functionality for the call message. 52 | * 53 | * @internal 54 | */ 55 | constructor(callMessageInfo: NativeCallMessageInfo) { 56 | super(); 57 | 58 | const { content, contentType, messageType } = 59 | validateCallMessage(callMessageInfo); 60 | 61 | this._content = content; 62 | this._contentType = contentType; 63 | this._messageType = messageType; 64 | this._voiceEventSid = callMessageInfo.voiceEventSid; 65 | } 66 | 67 | /** 68 | * {@inheritdoc CallMessage.content} 69 | * 70 | * @returns the content of the call message. 71 | */ 72 | getContent(): any { 73 | return this._content; 74 | } 75 | 76 | /** 77 | * {@inheritdoc CallMessage.contentType} 78 | * 79 | * @returns the content type of the call message. 80 | */ 81 | getContentType(): string { 82 | return this._contentType; 83 | } 84 | 85 | /** 86 | * {@inheritdoc CallMessage.messageType} 87 | * 88 | * @returns the message type of the call message. 89 | */ 90 | getMessageType(): string { 91 | return this._messageType; 92 | } 93 | 94 | /** 95 | * Get the message SID. 96 | * @returns 97 | * - A string representing the message SID. 98 | * - `undefined` if the call information has not yet been received from the 99 | * native layer. 100 | */ 101 | getSid(): string | undefined { 102 | return this._voiceEventSid; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/__mocks__/AudioDevice.ts: -------------------------------------------------------------------------------- 1 | import type { AudioDevice } from '../AudioDevice'; 2 | import type { 3 | NativeAudioDeviceInfo, 4 | NativeAudioDevicesInfo, 5 | } from '../type/AudioDevice'; 6 | 7 | export function createNativeAudioDeviceInfo(): NativeAudioDeviceInfo { 8 | return { 9 | uuid: 'mock-nativeaudiodeviceinfo-uuid', 10 | type: 'earpiece' as AudioDevice.Type, 11 | name: 'mock-nativeaudiodeviceinfo-name', 12 | }; 13 | } 14 | 15 | export function createNativeAudioDevicesInfo(): NativeAudioDevicesInfo { 16 | return { 17 | audioDevices: [ 18 | { 19 | uuid: 'mock-nativeaudiodeviceinfo-uuid-one', 20 | type: 'earpiece' as AudioDevice.Type, 21 | name: 'mock-nativeaudiodeviceinfo-name-one', 22 | }, 23 | { 24 | uuid: 'mock-nativeaudiodeviceinfo-uuid-two', 25 | type: 'speaker' as AudioDevice.Type, 26 | name: 'mock-nativeaudiodeviceinfo-name-two', 27 | }, 28 | { 29 | uuid: 'mock-nativeaudiodeviceinfo-uuid-three', 30 | type: 'bluetooth' as AudioDevice.Type, 31 | name: 'mock-nativeaudiodeviceinfo-name-three', 32 | }, 33 | ], 34 | selectedDevice: { 35 | uuid: 'mock-nativeaudiodeviceinfo-uuid-two', 36 | type: 'speaker' as AudioDevice.Type, 37 | name: 'mock-nativeaudiodeviceinfo-name-two', 38 | }, 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/__mocks__/Call.ts: -------------------------------------------------------------------------------- 1 | import type { NativeCallInfo } from '../type/Call'; 2 | import { Constants } from '../constants'; 3 | import { createNativeErrorInfo } from './Error'; 4 | import { createNativeCallMessageInfo } from './CallMessage'; 5 | 6 | export function createNativeCallInfo(): NativeCallInfo { 7 | return { 8 | uuid: 'mock-nativecallinfo-uuid', 9 | customParameters: { 10 | 'mock-nativecallinfo-custom-parameter-key1': 11 | 'mock-nativecallinfo-custom-parameter-value1', 12 | 'mock-nativecallinfo-custom-parameter-key2': 13 | 'mock-nativecallinfo-custom-parameter-value2', 14 | }, 15 | from: 'mock-nativecallinfo-from', 16 | isMuted: false, 17 | initialConnectedTimestamp: '2024-02-07T16:31:47.498-0800', 18 | isOnHold: false, 19 | sid: 'mock-nativecallinfo-sid', 20 | to: 'mock-nativecallinfo-to', 21 | }; 22 | } 23 | 24 | /** 25 | * Reusable default native call events. 26 | */ 27 | export const mockCallNativeEvents = { 28 | connected: { 29 | name: Constants.CallEventConnected, 30 | nativeEvent: { 31 | type: Constants.CallEventConnected, 32 | call: createNativeCallInfo(), 33 | }, 34 | }, 35 | connectFailure: { 36 | name: Constants.CallEventConnectFailure, 37 | nativeEvent: { 38 | type: Constants.CallEventConnectFailure, 39 | call: createNativeCallInfo(), 40 | error: createNativeErrorInfo(), 41 | }, 42 | }, 43 | disconnected: { 44 | name: Constants.CallEventDisconnected, 45 | nativeEvent: { 46 | type: Constants.CallEventDisconnected, 47 | call: createNativeCallInfo(), 48 | }, 49 | }, 50 | disconnectedWithError: { 51 | name: `${Constants.CallEventDisconnected} with error`, 52 | nativeEvent: { 53 | type: Constants.CallEventDisconnected, 54 | call: createNativeCallInfo(), 55 | error: createNativeErrorInfo(), 56 | }, 57 | }, 58 | reconnected: { 59 | name: Constants.CallEventReconnected, 60 | nativeEvent: { 61 | type: Constants.CallEventReconnected, 62 | call: createNativeCallInfo(), 63 | }, 64 | }, 65 | reconnecting: { 66 | name: Constants.CallEventReconnecting, 67 | nativeEvent: { 68 | type: Constants.CallEventReconnecting, 69 | call: createNativeCallInfo(), 70 | error: createNativeErrorInfo(), 71 | }, 72 | }, 73 | ringing: { 74 | name: Constants.CallEventRinging, 75 | nativeEvent: { 76 | type: Constants.CallEventRinging, 77 | call: createNativeCallInfo(), 78 | }, 79 | }, 80 | qualityWarningsChanged: { 81 | name: Constants.CallEventQualityWarningsChanged, 82 | nativeEvent: { 83 | type: Constants.CallEventQualityWarningsChanged, 84 | call: createNativeCallInfo(), 85 | [Constants.CallEventCurrentWarnings]: [ 86 | 'mock-callqualitywarningschangedevent-nativeevent-currentwarnings', 87 | ], 88 | [Constants.CallEventPreviousWarnings]: [ 89 | 'mock-callqualitywarningschangedevent-nativeevent-previouswarnings', 90 | ], 91 | }, 92 | }, 93 | messageReceived: { 94 | name: Constants.CallEventMessageReceived, 95 | nativeEvent: { 96 | type: Constants.CallEventMessageReceived, 97 | call: createNativeCallInfo(), 98 | callMessage: createNativeCallMessageInfo(), 99 | }, 100 | }, 101 | }; 102 | -------------------------------------------------------------------------------- /src/__mocks__/CallInvite.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | NativeCallInviteInfo, 3 | NativeCancelledCallInviteInfo, 4 | } from '../type/CallInvite'; 5 | import { Constants } from '../constants'; 6 | import { createNativeErrorInfo } from './Error'; 7 | import { createNativeCallMessageInfo } from './CallMessage'; 8 | 9 | export function createNativeCallInviteInfo(): NativeCallInviteInfo { 10 | return { 11 | uuid: 'mock-nativecallinviteinfo-uuid', 12 | callSid: 'mock-nativecallinviteinfo-callsid', 13 | customParameters: { 14 | 'mock-nativecallinviteinfo-custom-parameter-key1': 15 | 'mock-nativecallinviteinfo-custom-parameter-value1', 16 | 'mock-nativecallinviteinfo-custom-parameter-key2': 17 | 'mock-nativecallinviteinfo-custom-parameter-value2', 18 | }, 19 | from: 'mock-nativecallinviteinfo-from', 20 | to: 'mock-nativecallinviteinfo-to', 21 | }; 22 | } 23 | 24 | export function createNativeCancelledCallInviteInfo(): NativeCancelledCallInviteInfo { 25 | return { 26 | callSid: 'mock-nativecallinviteinfo-callsid', 27 | from: 'mock-nativecallinviteinfo-from', 28 | to: 'mock-nativecallinviteinfo-to', 29 | }; 30 | } 31 | 32 | /** 33 | * Reusable default native callInvite events. 34 | */ 35 | export function createMockNativeCallInviteEvents() { 36 | return { 37 | accepted: { 38 | [Constants.CallInviteEventKeyType]: 39 | Constants.CallInviteEventTypeValueAccepted, 40 | [Constants.CallInviteEventKeyCallSid]: 41 | 'mock-nativecallinviteinfo-callsid', 42 | callInvite: createNativeCallInviteInfo(), 43 | }, 44 | notificationTapped: { 45 | [Constants.CallInviteEventKeyType]: 46 | Constants.CallInviteEventTypeValueNotificationTapped, 47 | [Constants.CallInviteEventKeyCallSid]: 48 | 'mock-nativecallinviteinfo-callsid', 49 | callInvite: createNativeCallInviteInfo(), 50 | }, 51 | rejected: { 52 | [Constants.CallInviteEventKeyType]: 53 | Constants.CallInviteEventTypeValueRejected, 54 | [Constants.CallInviteEventKeyCallSid]: 55 | 'mock-nativecallinviteinfo-callsid', 56 | callInvite: createNativeCallInviteInfo(), 57 | }, 58 | cancelled: { 59 | [Constants.CallInviteEventKeyType]: 60 | Constants.CallInviteEventTypeValueCancelled, 61 | [Constants.CallInviteEventKeyCallSid]: 62 | 'mock-nativecallinviteinfo-callsid', 63 | cancelledCallInvite: createNativeCancelledCallInviteInfo(), 64 | }, 65 | cancelledWithError: { 66 | [Constants.CallInviteEventKeyType]: 67 | Constants.CallInviteEventTypeValueCancelled, 68 | [Constants.CallInviteEventKeyCallSid]: 69 | 'mock-nativecallinviteinfo-callsid', 70 | cancelledCallInvite: createNativeCancelledCallInviteInfo(), 71 | error: createNativeErrorInfo(), 72 | }, 73 | messageReceived: { 74 | [Constants.CallInviteEventKeyType]: Constants.CallEventMessageReceived, 75 | [Constants.CallInviteEventKeyCallSid]: 76 | 'mock-nativecallinviteinfo-callsid', 77 | callMessage: createNativeCallMessageInfo(), 78 | }, 79 | } as const; 80 | } 81 | -------------------------------------------------------------------------------- /src/__mocks__/CallMessage.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from '../constants'; 2 | import type { NativeCallMessageInfo } from '../type/CallMessage'; 3 | import { createNativeErrorInfo } from './Error'; 4 | 5 | export function createNativeCallMessageInfo(): NativeCallMessageInfo { 6 | return { 7 | content: { key1: 'mock-nativecallmessageinfo-content' }, 8 | contentType: 'application/json', 9 | messageType: 'user-defined-message', 10 | voiceEventSid: 'mock-nativecallmessageinfo-voiceEventSid', 11 | }; 12 | } 13 | 14 | export function createNativeCallMessageInfoSid( 15 | voiceEventSid: string 16 | ): NativeCallMessageInfo { 17 | return { 18 | content: { key1: 'mock-nativecallmessageinfo-content' }, 19 | contentType: 'application/json', 20 | messageType: 'user-defined-message', 21 | voiceEventSid, 22 | }; 23 | } 24 | 25 | export const mockCallMessageNativeEvents = { 26 | failure: { 27 | name: Constants.CallEventMessageFailure, 28 | nativeEvent: { 29 | type: Constants.CallEventMessageFailure, 30 | [Constants.VoiceEventSid]: '123', 31 | error: createNativeErrorInfo(), 32 | }, 33 | }, 34 | sent: { 35 | name: Constants.CallEventMessageSent, 36 | nativeEvent: { 37 | type: Constants.CallEventMessageSent, 38 | [Constants.VoiceEventSid]: '456', 39 | }, 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /src/__mocks__/Error.ts: -------------------------------------------------------------------------------- 1 | export function createNativeErrorInfo() { 2 | return { code: 0, message: 'mock-error-message' }; 3 | } 4 | -------------------------------------------------------------------------------- /src/__mocks__/RTCStats.ts: -------------------------------------------------------------------------------- 1 | import { RTCStats } from '../type/RTCStats'; 2 | 3 | export function createIceCandidatePairStats(): RTCStats.IceCandidatePairStats { 4 | return { 5 | activeCandidatePair: false, 6 | availableIncomingBitrate: 0, 7 | availableOutgoingBitrate: 0, 8 | bytesReceived: 0, 9 | bytesSent: 0, 10 | consentRequestsReceived: 0, 11 | consentRequestsSent: 0, 12 | consentResponsesReceived: 0, 13 | consentResponsesSent: 0, 14 | currentRoundTripTime: 0, 15 | localCandidateId: 'mock-statsreport-localcandidateid', 16 | localCandidateIp: 'mock-statsreport-localcandidateip', 17 | nominated: false, 18 | priority: 0, 19 | readable: false, 20 | relayProtocol: 'mock-statsreport-relayprotocol', 21 | remoteCandidateId: 'mock-statsreport-remotecandidateid', 22 | remoteCandidateIp: 'mock-statsreport-remotecandidateip', 23 | requestsReceieved: 0, 24 | requestsSent: 0, 25 | responsesRecieved: 0, 26 | responsesSent: 0, 27 | retransmissionsReceived: 0, 28 | retransmissionsSent: 0, 29 | state: RTCStats.IceCandidatePairState.STATE_IN_PROGRESS, 30 | totalRoundTripTime: 0, 31 | transportId: 'mock-statsreport-transportid', 32 | writeable: false, 33 | }; 34 | } 35 | 36 | export function createIceCandidateStats(): RTCStats.IceCandidateStats { 37 | return { 38 | candidateType: 'mock-icecandidatestats-candidatetype', 39 | deleted: false, 40 | ip: 'mock-icecandidatestats-ip', 41 | isRemote: false, 42 | port: 0, 43 | priority: 0, 44 | protocol: 'mock-icecandidatestats-protocol', 45 | transportId: 'mock-icecandidatestats-transportid', 46 | url: 'mock-icecandidatestats-url', 47 | }; 48 | } 49 | 50 | export function createLocalAudioTrackStats(): RTCStats.LocalAudioTrackStats { 51 | return { 52 | codec: 'mock-localaudiotrackstats-codec', 53 | packetsLost: 0, 54 | ssrc: 'mock-localaudiotrackstats-ssrc', 55 | timestamp: 0, 56 | trackId: 'mock-localaudiotrackstats-trackid', 57 | bytesSent: 0, 58 | packetsSent: 0, 59 | roundTripTime: 0, 60 | audioLevel: 0, 61 | jitter: 0, 62 | }; 63 | } 64 | 65 | export function createRemoteAudioTrackStats(): RTCStats.RemoteAudioTrackStats { 66 | return { 67 | codec: 'mock-remoteaudiotrackstats-codec', 68 | packetsLost: 0, 69 | ssrc: 'mock-remoteaudiotrackstats-ssrc', 70 | timestamp: 0, 71 | trackId: 'mock-remoteaudiotrackstats-trackid', 72 | bytesRecieved: 0, 73 | packetsReceived: 0, 74 | audioLevel: 0, 75 | jitter: 0, 76 | mos: 0, 77 | }; 78 | } 79 | 80 | export function createStatsReport(): RTCStats.StatsReport { 81 | return { 82 | iceCandidatePairStats: [createIceCandidatePairStats()], 83 | iceCandidateStats: [createIceCandidateStats()], 84 | localAudioTrackStats: [createLocalAudioTrackStats()], 85 | peerConnectionId: 'mock-statsreport-peerconnectionid', 86 | remoteAudioTrackStats: [createRemoteAudioTrackStats()], 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /src/__mocks__/Voice.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from '../constants'; 2 | import { createNativeAudioDevicesInfo } from './AudioDevice'; 3 | import { createNativeCallInviteInfo } from './CallInvite'; 4 | import { createNativeErrorInfo } from './Error'; 5 | 6 | /** 7 | * Reusable default native call events. 8 | */ 9 | export const mockVoiceNativeEvents = { 10 | audioDevicesUpdated: { 11 | name: Constants.VoiceEventAudioDevicesUpdated, 12 | nativeEvent: { 13 | type: Constants.VoiceEventAudioDevicesUpdated, 14 | ...createNativeAudioDevicesInfo(), 15 | }, 16 | }, 17 | callInvite: { 18 | name: Constants.VoiceEventTypeValueIncomingCallInvite, 19 | nativeEvent: { 20 | type: Constants.VoiceEventTypeValueIncomingCallInvite, 21 | callInvite: createNativeCallInviteInfo(), 22 | }, 23 | }, 24 | error: { 25 | name: Constants.VoiceEventError, 26 | nativeEvent: { 27 | type: Constants.VoiceEventError, 28 | error: createNativeErrorInfo(), 29 | }, 30 | }, 31 | registered: { 32 | name: Constants.VoiceEventRegistered, 33 | nativeEvent: { 34 | type: Constants.VoiceEventRegistered, 35 | }, 36 | }, 37 | unregistered: { 38 | name: Constants.VoiceEventUnregistered, 39 | nativeEvent: { 40 | type: Constants.VoiceEventUnregistered, 41 | }, 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/__mocks__/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used by Jest manual mocking and meant to be utilized by 3 | * perfomring `jest.mock('../common')` in test files. 4 | */ 5 | 6 | import { EventEmitter } from 'eventemitter3'; 7 | import type { Uuid } from '../type/common'; 8 | import { createNativeAudioDevicesInfo } from './AudioDevice'; 9 | import { createNativeCallInfo } from './Call'; 10 | import { createNativeCallInviteInfo } from './CallInvite'; 11 | import { createStatsReport } from './RTCStats'; 12 | 13 | export const NativeModule = { 14 | /** 15 | * Call Mocks 16 | */ 17 | call_disconnect: jest.fn().mockResolvedValue(undefined), 18 | call_getStats: jest.fn().mockResolvedValue(createStatsReport()), 19 | call_hold: jest.fn((_uuid: Uuid, hold: boolean) => Promise.resolve(hold)), 20 | call_isMuted: jest.fn().mockResolvedValue(false), 21 | call_isOnHold: jest.fn().mockResolvedValue(false), 22 | call_mute: jest.fn((_uuid: Uuid, mute: boolean) => Promise.resolve(mute)), 23 | call_postFeedback: jest.fn().mockResolvedValue(undefined), 24 | call_sendDigits: jest.fn().mockResolvedValue(undefined), 25 | call_sendMessage: jest 26 | .fn() 27 | .mockResolvedValue('mock-nativemodule-tracking-id'), 28 | 29 | /** 30 | * Call Invite Mocks 31 | */ 32 | callInvite_accept: jest.fn().mockResolvedValue(createNativeCallInfo()), 33 | callInvite_isValid: jest.fn().mockResolvedValue(false), 34 | callInvite_reject: jest.fn().mockResolvedValue(undefined), 35 | callInvite_updateCallerHandle: jest.fn().mockResolvedValue(undefined), 36 | 37 | /** 38 | * Voice Mocks 39 | */ 40 | voice_connect_android: jest.fn().mockResolvedValue(createNativeCallInfo()), 41 | voice_connect_ios: jest.fn().mockResolvedValue(createNativeCallInfo()), 42 | voice_getAudioDevices: jest 43 | .fn() 44 | .mockResolvedValue(createNativeAudioDevicesInfo()), 45 | voice_getCalls: jest.fn().mockResolvedValue([createNativeCallInfo()]), 46 | voice_getCallInvites: jest 47 | .fn() 48 | .mockResolvedValue([createNativeCallInviteInfo()]), 49 | voice_getDeviceToken: jest 50 | .fn() 51 | .mockResolvedValue('mock-nativemodule-devicetoken'), 52 | voice_getVersion: jest.fn().mockResolvedValue('mock-nativemodule-version'), 53 | voice_handleEvent: jest.fn().mockResolvedValue(true), 54 | voice_initializePushRegistry: jest.fn().mockResolvedValue(undefined), 55 | voice_register: jest.fn().mockResolvedValue(undefined), 56 | voice_selectAudioDevice: jest.fn().mockResolvedValue(undefined), 57 | voice_setCallKitConfiguration: jest.fn().mockResolvedValue(undefined), 58 | voice_showNativeAvRoutePicker: jest.fn().mockResolvedValue(undefined), 59 | voice_setIncomingCallContactHandleTemplate: jest 60 | .fn() 61 | .mockResolvedValue(undefined), 62 | voice_unregister: jest.fn().mockResolvedValue(undefined), 63 | 64 | system_isFullScreenNotificationEnabled: jest 65 | .fn() 66 | .mockResolvedValue(undefined), 67 | system_requestFullScreenNotificationPermission: jest 68 | .fn() 69 | .mockResolvedValue(undefined), 70 | }; 71 | 72 | export class MockNativeEventEmitter extends EventEmitter { 73 | addListenerSpies: [string | symbol, jest.Mock][] = []; 74 | 75 | addListener = jest.fn( 76 | (event: string | symbol, fn: (...args: any[]) => void, context?: any) => { 77 | const spy = jest.fn(fn); 78 | super.addListener(event, spy, context); 79 | this.addListenerSpies.push([event, spy]); 80 | return this; 81 | } 82 | ); 83 | 84 | expectListenerAndReturnSpy( 85 | invocation: number, 86 | event: string | symbol, 87 | fn: (...args: any[]) => void 88 | ) { 89 | expect(invocation).toBeGreaterThanOrEqual(0); 90 | expect(invocation).toBeLessThan(this.addListenerSpies.length); 91 | 92 | const [spyEvent, spyFn] = this.addListenerSpies[invocation]; 93 | expect(event).toBe(spyEvent); 94 | expect(fn).toEqual(spyFn.getMockImplementation()); 95 | 96 | return spyFn; 97 | } 98 | 99 | reset() { 100 | this.addListenerSpies = []; 101 | this.removeAllListeners(); 102 | } 103 | } 104 | 105 | export const NativeEventEmitter = new MockNativeEventEmitter(); 106 | 107 | class MockPlatform { 108 | get OS() { 109 | return 'uninitialized'; 110 | } 111 | } 112 | 113 | export const Platform = new MockPlatform(); 114 | -------------------------------------------------------------------------------- /src/__tests__/AudioDevice.test.ts: -------------------------------------------------------------------------------- 1 | import { createNativeAudioDeviceInfo } from '../__mocks__/AudioDevice'; 2 | import { AudioDevice } from '../AudioDevice'; 3 | import { NativeModule } from '../common'; 4 | 5 | const MockNativeModule = jest.mocked(NativeModule); 6 | 7 | jest.mock('../common'); 8 | 9 | let audioDevice: AudioDevice; 10 | 11 | beforeEach(() => { 12 | jest.clearAllMocks(); 13 | 14 | audioDevice = new AudioDevice(createNativeAudioDeviceInfo()); 15 | }); 16 | 17 | describe('AudioDevice class', () => { 18 | describe('data members', () => { 19 | describe('.type', () => { 20 | it('contains the type of the AudioDevice', () => { 21 | expect(audioDevice.type).toBe(createNativeAudioDeviceInfo().type); 22 | }); 23 | }); 24 | 25 | describe('.uuid', () => { 26 | it('contains the uuid of the AudioDevice', () => { 27 | expect(audioDevice.uuid).toBe(createNativeAudioDeviceInfo().uuid); 28 | }); 29 | }); 30 | 31 | describe('.name', () => { 32 | it('contains the name of the AudioDevice', () => { 33 | expect(audioDevice.name).toBe(createNativeAudioDeviceInfo().name); 34 | }); 35 | }); 36 | }); 37 | 38 | describe('methods', () => { 39 | describe('.select', () => { 40 | it('invokes the native module', async () => { 41 | await audioDevice.select(); 42 | expect( 43 | jest.mocked(MockNativeModule.voice_selectAudioDevice).mock.calls 44 | ).toEqual([[createNativeAudioDeviceInfo().uuid]]); 45 | }); 46 | 47 | it('returns a Promise', async () => { 48 | const selectPromise = audioDevice.select(); 49 | await expect(selectPromise).resolves.toBeUndefined(); 50 | }); 51 | }); 52 | }); 53 | }); 54 | 55 | describe('AudioDevice namespace', () => { 56 | describe('exports enumerations', () => { 57 | it('Type', () => { 58 | expect(AudioDevice.Type).toBeDefined(); 59 | expect(typeof AudioDevice.Type).toBe('object'); 60 | }); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /src/__tests__/CallMessage/CallMessage.test.ts: -------------------------------------------------------------------------------- 1 | import { validateCallMessage } from '../../CallMessage/CallMessage'; 2 | 3 | describe('validateCallMessage', () => { 4 | it('should return a valid message', () => { 5 | const result = validateCallMessage({ 6 | content: { foo: 'bar' }, 7 | contentType: 'application/json', 8 | messageType: 'user-defined-message', 9 | }); 10 | expect(result).toStrictEqual({ 11 | content: '{"foo":"bar"}', 12 | contentType: 'application/json', 13 | messageType: 'user-defined-message', 14 | }); 15 | }); 16 | 17 | it('should default "contentType" to "application/json"', () => { 18 | const result = validateCallMessage({ 19 | content: { foo: 'bar' }, 20 | messageType: 'user-defined-message', 21 | }); 22 | expect(result.contentType).toStrictEqual('application/json'); 23 | }); 24 | 25 | it('should throw an error if content is null', () => { 26 | const invalidMessage = { 27 | content: null, 28 | contentType: 'application/json', 29 | messageType: 'user-defined-message', 30 | }; 31 | expect(() => validateCallMessage(invalidMessage)).toThrow( 32 | '"content" must be defined and not "null".' 33 | ); 34 | }); 35 | 36 | it('should throw an error if content is undefined', () => { 37 | const invalidMessage = { 38 | content: undefined, 39 | contentType: 'application/json', 40 | messageType: 'user-defined-message', 41 | }; 42 | expect(() => validateCallMessage(invalidMessage)).toThrow( 43 | '"content" must be defined and not "null".' 44 | ); 45 | }); 46 | 47 | it('should throw an error if the messageType is not a string', () => { 48 | const invalidMessageTypes = [undefined, 10, null, {}, true]; 49 | expect.assertions(invalidMessageTypes.length); 50 | 51 | const testMessageType = (messageType: any) => { 52 | const invalidMessage = { 53 | content: { foo: 'bar' }, 54 | contentType: 'application/json', 55 | messageType, 56 | }; 57 | expect(() => validateCallMessage(invalidMessage)).toThrow( 58 | '"messageType" must be of type "string".' 59 | ); 60 | }; 61 | 62 | invalidMessageTypes.forEach(testMessageType); 63 | }); 64 | 65 | it('should throw an error if the contentType is defined and not a string', () => { 66 | const invalidContentTypes = [10, null, {}, true]; 67 | 68 | const testContentType = (contentType: any) => { 69 | const invalidMessage = { 70 | content: { foo: 'bar' }, 71 | contentType, 72 | messageType: 'user-defined-message', 73 | }; 74 | expect(() => validateCallMessage(invalidMessage)).toThrow( 75 | 'If "contentType" is present, it must be of type "string".' 76 | ); 77 | }; 78 | 79 | invalidContentTypes.forEach(testContentType); 80 | }); 81 | 82 | it('should stringify content that is not a string', () => { 83 | const nonStringContent = [ 84 | [{ foo: 'bar' }, '{"foo":"bar"}'], 85 | [10, '10'], 86 | [true, 'true'], 87 | ]; 88 | expect.assertions(nonStringContent.length); 89 | 90 | for (const [inputContent, expectedContent] of nonStringContent) { 91 | const { content: validatedContent } = validateCallMessage({ 92 | content: inputContent, 93 | messageType: 'foobar', 94 | }); 95 | 96 | expect(validatedContent).toStrictEqual(expectedContent); 97 | } 98 | }); 99 | 100 | it('should not stringify content that is a string', () => { 101 | const { content } = validateCallMessage({ 102 | content: 'foobar', 103 | messageType: 'foobar', 104 | }); 105 | expect(content).toStrictEqual('foobar'); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/__tests__/RTCStats.test.ts: -------------------------------------------------------------------------------- 1 | import { RTCStats } from '../type/RTCStats'; 2 | 3 | describe('RTCStats', () => { 4 | describe('IceCandidatePairState', () => { 5 | it('is exported an enumeration', () => { 6 | expect(RTCStats.IceCandidatePairState).toBeDefined(); 7 | expect(typeof RTCStats.IceCandidatePairState).toBe('object'); 8 | }); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /src/__tests__/constants.test.ts: -------------------------------------------------------------------------------- 1 | import { Constants } from '../constants'; 2 | 3 | describe('Constants', () => { 4 | it('is exported as an enumeration', () => { 5 | expect(Constants).toBeDefined(); 6 | expect(typeof Constants).toBe('object'); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /src/__tests__/error/generated.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ============================================================================= 3 | * NOTE(mhuynh): if tests fail in this file, ensure that errors have been 4 | * generated. Try `yarn run build:errors`. 5 | * ============================================================================= 6 | */ 7 | 8 | import { errorsByCode } from '../../error/generated'; 9 | import { TwilioErrors } from '../../index'; 10 | 11 | jest.mock('../../common'); 12 | 13 | /** 14 | * NOTE(mhuynh): We can import the USED_ERRORS array from the generation script 15 | * to check that the generated errors actually contain all the expected/desired 16 | * errors. 17 | * 18 | * We need to ts-ignore the import since the file is plain JS. 19 | */ 20 | // @ts-ignore 21 | import { ERRORS } from '../../../scripts/errors.js'; 22 | const TYPED_ERRORS = ERRORS as string[]; 23 | 24 | const getNamesFromExport = () => { 25 | return Object.values(TwilioErrors).reduce<{ 26 | namespaced: string[]; 27 | nonNamespaced: string[]; 28 | }>( 29 | (reduction, errorOrNamespace) => { 30 | switch (typeof errorOrNamespace) { 31 | case 'function': 32 | return { 33 | ...reduction, 34 | nonNamespaced: [...reduction.nonNamespaced, errorOrNamespace.name], 35 | }; 36 | case 'object': 37 | const errorNames = Object.values(errorOrNamespace).map( 38 | (errorConstructorOrConstant) => { 39 | switch (typeof errorConstructorOrConstant) { 40 | case 'function': 41 | return errorConstructorOrConstant.name; 42 | } 43 | } 44 | ); 45 | return { 46 | ...reduction, 47 | namespaced: [...reduction.namespaced, ...errorNames], 48 | }; 49 | } 50 | }, 51 | { 52 | nonNamespaced: [], 53 | namespaced: [], 54 | } 55 | ); 56 | }; 57 | 58 | describe('generated errors', () => { 59 | it('contains all the expected error classes', () => { 60 | const ErrorNamespaces = Object.entries(TwilioErrors).filter(([k]) => { 61 | return k !== 'errorsByCode'; 62 | }); 63 | 64 | const generatedErrorNames = ErrorNamespaces.flatMap( 65 | ([namespace, namespaceErrors]) => { 66 | return Object.keys(namespaceErrors).flatMap((errorName) => { 67 | return `${namespace}.${errorName}`; 68 | }); 69 | } 70 | ); 71 | 72 | expect(generatedErrorNames.sort()).toStrictEqual(TYPED_ERRORS.sort()); 73 | }); 74 | 75 | for (const [code, ErrorClass] of errorsByCode.entries()) { 76 | describe(`${ErrorClass.name} - ${code}`, () => { 77 | it('constructs', () => { 78 | expect(() => new ErrorClass('foobar')).not.toThrow(); 79 | }); 80 | 81 | it('defaults the message to the explanation', () => { 82 | let error: InstanceType | null = null; 83 | expect( 84 | () => (error = new (ErrorClass as any)(undefined)) 85 | ).not.toThrow(); 86 | expect(error).not.toBeNull(); 87 | const msg = `${error!.name} (${error!.code}): ${error!.explanation}`; 88 | expect(error!.message).toBe(msg); 89 | }); 90 | }); 91 | } 92 | }); 93 | 94 | describe('errorsByCode', () => { 95 | it('is a Map', () => { 96 | expect(errorsByCode).toBeInstanceOf(Map); 97 | }); 98 | 99 | it('contains "undefined" for an error code that does not exist', () => { 100 | expect(errorsByCode.get(999999)).toBeUndefined(); 101 | }); 102 | 103 | it('contains an entry for every exported error', () => { 104 | const namesFromMap = Array.from(errorsByCode.values()).map( 105 | (errorConstructor) => errorConstructor.name 106 | ); 107 | 108 | expect(namesFromMap.sort()).toStrictEqual( 109 | getNamesFromExport().namespaced.sort() 110 | ); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /src/__tests__/error/index.test.ts: -------------------------------------------------------------------------------- 1 | import { TwilioError } from '../../error/TwilioError'; 2 | import { InvalidArgumentError } from '../../error/InvalidArgumentError'; 3 | import { InvalidStateError } from '../../error/InvalidStateError'; 4 | 5 | describe('TwilioError', () => { 6 | it('sets an error name', () => { 7 | const error = new TwilioError('mock-error-message'); 8 | expect(error.name).toBe('TwilioError'); 9 | }); 10 | 11 | it.each([[undefined], [0]])('sets a code "%o"', (code) => { 12 | const error = new TwilioError('mock-error-message', code); 13 | expect(error.code).toBe(code); 14 | }); 15 | 16 | it('properly sets the prototype', () => { 17 | const error = new TwilioError('mock-error-message'); 18 | expect(error).toBeInstanceOf(TwilioError); 19 | }); 20 | }); 21 | 22 | [InvalidStateError, InvalidArgumentError].forEach((ErrorConstructor) => { 23 | describe(ErrorConstructor, () => { 24 | it('sets an error name', () => { 25 | const error = new ErrorConstructor('mock-error-message'); 26 | expect(error.name).toBe(ErrorConstructor.name); 27 | }); 28 | 29 | it('properly sets the prototype', () => { 30 | const error = new ErrorConstructor('mock-error-message'); 31 | expect(error).toBeInstanceOf(ErrorConstructor); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/__tests__/error/utility.test.ts: -------------------------------------------------------------------------------- 1 | import { constructTwilioError } from '../../error/utility'; 2 | 3 | let MockInvalidArgumentError: jest.Mock; 4 | let mockGetErrorsByCode: { get: jest.Mock }; 5 | let MockTwilioError: jest.Mock; 6 | 7 | jest.mock('../../error/InvalidArgumentError', () => ({ 8 | InvalidArgumentError: (MockInvalidArgumentError = jest.fn()), 9 | })); 10 | jest.mock('../../error/generated', () => ({ 11 | errorsByCode: (mockGetErrorsByCode = { get: jest.fn() }), 12 | })); 13 | jest.mock('../../error/TwilioError', () => ({ 14 | TwilioError: (MockTwilioError = jest.fn()), 15 | })); 16 | 17 | beforeEach(() => { 18 | jest.clearAllMocks(); 19 | mockGetErrorsByCode.get.mockReset(); 20 | }); 21 | 22 | describe('constructTwilioError', () => { 23 | it('should throw if passed an invalid message', () => { 24 | expect(() => (constructTwilioError as any)(null)).toThrowError( 25 | MockInvalidArgumentError 26 | ); 27 | 28 | expect(MockInvalidArgumentError.mock.calls).toEqual([ 29 | ['The "message" argument is not of type "string".'], 30 | ]); 31 | }); 32 | 33 | it('should throw if passed an invalid code', () => { 34 | expect(() => (constructTwilioError as any)('foobar', null)).toThrowError( 35 | MockInvalidArgumentError 36 | ); 37 | 38 | expect(MockInvalidArgumentError.mock.calls).toEqual([ 39 | ['The "code" argument is not of type "number".'], 40 | ]); 41 | }); 42 | 43 | it('should construct a mapped error code', () => { 44 | const MockError = jest.fn(); 45 | mockGetErrorsByCode.get.mockImplementation(() => MockError); 46 | const message = 'foobar-error-message'; 47 | const code = 99999; 48 | const error = constructTwilioError(message, code); 49 | expect(error).toBeInstanceOf(MockError); 50 | expect(MockError.mock.calls).toEqual([[message]]); 51 | }); 52 | 53 | it('should construct the default TwilioError', () => { 54 | mockGetErrorsByCode.get.mockImplementation(() => undefined); 55 | const message = 'foobar-error-message'; 56 | const code = 99999; 57 | const error = constructTwilioError(message, code); 58 | expect(error).toBeInstanceOf(MockTwilioError); 59 | expect(MockTwilioError.mock.calls).toEqual([[message, code]]); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2022 Twilio, Inc. All rights reserved. Licensed under the Twilio 3 | * license. 4 | * 5 | * See LICENSE in the project root for license information. 6 | */ 7 | 8 | import * as ReactNative from 'react-native'; 9 | import type { TwilioVoiceReactNative as TwilioVoiceReactNativeType } from './type/NativeModule'; 10 | 11 | export const NativeModule = ReactNative.NativeModules 12 | .TwilioVoiceReactNative as TwilioVoiceReactNativeType; 13 | export const NativeEventEmitter = new ReactNative.NativeEventEmitter( 14 | NativeModule 15 | ); 16 | export const Platform = ReactNative.Platform; 17 | -------------------------------------------------------------------------------- /src/error/InvalidArgumentError.ts: -------------------------------------------------------------------------------- 1 | import { TwilioError } from './TwilioError'; 2 | 3 | /** 4 | * Error describing that an SDK function is invoked with an invalid argument. 5 | * 6 | * @public 7 | */ 8 | export class InvalidArgumentError extends TwilioError { 9 | description: string = 'Invalid argument error.'; 10 | explanation: string = 11 | 'The SDK has encountered a situation where invalid arguments were passed.'; 12 | 13 | constructor(message: string) { 14 | super(message); 15 | 16 | Object.setPrototypeOf(this, InvalidArgumentError.prototype); 17 | this.name = InvalidArgumentError.name; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/error/InvalidStateError.ts: -------------------------------------------------------------------------------- 1 | import { TwilioError } from './TwilioError'; 2 | 3 | /** 4 | * Error describing that the SDK has entered or is attempting to enter an 5 | * invalid state. 6 | * 7 | * @public 8 | */ 9 | export class InvalidStateError extends TwilioError { 10 | description: string = 'Invalid state error.'; 11 | explanation: string = 'The SDK has entered an invalid state.'; 12 | 13 | constructor(message: string) { 14 | super(message); 15 | 16 | Object.setPrototypeOf(this, InvalidStateError.prototype); 17 | this.name = InvalidStateError.name; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/error/TwilioError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generic Twilio error that the SDK will raise when encountering an error. Can 3 | * be used to describe backend errors. 4 | * 5 | * @public 6 | */ 7 | export class TwilioError extends Error { 8 | causes: string[] = []; 9 | code: number | undefined; 10 | description: string = 'Generic Twilio error.'; 11 | explanation: string = 'The SDK has encountered an unexpected error.'; 12 | solutions: string[] = []; 13 | 14 | constructor(message: string, code?: number) { 15 | super(message); 16 | 17 | this.code = code; 18 | 19 | Object.setPrototypeOf(this, TwilioError.prototype); 20 | this.name = TwilioError.name; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/error/UnsupportedPlatformError.ts: -------------------------------------------------------------------------------- 1 | import { TwilioError } from './TwilioError'; 2 | 3 | /** 4 | * Error describing that the an unsupported platform other than Android 5 | * or iOS has been detected. 6 | * 7 | * @public 8 | */ 9 | export class UnsupportedPlatformError extends TwilioError { 10 | description: string = 'Unsupported platform error.'; 11 | explanation: string = 'An unsupported platform has been detected.'; 12 | 13 | constructor(message: string) { 14 | super(message); 15 | 16 | Object.setPrototypeOf(this, UnsupportedPlatformError.prototype); 17 | this.name = UnsupportedPlatformError.name; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/error/index.ts: -------------------------------------------------------------------------------- 1 | export { InvalidArgumentError } from './InvalidArgumentError'; 2 | export { InvalidStateError } from './InvalidStateError'; 3 | export { UnsupportedPlatformError } from './UnsupportedPlatformError'; 4 | export { TwilioError } from './TwilioError'; 5 | export { 6 | AuthorizationErrors, 7 | ClientErrors, 8 | ForbiddenErrors, 9 | GeneralErrors, 10 | MalformedRequestErrors, 11 | MediaErrors, 12 | RegistrationErrors, 13 | ServerErrors, 14 | SignalingErrors, 15 | SIPServerErrors, 16 | TwiMLErrors, 17 | UserMediaErrors, 18 | } from './generated'; 19 | -------------------------------------------------------------------------------- /src/error/utility.ts: -------------------------------------------------------------------------------- 1 | import { errorsByCode } from './generated'; 2 | import { TwilioError } from './TwilioError'; 3 | import { InvalidArgumentError } from './InvalidArgumentError'; 4 | 5 | /** 6 | * Uses the generated error-code map to create the appropriate error. 7 | * If the code is "unexpected" such that there is no constructor for that 8 | * specific code, this function will default to a generic {@link TwilioError}. 9 | * 10 | * @param message an error message 11 | * @param code a Twilio error code, for example `31209` 12 | * 13 | * @returns a {@link TwilioError} or appropriate sub-class 14 | */ 15 | export function constructTwilioError( 16 | message: string, 17 | code: number 18 | ): TwilioError { 19 | if (typeof message !== 'string') { 20 | throw new InvalidArgumentError( 21 | 'The "message" argument is not of type "string".' 22 | ); 23 | } 24 | 25 | if (typeof code !== 'number') { 26 | throw new InvalidArgumentError( 27 | 'The "code" argument is not of type "number".' 28 | ); 29 | } 30 | 31 | const ErrorClass = errorsByCode.get(code); 32 | 33 | return typeof ErrorClass !== 'undefined' 34 | ? new ErrorClass(message) 35 | : new TwilioError(message, code); 36 | } 37 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright © 2022 Twilio, Inc. All rights reserved. Licensed under the Twilio 2 | // license. 3 | 4 | // See LICENSE in the project root for license information. 5 | 6 | /** 7 | * Provides access to Twilio Programmable Voice for React Native applications 8 | * running on iOS and Android devices. 9 | * 10 | * @packageDocumentation 11 | */ 12 | export { Voice } from './Voice'; 13 | export { AudioDevice } from './AudioDevice'; 14 | export { Call } from './Call'; 15 | export { CallInvite } from './CallInvite'; 16 | export { CallMessage } from './CallMessage/CallMessage'; 17 | export { IncomingCallMessage } from './CallMessage/IncomingCallMessage'; 18 | export { OutgoingCallMessage } from './CallMessage/OutgoingCallMessage'; 19 | export { CustomParameters } from './type/common'; 20 | export { CallKit } from './type/CallKit'; 21 | export { RTCStats } from './type/RTCStats'; 22 | 23 | import * as TwilioErrors from './error'; 24 | export { TwilioErrors }; 25 | -------------------------------------------------------------------------------- /src/type/AudioDevice.ts: -------------------------------------------------------------------------------- 1 | import type { AudioDevice } from '../AudioDevice'; 2 | import type { Constants } from '../constants'; 3 | import type { Uuid } from './common'; 4 | 5 | export interface NativeAudioDeviceInfo { 6 | uuid: Uuid; 7 | type: AudioDevice.Type; 8 | name: string; 9 | } 10 | 11 | export interface NativeAudioDevicesInfo { 12 | audioDevices: NativeAudioDeviceInfo[]; 13 | selectedDevice?: NativeAudioDeviceInfo; 14 | } 15 | 16 | export interface NativeAudioDevicesUpdatedEvent extends NativeAudioDevicesInfo { 17 | type: Constants.VoiceEventAudioDevicesUpdated; 18 | } 19 | -------------------------------------------------------------------------------- /src/type/Call.ts: -------------------------------------------------------------------------------- 1 | import type { Constants } from '../constants'; 2 | import type { CustomParameters, Uuid } from './common'; 3 | import type { NativeErrorInfo } from './Error'; 4 | import type { Call } from '../Call'; 5 | import type { NativeCallMessageInfo } from './CallMessage'; 6 | 7 | export interface NativeCallInfo { 8 | uuid: Uuid; 9 | customParameters?: CustomParameters; 10 | from?: string; 11 | [Constants.CallInfoInitialConnectedTimestamp]?: string; 12 | isMuted?: boolean; 13 | isOnHold?: boolean; 14 | sid?: string; 15 | state?: Call.State; 16 | to?: string; 17 | } 18 | 19 | export interface NativeCallConnectedEvent { 20 | type: Constants.CallEventConnected; 21 | call: NativeCallInfo; 22 | } 23 | 24 | export interface NativeCallConnectFailureEvent { 25 | type: Constants.CallEventConnectFailure; 26 | call: NativeCallInfo; 27 | error: NativeErrorInfo; 28 | } 29 | 30 | export interface NativeCallReconnectingEvent { 31 | type: Constants.CallEventReconnecting; 32 | call: NativeCallInfo; 33 | error: NativeErrorInfo; 34 | } 35 | 36 | export interface NativeCallReconnectedEvent { 37 | type: Constants.CallEventReconnected; 38 | call: NativeCallInfo; 39 | } 40 | 41 | export interface NativeCallDisconnectedEvent { 42 | type: Constants.CallEventDisconnected; 43 | call: NativeCallInfo; 44 | error?: NativeErrorInfo; 45 | } 46 | 47 | export interface NativeCallRingingEvent { 48 | type: Constants.CallEventRinging; 49 | call: NativeCallInfo; 50 | } 51 | 52 | export type NativeCallQualityWarnings = string[]; 53 | 54 | export interface NativeCallQualityWarningsEvent { 55 | type: Constants.CallEventQualityWarningsChanged; 56 | call: NativeCallInfo; 57 | [Constants.CallEventCurrentWarnings]: NativeCallQualityWarnings; 58 | [Constants.CallEventPreviousWarnings]: NativeCallQualityWarnings; 59 | } 60 | 61 | export interface NativeCallMessageReceivedEvent { 62 | type: Constants.CallEventMessageReceived; 63 | call: NativeCallInfo; 64 | [Constants.CallMessage]: NativeCallMessageInfo; 65 | } 66 | 67 | export type NativeCallEvent = 68 | | NativeCallConnectedEvent 69 | | NativeCallConnectFailureEvent 70 | | NativeCallReconnectingEvent 71 | | NativeCallReconnectedEvent 72 | | NativeCallDisconnectedEvent 73 | | NativeCallRingingEvent 74 | | NativeCallQualityWarningsEvent 75 | | NativeCallMessageReceivedEvent; 76 | 77 | export type NativeCallEventType = 78 | | Constants.CallEventConnectFailure 79 | | Constants.CallEventConnected 80 | | Constants.CallEventDisconnected 81 | | Constants.CallEventQualityWarningsChanged 82 | | Constants.CallEventReconnected 83 | | Constants.CallEventReconnecting 84 | | Constants.CallEventRinging 85 | | Constants.CallEventMessageReceived; 86 | 87 | export type NativeCallFeedbackIssue = 88 | | Constants.CallFeedbackIssueAudioLatency 89 | | Constants.CallFeedbackIssueChoppyAudio 90 | | Constants.CallFeedbackIssueDroppedCall 91 | | Constants.CallFeedbackIssueEcho 92 | | Constants.CallFeedbackIssueNoisyCall 93 | | Constants.CallFeedbackIssueNotReported 94 | | Constants.CallFeedbackIssueOneWayAudio; 95 | 96 | export type NativeCallFeedbackScore = 97 | | Constants.CallFeedbackScoreNotReported 98 | | Constants.CallFeedbackScoreOne 99 | | Constants.CallFeedbackScoreTwo 100 | | Constants.CallFeedbackScoreThree 101 | | Constants.CallFeedbackScoreFour 102 | | Constants.CallFeedbackScoreFive; 103 | -------------------------------------------------------------------------------- /src/type/CallInvite.ts: -------------------------------------------------------------------------------- 1 | import type { Constants } from '../constants'; 2 | import type { CustomParameters, Uuid } from './common'; 3 | import type { NativeErrorInfo } from './Error'; 4 | import type { NativeCallMessageInfo } from './CallMessage'; 5 | 6 | export interface BaseNativeCallInviteEvent { 7 | callSid: string; 8 | } 9 | 10 | export interface NativeCallInviteInfo { 11 | uuid: Uuid; 12 | callSid: string; 13 | customParameters?: CustomParameters; 14 | from: string; 15 | to: string; 16 | } 17 | 18 | export interface NativeCallInviteAcceptedEvent 19 | extends BaseNativeCallInviteEvent { 20 | type: Constants.CallInviteEventTypeValueAccepted; 21 | callInvite: NativeCallInviteInfo; 22 | } 23 | 24 | export interface NativeCallInviteNotificationTappedEvent 25 | extends BaseNativeCallInviteEvent { 26 | type: Constants.CallInviteEventTypeValueNotificationTapped; 27 | } 28 | 29 | export interface NativeCallInviteRejectedEvent 30 | extends BaseNativeCallInviteEvent { 31 | type: Constants.CallInviteEventTypeValueRejected; 32 | callInvite: NativeCallInviteInfo; 33 | } 34 | 35 | export interface NativeCallInviteCancelledEvent 36 | extends BaseNativeCallInviteEvent { 37 | type: Constants.CallInviteEventTypeValueCancelled; 38 | cancelledCallInvite: NativeCancelledCallInviteInfo; 39 | error?: NativeErrorInfo; 40 | } 41 | 42 | export interface NativeCancelledCallInviteInfo { 43 | callSid: string; 44 | from: string; 45 | to: string; 46 | } 47 | 48 | export interface NativeCallInviteMessageReceivedEvent 49 | extends BaseNativeCallInviteEvent { 50 | type: Constants.CallEventMessageReceived; 51 | [Constants.CallMessage]: NativeCallMessageInfo; 52 | } 53 | 54 | export type NativeCallInviteEvent = 55 | | NativeCallInviteNotificationTappedEvent 56 | | NativeCallInviteAcceptedEvent 57 | | NativeCallInviteRejectedEvent 58 | | NativeCallInviteCancelledEvent 59 | | NativeCallInviteMessageReceivedEvent; 60 | -------------------------------------------------------------------------------- /src/type/CallKit.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @public 3 | * CallKit related types. 4 | */ 5 | export namespace CallKit { 6 | /** 7 | * @public 8 | * iOS CallKit configuration options. 9 | */ 10 | export type ConfigurationOptions = { 11 | /** 12 | * Filename of a 80x80 PNG image that will show in the system call UI as the app icon. 13 | */ 14 | callKitIconTemplateImageData: string; 15 | /** 16 | * Include call history in system recents (`true` by default). 17 | * 18 | * @remarks 19 | * Only supported on iOS 11 and newer versions. 20 | */ 21 | callKitIncludesCallsInRecents: boolean; 22 | /** 23 | * Maximum number of call groups (`2` by default). 24 | */ 25 | callKitMaximumCallGroups: number; 26 | /** 27 | * Maximum number of calls per group (`5` by default). 28 | */ 29 | callKitMaximumCallsPerCallGroup: number; 30 | /** 31 | * Filename of the incoming call ringing tone. 32 | */ 33 | callKitRingtoneSound: string; 34 | /** 35 | * Supported handle types. 36 | * 37 | * @remarks 38 | * See {@link CallKit.HandleType}. 39 | */ 40 | callKitSupportedHandleTypes: HandleType[]; 41 | }; 42 | 43 | /** 44 | * @public 45 | * Enumeration of all supported handle types by iOS CallKit. 46 | */ 47 | export enum HandleType { 48 | /** 49 | * Generic handle. 50 | */ 51 | Generic = 0, 52 | /** 53 | * Phone number handle. 54 | */ 55 | PhoneNumber = 1, 56 | /** 57 | * Email address handle. 58 | */ 59 | EmailAddress = 2, 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/type/CallMessage.ts: -------------------------------------------------------------------------------- 1 | import type { Constants } from '../constants'; 2 | import type { NativeErrorInfo } from './Error'; 3 | 4 | export interface NativeCallMessageInfo { 5 | [Constants.CallMessageContent]: any; 6 | [Constants.CallMessageContentType]: string; 7 | [Constants.CallMessageMessageType]: string; 8 | [Constants.VoiceEventSid]?: string; 9 | } 10 | 11 | export interface NativeCallMessageEventBase { 12 | [Constants.VoiceEventSid]: string; 13 | } 14 | 15 | export interface NativeCallMessageFailureEvent 16 | extends NativeCallMessageEventBase { 17 | type: Constants.CallEventMessageFailure; 18 | error: NativeErrorInfo; 19 | } 20 | 21 | export interface NativeCallMessageSentEvent extends NativeCallMessageEventBase { 22 | type: Constants.CallEventMessageSent; 23 | } 24 | 25 | export type NativeCallMessageEvent = 26 | | NativeCallMessageFailureEvent 27 | | NativeCallMessageSentEvent; 28 | 29 | export type NativeCallMessageEventType = 30 | | Constants.CallEventMessageFailure 31 | | Constants.CallEventMessageSent; 32 | -------------------------------------------------------------------------------- /src/type/Error.ts: -------------------------------------------------------------------------------- 1 | import type { Constants } from '../constants'; 2 | 3 | export interface NativeErrorInfo { 4 | code: number; 5 | message: string; 6 | } 7 | 8 | export interface NativeErrorEvent { 9 | type: Constants.VoiceEventError; 10 | error: NativeErrorInfo; 11 | } 12 | -------------------------------------------------------------------------------- /src/type/NativeModule.ts: -------------------------------------------------------------------------------- 1 | import type { NativeModulesStatic } from 'react-native'; 2 | import type { CallInvite } from '../CallInvite'; 3 | import type { NativeAudioDevicesInfo } from './AudioDevice'; 4 | import type { 5 | NativeCallInfo, 6 | NativeCallFeedbackIssue, 7 | NativeCallFeedbackScore, 8 | } from './Call'; 9 | import type { NativeCallInviteInfo } from './CallInvite'; 10 | import type { Uuid } from './common'; 11 | import type { RTCStats } from './RTCStats'; 12 | 13 | export interface TwilioVoiceReactNative extends NativeModulesStatic { 14 | /** 15 | * Native types. 16 | * 17 | * The following event related functions are required by the React Native 18 | * bindings. 19 | */ 20 | addListener: (eventType: string) => void; 21 | removeListeners: (count: number) => void; 22 | 23 | /** 24 | * Call bindings. 25 | */ 26 | call_disconnect(callUuid: Uuid): Promise; 27 | call_getStats(callUuid: Uuid): Promise; 28 | call_hold(callUuid: Uuid, hold: boolean): Promise; 29 | call_isOnHold(callUuid: Uuid): Promise; 30 | call_isMuted(callUuid: Uuid): Promise; 31 | call_mute(callUuid: Uuid, mute: boolean): Promise; 32 | call_postFeedback( 33 | callUuid: Uuid, 34 | score: NativeCallFeedbackScore, 35 | issue: NativeCallFeedbackIssue 36 | ): Promise; 37 | call_sendDigits(callUuid: Uuid, digits: string): Promise; 38 | call_sendMessage( 39 | callUuid: Uuid, 40 | content: string, 41 | contentType: string, 42 | messageType: string 43 | ): Promise; 44 | 45 | /** 46 | * Call Invite bindings. 47 | */ 48 | callInvite_accept( 49 | callInviteUuid: Uuid, 50 | acceptOptions: CallInvite.AcceptOptions 51 | ): Promise; 52 | callInvite_isValid(callInviteUuid: Uuid): Promise; 53 | callInvite_reject(callInviteUuid: Uuid): Promise; 54 | callInvite_updateCallerHandle( 55 | callInviteUuid: Uuid, 56 | handle: string 57 | ): Promise; 58 | 59 | /** 60 | * Voice bindings. 61 | */ 62 | voice_connect_android( 63 | token: string, 64 | twimlParams: Record, 65 | notificationDisplayName: string | undefined 66 | ): Promise; 67 | voice_connect_ios( 68 | token: string, 69 | twimlParams: Record, 70 | contactHandle: string 71 | ): Promise; 72 | voice_initializePushRegistry(): Promise; 73 | voice_setCallKitConfiguration( 74 | configuration: Record 75 | ): Promise; 76 | voice_setIncomingCallContactHandleTemplate(template?: string): Promise; 77 | voice_getAudioDevices(): Promise; 78 | voice_getCalls(): Promise; 79 | voice_getCallInvites(): Promise; 80 | voice_getDeviceToken(): Promise; 81 | voice_getVersion(): Promise; 82 | voice_handleEvent(remoteMessage: Record): Promise; 83 | voice_register(accessToken: string): Promise; 84 | voice_selectAudioDevice(audioDeviceUuid: Uuid): Promise; 85 | voice_showNativeAvRoutePicker(): Promise; 86 | voice_unregister(accessToken: string): Promise; 87 | 88 | /** 89 | * System/permissions related bindings. 90 | */ 91 | system_isFullScreenNotificationEnabled(): Promise; 92 | system_requestFullScreenNotificationPermission(): Promise; 93 | } 94 | -------------------------------------------------------------------------------- /src/type/RTCStats.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Types related to WebRTC stats. 3 | * 4 | * @public 5 | */ 6 | export namespace RTCStats { 7 | export enum IceCandidatePairState { 8 | STATE_FAILED = 'STATE_FAILED', 9 | STATE_FROZEN = 'STATE_FROZEN', 10 | STATE_IN_PROGRESS = 'STATE_IN_PROGRESS', 11 | STATE_SUCCEEDED = 'STATE_SUCCEEDED', 12 | STATE_WAITING = 'STATE_WAITING', 13 | } 14 | 15 | export interface IceCandidatePairStats { 16 | activeCandidatePair: boolean; 17 | availableIncomingBitrate: number; 18 | availableOutgoingBitrate: number; 19 | bytesReceived: number; 20 | bytesSent: number; 21 | consentRequestsReceived: number; 22 | consentRequestsSent: number; 23 | consentResponsesReceived: number; 24 | consentResponsesSent: number; 25 | currentRoundTripTime: number; 26 | localCandidateId: string; 27 | localCandidateIp: string; 28 | nominated: boolean; 29 | priority: number; 30 | readable: boolean; 31 | relayProtocol: string; 32 | remoteCandidateId: string; 33 | remoteCandidateIp: string; 34 | requestsReceieved: number; 35 | requestsSent: number; 36 | responsesRecieved: number; 37 | responsesSent: number; 38 | retransmissionsReceived: number; 39 | retransmissionsSent: number; 40 | state: IceCandidatePairState; 41 | totalRoundTripTime: number; 42 | transportId: string; 43 | writeable: boolean; 44 | } 45 | 46 | export interface IceCandidateStats { 47 | candidateType: string; 48 | deleted: boolean; 49 | ip: string; 50 | isRemote: boolean; 51 | port: number; 52 | priority: number; 53 | protocol: string; 54 | transportId: string; 55 | url: string; 56 | } 57 | 58 | export interface BaseTrackStats { 59 | codec: string; 60 | packetsLost: number; 61 | ssrc: string; 62 | timestamp: number; 63 | trackId: string; 64 | } 65 | 66 | export interface LocalTrackStats extends BaseTrackStats { 67 | bytesSent: number; 68 | packetsSent: number; 69 | roundTripTime: number; 70 | } 71 | 72 | export interface LocalAudioTrackStats extends LocalTrackStats { 73 | audioLevel: number; 74 | jitter: number; 75 | } 76 | 77 | export interface RemoteTrackStats extends BaseTrackStats { 78 | bytesRecieved: number; 79 | packetsReceived: number; 80 | } 81 | 82 | export interface RemoteAudioTrackStats extends RemoteTrackStats { 83 | audioLevel: number; 84 | jitter: number; 85 | mos: number; 86 | } 87 | 88 | /** 89 | * WebRTC stats report. Contains diagnostics information about 90 | * `RTCPeerConnection`s and summarizes data for an ongoing call. 91 | */ 92 | export interface StatsReport { 93 | iceCandidatePairStats: IceCandidatePairStats[]; 94 | iceCandidateStats: IceCandidateStats[]; 95 | localAudioTrackStats: LocalAudioTrackStats[]; 96 | peerConnectionId: string; 97 | remoteAudioTrackStats: RemoteAudioTrackStats[]; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/type/Voice.ts: -------------------------------------------------------------------------------- 1 | import type { Constants } from '../constants'; 2 | import type { NativeAudioDevicesUpdatedEvent } from './AudioDevice'; 3 | import type { NativeCallInviteInfo } from './CallInvite'; 4 | import type { NativeErrorEvent } from './Error'; 5 | 6 | export interface NativeRegisteredEvent { 7 | type: Constants.VoiceEventRegistered; 8 | } 9 | 10 | export interface NativeUnregisteredEvent { 11 | type: Constants.VoiceEventUnregistered; 12 | } 13 | 14 | export interface NativeCallInviteIncomingEvent { 15 | [Constants.VoiceEventType]: Constants.VoiceEventTypeValueIncomingCallInvite; 16 | callInvite: NativeCallInviteInfo; 17 | } 18 | 19 | export type NativeVoiceEvent = 20 | | NativeAudioDevicesUpdatedEvent 21 | | NativeCallInviteIncomingEvent 22 | | NativeErrorEvent 23 | | NativeRegisteredEvent 24 | | NativeUnregisteredEvent; 25 | 26 | export type NativeVoiceEventType = 27 | | Constants.VoiceEventAudioDevicesUpdated 28 | | Constants.VoiceEventTypeValueIncomingCallInvite 29 | | Constants.VoiceEventError 30 | | Constants.VoiceEventRegistered 31 | | Constants.VoiceEventUnregistered; 32 | -------------------------------------------------------------------------------- /src/type/common.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Call custom parameters. If custom parameters are present for a call, then 3 | * it will have this typing. 4 | * 5 | * @remarks 6 | * - `Call`s will have a method to access custom parameters, see 7 | * {@link (Call:class).getCustomParameters}. 8 | * - `CallInvite`s will have a method to access custom parameters for the call 9 | * that is associated with the invite, see 10 | * {@link (CallInvite:class).getCustomParameters}. 11 | * 12 | * @public 13 | */ 14 | export type CustomParameters = Record; 15 | 16 | export type Uuid = string; 17 | -------------------------------------------------------------------------------- /test/app/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /test/app/.detoxrc.js: -------------------------------------------------------------------------------- 1 | /** @type {Detox.DetoxConfig} */ 2 | module.exports = { 3 | testRunner: { 4 | args: { 5 | '$0': 'jest', 6 | config: 'e2e/jest.config.js' 7 | }, 8 | jest: { 9 | setupTimeout: 120000 10 | } 11 | }, 12 | apps: { 13 | 'ios.debug': { 14 | type: 'ios.app', 15 | binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/TwilioVoiceExampleNewArch.app', 16 | build: 'xcodebuild -workspace ios/TwilioVoiceExampleNewArch.xcworkspace -scheme TwilioVoiceExampleNewArch -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build' 17 | }, 18 | 'ios.release': { 19 | type: 'ios.app', 20 | binaryPath: 'ios/build/Build/Products/Release-iphonesimulator/TwilioVoiceExampleNewArch.app', 21 | build: 'xcodebuild -workspace ios/TwilioVoiceExampleNewArch.xcworkspace -scheme TwilioVoiceExampleNewArch -configuration Release -sdk iphonesimulator -derivedDataPath ios/build' 22 | }, 23 | 'android.debug': { 24 | type: 'android.apk', 25 | binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', 26 | build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug', 27 | reversePorts: [ 28 | 8081 29 | ] 30 | }, 31 | 'android.release': { 32 | type: 'android.apk', 33 | binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', 34 | build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release' 35 | } 36 | }, 37 | devices: { 38 | simulator: { 39 | type: 'ios.simulator', 40 | device: { 41 | type: 'iPhone 16' 42 | } 43 | }, 44 | attached: { 45 | type: 'android.attached', 46 | device: { 47 | adbName: '.*' 48 | } 49 | }, 50 | emulator: { 51 | type: 'android.emulator', 52 | device: { 53 | avdName: 'TwilioVoiceReactNativeReferenceApp_AVD' 54 | } 55 | } 56 | }, 57 | configurations: { 58 | 'ios.sim.debug': { 59 | device: 'simulator', 60 | app: 'ios.debug' 61 | }, 62 | 'ios.sim.release': { 63 | device: 'simulator', 64 | app: 'ios.release' 65 | }, 66 | 'android.att.debug': { 67 | device: 'attached', 68 | app: 'android.debug' 69 | }, 70 | 'android.att.release': { 71 | device: 'attached', 72 | app: 'android.release' 73 | }, 74 | 'android.emu.debug': { 75 | device: 'emulator', 76 | app: 'android.debug' 77 | }, 78 | 'android.emu.release': { 79 | device: 'emulator', 80 | app: 'android.release' 81 | } 82 | } 83 | }; 84 | -------------------------------------------------------------------------------- /test/app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@react-native', 4 | }; 5 | -------------------------------------------------------------------------------- /test/app/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | **/.xcode.env.local 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | *.hprof 33 | .cxx/ 34 | *.keystore 35 | !debug.keystore 36 | 37 | # node.js 38 | # 39 | node_modules/ 40 | npm-debug.log 41 | yarn-error.log 42 | 43 | # fastlane 44 | # 45 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 46 | # screenshots whenever they are needed. 47 | # For more information about the recommended setup visit: 48 | # https://docs.fastlane.tools/best-practices/source-control/ 49 | 50 | **/fastlane/report.xml 51 | **/fastlane/Preview.html 52 | **/fastlane/screenshots 53 | **/fastlane/test_output 54 | 55 | # Bundle artifact 56 | *.jsbundle 57 | 58 | # Ruby / CocoaPods 59 | **/Pods/ 60 | /vendor/bundle/ 61 | 62 | # Temporary files created by Metro to check the health of the file watcher 63 | .metro-health-check* 64 | 65 | # testing 66 | /coverage 67 | 68 | # Yarn 69 | .yarn/* 70 | !.yarn/patches 71 | !.yarn/plugins 72 | !.yarn/releases 73 | !.yarn/sdks 74 | !.yarn/versions 75 | 76 | # Secrets 77 | /src/e2e-tests-token.*.ts 78 | /android/app/google-services.json 79 | -------------------------------------------------------------------------------- /test/app/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSameLine: true, 4 | bracketSpacing: false, 5 | singleQuote: true, 6 | trailingComma: 'all', 7 | }; 8 | -------------------------------------------------------------------------------- /test/app/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /test/app/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures. 7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' 8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' 9 | gem 'xcodeproj', '< 1.26.0' 10 | gem 'concurrent-ruby', '< 1.3.4' 11 | -------------------------------------------------------------------------------- /test/app/README.md: -------------------------------------------------------------------------------- 1 | This is a new [**React Native**](https://reactnative.dev) project, bootstrapped using [`@react-native-community/cli`](https://github.com/react-native-community/cli). 2 | 3 | # Getting Started 4 | 5 | > **Note**: Make sure you have completed the [Set Up Your Environment](https://reactnative.dev/docs/set-up-your-environment) guide before proceeding. 6 | 7 | ## Step 1: Start Metro 8 | 9 | First, you will need to run **Metro**, the JavaScript build tool for React Native. 10 | 11 | To start the Metro dev server, run the following command from the root of your React Native project: 12 | 13 | ```sh 14 | # Using npm 15 | npm start 16 | 17 | # OR using Yarn 18 | yarn start 19 | ``` 20 | 21 | ## Step 2: Build and run your app 22 | 23 | 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: 24 | 25 | ### Android 26 | 27 | ```sh 28 | # Using npm 29 | npm run android 30 | 31 | # OR using Yarn 32 | yarn android 33 | ``` 34 | 35 | ### iOS 36 | 37 | For iOS, remember to install CocoaPods dependencies (this only needs to be run on first clone or after updating native deps). 38 | 39 | The first time you create a new project, run the Ruby bundler to install CocoaPods itself: 40 | 41 | ```sh 42 | bundle install 43 | ``` 44 | 45 | Then, and every time you update your native dependencies, run: 46 | 47 | ```sh 48 | bundle exec pod install 49 | ``` 50 | 51 | For more information, please visit [CocoaPods Getting Started guide](https://guides.cocoapods.org/using/getting-started.html). 52 | 53 | ```sh 54 | # Using npm 55 | npm run ios 56 | 57 | # OR using Yarn 58 | yarn ios 59 | ``` 60 | 61 | If everything is set up correctly, you should see your new app running in the Android Emulator, iOS Simulator, or your connected device. 62 | 63 | This is one way to run your app — you can also build it directly from Android Studio or Xcode. 64 | 65 | ## Step 3: Modify your app 66 | 67 | Now that you have successfully run the app, let's make changes! 68 | 69 | 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). 70 | 71 | When you want to forcefully reload, for example to reset the state of your app, you can perform a full reload: 72 | 73 | - **Android**: Press the R key twice or select **"Reload"** from the **Dev Menu**, accessed via Ctrl + M (Windows/Linux) or Cmd ⌘ + M (macOS). 74 | - **iOS**: Press R in iOS Simulator. 75 | 76 | ## Congratulations! :tada: 77 | 78 | You've successfully run and modified your React Native App. :partying_face: 79 | 80 | ### Now what? 81 | 82 | - 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). 83 | - If you're curious to learn more about React Native, check out the [docs](https://reactnative.dev/docs/getting-started). 84 | 85 | # Troubleshooting 86 | 87 | If you're having issues getting the above steps to work, see the [Troubleshooting](https://reactnative.dev/docs/troubleshooting) page. 88 | 89 | # Learn More 90 | 91 | To learn more about React Native, take a look at the following resources: 92 | 93 | - [React Native Website](https://reactnative.dev) - learn more about React Native. 94 | - [Getting Started](https://reactnative.dev/docs/environment-setup) - an **overview** of React Native and how setup your environment. 95 | - [Learn the Basics](https://reactnative.dev/docs/getting-started) - a **guided tour** of the React Native **basics**. 96 | - [Blog](https://reactnative.dev/blog) - read the latest official React Native **Blog** posts. 97 | - [`@facebook/react-native`](https://github.com/facebook/react-native) - the Open Source; GitHub **repository** for React Native. 98 | -------------------------------------------------------------------------------- /test/app/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/test/app/android/app/debug.keystore -------------------------------------------------------------------------------- /test/app/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /test/app/android/app/src/androidTest/java/com/example/twiliovoicereactnative/DetoxTest.java: -------------------------------------------------------------------------------- 1 | package com.example.twiliovoicereactnative; 2 | 3 | import com.wix.detox.Detox; 4 | import com.wix.detox.config.DetoxConfig; 5 | 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import androidx.test.ext.junit.runners.AndroidJUnit4; 11 | import androidx.test.filters.LargeTest; 12 | import androidx.test.rule.ActivityTestRule; 13 | 14 | @RunWith(AndroidJUnit4.class) 15 | @LargeTest 16 | public class DetoxTest { 17 | @Rule 18 | public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false); 19 | 20 | @Test 21 | public void runDetoxTests() { 22 | DetoxConfig detoxConfig = new DetoxConfig(); 23 | detoxConfig.idlePolicyConfig.masterTimeoutSec = 90; 24 | detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60; 25 | detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60); 26 | 27 | Detox.runTests(mActivityRule, detoxConfig); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/app/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /test/app/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/app/android/app/src/main/java/com/example/twiliovoicereactnative/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.twiliovoicereactnative 2 | 3 | import android.Manifest 4 | import android.content.Intent 5 | import android.os.Build 6 | import android.os.Bundle 7 | import android.os.PersistableBundle 8 | import android.widget.Toast 9 | import com.facebook.react.ReactActivity 10 | import com.facebook.react.ReactActivityDelegate 11 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 12 | import com.facebook.react.defaults.DefaultReactActivityDelegate 13 | import com.twiliovoicereactnative.VoiceActivityProxy 14 | 15 | class MainActivity : ReactActivity() { 16 | private val voiceActivityProxy: VoiceActivityProxy = VoiceActivityProxy( 17 | this 18 | ) { permission -> 19 | if (Manifest.permission.RECORD_AUDIO.equals(permission)) { 20 | Toast.makeText( 21 | this@MainActivity, 22 | "Microphone permissions needed. Please allow in your application settings.", 23 | Toast.LENGTH_LONG 24 | ).show() 25 | } else if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) && 26 | Manifest.permission.BLUETOOTH_CONNECT.equals(permission) 27 | ) { 28 | Toast.makeText( 29 | this@MainActivity, 30 | "Bluetooth permissions needed. Please allow in your application settings.", 31 | Toast.LENGTH_LONG 32 | ).show() 33 | } else if ((Build.VERSION.SDK_INT > Build.VERSION_CODES.S_V2) && 34 | Manifest.permission.POST_NOTIFICATIONS.equals(permission) 35 | ) { 36 | Toast.makeText( 37 | this@MainActivity, 38 | "Notification permissions needed. Please allow in your application settings.", 39 | Toast.LENGTH_LONG 40 | ).show() 41 | } 42 | } 43 | 44 | /** 45 | * Returns the name of the main component registered from JavaScript. This is used to schedule 46 | * rendering of the component. 47 | */ 48 | override fun getMainComponentName(): String = "TwilioVoiceExampleNewArch" 49 | 50 | /** 51 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 52 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 53 | */ 54 | override fun createReactActivityDelegate(): ReactActivityDelegate = 55 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) 56 | 57 | override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) { 58 | super.onCreate(savedInstanceState, persistentState) 59 | voiceActivityProxy.onCreate(savedInstanceState) 60 | } 61 | 62 | override fun onDestroy() { 63 | super.onDestroy() 64 | voiceActivityProxy.onDestroy() 65 | } 66 | 67 | override fun onNewIntent(intent: Intent?) { 68 | super.onNewIntent(intent) 69 | voiceActivityProxy.onNewIntent(intent) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /test/app/android/app/src/main/java/com/example/twiliovoicereactnative/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package com.example.twiliovoicereactnative 2 | 3 | import android.app.Application 4 | import com.facebook.react.PackageList 5 | import com.facebook.react.ReactApplication 6 | import com.facebook.react.ReactHost 7 | import com.facebook.react.ReactNativeHost 8 | import com.facebook.react.ReactPackage 9 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load 10 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost 11 | import com.facebook.react.defaults.DefaultReactNativeHost 12 | import com.facebook.react.soloader.OpenSourceMergedSoMapping 13 | import com.facebook.soloader.SoLoader 14 | import com.twiliovoicereactnative.VoiceApplicationProxy 15 | 16 | class MainApplication : Application(), ReactApplication { 17 | private val voiceApplicationProxy = VoiceApplicationProxy(this) 18 | 19 | override val reactNativeHost: ReactNativeHost = 20 | object : DefaultReactNativeHost(this) { 21 | override fun getPackages(): List = 22 | PackageList(this).packages.apply { 23 | // Packages that cannot be autolinked yet can be added manually here, for example: 24 | // add(MyReactNativePackage()) 25 | } 26 | 27 | override fun getJSMainModuleName(): String = "index" 28 | 29 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 30 | 31 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 32 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 33 | } 34 | 35 | override val reactHost: ReactHost 36 | get() = getDefaultReactHost(applicationContext, reactNativeHost) 37 | 38 | override fun onCreate() { 39 | super.onCreate() 40 | voiceApplicationProxy.onCreate() 41 | SoLoader.init(this, OpenSourceMergedSoMapping) 42 | if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { 43 | // If you opted-in for the New Architecture, we load the native entry point for this app. 44 | load() 45 | } 46 | } 47 | 48 | override fun onTerminate() { 49 | super.onTerminate() 50 | voiceApplicationProxy.onTerminate() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/test/app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/test/app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/test/app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/test/app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/test/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/test/app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/test/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/test/app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/test/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/test/app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | TwilioVoiceExampleNewArch 3 | 4 | -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/app/android/app/src/main/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10.0.2.2 5 | localhost 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/app/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | buildToolsVersion = "35.0.0" 4 | minSdkVersion = 24 5 | compileSdkVersion = 35 6 | targetSdkVersion = 34 7 | ndkVersion = "27.1.12297006" 8 | kotlinVersion = "2.0.21" 9 | } 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle") 16 | classpath("com.facebook.react:react-native-gradle-plugin") 17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") 18 | classpath("com.google.gms:google-services:4.4.1") 19 | } 20 | } 21 | 22 | allprojects { 23 | repositories { 24 | maven { 25 | url("$rootDir/../node_modules/detox/Detox-android") 26 | } 27 | } 28 | } 29 | 30 | apply plugin: "com.facebook.react.rootproject" 31 | -------------------------------------------------------------------------------- /test/app/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | 25 | # Use this property to specify which architecture you want to build. 26 | # You can also override it from the CLI using 27 | # ./gradlew -PreactNativeArchitectures=x86_64 28 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 29 | 30 | # Use this property to enable support to the new architecture. 31 | # This will allow you to use TurboModules and the Fabric render in 32 | # your application. You should enable this flag either if you want 33 | # to write custom TurboModules/Fabric components OR use libraries that 34 | # are providing them. 35 | newArchEnabled=true 36 | 37 | # Use this property to enable or disable the Hermes JS engine. 38 | # If set to false, you will be using JSC instead. 39 | hermesEnabled=true 40 | -------------------------------------------------------------------------------- /test/app/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/twilio/twilio-voice-react-native/f132310bc29034f3b6fc7af05050e00263257ec3/test/app/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /test/app/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /test/app/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /test/app/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } 2 | plugins { id("com.facebook.react.settings") } 3 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } 4 | rootProject.name = 'TwilioVoiceExampleNewArch' 5 | include ':app' 6 | includeBuild('../node_modules/@react-native/gradle-plugin') 7 | -------------------------------------------------------------------------------- /test/app/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TwilioVoiceExampleNewArch", 3 | "displayName": "TwilioVoiceExampleNewArch" 4 | } 5 | -------------------------------------------------------------------------------- /test/app/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ['module:@react-native/babel-preset'], 3 | }; 4 | -------------------------------------------------------------------------------- /test/app/e2e/common/logParser.ts: -------------------------------------------------------------------------------- 1 | import type { EventLogItem } from '../../src/type'; 2 | 3 | export const getLog = async (): Promise> => { 4 | const eventLogAttr = await element(by.id('event_log')).getAttributes(); 5 | if (!('label' in eventLogAttr) || !eventLogAttr.label) { 6 | throw new Error('cannot parse event log label'); 7 | } 8 | 9 | const log: string = eventLogAttr.label; 10 | 11 | return JSON.parse(log); 12 | }; 13 | 14 | export const pollValidateLog = async ( 15 | validator: (log: Array) => boolean, 16 | loops: number = 5, 17 | ) => { 18 | let wasValid = false; 19 | for (let i = 0; i < loops; i++) { 20 | const log = await getLog(); 21 | if (validator(log)) { 22 | wasValid = true; 23 | break; 24 | } else { 25 | await new Promise((resolve) => setTimeout(resolve, 5000)); 26 | } 27 | } 28 | return wasValid; 29 | }; 30 | 31 | export const getRegExpMatch = async (regExp: RegExp) => { 32 | let log = await getLog(); 33 | let regExpMatchGroup; 34 | for (const entry of [...log].reverse()) { 35 | const m = entry.content.match(regExp); 36 | if (m) { 37 | regExpMatchGroup = m[1]; 38 | break; 39 | } 40 | } 41 | return regExpMatchGroup; 42 | }; 43 | -------------------------------------------------------------------------------- /test/app/e2e/common/twilioClient.ts: -------------------------------------------------------------------------------- 1 | import twilio from 'twilio'; 2 | 3 | export const bootstrapTwilioClient = () => { 4 | const accountSid = process.env.ACCOUNT_SID; 5 | const authToken = process.env.AUTH_TOKEN; 6 | const mockClientId = process.env.CLIENT_IDENTITY; 7 | 8 | if ( 9 | [accountSid, authToken, mockClientId].some((v) => typeof v !== 'string') 10 | ) { 11 | throw new Error('Missing env var.'); 12 | } 13 | 14 | const twilioClient = twilio(accountSid, authToken); 15 | 16 | return { twilioClient, clientId: mockClientId as string }; 17 | }; 18 | -------------------------------------------------------------------------------- /test/app/e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@jest/types').Config.InitialOptions} */ 2 | module.exports = { 3 | rootDir: '..', 4 | testMatch: ['/e2e/**/*.test.ts'], 5 | testTimeout: 120000, 6 | maxWorkers: 1, 7 | globalSetup: 'detox/runners/jest/globalSetup', 8 | globalTeardown: 'detox/runners/jest/globalTeardown', 9 | reporters: ['detox/runners/jest/reporter'], 10 | testEnvironment: 'detox/runners/jest/testEnvironment', 11 | verbose: true, 12 | }; 13 | -------------------------------------------------------------------------------- /test/app/e2e/suites/registration.test.ts: -------------------------------------------------------------------------------- 1 | import { device, element, expect as detoxExpect, by, waitFor } from 'detox'; 2 | 3 | const DEFAULT_TIMEOUT = 10000; 4 | 5 | describe('registration', () => { 6 | const register = async () => { 7 | await element(by.text('REGISTER')).tap(); 8 | await waitFor(element(by.text('Registered: true'))) 9 | .toBeVisible() 10 | .withTimeout(DEFAULT_TIMEOUT); 11 | }; 12 | 13 | beforeAll(async () => { 14 | await device.launchApp(); 15 | }); 16 | 17 | beforeEach(async () => { 18 | await device.reloadReactNative(); 19 | }); 20 | 21 | it('should start unregistered', async () => { 22 | await detoxExpect(element(by.text('Registered: false'))).toBeVisible(); 23 | }); 24 | 25 | if (device.getPlatform() === 'android') { 26 | it('should register', async () => { 27 | await register(); 28 | }); 29 | 30 | it('should unregister', async () => { 31 | await register(); 32 | 33 | await element(by.text('UNREGISTER')).tap(); 34 | await waitFor(element(by.text('Registered: false'))) 35 | .toBeVisible() 36 | .withTimeout(DEFAULT_TIMEOUT); 37 | }); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /test/app/e2e/suites/voice.test.ts: -------------------------------------------------------------------------------- 1 | import { device, element, expect as detoxExpect, by } from 'detox'; 2 | import { expect as jestExpect } from 'expect'; 3 | 4 | describe('voice', () => { 5 | beforeAll(async () => { 6 | await device.launchApp(); 7 | }); 8 | 9 | beforeEach(async () => { 10 | await device.reloadReactNative(); 11 | }); 12 | 13 | it('should show a valid SDK version', async () => { 14 | const el = element(by.id('sdk_version')); 15 | await detoxExpect(el).toBeVisible(); 16 | const sdkVersionAttr = await el.getAttributes(); 17 | if (!('text' in sdkVersionAttr)) { 18 | throw new Error('could not parse text of sdk version element'); 19 | } 20 | const sdkVersionText = sdkVersionAttr.text; 21 | jestExpect(sdkVersionText).toMatch('SDK Version: '); 22 | jestExpect(sdkVersionText).not.toMatch('SDK Version: unknown'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/app/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @format 3 | */ 4 | 5 | import {AppRegistry} from 'react-native'; 6 | import App from './src/App'; 7 | import {name as appName} from './app.json'; 8 | 9 | AppRegistry.registerComponent(appName, () => App); 10 | -------------------------------------------------------------------------------- /test/app/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /test/app/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Resolve react_native_pods.rb with node to allow for hoisting 2 | require Pod::Executable.execute_command('node', ['-p', 3 | 'require.resolve( 4 | "react-native/scripts/react_native_pods.rb", 5 | {paths: [process.argv[1]]}, 6 | )', __dir__]).strip 7 | 8 | platform :ios, min_ios_version_supported 9 | prepare_react_native_project! 10 | 11 | linkage = ENV['USE_FRAMEWORKS'] 12 | if linkage != nil 13 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 14 | use_frameworks! :linkage => linkage.to_sym 15 | end 16 | 17 | target 'TwilioVoiceExampleNewArch' do 18 | config = use_native_modules! 19 | 20 | use_react_native!( 21 | :path => config[:reactNativePath], 22 | # An absolute path to your application root. 23 | :app_path => "#{Pod::Config.instance.installation_root}/.." 24 | ) 25 | 26 | post_install do |installer| 27 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 28 | react_native_post_install( 29 | installer, 30 | config[:reactNativePath], 31 | :mac_catalyst_enabled => false, 32 | # :ccache_enabled => true 33 | ) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/app/ios/TwilioVoiceExampleNewArch.xcodeproj/xcshareddata/xcschemes/TwilioVoiceExampleNewArch.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /test/app/ios/TwilioVoiceExampleNewArch/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import React 3 | import React_RCTAppDelegate 4 | import ReactAppDependencyProvider 5 | 6 | @main 7 | class AppDelegate: RCTAppDelegate { 8 | override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 9 | self.moduleName = "TwilioVoiceExampleNewArch" 10 | self.dependencyProvider = RCTAppDependencyProvider() 11 | 12 | // You can add your custom initial props in the dictionary below. 13 | // They will be passed down to the ViewController used by React Native. 14 | self.initialProps = [:] 15 | 16 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 17 | } 18 | 19 | override func sourceURL(for bridge: RCTBridge) -> URL? { 20 | self.bundleURL() 21 | } 22 | 23 | override func bundleURL() -> URL? { 24 | #if DEBUG 25 | RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") 26 | #else 27 | Bundle.main.url(forResource: "main", withExtension: "jsbundle") 28 | #endif 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/app/ios/TwilioVoiceExampleNewArch/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/app/ios/TwilioVoiceExampleNewArch/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/app/ios/TwilioVoiceExampleNewArch/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | TwilioVoiceExampleNewArch 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSAllowsLocalNetworking 32 | 33 | 34 | NSLocationWhenInUseUsageDescription 35 | 36 | UIBackgroundModes 37 | 38 | audio 39 | voip 40 | 41 | UILaunchStoryboardName 42 | LaunchScreen 43 | UIRequiredDeviceCapabilities 44 | 45 | arm64 46 | 47 | UISupportedInterfaceOrientations 48 | 49 | UIInterfaceOrientationPortrait 50 | UIInterfaceOrientationLandscapeLeft 51 | UIInterfaceOrientationLandscapeRight 52 | 53 | UIViewControllerBasedStatusBarAppearance 54 | 55 | NSMicrophoneUsageDescription 56 | foobar 57 | 58 | 59 | -------------------------------------------------------------------------------- /test/app/ios/TwilioVoiceExampleNewArch/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /test/app/ios/TwilioVoiceExampleNewArch/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryUserDefaults 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | CA92.1 21 | 22 | 23 | 24 | NSPrivacyAccessedAPIType 25 | NSPrivacyAccessedAPICategorySystemBootTime 26 | NSPrivacyAccessedAPITypeReasons 27 | 28 | 35F9.1 29 | 30 | 31 | 32 | NSPrivacyCollectedDataTypes 33 | 34 | NSPrivacyTracking 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/app/ios/TwilioVoiceExampleNewArch/TwilioVoiceExampleNewArch.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | }; 4 | -------------------------------------------------------------------------------- /test/app/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); 2 | const path = require('path'); 3 | const exclusionList = require('metro-config/src/defaults/exclusionList'); 4 | const escape = require('escape-string-regexp'); 5 | const pak = require('../../package.json'); 6 | 7 | const root = path.resolve(__dirname, '../..'); 8 | 9 | const modules = Object.keys({ 10 | ...pak.peerDependencies, 11 | }); 12 | 13 | /** 14 | * Metro configuration 15 | * https://reactnative.dev/docs/metro 16 | * 17 | * @type {import('@react-native/metro-config').MetroConfig} 18 | */ 19 | const config = { 20 | projectRoot: __dirname, 21 | watchFolders: [root], 22 | 23 | // We need to make sure that only one version is loaded for peerDependencies 24 | // So we exclude them at the root, and alias them to the versions in example's node_modules 25 | resolver: { 26 | blacklistRE: exclusionList( 27 | modules.map( 28 | (m) => 29 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) 30 | ) 31 | ), 32 | 33 | extraNodeModules: modules.reduce((acc, name) => { 34 | acc[name] = path.join(__dirname, 'node_modules', name); 35 | return acc; 36 | }, {}), 37 | }, 38 | 39 | transformer: { 40 | getTransformOptions: async () => ({ 41 | transform: { 42 | experimentalImportSupport: false, 43 | inlineRequires: true, 44 | }, 45 | }), 46 | }, 47 | }; 48 | 49 | module.exports = mergeConfig(getDefaultConfig(__dirname), config); 50 | -------------------------------------------------------------------------------- /test/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TwilioVoiceReactNativeExample", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "lint": "eslint .", 9 | "start": "react-native start", 10 | "test": "jest", 11 | "detox:build": "detox build", 12 | "detox:test": "detox test", 13 | "detox:relay-server": "node ./e2e/relay/server.js" 14 | }, 15 | "dependencies": { 16 | "@twilio/voice-react-native-sdk": "link:../../", 17 | "react": "18.3.1", 18 | "react-native": "0.77.0" 19 | }, 20 | "devDependencies": { 21 | "@babel/core": "^7.25.2", 22 | "@babel/preset-env": "^7.25.3", 23 | "@babel/runtime": "^7.25.0", 24 | "@react-native-community/cli": "15.0.1", 25 | "@react-native-community/cli-platform-android": "15.0.1", 26 | "@react-native-community/cli-platform-ios": "15.0.1", 27 | "@react-native/babel-preset": "0.77.0", 28 | "@react-native/eslint-config": "0.77.0", 29 | "@react-native/metro-config": "0.77.0", 30 | "@react-native/typescript-config": "0.77.0", 31 | "@types/jest": "^29.5.13", 32 | "@types/react": "^18.2.6", 33 | "@types/react-test-renderer": "^18.0.0", 34 | "detox": "^20.36.1", 35 | "eslint": "^8.19.0", 36 | "jest": "^29.6.3", 37 | "prettier": "2.8.8", 38 | "react-test-renderer": "18.3.1", 39 | "typescript": "5.0.4" 40 | }, 41 | "engines": { 42 | "node": ">=18" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/app/src/Grid.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { StyleSheet, View } from 'react-native'; 3 | 4 | export interface GridProps { 5 | gridComponents: JSX.Element[][]; 6 | horizontalGapSize?: number; 7 | verticalGapSize?: number; 8 | } 9 | 10 | export default function Grid(props: GridProps) { 11 | const styles = React.useMemo( 12 | () => 13 | StyleSheet.create({ 14 | row: { 15 | flexDirection: 'row', 16 | }, 17 | expand: { 18 | flex: 1, 19 | }, 20 | gapBottom: { 21 | marginBottom: props.verticalGapSize, 22 | }, 23 | gapRight: { 24 | marginRight: props.horizontalGapSize, 25 | }, 26 | }), 27 | [props.verticalGapSize, props.horizontalGapSize] 28 | ); 29 | 30 | const mapCell = React.useCallback( 31 | (cell: JSX.Element, idx: number, arr: Array) => ( 32 | 40 | {cell} 41 | 42 | ), 43 | [styles] 44 | ); 45 | 46 | const mapRow = React.useCallback( 47 | (row: JSX.Element[], idx: number, arr: Array) => ( 48 | 54 | {row.map(mapCell)} 55 | 56 | ), 57 | [styles, mapCell] 58 | ); 59 | 60 | return <>{props.gridComponents.map(mapRow)}; 61 | } 62 | -------------------------------------------------------------------------------- /test/app/src/components/CallMessage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Button } from 'react-native'; 3 | import Grid from '../Grid'; 4 | import type { BoundCallMethod, BoundCallInvite } from '../type'; 5 | 6 | export enum CallMessageContext { 7 | Call = 'Call', 8 | CallInvite = 'CallInvite', 9 | } 10 | 11 | interface CallMessageComponentProps { 12 | context: CallMessageContext; 13 | callMethod?: BoundCallMethod | null; 14 | recentCallInvite?: BoundCallInvite | null; 15 | sendMessageNoOp: () => void; 16 | } 17 | 18 | const MESSAGE_CONTENT_EXCEEDING_MAX_PAYLOAD_SIZE = Array(10000) 19 | .fill('foobar') 20 | .join(''); 21 | 22 | export default function CallMessageComponent({ 23 | context, 24 | callMethod, 25 | recentCallInvite, 26 | sendMessageNoOp, 27 | }: CallMessageComponentProps) { 28 | const validMessageContent = 29 | context === CallMessageContext.Call 30 | ? { ahoy: 'This is a message from a Call' } 31 | : { ahoy: 'This is a message from a Call Invite' }; 32 | 33 | const validMessage = { 34 | content: validMessageContent, 35 | contentType: 'application/json', 36 | messageType: 'user-defined-message', 37 | }; 38 | 39 | const handleSendValidMessage = () => { 40 | context === CallMessageContext.Call 41 | ? callMethod?.sendMessage(validMessage) 42 | : recentCallInvite?.sendMessage(validMessage); 43 | }; 44 | 45 | const largeMessage = { 46 | content: MESSAGE_CONTENT_EXCEEDING_MAX_PAYLOAD_SIZE, 47 | contentType: 'application/json', 48 | messageType: 'user-defined-message', 49 | }; 50 | 51 | const handleSendLargeMessage = () => { 52 | context === CallMessageContext.Call 53 | ? callMethod?.sendMessage(largeMessage) 54 | : recentCallInvite?.sendMessage(largeMessage); 55 | }; 56 | 57 | const invalidContentTypeMessage = { 58 | content: { foo: 'bar' }, 59 | contentType: 'not a real content type foobar', 60 | messageType: 'user-defined-message', 61 | }; 62 | 63 | const handleSendInvalidContentType = () => { 64 | context === CallMessageContext.Call 65 | ? callMethod?.sendMessage(invalidContentTypeMessage) 66 | : recentCallInvite?.sendMessage(invalidContentTypeMessage); 67 | }; 68 | 69 | const invalidMessageTypeMessage = { 70 | content: { foo: 'bar' }, 71 | contentType: 'application/json', 72 | messageType: 'not a real message type foobar', 73 | }; 74 | 75 | const handleSendInvalidMessageType = () => { 76 | context === CallMessageContext.Call 77 | ? callMethod?.sendMessage(invalidMessageTypeMessage) 78 | : recentCallInvite?.sendMessage(invalidMessageTypeMessage); 79 | }; 80 | 81 | return ( 82 | , 89 |