├── .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 | [](https://www.npmjs.com/package/%40twilio/voice-react-native-sdk) [](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 | ,
93 | ],
94 | [
95 | ,
99 | ,
103 | ],
104 | ]}
105 | />
106 | );
107 | }
108 |
--------------------------------------------------------------------------------
/test/app/src/tokenUtility.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { token } from './e2e-tests-token';
3 |
4 | export function generateAccessToken() {
5 | return token;
6 | }
7 |
--------------------------------------------------------------------------------
/test/app/src/type.ts:
--------------------------------------------------------------------------------
1 | import type { Call, CallMessage } from '@twilio/voice-react-native-sdk';
2 |
3 | export interface BoundCallMethod {
4 | disconnect: () => void;
5 | getStats: () => void;
6 | hold: () => void;
7 | mute: () => void;
8 | postFeedback: (score: Call.Score, issue: Call.Issue) => () => void;
9 | sendDigits: (digits: string) => void;
10 | sendMessage: (message: CallMessage) => void;
11 | }
12 |
13 | export interface BoundCallInfo {
14 | customParameters: Record;
15 | from?: string;
16 | to?: string;
17 | state?: Call.State;
18 | sid?: string;
19 | initialConnectedTimestamp?: Date;
20 | isMuted?: boolean;
21 | isOnHold?: boolean;
22 | }
23 |
24 | export interface BoundCallInvite {
25 | accept: () => void;
26 | callSid: string;
27 | customParameters: Record;
28 | from: string;
29 | to: string;
30 | reject: () => void;
31 | sendMessage: (message: CallMessage) => void;
32 | }
33 |
34 | export interface EventLogItem {
35 | id: string;
36 | content: string;
37 | }
38 |
--------------------------------------------------------------------------------
/test/scripts/common.mjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Attempt to parse an environment variable from the process.
5 | * @param {string} envVarKey The name of the environment variable.
6 | * @throws {Error} Will throw if the environment variable is missing.
7 | * @returns {string}
8 | */
9 | export function parseEnvVar(envVarKey) {
10 | const envVarValue = process.env[envVarKey];
11 | if (typeof envVarValue === 'undefined') {
12 | throw new Error(`Environment variable with key "${envVarKey}" is missing.`);
13 | }
14 | if (envVarValue === '') {
15 | throw new Error(
16 | `Environment variable with key "${envVarKey}" evaluated to the empty string.`
17 | );
18 | }
19 | return envVarValue;
20 | }
21 |
22 | /**
23 | * Parse the script arguments.
24 | * @throws {Error} Will throw if the script was not executed with the correct
25 | * number of arguments or if an argument is invalid.
26 | */
27 | export function parseScriptArgument() {
28 | if (process.argv.length !== 4) {
29 | throw new Error('Incorrect number of arguments.');
30 | }
31 |
32 | const identity = process.argv[2];
33 | if (identity === '') {
34 | throw new Error('Identity evaluated to empty string.');
35 | }
36 |
37 | const path = process.argv[3];
38 | if (path === '') {
39 | throw new Error('Path evaluted to empty string.');
40 | }
41 |
42 | return { identity, path };
43 | }
44 |
--------------------------------------------------------------------------------
/test/scripts/gen-token.mjs:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import twilio from 'twilio';
4 | import { parseEnvVar, parseScriptArgument } from './common.mjs';
5 | import { writeFileSync } from 'node:fs';
6 |
7 | const { AccessToken } = twilio.jwt;
8 |
9 | /**
10 | * Generate a configured access token using environment variables.
11 | * @param {string} identity The identity to vend the token with.
12 | * @throws {Error} Will throw if any required environment variable is missing or
13 | * if the identity is invalid.
14 | * @returns {AccessToken}
15 | */
16 | function generateToken(identity) {
17 | if (typeof identity !== 'string') {
18 | throw new Error('Identity not of type "string".');
19 | }
20 | if (identity === '') {
21 | throw new Error('Identity evaluated to the empty string.');
22 | }
23 |
24 | const accountSid = parseEnvVar('ACCOUNT_SID');
25 | const apiKeySid = parseEnvVar('API_KEY_SID');
26 | const apiKeySecret = parseEnvVar('API_KEY_SECRET');
27 | const outgoingApplicationSid = parseEnvVar('OUTGOING_APPLICATION_SID');
28 | const pushCredentialSid = parseEnvVar('PUSH_CREDENTIAL_SID');
29 |
30 | const accessToken = new AccessToken(accountSid, apiKeySid, apiKeySecret, {
31 | identity,
32 | });
33 |
34 | const voiceGrant = new AccessToken.VoiceGrant({
35 | incomingAllow: true,
36 | outgoingApplicationSid,
37 | pushCredentialSid,
38 | });
39 |
40 | accessToken.addGrant(voiceGrant);
41 |
42 | return accessToken;
43 | }
44 |
45 | /**
46 | * Wrap an access token string in the necessary template.
47 | * @param {string} accessToken The access token in JWT form.
48 | */
49 | function templateAccessToken(accessToken) {
50 | const TEMPLATE = (token) => `export const token =\n '${token}';\n`;
51 | return TEMPLATE(accessToken);
52 | }
53 |
54 | /**
55 | * Main function. Executed on script start.
56 | */
57 | function main() {
58 | const { identity, path } = parseScriptArgument();
59 | const accessToken = generateToken(identity);
60 | const accessTokenJwt = accessToken.toJwt();
61 | const templatedAccessTokenJwt = templateAccessToken(accessTokenJwt);
62 | writeFileSync(path, templatedAccessTokenJwt, {
63 | flag: 'wx' /** wx prevents overwriting existing files */,
64 | });
65 | }
66 |
67 | main();
68 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "extends": "./tsconfig",
4 | "exclude": ["test/app", "test/TwilioVoiceExampleNewArch"]
5 | }
6 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "@twilio/voice-react-native-sdk": ["./src/index"]
6 | },
7 | "allowUnreachableCode": false,
8 | "allowUnusedLabels": false,
9 | "esModuleInterop": true,
10 | "importsNotUsedAsValues": "error",
11 | "forceConsistentCasingInFileNames": true,
12 | "jsx": "react",
13 | "lib": ["esnext"],
14 | "module": "esnext",
15 | "moduleResolution": "node",
16 | "noFallthroughCasesInSwitch": true,
17 | "noImplicitReturns": true,
18 | "noImplicitUseStrict": false,
19 | "noStrictGenericChecks": false,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "resolveJsonModule": true,
23 | "skipLibCheck": true,
24 | "strict": true,
25 | "target": "esnext"
26 | },
27 | }
28 |
--------------------------------------------------------------------------------
/twilio-voice-react-native.podspec:
--------------------------------------------------------------------------------
1 | require "json"
2 |
3 | package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4 |
5 | Pod::Spec.new do |s|
6 | s.name = "twilio-voice-react-native"
7 | s.version = package["version"]
8 | s.summary = package["description"]
9 | s.homepage = package["homepage"]
10 | s.license = package["license"]
11 | s.authors = package["author"]
12 |
13 | s.platforms = { :ios => "11.0" }
14 | s.source = { :git => "https://github.com/mhuynh5757/twilio-voice-react-native.git", :tag => "#{s.version}" }
15 |
16 | s.source_files = "ios/**/*.{h,m,mm}"
17 |
18 | s.dependency "React-Core"
19 | s.dependency "TwilioVoice", "6.12.1"
20 | s.xcconfig = { 'VALID_ARCHS' => 'arm64 x86_64' }
21 | s.pod_target_xcconfig = { 'VALID_ARCHS[sdk=iphoneos*]' => 'arm64', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'arm64 x86_64' }
22 | end
23 |
--------------------------------------------------------------------------------