From fbc084b8b4931cc5778da572f67c9e1e020b7d2a Mon Sep 17 00:00:00 2001 From: isaacakakpo1 Date: Mon, 11 Nov 2024 12:42:41 +0100 Subject: [PATCH] Handle Late Notifications as missed calls --- .lh/README.md.json | 6 +- .../service/notification_service.dart.json | 18 ++++++ .lh/lib/view/screen/home_screen.dart.json | 6 +- README.md | 17 ++++++ lib/main.dart | 15 +++++ lib/service/notification_service.dart | 61 +++++++++++++++++-- lib/view/screen/home_screen.dart | 2 +- 7 files changed, 118 insertions(+), 7 deletions(-) create mode 100644 .lh/lib/service/notification_service.dart.json diff --git a/.lh/README.md.json b/.lh/README.md.json index a08130e..8c85b49 100644 --- a/.lh/README.md.json +++ b/.lh/README.md.json @@ -3,7 +3,7 @@ "activeCommit": 0, "commits": [ { - "activePatchIndex": 3, + "activePatchIndex": 4, "patches": [ { "date": 1724322403635, @@ -20,6 +20,10 @@ { "date": 1724334988739, "content": "Index: \n===================================================================\n--- \n+++ \n@@ -0,0 +1,461 @@\n+![Pub Version](https://img.shields.io/pub/v/telnyx_webrtc?color=blue&logo=telnyx)\n+[![Flutter Test](https://github.com/team-telnyx/telnyx-webrtc-flutter/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/team-telnyx/telnyx-webrtc-flutter/actions/workflows/unit_tests.yml)\n+\n+# Telnyx Flutter Voice SDK\n+\n+\n+Enable Telnyx real-time communication services on Flutter applications (Android / iOS / Web) :telephone_receiver: :fire:\n+\n+## Features\n+- [x] Create / Receive calls\n+- [x] Hold calls\n+- [x] Mute calls\n+- [x] Dual Tone Multi Frequency\n+\n+## Usage\n+\n+### SIP Credentials\n+In order to start making and receiving calls using the TelnyxRTC SDK you will need to get SIP Credentials:\n+\n+![Screenshot 2022-07-15 at 13 51 45](https://user-images.githubusercontent.com/9112652/179226614-f0477f38-6131-4cef-9c7a-3366f23a89b6.png)\n+\n+1. Access to https://portal.telnyx.com/\n+2. Sign up for a Telnyx Account.\n+3. Create a Credential Connection to configure how you connect your calls.\n+4. Create an Outbound Voice Profile to configure your outbound call settings and assign it to your Credential Connection.\n+\n+For more information on how to generate SIP credentials check the [Telnyx WebRTC quickstart guide](https://developers.telnyx.com/docs/v2/webrtc/quickstart).\n+\n+### Platform Specific Configuration\n+\n+## Android\n+If you are implementing the SDK into an Android application it is important to remember to add the following permissions to your AndroidManifest in order to allow Audio and Internet permissions:\n+\n+```xml\n+ \n+ \n+ \n+```\n+\n+## iOS\n+on the iOS platform, you need to add the microphone permission to your Info.plist file:\n+\n+```xml\n+ NSMicrophoneUsageDescription\n+ $(PRODUCT_NAME) Microphone Usage!\n+```\n+\n+### Telnyx Client\n+TelnyxClient() is the core class of the SDK, and can be used to connect to our backend socket connection, create calls, check state and disconnect, etc.\n+\n+Once an instance is created, you can call the .connect() method to connect to the socket. An error will appear as a socket response if there is no network available:\n+\n+```dart\n+ TelnyxClient _telnyxClient = TelnyxClient();\n+ _telnyxClient.connect();\n+```\n+\n+### Logging into Telnyx Client\n+To log into the Telnyx WebRTC client, you'll need to authenticate using a Telnyx SIP Connection. Follow our [quickstart guide](https://developers.telnyx.com/docs/v2/webrtc/quickstart) to create **JWTs** (JSON Web Tokens) to authenticate. To log in with a token we use the tokinLogin() method. You can also authenticate directly with the SIP Connection `username` and `password` with the credentialLogin() method:\n+\n+ ```dart\n+ _telnyxClient.tokenLogin(tokenConfig)\n+ //OR\n+ _telnyxClient.credentialLogin(credentialConfig) \n+ ```\n+\n+**Note:** **tokenConfig** and **credentialConfig** are simple classes that represent login settings for the client to use. They look like this:\n+\n+ ```dart\n+ /// Creates an instance of CredentialConfig which can be used to log in\n+///\n+/// Uses the [sipUser] and [sipPassword] fields to log in\n+/// [sipCallerIDName] and [sipCallerIDNumber] will be the Name and Number associated\n+/// [notificationToken] is the token used to register the device for notifications if required (FCM or APNS)\n+/// The [autoReconnect] flag decided whether or not to attempt a reconnect (3 attempts) in the case of a login failure with\n+/// legitimate credentials\n+class CredentialConfig {\n+ CredentialConfig(this.sipUser, this.sipPassword, this.sipCallerIDName,\n+ this.sipCallerIDNumber, this.notificationToken, this.autoReconnect);\n+\n+ final String sipUser;\n+ final String sipPassword;\n+ final String sipCallerIDName;\n+ final String sipCallerIDNumber;\n+ final String? notificationToken;\n+ final bool? autoReconnect;\n+}\n+\n+/// Creates an instance of TokenConfig which can be used to log in\n+///\n+/// Uses the [sipToken] field to log in\n+/// [sipCallerIDName] and [sipCallerIDNumber] will be the Name and Number associated\n+/// [notificationToken] is the token used to register the device for notifications if required (FCM or APNS)\n+/// The [autoReconnect] flag decided whether or not to attempt a reconnect (3 attempts) in the case of a login failure with\n+/// a legitimate token\n+class TokenConfig {\n+ TokenConfig(this.sipToken, this.sipCallerIDName, this.sipCallerIDNumber,\n+ this.notificationToken, this.autoReconnect);\n+\n+ final String sipToken;\n+ final String sipCallerIDName;\n+ final String sipCallerIDNumber;\n+ final String? notificationToken;\n+ final bool? autoReconnect;\n+}\n+ ```\n+ \n+### Adding push notifications - Android platform\n+The Android platform makes use of Firebase Cloud Messaging in order to deliver push notifications. To receive notifications when receiving calls on your Android mobile device you will have to enable Firebase Cloud Messaging within your application.\n+For a detailed tutorial, please visit our official [Push Notification Docs](https://developers.telnyx.com/docs/v2/webrtc/push-notifications?type=Android).\n+The Demo app uses the [FlutterCallkitIncoming](https://pub.dev/packages/flutter_callkit_incoming) plugin to show incoming calls. To show a notification when receiving a call, you can follow the steps below:\n+1. Listen for Background Push Notifications, Implement the `FirebaseMessaging.onBackgroundMessage` method in your `main` method\n+```dart\n+\n+@pragma('vm:entry-point')\n+Future main() async {\n+ WidgetsFlutterBinding.ensureInitialized();\n+\n+ if (defaultTargetPlatform == TargetPlatform.android) {\n+ // Android Only - Push Notifications\n+ await Firebase.initializeApp();\n+ FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);\n+ \n+ await FirebaseMessaging.instance\n+ .setForegroundNotificationPresentationOptions(\n+ alert: true,\n+ badge: true,\n+ sound: true,\n+ );\n+ }\n+ runApp(const MyApp());\n+}\n+```\n+\n+2. Optionally Add the `metadata` to CallKitParams `extra` field\n+```dart\n+\n+ static Future showNotification(RemoteMessage message) {\n+ CallKitParams callKitParams = CallKitParams(\n+ android:...,\n+ ios:...,\n+ extra: message.data,\n+ )\n+ await FlutterCallkitIncoming.showCallkitIncoming(callKitParams);\n+ }\n+```\n+\n+\n+3. Handle the push notification in the `_firebaseMessagingBackgroundHandler` method\n+```dart\n+\n+Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async {\n+ //show notifcation\n+ showNotification(message);\n+ \n+ //Listen to action from FlutterCallkitIncoming\n+ FlutterCallkitIncoming.onEvent.listen((CallEvent? event) async {\n+ switch (event!.event) {\n+ case Event.actionCallAccept:\n+ // Set the telnyx metadata for access when the app comes to foreground\n+ TelnyxClient.setPushMetaData(\n+ message.data, isAnswer: true, isDecline: false);\n+ break;\n+ case Event.actionCallDecline:\n+ /*\n+ * When the user declines the call from the push notification, the app will no longer be visible, and we have to\n+ * handle the endCall user here.\n+ * Login to the TelnyxClient and end the call\n+ * */\n+ ...\n+ }});\n+}\n+\n+\n+```\n+\n+4. Use the `TelnyxClient.getPushMetaData()` method to retrieve the metadata when the app comes to the foreground. This data is only available on 1st access and becomes `null` afterward.\n+```dart\n+ Future _handlePushNotification() async {\n+ final data = await TelnyxClient.getPushMetaData();\n+ PushMetaData? pushMetaData = PushMetaData.fromJson(data);\n+ if (pushMetaData != null) {\n+ _telnyxClient.handlePushNotification(pushMetaData, credentialConfig, tokenConfig);\n+ }\n+ }\n+```\n+\n+5. To Handle push calls on foreground, Listen for Call Events and invoke the `handlePushNotification` method\n+```dart\n+FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {\n+ switch (event!.event) {\n+ case Event.actionCallIncoming:\n+ // retrieve the push metadata from extras\n+ final data = await TelnyxClient.getPushData();\n+ ...\n+ _telnyxClient.handlePushNotification(pushMetaData, credentialConfig, tokenConfig);\n+ break;\n+ case Event.actionCallStart:\n+ ....\n+ break;\n+ case Event.actionCallAccept:\n+ ...\n+ logger.i('Call Accepted Attach Call');\n+ break;\n+ });\n+```\n+\n+#### Best Practices for Push Notifications on Android \n+1. Request for Notification Permissions for android 13+ devices to show push notifications. More information can be found [here](https://developer.android.com/develop/ui/views/notifications/notification-permission)\n+2. Push Notifications only work in foreground for apps that are run in `debug` mode (You will not receive push notifications when you terminate the app while running in debug mode).\n+3. On Foreground calls, you can use the `FirebaseMessaging.onMessage.listen` method to listen for incoming calls and show a notification.\n+```dart\n+ FirebaseMessaging.onMessage.listen((RemoteMessage message) {\n+ TelnyxClient.setPushMetaData(message.data);\n+ NotificationService.showNotification(message);\n+ mainViewModel.callFromPush = true;\n+ });\n+```\n+4. To handle push notifications on the background, use the `FirebaseMessaging.onBackgroundMessage` method to listen for incoming calls and show a notification and make sure to set the ` TelnyxClient.setPushMetaData` when user answers the call.\n+```dart \n+ TelnyxClient.setPushMetaData(\n+ message.data, isAnswer: true, isDecline: false);\n+```\n+5. When you call the `telnyxClient.handlePushNotification` it connects to the `telnyxClient`, make sure not to call the `telnyxClient.connect()` method after this. e.g an Edge case might be if you call `telnyxClient.connect()` on Widget `init` method it\n+ will always call the `connect` method\n+\n+\n+6. Early Answer/Decline : Users may answer/decline the call too early before a socket connection is established. To handle this situation,\n+assert if the `IncomingInviteParams` is not null and only accept/decline if this is availalble. \n+```dart\n+bool waitingForInvite = false;\n+\n+void accept() {\n+\n+if (_incomingInvite != null) {\n+ // accept the call if the incomingInvite arrives on time \n+ _currentCall = _telnyxClient.acceptCall(\n+ _incomingInvite!, _localName, _localNumber, \"State\");\n+ } else {\n+ // set waitingForInvite to true if we have an early accept\n+ waitingForInvite = true;\n+ }\n+}\n+\n+\n+ _telnyxClient.onSocketMessageReceived = (TelnyxMessage message) {\n+ switch (message.socketMethod) {\n+ ...\n+ case SocketMethod.INVITE:\n+ {\n+ if (callFromPush) {\n+ // For early accept of call\n+ if (waitingForInvite) {\n+ //accept the call\n+ accept();\n+ waitingForInvite = false;\n+ }\n+ callFromPush = false;\n+ }\n+\n+ }\n+ ...\n+ }\n+ }\n+```\n+\n+\n+ \n+### Adding push notifications - iOS platform\n+The iOS Platform makes use of the Apple Push Notification Service (APNS) and Pushkit in order to deliver and receive push notifications\n+For a detailed tutorial, please visit our official [Push Notification Docs](https://developers.telnyx.com/docs/v2/webrtc/push-notifications?lang=ios)\n+1. Register/Invalidate the push device token for iOS\n+```swift\n+ func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {\n+ print(credentials.token)\n+ let deviceToken = credentials.token.map { String(format: \"%02x\", $0) }.joined()\n+ //Save deviceToken to your server\n+ SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken)\n+ }\n+ \n+ func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {\n+ SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(\"\")\n+ }\n+```\n+\n+2. For foreground calls to work, you need to register with callkit on the restorationHandler delegate function. You can also choose to register with callkit using iOS official documentation on\n+ [CallKit](https://developer.apple.com/documentation/callkit/).\n+```swift\n+ override func application(_ application: UIApplication,\n+ continue userActivity: NSUserActivity,\n+ restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {\n+ \n+ let nameCaller = handleObj.getDecryptHandle()[\"nameCaller\"] as? String ?? \"\"\n+ let handle = handleObj.getDecryptHandle()[\"handle\"] as? String ?? \"\"\n+ let data = flutter_callkit_incoming.Data(id: UUID().uuidString, nameCaller: nameCaller, handle: handle, type: isVideo ? 1 : 0)\n+ //set more data...\n+ data.nameCaller = \"dummy\"\n+ SwiftFlutterCallkitIncomingPlugin.sharedInstance?.startCall(data, fromPushKit: true)\n+ \n+ } \n+```\n+3. Listen for incoming calls in AppDelegate.swift class\n+```swift \n+ func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {\n+ print(\"didReceiveIncomingPushWith\")\n+ guard type == .voIP else { return }\n+ \n+ if let metadata = payload.dictionaryPayload[\"metadata\"] as? [String: Any] {\n+ var callID = UUID.init().uuidString\n+ if let newCallId = (metadata[\"call_id\"] as? String),\n+ !newCallId.isEmpty {\n+ callID = newCallId\n+ }\n+ let callerName = (metadata[\"caller_name\"] as? String) ?? \"\"\n+ let callerNumber = (metadata[\"caller_number\"] as? String) ?? \"\"\n+ \n+ let id = payload.dictionaryPayload[\"call_id\"] as? String ?? UUID().uuidString\n+ \n+ let data = flutter_callkit_incoming.Data(id: id, nameCaller: callerName, handle: callerNumber, type: isVideo ? 1 : 0)\n+ data.extra = payload.dictionaryPayload as NSDictionary\n+ data.normalHandle = 1 \n+ \n+ let caller = callerName.isEmpty ? (callerNumber.isEmpty ? \"Unknown\" : callerNumber) : callerName\n+ let uuid = UUID(uuidString: callID)\n+ \n+ data.uuid = uuid!.uuidString\n+ data.nameCaller = caller\n+ \n+ SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true)\n+ }\n+ }\n+```\n+\n+4. Listen for Call Events and invoke the `handlePushNotification` method\n+```dart\n+ FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {\n+ switch (event!.event) {\n+ case Event.actionCallIncoming:\n+ // retrieve the push metadata from extras\n+ PushMetaData? pushMetaData = PushMetaData.fromJson(event.body['extra']['metadata']);\n+ _telnyxClient.handlePushNotification(pushMetaData, credentialConfig, tokenConfig);\n+ break;\n+ case Event.actionCallStart:\n+ ....\n+ break;\n+ case Event.actionCallAccept:\n+ ...\n+ logger.i('Call Accepted Attach Call');\n+ break;\n+ });\n+```\n+\n+\n+\n+#### Best Practices for Push Notifications on iOS\n+1. Push Notifications only work in foreground for apps that are run in `debug` mode (You will not receive push notifications when you terminate the app while running in debug mode). Make sure you are in `release` mode. Preferably test using Testfight or Appstore.\n+To test if push notifications are working, disconnect the telnyx client (while app is in foreground) and make a call to the device. You should receive a push notification.\n+\n+\n+### Creating a call invitation\n+In order to make a call invitation, we first create an instance of the Call class with the .call instance. This creates a Call class which can be used to interact with calls (invite, accept, decline, etc).\n+To then send an invite, we can use the .newInvite() method which requires you to provide your callerName, callerNumber, the destinationNumber (or SIP credential), and your clientState (any String value).\n+\n+```dart\n+ _telnyxClient\n+ .call\n+ .newInvite(\"callerName\", \"000000000\", destination, \"State\");\n+```\n+\n+### Accepting a call\n+In order to be able to accept a call, we first need to listen for invitations. We do this by getting the Telnyx Socket Response callbacks:\n+\n+```dart\n+ // Observe Socket Messages Received\n+_telnyxClient.onSocketMessageReceived = (TelnyxMessage message) {\n+ switch (message.socketMethod) {\n+ case SocketMethod.CLIENT_READY:\n+ {\n+ // Fires once client has correctly been setup and logged into, you can now make calls. \n+ break;\n+ }\n+ case SocketMethod.LOGIN:\n+ {\n+ // Handle a successful login - Update UI or Navigate to new screen, etc. \n+ break;\n+ }\n+ case SocketMethod.INVITE:\n+ {\n+ // Handle an invitation Update UI or Navigate to new screen, etc. \n+ // Then, through an answer button of some kind we can accept the call with:\n+ _incomingInvite = message.message.inviteParams;\n+ _telnyxClient.createCall().acceptCall(\n+ _incomingInvite, \"callerName\", \"000000000\", \"State\");\n+ break;\n+ }\n+ case SocketMethod.ANSWER:\n+ {\n+ // Handle a received call answer - Update UI or Navigate to new screen, etc.\n+ break;\n+ }\n+ case SocketMethod.BYE:\n+ {\n+ // Handle a call rejection or ending - Update UI or Navigate to new screen, etc.\n+ break;\n+ }\n+ }\n+ notifyListeners();\n+};\n+```\n+\n+We can then use this method to create a listener that listens for an invitation and, in this case, answers it straight away. A real implementation would be more suited to show some UI and allow manual accept / decline operations. \n+\n+### Decline / End Call\n+\n+In order to end a call, we can get a stored instance of Call and call the .endCall(callID) method. To decline an incoming call we first create the call with the .createCall() method and then call the .endCall(callID) method:\n+\n+```dart\n+ if (_ongoingCall) {\n+ _telnyxClient.call.endCall(_telnyxClient.call.callId);\n+ } else {\n+ _telnyxClient.createCall().endCall(_incomingInvite?.callID);\n+ }\n+```\n+\n+### DTMF (Dual Tone Multi Frequency)\n+\n+In order to send a DTMF message while on a call you can call the .dtmf(callID, tone), method where tone is a String value of the character you would like pressed:\n+\n+```dart\n+ _telnyxClient.call.dtmf(_telnyxClient.call.callId, tone);\n+```\n+\n+### Mute a call\n+\n+To mute a call, you can simply call the .onMuteUnmutePressed() method:\n+\n+```dart\n+ _telnyxClient.call.onMuteUnmutePressed();\n+```\n+\n+### Toggle loud speaker\n+\n+To toggle loud speaker, you can simply call .enableSpeakerPhone(bool):\n+\n+```dart\n+ _telnyxClient.call.enableSpeakerPhone(true);\n+```\n+\n+### Put a call on hold\n+\n+To put a call on hold, you can simply call the .onHoldUnholdPressed() method:\n+\n+```dart\n+ _telnyxClient.call.onHoldUnholdPressed();\n+```\n+\n+Questions? Comments? Building something rad? [Join our Slack channel](https://joinslack.telnyx.com/) and share.\n+\n+## License\n+\n+[`MIT Licence`](./LICENSE) © [Telnyx](https://github.com/team-telnyx)\n" + }, + { + "date": 1731275506040, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -421,477 +421,25 @@\n _telnyxClient.createCall().endCall(_incomingInvite?.callID);\n }\n ```\n \n-### DTMF (Dual Tone Multi Frequency)\n+### Handling Late Notifications \n+If notifcations arrive very late due to no internet connectivity, It is good to always flag it as a missed call. You can do that using the \n+code snippet below : \n \n-In order to send a DTMF message while on a call you can call the .dtmf(callID, tone), method where tone is a String value of the character you would like pressed:\n-\n ```dart\n- _telnyxClient.call.dtmf(_telnyxClient.call.callId, tone);\n-```\n+const CALL_MISSED_TIMEOUT = 60;\n \n-### Mute a call\n+ DateTime nowTime = DateTime.now();\n+ Duration? difference = nowTime?.difference(message.sentTime!);\n \n-To mute a call, you can simply call the .onMuteUnmutePressed() method:\n-\n-```dart\n- _telnyxClient.call.onMuteUnmutePressed();\n-```\n-\n-### Toggle loud speaker\n-\n-To toggle loud speaker, you can simply call .enableSpeakerPhone(bool):\n-\n-```dart\n- _telnyxClient.call.enableSpeakerPhone(true);\n-```\n-\n-### Put a call on hold\n-\n-To put a call on hold, you can simply call the .onHoldUnholdPressed() method:\n-\n-```dart\n- _telnyxClient.call.onHoldUnholdPressed();\n-```\n-\n-Questions? Comments? Building something rad? [Join our Slack channel](https://joinslack.telnyx.com/) and share.\n-\n-## License\n-\n-[`MIT Licence`](./LICENSE) © [Telnyx](https://github.com/team-telnyx)\n-![Pub Version](https://img.shields.io/pub/v/telnyx_webrtc?color=blue&logo=telnyx)\n-[![Flutter Test](https://github.com/team-telnyx/telnyx-webrtc-flutter/actions/workflows/unit_tests.yml/badge.svg)](https://github.com/team-telnyx/telnyx-webrtc-flutter/actions/workflows/unit_tests.yml)\n-\n-# Telnyx Flutter Voice SDK\n-\n-\n-Enable Telnyx real-time communication services on Flutter applications (Android / iOS / Web) :telephone_receiver: :fire:\n-\n-## Features\n-- [x] Create / Receive calls\n-- [x] Hold calls\n-- [x] Mute calls\n-- [x] Dual Tone Multi Frequency\n-\n-## Usage\n-\n-### SIP Credentials\n-In order to start making and receiving calls using the TelnyxRTC SDK you will need to get SIP Credentials:\n-\n-![Screenshot 2022-07-15 at 13 51 45](https://user-images.githubusercontent.com/9112652/179226614-f0477f38-6131-4cef-9c7a-3366f23a89b6.png)\n-\n-1. Access to https://portal.telnyx.com/\n-2. Sign up for a Telnyx Account.\n-3. Create a Credential Connection to configure how you connect your calls.\n-4. Create an Outbound Voice Profile to configure your outbound call settings and assign it to your Credential Connection.\n-\n-For more information on how to generate SIP credentials check the [Telnyx WebRTC quickstart guide](https://developers.telnyx.com/docs/v2/webrtc/quickstart).\n-\n-### Platform Specific Configuration\n-\n-## Android\n-If you are implementing the SDK into an Android application it is important to remember to add the following permissions to your AndroidManifest in order to allow Audio and Internet permissions:\n-\n-```xml\n- \n- \n- \n-```\n-\n-## iOS\n-on the iOS platform, you need to add the microphone permission to your Info.plist file:\n-\n-```xml\n- NSMicrophoneUsageDescription\n- $(PRODUCT_NAME) Microphone Usage!\n-```\n-\n-### Telnyx Client\n-TelnyxClient() is the core class of the SDK, and can be used to connect to our backend socket connection, create calls, check state and disconnect, etc.\n-\n-Once an instance is created, you can call the .connect() method to connect to the socket. An error will appear as a socket response if there is no network available:\n-\n-```dart\n- TelnyxClient _telnyxClient = TelnyxClient();\n- _telnyxClient.connect();\n-```\n-\n-### Logging into Telnyx Client\n-To log into the Telnyx WebRTC client, you'll need to authenticate using a Telnyx SIP Connection. Follow our [quickstart guide](https://developers.telnyx.com/docs/v2/webrtc/quickstart) to create **JWTs** (JSON Web Tokens) to authenticate. To log in with a token we use the tokinLogin() method. You can also authenticate directly with the SIP Connection `username` and `password` with the credentialLogin() method:\n-\n- ```dart\n- _telnyxClient.tokenLogin(tokenConfig)\n- //OR\n- _telnyxClient.credentialLogin(credentialConfig) \n- ```\n-\n-**Note:** **tokenConfig** and **credentialConfig** are simple classes that represent login settings for the client to use. They look like this:\n-\n- ```dart\n- /// Creates an instance of CredentialConfig which can be used to log in\n-///\n-/// Uses the [sipUser] and [sipPassword] fields to log in\n-/// [sipCallerIDName] and [sipCallerIDNumber] will be the Name and Number associated\n-/// [notificationToken] is the token used to register the device for notifications if required (FCM or APNS)\n-/// The [autoReconnect] flag decided whether or not to attempt a reconnect (3 attempts) in the case of a login failure with\n-/// legitimate credentials\n-class CredentialConfig {\n- CredentialConfig(this.sipUser, this.sipPassword, this.sipCallerIDName,\n- this.sipCallerIDNumber, this.notificationToken, this.autoReconnect);\n-\n- final String sipUser;\n- final String sipPassword;\n- final String sipCallerIDName;\n- final String sipCallerIDNumber;\n- final String? notificationToken;\n- final bool? autoReconnect;\n+ if (difference.inSeconds > CALL_MISSED_TIMEOUT) {\n+ NotificationService.showMissedCallNotification(message);\n+ return;\n }\n-\n-/// Creates an instance of TokenConfig which can be used to log in\n-///\n-/// Uses the [sipToken] field to log in\n-/// [sipCallerIDName] and [sipCallerIDNumber] will be the Name and Number associated\n-/// [notificationToken] is the token used to register the device for notifications if required (FCM or APNS)\n-/// The [autoReconnect] flag decided whether or not to attempt a reconnect (3 attempts) in the case of a login failure with\n-/// a legitimate token\n-class TokenConfig {\n- TokenConfig(this.sipToken, this.sipCallerIDName, this.sipCallerIDNumber,\n- this.notificationToken, this.autoReconnect);\n-\n- final String sipToken;\n- final String sipCallerIDName;\n- final String sipCallerIDNumber;\n- final String? notificationToken;\n- final bool? autoReconnect;\n-}\n- ```\n- \n-### Adding push notifications - Android platform\n-The Android platform makes use of Firebase Cloud Messaging in order to deliver push notifications. To receive notifications when receiving calls on your Android mobile device you will have to enable Firebase Cloud Messaging within your application.\n-For a detailed tutorial, please visit our official [Push Notification Docs](https://developers.telnyx.com/docs/v2/webrtc/push-notifications?type=Android).\n-The Demo app uses the [FlutterCallkitIncoming](https://pub.dev/packages/flutter_callkit_incoming) plugin to show incoming calls. To show a notification when receiving a call, you can follow the steps below:\n-1. Listen for Background Push Notifications, Implement the `FirebaseMessaging.onBackgroundMessage` method in your `main` method\n-```dart\n-\n-@pragma('vm:entry-point')\n-Future main() async {\n- WidgetsFlutterBinding.ensureInitialized();\n-\n- if (defaultTargetPlatform == TargetPlatform.android) {\n- // Android Only - Push Notifications\n- await Firebase.initializeApp();\n- FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);\n- \n- await FirebaseMessaging.instance\n- .setForegroundNotificationPresentationOptions(\n- alert: true,\n- badge: true,\n- sound: true,\n- );\n- }\n- runApp(const MyApp());\n-}\n ```\n \n-2. Optionally Add the `metadata` to CallKitParams `extra` field\n-```dart\n \n- static Future showNotification(RemoteMessage message) {\n- CallKitParams callKitParams = CallKitParams(\n- android:...,\n- ios:...,\n- extra: message.data,\n- )\n- await FlutterCallkitIncoming.showCallkitIncoming(callKitParams);\n- }\n-```\n-\n-\n-3. Handle the push notification in the `_firebaseMessagingBackgroundHandler` method\n-```dart\n-\n-Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async {\n- //show notifcation\n- showNotification(message);\n- \n- //Listen to action from FlutterCallkitIncoming\n- FlutterCallkitIncoming.onEvent.listen((CallEvent? event) async {\n- switch (event!.event) {\n- case Event.actionCallAccept:\n- // Set the telnyx metadata for access when the app comes to foreground\n- TelnyxClient.setPushMetaData(\n- message.data, isAnswer: true, isDecline: false);\n- break;\n- case Event.actionCallDecline:\n- /*\n- * When the user declines the call from the push notification, the app will no longer be visible, and we have to\n- * handle the endCall user here.\n- * Login to the TelnyxClient and end the call\n- * */\n- ...\n- }});\n-}\n-\n-\n-```\n-\n-4. Use the `TelnyxClient.getPushMetaData()` method to retrieve the metadata when the app comes to the foreground. This data is only available on 1st access and becomes `null` afterward.\n-```dart\n- Future _handlePushNotification() async {\n- final data = await TelnyxClient.getPushMetaData();\n- PushMetaData? pushMetaData = PushMetaData.fromJson(data);\n- if (pushMetaData != null) {\n- _telnyxClient.handlePushNotification(pushMetaData, credentialConfig, tokenConfig);\n- }\n- }\n-```\n-\n-5. To Handle push calls on foreground, Listen for Call Events and invoke the `handlePushNotification` method\n-```dart\n-FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {\n- switch (event!.event) {\n- case Event.actionCallIncoming:\n- // retrieve the push metadata from extras\n- final data = await TelnyxClient.getPushData();\n- ...\n- _telnyxClient.handlePushNotification(pushMetaData, credentialConfig, tokenConfig);\n- break;\n- case Event.actionCallStart:\n- ....\n- break;\n- case Event.actionCallAccept:\n- ...\n- logger.i('Call Accepted Attach Call');\n- break;\n- });\n-```\n-\n-#### Best Practices for Push Notifications on Android \n-1. Request for Notification Permissions for android 13+ devices to show push notifications. More information can be found [here](https://developer.android.com/develop/ui/views/notifications/notification-permission)\n-2. Push Notifications only work in foreground for apps that are run in `debug` mode (You will not receive push notifications when you terminate the app while running in debug mode).\n-3. On Foreground calls, you can use the `FirebaseMessaging.onMessage.listen` method to listen for incoming calls and show a notification.\n-```dart\n- FirebaseMessaging.onMessage.listen((RemoteMessage message) {\n- TelnyxClient.setPushMetaData(message.data);\n- NotificationService.showNotification(message);\n- mainViewModel.callFromPush = true;\n- });\n-```\n-4. To handle push notifications on the background, use the `FirebaseMessaging.onBackgroundMessage` method to listen for incoming calls and show a notification and make sure to set the ` TelnyxClient.setPushMetaData` when user answers the call.\n-```dart \n- TelnyxClient.setPushMetaData(\n- message.data, isAnswer: true, isDecline: false);\n-```\n-5. When you call the `telnyxClient.handlePushNotification` it connects to the `telnyxClient`, make sure not to call the `telnyxClient.connect()` method after this. e.g an Edge case might be if you call `telnyxClient.connect()` on Widget `init` method it\n- will always call the `connect` method\n-\n-\n-6. Early Answer/Decline : Users may answer/decline the call too early before a socket connection is established. To handle this situation,\n-assert if the `IncomingInviteParams` is not null and only accept/decline if this is availalble. \n-```dart\n- bool waitingForInvite = false;\n-\n-void accept() {\n-\n-if (_incomingInvite != null) {\n- // accept the call if the incomingInvite arrives on time \n- _currentCall = _telnyxClient.acceptCall(\n- _incomingInvite!, _localName, _localNumber, \"State\");\n- } else {\n- // set waitingForInvite to true if we have an early accept\n- waitingForInvite = true;\n- }\n-}\n-\n-\n- _telnyxClient.onSocketMessageReceived = (TelnyxMessage message) {\n- switch (message.socketMethod) {\n- ...\n- case SocketMethod.INVITE:\n- {\n- if (callFromPush) {\n- // For early accept of call\n- if (waitingForInvite) {\n- //accept the call\n- accept();\n- waitingForInvite = false;\n- }\n- callFromPush = false;\n- }\n-\n- }\n- ...\n- }\n- }\n-\n-\n- if (_incomingInvite != null) {\n-\n- }\n-\n-\n-\n-```\n-\n-\n- \n-### Adding push notifications - iOS platform\n-The iOS Platform makes use of the Apple Push Notification Service (APNS) and Pushkit in order to deliver and receive push notifications\n-For a detailed tutorial, please visit our official [Push Notification Docs](https://developers.telnyx.com/docs/v2/webrtc/push-notifications?lang=ios)\n-1. Register/Invalidate the push device token for iOS\n-```swift\n- func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {\n- print(credentials.token)\n- let deviceToken = credentials.token.map { String(format: \"%02x\", $0) }.joined()\n- //Save deviceToken to your server\n- SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken)\n- }\n- \n- func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {\n- SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(\"\")\n- }\n-```\n-\n-2. For foreground calls to work, you need to register with callkit on the restorationHandler delegate function. You can also choose to register with callkit using iOS official documentation on\n- [CallKit](https://developer.apple.com/documentation/callkit/).\n-```swift\n- override func application(_ application: UIApplication,\n- continue userActivity: NSUserActivity,\n- restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {\n- \n- let nameCaller = handleObj.getDecryptHandle()[\"nameCaller\"] as? String ?? \"\"\n- let handle = handleObj.getDecryptHandle()[\"handle\"] as? String ?? \"\"\n- let data = flutter_callkit_incoming.Data(id: UUID().uuidString, nameCaller: nameCaller, handle: handle, type: isVideo ? 1 : 0)\n- //set more data...\n- data.nameCaller = \"dummy\"\n- SwiftFlutterCallkitIncomingPlugin.sharedInstance?.startCall(data, fromPushKit: true)\n- \n- } \n-```\n-3. Listen for incoming calls in AppDelegate.swift class\n-```swift \n- func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) {\n- print(\"didReceiveIncomingPushWith\")\n- guard type == .voIP else { return }\n- \n- if let metadata = payload.dictionaryPayload[\"metadata\"] as? [String: Any] {\n- var callID = UUID.init().uuidString\n- if let newCallId = (metadata[\"call_id\"] as? String),\n- !newCallId.isEmpty {\n- callID = newCallId\n- }\n- let callerName = (metadata[\"caller_name\"] as? String) ?? \"\"\n- let callerNumber = (metadata[\"caller_number\"] as? String) ?? \"\"\n- \n- let id = payload.dictionaryPayload[\"call_id\"] as? String ?? UUID().uuidString\n- \n- let data = flutter_callkit_incoming.Data(id: id, nameCaller: callerName, handle: callerNumber, type: isVideo ? 1 : 0)\n- data.extra = payload.dictionaryPayload as NSDictionary\n- data.normalHandle = 1 \n- \n- let caller = callerName.isEmpty ? (callerNumber.isEmpty ? \"Unknown\" : callerNumber) : callerName\n- let uuid = UUID(uuidString: callID)\n- \n- data.uuid = uuid!.uuidString\n- data.nameCaller = caller\n- \n- SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true)\n- }\n- }\n-```\n-\n-4. Listen for Call Events and invoke the `handlePushNotification` method\n-```dart\n- FlutterCallkitIncoming.onEvent.listen((CallEvent? event) {\n- switch (event!.event) {\n- case Event.actionCallIncoming:\n- // retrieve the push metadata from extras\n- PushMetaData? pushMetaData = PushMetaData.fromJson(event.body['extra']['metadata']);\n- _telnyxClient.handlePushNotification(pushMetaData, credentialConfig, tokenConfig);\n- break;\n- case Event.actionCallStart:\n- ....\n- break;\n- case Event.actionCallAccept:\n- ...\n- logger.i('Call Accepted Attach Call');\n- break;\n- });\n-```\n-\n-\n-\n-#### Best Practices for Push Notifications on iOS\n-1. Push Notifications only work in foreground for apps that are run in `debug` mode (You will not receive push notifications when you terminate the app while running in debug mode). Make sure you are in `release` mode. Preferably test using Testfight or Appstore.\n-To test if push notifications are working, disconnect the telnyx client (while app is in foreground) and make a call to the device. You should receive a push notification.\n-\n-\n-### Creating a call invitation\n-In order to make a call invitation, we first create an instance of the Call class with the .call instance. This creates a Call class which can be used to interact with calls (invite, accept, decline, etc).\n-To then send an invite, we can use the .newInvite() method which requires you to provide your callerName, callerNumber, the destinationNumber (or SIP credential), and your clientState (any String value).\n-\n-```dart\n- _telnyxClient\n- .call\n- .newInvite(\"callerName\", \"000000000\", destination, \"State\");\n-```\n-\n-### Accepting a call\n-In order to be able to accept a call, we first need to listen for invitations. We do this by getting the Telnyx Socket Response callbacks:\n-\n-```dart\n- // Observe Socket Messages Received\n-_telnyxClient.onSocketMessageReceived = (TelnyxMessage message) {\n- switch (message.socketMethod) {\n- case SocketMethod.CLIENT_READY:\n- {\n- // Fires once client has correctly been setup and logged into, you can now make calls. \n- break;\n- }\n- case SocketMethod.LOGIN:\n- {\n- // Handle a successful login - Update UI or Navigate to new screen, etc. \n- break;\n- }\n- case SocketMethod.INVITE:\n- {\n- // Handle an invitation Update UI or Navigate to new screen, etc. \n- // Then, through an answer button of some kind we can accept the call with:\n- _incomingInvite = message.message.inviteParams;\n- _telnyxClient.createCall().acceptCall(\n- _incomingInvite, \"callerName\", \"000000000\", \"State\");\n- break;\n- }\n- case SocketMethod.ANSWER:\n- {\n- // Handle a received call answer - Update UI or Navigate to new screen, etc.\n- break;\n- }\n- case SocketMethod.BYE:\n- {\n- // Handle a call rejection or ending - Update UI or Navigate to new screen, etc.\n- break;\n- }\n- }\n- notifyListeners();\n-};\n-```\n-\n-We can then use this method to create a listener that listens for an invitation and, in this case, answers it straight away. A real implementation would be more suited to show some UI and allow manual accept / decline operations. \n-\n-### Decline / End Call\n-\n-In order to end a call, we can get a stored instance of Call and call the .endCall(callID) method. To decline an incoming call we first create the call with the .createCall() method and then call the .endCall(callID) method:\n-\n-```dart\n- if (_ongoingCall) {\n- _telnyxClient.call.endCall(_telnyxClient.call.callId);\n- } else {\n- _telnyxClient.createCall().endCall(_incomingInvite?.callID);\n- }\n-```\n-\n ### DTMF (Dual Tone Multi Frequency)\n \n In order to send a DTMF message while on a call you can call the .dtmf(callID, tone), method where tone is a String value of the character you would like pressed:\n \n" } ], "date": 1724322403635, diff --git a/.lh/lib/service/notification_service.dart.json b/.lh/lib/service/notification_service.dart.json new file mode 100644 index 0000000..95646f7 --- /dev/null +++ b/.lh/lib/service/notification_service.dart.json @@ -0,0 +1,18 @@ +{ + "sourceFile": "lib/service/notification_service.dart", + "activeCommit": 0, + "commits": [ + { + "activePatchIndex": 0, + "patches": [ + { + "date": 1731272573644, + "content": "Index: \n===================================================================\n--- \n+++ \n" + } + ], + "date": 1731272573644, + "name": "Commit-0", + "content": "import 'dart:convert';\nimport 'package:firebase_messaging/firebase_messaging.dart';\nimport 'package:flutter/foundation.dart';\nimport 'package:flutter_callkit_incoming/entities/android_params.dart';\nimport 'package:flutter_callkit_incoming/entities/call_kit_params.dart';\nimport 'package:flutter_callkit_incoming/entities/ios_params.dart';\nimport 'package:flutter_callkit_incoming/entities/notification_params.dart';\nimport 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';\nimport 'package:uuid/uuid.dart';\nimport 'package:logger/logger.dart';\nimport 'package:telnyx_webrtc/model/push_notification.dart';\n\n\nclass NotificationService {\n\n static Future showNotification(RemoteMessage message) async {\n var logger = Logger();\n print('Received Incoming NotificationService!');\n logger.i('Received Incoming NotificationService! from background');\n var metadata = PushMetaData.fromJson(jsonDecode(message.data[\"metadata\"]));\n var received = message.data[\"message\"];\n var currentUuid = const Uuid().v4();\n\n\n CallKitParams callKitParams = CallKitParams(\n id: currentUuid,\n nameCaller: metadata.caller_name,\n appName: 'Telnyx Flutter Voice',\n avatar: 'https://i.pravatar.cc/100',\n handle: metadata.caller_number,\n type: 0,\n textAccept: 'Accept',\n textDecline: 'Decline',\n missedCallNotification: const NotificationParams(\n showNotification: true,\n isShowCallback: true,\n subtitle: 'Missed call',\n callbackText: 'Call back',\n ),\n duration: 30000,\n extra: message.data,\n headers: {'platform': 'flutter'},\n android: const AndroidParams(\n isCustomNotification: true,\n isShowLogo: false,\n ringtonePath: 'system_ringtone_default',\n backgroundColor: '#0955fa',\n backgroundUrl: 'https://i.pravatar.cc/500',\n actionColor: '#4CAF50',\n textColor: '#ffffff',\n incomingCallNotificationChannelName: \"Incoming Call\",\n missedCallNotificationChannelName: \"Missed Call\"),\n ios: const IOSParams(\n iconName: 'CallKitLogo',\n handleType: 'generic',\n supportsVideo: true,\n maximumCallGroups: 2,\n maximumCallsPerCallGroup: 1,\n audioSessionMode: 'default',\n audioSessionActive: true,\n audioSessionPreferredSampleRate: 44100.0,\n audioSessionPreferredIOBufferDuration: 0.005,\n supportsDTMF: true,\n supportsHolding: true,\n supportsGrouping: false,\n supportsUngrouping: false,\n ringtonePath: 'system_ringtone_default',\n ),\n );\n\n\n await FlutterCallkitIncoming.showCallkitIncoming(callKitParams);\n }\n\n static Future showMissedCallNotification(RemoteMessage message) async {\n var logger = Logger();\n print('Received Incoming NotificationService!');\n logger.i('Received Incoming NotificationService! from background');\n var metadata = PushMetaData.fromJson(jsonDecode(message.data[\"metadata\"]));\n var received = message.data[\"message\"];\n var currentUuid = const Uuid().v4();\n\n\n CallKitParams callKitParams = CallKitParams(\n id: currentUuid,\n nameCaller: metadata.caller_name,\n appName: 'Telnyx Flutter Voice',\n avatar: 'https://i.pravatar.cc/100',\n handle: metadata.caller_number,\n type: 0,\n textAccept: 'Accept',\n textDecline: 'Decline',\n missedCallNotification: const NotificationParams(\n showNotification: true,\n isShowCallback: true,\n subtitle: 'Missed call',\n callbackText: 'Call back',\n ),\n duration: 30000,\n extra: message.data,\n headers: {'platform': 'flutter'},\n android: const AndroidParams(\n isCustomNotification: true,\n isShowLogo: false,\n ringtonePath: 'system_ringtone_default',\n backgroundColor: '#0955fa',\n backgroundUrl: 'https://i.pravatar.cc/500',\n actionColor: '#4CAF50',\n textColor: '#ffffff',\n incomingCallNotificationChannelName: \"Incoming Call\",\n missedCallNotificationChannelName: \"Missed Call\"),\n ios: const IOSParams(\n iconName: 'CallKitLogo',\n handleType: 'generic',\n supportsVideo: true,\n maximumCallGroups: 2,\n maximumCallsPerCallGroup: 1,\n audioSessionMode: 'default',\n audioSessionActive: true,\n audioSessionPreferredSampleRate: 44100.0,\n audioSessionPreferredIOBufferDuration: 0.005,\n supportsDTMF: true,\n supportsHolding: true,\n supportsGrouping: false,\n supportsUngrouping: false,\n ringtonePath: 'system_ringtone_default',\n ),\n );\n\n\n await FlutterCallkitIncoming.showMissCallNotification(callKitParams);\n }\n}\n" + } + ] +} \ No newline at end of file diff --git a/.lh/lib/view/screen/home_screen.dart.json b/.lh/lib/view/screen/home_screen.dart.json index 3bfc406..74a08c1 100644 --- a/.lh/lib/view/screen/home_screen.dart.json +++ b/.lh/lib/view/screen/home_screen.dart.json @@ -3,7 +3,7 @@ "activeCommit": 0, "commits": [ { - "activePatchIndex": 1, + "activePatchIndex": 2, "patches": [ { "date": 1730906592729, @@ -12,6 +12,10 @@ { "date": 1730980825909, "content": "Index: \n===================================================================\n--- \n+++ \n@@ -22,9 +22,9 @@\n \n @override\n void initState() {\n super.initState();\n- destinationController.text = 'isaac33882';\n+ destinationController.text = 'isaac33882destination_number_here';\n }\n \n void _observeResponses() {\n Provider.of(context, listen: true).observeResponses();\n" + }, + { + "date": 1731267138541, + "content": "Index: \n===================================================================\n--- \n+++ \n@@ -22,9 +22,9 @@\n \n @override\n void initState() {\n super.initState();\n- destinationController.text = 'isaac33882destination_number_here';\n+ destinationController.text = 'isaac33882';\n }\n \n void _observeResponses() {\n Provider.of(context, listen: true).observeResponses();\n" } ], "date": 1730906592729, diff --git a/README.md b/README.md index fd6e6a0..ae31781 100644 --- a/README.md +++ b/README.md @@ -422,6 +422,23 @@ In order to end a call, we can get a stored instance of Call and call the .endCa } ``` +### Handling Late Notifications +If notifcations arrive very late due to no internet connectivity, It is good to always flag it as a missed call. You can do that using the +code snippet below : + +```dart +const CALL_MISSED_TIMEOUT = 60; + + DateTime nowTime = DateTime.now(); + Duration? difference = nowTime?.difference(message.sentTime!); + + if (difference.inSeconds > CALL_MISSED_TIMEOUT) { + NotificationService.showMissedCallNotification(message); + return; +} +``` + + ### DTMF (Dual Tone Multi Frequency) In order to send a DTMF message while on a call you can call the .dtmf(callID, tone), method where tone is a String value of the character you would like pressed: diff --git a/lib/main.dart b/lib/main.dart index 4b908ba..7059632 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -275,6 +275,21 @@ class _MyAppState extends State { // Android Only - Push Notifications FirebaseMessaging.onMessage.listen((RemoteMessage message) { logger.i('OnMessage :: Notification Message: ${message.data}'); + DateTime nowTime = DateTime.now(); + Duration? difference = nowTime?.difference(message.sentTime!); + + if (difference != null) { + logger.i( + 'OnMessage :: Notification difference: ${difference.inSeconds}'); + if (difference.inSeconds > CALL_MISSED_TIMEOUT) { + logger.i('OnMessage :: Notification Message: Missed Call'); + // You can simulate a missed call here + NotificationService.showMissedCallNotification(message); + return; + } + } + + logger.i('OnMessage Time :: Notification Message: ${message.sentTime}'); TelnyxClient.setPushMetaData(message.data); NotificationService.showNotification(message); mainViewModel.callFromPush = true; diff --git a/lib/service/notification_service.dart b/lib/service/notification_service.dart index d6a15fd..f76de83 100644 --- a/lib/service/notification_service.dart +++ b/lib/service/notification_service.dart @@ -10,9 +10,7 @@ import 'package:uuid/uuid.dart'; import 'package:logger/logger.dart'; import 'package:telnyx_webrtc/model/push_notification.dart'; - class NotificationService { - static Future showNotification(RemoteMessage message) async { var logger = Logger(); print('Received Incoming NotificationService!'); @@ -21,7 +19,6 @@ class NotificationService { var received = message.data["message"]; var currentUuid = const Uuid().v4(); - CallKitParams callKitParams = CallKitParams( id: currentUuid, nameCaller: metadata.caller_name, @@ -68,7 +65,63 @@ class NotificationService { ), ); - await FlutterCallkitIncoming.showCallkitIncoming(callKitParams); } + + static Future showMissedCallNotification(RemoteMessage message) async { + var logger = Logger(); + print('Received Incoming NotificationService!'); + logger.i('Received Incoming NotificationService! from background'); + var metadata = PushMetaData.fromJson(jsonDecode(message.data["metadata"])); + var received = message.data["message"]; + var currentUuid = const Uuid().v4(); + + CallKitParams callKitParams = CallKitParams( + id: currentUuid, + nameCaller: metadata.caller_name, + appName: 'Telnyx Flutter Voice', + avatar: 'https://i.pravatar.cc/100', + handle: metadata.caller_number, + type: 0, + textAccept: 'Accept', + textDecline: 'Decline', + missedCallNotification: const NotificationParams( + showNotification: true, + isShowCallback: true, + subtitle: 'Missed call', + callbackText: 'Call back', + ), + duration: 30000, + extra: message.data, + headers: {'platform': 'flutter'}, + android: const AndroidParams( + isCustomNotification: true, + isShowLogo: false, + ringtonePath: 'system_ringtone_default', + backgroundColor: '#0955fa', + backgroundUrl: 'https://i.pravatar.cc/500', + actionColor: '#4CAF50', + textColor: '#ffffff', + incomingCallNotificationChannelName: "Incoming Call", + missedCallNotificationChannelName: "Missed Call"), + ios: const IOSParams( + iconName: 'CallKitLogo', + handleType: 'generic', + supportsVideo: true, + maximumCallGroups: 2, + maximumCallsPerCallGroup: 1, + audioSessionMode: 'default', + audioSessionActive: true, + audioSessionPreferredSampleRate: 44100.0, + audioSessionPreferredIOBufferDuration: 0.005, + supportsDTMF: true, + supportsHolding: true, + supportsGrouping: false, + supportsUngrouping: false, + ringtonePath: 'system_ringtone_default', + ), + ); + + await FlutterCallkitIncoming.showMissCallNotification(callKitParams); + } } diff --git a/lib/view/screen/home_screen.dart b/lib/view/screen/home_screen.dart index 139457b..ebf846c 100644 --- a/lib/view/screen/home_screen.dart +++ b/lib/view/screen/home_screen.dart @@ -23,7 +23,7 @@ class _HomeScreenState extends State { @override void initState() { super.initState(); - destinationController.text = 'isaac33882destination_number_here'; + destinationController.text = 'isaac33882'; } void _observeResponses() {