diff --git a/README.md b/README.md index 51210f4..f681dd4 100644 --- a/README.md +++ b/README.md @@ -123,9 +123,9 @@ function displayNotification(uid:string, avatar?:string, timeout?:number, foregr | `channelName` | `string` | Channel name of the notification. | Yes | | `notificationIcon` | `string` | Icon of the notification (mipmap). | Yes | | `notificationTitle` | `string` | Title of the notification. | Yes | -| `notificationBody` | `string` | Body text of the notification. | Yes | -| `answerText` | `string` | Label for the answer button. | Yes | -| `declineText` | `string` | Label for the decline button. | Yes | +| `notificationBody` | `string` | Body text of the notification. On Android 12 and above, if the notificationBody is empty, the incoming call notification will display the description from CallStyle instead of this property. | No | +| `answerText` | `string` | Label for the answer button. On Android 12 and above, the incoming call notification displays the answerText from CallStyle instead of this property. | Yes | +| `declineText` | `string` | Label for the decline button. On Android 12 and above, the incoming call notification displays the declineText from CallStyle instead of this property. | Yes | | `notificationColor` | `string` (optional) | Color of the notification. | No | | `notificationSound` | `string` (optional) | Sound for the notification (raw). | No | | `mainComponent` | `string` (optional) | Main component name for a custom incoming call screen. | No | diff --git a/example/android/app/src/main/res/raw/skype_ring.mp3 b/example/android/app/src/main/res/raw/skype_ring.mp3 new file mode 100644 index 0000000..a770eb2 Binary files /dev/null and b/example/android/app/src/main/res/raw/skype_ring.mp3 differ diff --git a/example/package.json b/example/package.json index 99d1ee4..8f2963b 100644 --- a/example/package.json +++ b/example/package.json @@ -10,10 +10,13 @@ "build:ios": "cd ios && xcodebuild -workspace FullScreenNotificationIncomingCallExample.xcworkspace -scheme FullScreenNotificationIncomingCallExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" }, "dependencies": { + "@react-native-community/checkbox": "^0.5.17", "@react-native-community/masked-view": "^0.1.11", + "@react-native-picker/picker": "^2.9.0", "@react-navigation/native": "^5.8.0", "@react-navigation/stack": "^5.10.0", "@types/react": "18.3.1", + "add": "^2.0.6", "react": "18.2.0", "react-native": "0.74.1", "react-native-background-timer": "^2.4.1", @@ -22,7 +25,8 @@ "react-native-permissions": "^3.8.0", "react-native-safe-area-context": "^4.4.1", "react-native-screens": "3.29.0", - "uuid-random": "^1.3.2" + "uuid-random": "^1.3.2", + "yarn": "^1.22.22" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/example/src/CustomIncomingCall/index.tsx b/example/src/CustomIncomingCall/index.tsx index ad6e023..624d37e 100644 --- a/example/src/CustomIncomingCall/index.tsx +++ b/example/src/CustomIncomingCall/index.tsx @@ -1,29 +1,46 @@ import * as React from 'react'; -import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; import type { CustomIncomingActivityProps } from 'react-native-full-screen-notification-incoming-call'; import RNNotificationCall from '../../../src/index'; + export default function CustomIncomingCall(props: CustomIncomingActivityProps) { console.log('props===', props); const payload = JSON.parse(props.payload); console.log('payload', payload); + return ( - { - RNNotificationCall.declineCall(props.uuid, props.payload); + {/* Caller Info */} + {props.name || 'Unknown Caller'} + - Decline - - { - RNNotificationCall.answerCall(props.uuid, props.payload); - }} - > - Answer - + style={styles.callerImage} + /> + + {/* Decline and Answer Buttons */} + + { + RNNotificationCall.declineCall(props.uuid, props.payload); + }} + > + Decline + + + { + RNNotificationCall.answerCall(props.uuid, props.payload); + }} + > + Answer + + ); } @@ -33,11 +50,43 @@ const styles = StyleSheet.create({ flex: 1, alignItems: 'center', justifyContent: 'center', - backgroundColor: 'white', + backgroundColor: '#f2f2f2', + paddingHorizontal: 20, + }, + callerName: { + fontSize: 24, + fontWeight: 'bold', + marginVertical: 10, + color: '#333', + }, + callerImage: { + width: 100, + height: 100, + borderRadius: 50, + marginBottom: 30, + }, + buttonContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + width: '80%', + marginTop: 30, + }, + button: { + flex: 1, + alignItems: 'center', + paddingVertical: 15, + marginHorizontal: 10, + borderRadius: 10, + }, + declineButton: { + backgroundColor: '#ff4d4d', + }, + answerButton: { + backgroundColor: '#4CAF50', }, - box: { - width: 60, - height: 60, - marginVertical: 20, + buttonText: { + color: '#fff', + fontSize: 18, + fontWeight: '600', }, }); diff --git a/example/src/Detail/index.tsx b/example/src/Detail/index.tsx index 11c7f8d..fc4a2a6 100644 --- a/example/src/Detail/index.tsx +++ b/example/src/Detail/index.tsx @@ -3,18 +3,62 @@ import { useNavigation } from '@react-navigation/native'; import { StyleSheet, View, Text, TouchableOpacity } from 'react-native'; import { CallKeepService } from '../services/CallKeepService'; -export default function Detail() { +export default function InCallScreen() { const navigation = useNavigation(); + const [isMuted, setIsMuted] = React.useState(false); // Mute state + const [isOnHold, setIsOnHold] = React.useState(false); // Hold state + + // Handle end call + const handleEndCall = () => { + CallKeepService.instance().endAllCall(); + navigation.goBack(); + }; + + // Handle mute toggle + const handleMute = () => { + setIsMuted((prev) => !prev); + // Implement mute logic if needed here (e.g., CallKeepService mute call) + }; + + // Handle hold toggle + const handleHold = () => { + setIsOnHold((prev) => !prev); + // Implement hold logic if needed here + }; + return ( - { - navigation.goBack(); - CallKeepService.instance().endAllCall(); - }} - > - Go back - + + {/* Display the caller's info or call status */} + In Call + Caller: John Doe + + {/* Call Control Buttons */} + + + {isMuted ? 'Unmute' : 'Mute'} + + + + + {isOnHold ? 'Resume' : 'Hold'} + + + + + End Call + + + ); } @@ -22,13 +66,48 @@ export default function Detail() { const styles = StyleSheet.create({ container: { flex: 1, + justifyContent: 'center', alignItems: 'center', + backgroundColor: '#333', + }, + inCallContainer: { justifyContent: 'center', - backgroundColor: 'blue', + alignItems: 'center', + }, + callStatus: { + fontSize: 24, + fontWeight: 'bold', + color: 'white', + marginBottom: 20, + }, + callerInfo: { + fontSize: 18, + color: 'white', + marginBottom: 40, + }, + inCallButtons: { + flexDirection: 'row', // Use row to align buttons horizontally + justifyContent: 'space-around', // Space between buttons + width: '80%', // Give enough space for buttons to fit + }, + button: { + width: 80, // Give each button a fixed width + paddingVertical: 15, + borderRadius: 8, + alignItems: 'center', + }, + muteButton: { + backgroundColor: '#FFC107', // Yellow for mute + }, + holdButton: { + backgroundColor: '#2196F3', // Blue for hold + }, + endCallButton: { + backgroundColor: '#FF5722', // Red for end call }, - box: { - width: 60, - height: 60, - marginVertical: 20, + buttonText: { + color: 'white', + fontSize: 14, + fontWeight: 'bold', }, }); diff --git a/example/src/Home/index.tsx b/example/src/Home/index.tsx index f93b63a..291aa6d 100644 --- a/example/src/Home/index.tsx +++ b/example/src/Home/index.tsx @@ -1,54 +1,172 @@ -import * as React from 'react'; -import RNNotificationCall from '../../../src/index'; -import { StyleSheet, View, Text, TouchableOpacity } from 'react-native'; +import React, { useState } from 'react'; +import { + View, + Text, + TextInput, + Image, + StyleSheet, + TouchableOpacity, +} from 'react-native'; import ramdomUuid from 'uuid-random'; import { useNavigation } from '@react-navigation/native'; import { CallKeepService } from '../services/CallKeepService'; +import CheckBox from '@react-native-community/checkbox'; +import { Picker } from '@react-native-picker/picker'; CallKeepService.instance().setupCallKeep(); -export default function Home() { + +// Define ringtone channel IDs +const ringtoneChannelIds = { + default: 'com.abc.incomingcall', + skype_ring: 'com.skype.incomingcall', +}; + +const IncomingCallDemo = () => { const navigation = useNavigation(); CallKeepService.navigation = navigation; - const display = () => { - // Start a timer that runs once after X milliseconds - //rest of code will be performing for iOS on background too - const uuid = ramdomUuid(); - CallKeepService.instance().displayCall(uuid); - }; - const onHide = () => { - RNNotificationCall.hideNotification(); + + const [callerName, setCallerName] = useState('John Doe'); + const [callerImageURL, setCallerImageURL] = useState( + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQKet-b99huP_BtZT_HUqvsaSz32lhrcLtIDQ&s' + ); + const [selectedRingtone, setSelectedRingtone] = useState< + 'default' | 'skype_ring' + >('default'); + const [useCustomIncomingCallUI, setUseCustomIncomingCallUI] = useState(false); + const [isVideoCall, setIsVideoCall] = useState(true); + + // Function to trigger the incoming call notification + const displayIncomingCall = () => { + const callUUID = ramdomUuid(); + CallKeepService.instance().displayCall({ + uuid: callUUID, + handle: 'number', + localizedCallerName: callerName, + handleType: 'number', + callerImage: callerImageURL, + hasVideo: isVideoCall, + other: { + ringtone: selectedRingtone !== 'default' ? selectedRingtone : null, + mainComponent: useCustomIncomingCallUI ? 'MyReactNativeApp' : null, + channelId: + ringtoneChannelIds[selectedRingtone] || ringtoneChannelIds.default, + }, + }); }; + return ( - navigation.navigate('Detail')} - style={styles.button} - > - Go to detail - - - Display - + Incoming Call Notification + + {/* Input for Caller Name */} + Caller Name: + + + {/* Input for Caller Image */} + Caller Image URL: + + + + {!!callerImageURL && ( + + )} + + + {/* Ringtone Picker */} + Ringtone: + + + + + + - - Hide + {/* Checkbox for Is Video Incoming */} + Is Video Call: + + + {/* Checkbox for Custom UI */} + Use Custom Incoming Call UI: + + + {/* Button to Simulate Incoming Call */} + + Display Incoming Call ); -} +}; const styles = StyleSheet.create({ container: { flex: 1, + padding: 20, + }, + header: { + fontSize: 24, + fontWeight: 'bold', + marginBottom: 20, + textAlign: 'center', + }, + label: { + fontSize: 16, + marginVertical: 10, + }, + input: { + borderWidth: 1, + borderColor: '#ddd', + borderRadius: 8, + padding: 10, + marginBottom: 10, + color: 'black', + }, + imagePreviewContainer: { alignItems: 'center', - justifyContent: 'center', + marginBottom: 20, + }, + imagePreview: { + width: 100, + height: 100, + borderRadius: 50, }, - box: { - width: 60, - height: 60, - marginVertical: 20, + pickerContainer: { + borderColor: 'gray', + borderWidth: 1, }, button: { - marginVertical: 10, + backgroundColor: '#4CAF50', + padding: 15, + borderRadius: 8, + alignItems: 'center', + marginTop: 20, + }, + buttonText: { + color: '#fff', + fontSize: 16, }, }); + +export default IncomingCallDemo; diff --git a/example/src/services/CallKeepService.ts b/example/src/services/CallKeepService.ts index 52d987f..8efe0b6 100644 --- a/example/src/services/CallKeepService.ts +++ b/example/src/services/CallKeepService.ts @@ -4,6 +4,7 @@ import RNNotificationCall, { type DeclinePayload, } from '../../../src/index'; import RNCallKeep from 'react-native-callkeep'; +import type { HandleType } from 'react-native-callkeep'; import { check, PERMISSIONS, @@ -15,7 +16,7 @@ const appName = 'Incoming-Test'; const isAndroid = Platform.OS === 'android'; const answerOption = { channelId: 'com.abc.incomingcall', - channelName: 'Incoming video call', + channelName: 'Incoming Call', notificationIcon: 'ic_launcher', //mipmap notificationTitle: 'Linh Vo', answerText: 'Answer', @@ -28,6 +29,7 @@ const answerOption = { export class CallKeepService { private static _instance?: CallKeepService; static navigation: any; + private static otherInformation: any; constructor() { //setup callkeep // this.setupCallKeep(); @@ -105,25 +107,35 @@ export class CallKeepService { RNCallKeep.addEventListener('endCall', this.onCallKeepEndCallAction); if (isAndroid) { //event only on android - RNCallKeep.addEventListener('showIncomingCallUi', ({ callUUID }) => { - RNNotificationCall.displayNotification( - callUUID, - 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQKet-b99huP_BtZT_HUqvsaSz32lhrcLtIDQ&s', - 30000, - { - ...answerOption, - channelId: 'com.abc.incomingcall', - channelName: 'Incoming video call', - notificationTitle: 'Linh Vo', - notificationBody: 'Incoming video call', - isVideo: true, - // mainComponent: "MyReactNativeApp", - payload: { - extra: 'extra', - }, - } - ); - }); + RNCallKeep.addEventListener( + 'showIncomingCallUi', + // @ts-ignore:next-line + ({ callUUID, name, hasVideo = 'false' }) => { + const isVideo = hasVideo === 'true'; + RNNotificationCall.displayNotification( + callUUID, + 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQKet-b99huP_BtZT_HUqvsaSz32lhrcLtIDQ&s', + 30000, + { + ...answerOption, + channelId: + CallKeepService.otherInformation?.channelId || + 'com.abc.incomingcall', + channelName: 'Incoming Call', + notificationTitle: name, + notificationBody: isVideo + ? 'Incoming video call' + : 'Incoming call', + isVideo: isVideo, + mainComponent: CallKeepService.otherInformation?.mainComponent, + notificationSound: CallKeepService.otherInformation?.ringtone, + payload: { + extra: 'extra', + }, + } + ); + } + ); // Listen to headless action events RNNotificationCall.addEventListener( 'endCall', @@ -167,18 +179,37 @@ export class CallKeepService { //You need to call RNCallKeep.endCall(callUUID) to end call } - async displayCall(uuid: string) { + async displayCall(data: { + uuid: string; + handle: string; + localizedCallerName: string; + handleType: HandleType; + hasVideo: boolean; + callerImage?: string; + other?: any; + }) { + const { + uuid, + handle, + localizedCallerName, + handleType, + hasVideo, + callerImage, + other, + } = data; const granted = await check(PERMISSIONS.ANDROID.READ_PHONE_NUMBERS); //only display call when permission granted if (granted !== RESULTS.GRANTED) return; - console.log('display call', uuid); + CallKeepService.otherInformation = other; RNCallKeep.displayIncomingCall( uuid, - 'Linh Vo', - 'Linh Vo', - 'number', - true, - undefined + handle, + localizedCallerName, + handleType, + hasVideo, + { + callerImage, + } ); } endAllCall() { diff --git a/src/index.tsx b/src/index.tsx index c763a24..b8d2ed5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -57,7 +57,7 @@ export interface ForegroundOptionsModel { /** Title of the notification */ notificationTitle: string; /** Body of the notification */ - notificationBody: string; + notificationBody?: string | null; /** Label for the answer button */ answerText: string; /** Label for the decline button */ @@ -76,7 +76,7 @@ export interface ForegroundOptionsModel { /** * Properties for the custom incoming activity */ -export interface CustomIncomingActivityProps { +export interface CustomIncomingActivityProps extends ForegroundOptionsModel { /** * Unique identifier for the call. * This ID helps to distinguish between different call instances. @@ -86,6 +86,8 @@ export interface CustomIncomingActivityProps { info?: string; /** Unique identifier for the call */ uuid: string; + /** Caller name */ + name: string; /** * Additional data related to the call (optional). * This can be any JSON string containing extra information about the call. diff --git a/yarn.lock b/yarn.lock index 7859a00..04e1e35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2615,6 +2615,20 @@ __metadata: languageName: node linkType: hard +"@react-native-community/checkbox@npm:^0.5.17": + version: 0.5.17 + resolution: "@react-native-community/checkbox@npm:0.5.17" + peerDependencies: + react: "*" + react-native: ">= 0.62" + react-native-windows: ">=0.62" + peerDependenciesMeta: + react-native-windows: + optional: true + checksum: 98cddff11b976a1f712bfc4d7de946d5e102108bedc016bbcb668b3917757dd5092b5b93f651f740508dd8437ba3a0ce5ca15cef3ebf7bcfc15a490b0b7d0d6b + languageName: node + linkType: hard + "@react-native-community/cli-clean@npm:13.6.6": version: 13.6.6 resolution: "@react-native-community/cli-clean@npm:13.6.6" @@ -2806,6 +2820,16 @@ __metadata: languageName: node linkType: hard +"@react-native-picker/picker@npm:^2.9.0": + version: 2.9.0 + resolution: "@react-native-picker/picker@npm:2.9.0" + peerDependencies: + react: "*" + react-native: "*" + checksum: 3f88ecbad85f071861bbd16c0d4891dd5eff13d638e6ecfe68da279a07e0a31dfa7844ae329fe2a2144695456f1c96d1be38e10185475dd164f8e918d6d083a1 + languageName: node + linkType: hard + "@react-native/assets-registry@npm:0.74.83": version: 0.74.83 resolution: "@react-native/assets-registry@npm:0.74.83" @@ -3633,6 +3657,13 @@ __metadata: languageName: node linkType: hard +"add@npm:^2.0.6": + version: 2.0.6 + resolution: "add@npm:2.0.6" + checksum: e2d23d40494565dfed4acd65e478570c444db5ac6c053551ed429c39ea0f2c99d83df63e7befec936df601827d2254d06a2fb6f7dcfd2022e810b25eab818b8c + languageName: node + linkType: hard + "agent-base@npm:^7.0.2, agent-base@npm:^7.1.0, agent-base@npm:^7.1.1": version: 7.1.1 resolution: "agent-base@npm:7.1.1" @@ -11094,13 +11125,16 @@ __metadata: "@babel/core": ^7.20.0 "@babel/preset-env": ^7.20.0 "@babel/runtime": ^7.20.0 + "@react-native-community/checkbox": ^0.5.17 "@react-native-community/masked-view": ^0.1.11 + "@react-native-picker/picker": ^2.9.0 "@react-native/babel-preset": 0.74.83 "@react-native/metro-config": 0.74.83 "@react-native/typescript-config": 0.74.83 "@react-navigation/native": ^5.8.0 "@react-navigation/stack": ^5.10.0 "@types/react": 18.3.1 + add: ^2.0.6 babel-plugin-module-resolver: ^5.0.0 react: 18.2.0 react-native: 0.74.1 @@ -11111,6 +11145,7 @@ __metadata: react-native-safe-area-context: ^4.4.1 react-native-screens: 3.29.0 uuid-random: ^1.3.2 + yarn: ^1.22.22 languageName: unknown linkType: soft @@ -13755,6 +13790,16 @@ __metadata: languageName: node linkType: hard +"yarn@npm:^1.22.22": + version: 1.22.22 + resolution: "yarn@npm:1.22.22" + bin: + yarn: bin/yarn.js + yarnpkg: bin/yarn.js + checksum: 59aeef5ccfd3347287f939448e6d3594f0a42f74025b9bdc2a277641c1d4070c07a38b6e7c35e695f77410b0269a5a43c78535786564f86f39c9f781e6efa311 + languageName: node + linkType: hard + "yn@npm:3.1.1": version: 3.1.1 resolution: "yn@npm:3.1.1"