From 8f8a01f8caa223a05a1d8ee35e7e9308aa55b67e Mon Sep 17 00:00:00 2001 From: Arthur Islamov Date: Mon, 14 Dec 2020 12:28:50 -0800 Subject: [PATCH 1/7] Fixed app crash when receiving voip push notification in background --- README.md | 22 ++++++++++++++++++++++ ios/RNTwilioVoice/RNTwilioVoice.h | 3 +++ 2 files changed, 25 insertions(+) diff --git a/README.md b/README.md index 8500b4a0..507c50a7 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,28 @@ To pass caller's name to CallKit via Voip push notification add custom parameter ``` +If your app gets killed after receiving push notification you must initialize CallKit on start + +```obj-c +// add import +#import + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; + // ... + + // add these three lines + RNTwilioVoice *voice = [bridge moduleForName:@"RNTwilioVoice"]; + [voice initPushRegistry]; + // you can pass same arguments as from your JS code + [voice configureCallKit:@{ @"appName" : @"YOUR FANCY APP NAME" }]; + return YES; +} +``` + #### VoIP Service Certificate Twilio Programmable Voice for iOS utilizes Apple's VoIP Services and VoIP "Push Notifications" instead of FCM. You will need a VoIP Service Certificate from Apple to receive calls. Follow [the official Twilio instructions](https://github.com/twilio/voice-quickstart-ios#7-create-voip-service-certificate) to complete this step. diff --git a/ios/RNTwilioVoice/RNTwilioVoice.h b/ios/RNTwilioVoice/RNTwilioVoice.h index 117b1d6f..73930d66 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.h +++ b/ios/RNTwilioVoice/RNTwilioVoice.h @@ -8,4 +8,7 @@ @interface RNTwilioVoice : RCTEventEmitter +- (void)initPushRegistry; +- (void)configureCallKit: (NSDictionary *)params; + @end From 3d8188d14ab05b553a44ee24700010524ae16cb8 Mon Sep 17 00:00:00 2001 From: Arthur Islamov Date: Wed, 10 Nov 2021 16:07:11 -0800 Subject: [PATCH 2/7] Updated twilio SDK version --- RNTwilioVoice.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RNTwilioVoice.podspec b/RNTwilioVoice.podspec index 398bedf8..651ec3c1 100644 --- a/RNTwilioVoice.podspec +++ b/RNTwilioVoice.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.source = { git: 'https://github.com/hoxfon/react-native-twilio-programmable-voice', tag: s.version } s.dependency 'React-Core' - s.dependency 'TwilioVoice', '~> 5.2.0' + s.dependency 'TwilioVoice', '~> 5.5.2' s.xcconfig = { 'FRAMEWORK_SEARCH_PATHS' => '${PODS_ROOT}/TwilioVoice/Build/iOS' } s.frameworks = 'TwilioVoice' s.preserve_paths = 'LICENSE', 'README.md', 'package.json', 'index.js' From 8cfe9d68d867307444865065db8f0b2a0720a40d Mon Sep 17 00:00:00 2001 From: Arthur Islamov Date: Tue, 16 Nov 2021 12:52:21 -0800 Subject: [PATCH 3/7] VoIP push notification handling WIP --- index.js | 4 ++++ ios/RNTwilioVoice/RNTwilioVoice.h | 2 +- ios/RNTwilioVoice/RNTwilioVoice.m | 30 +++++++++++++++++++++++++++++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index b84b91bf..05b7d489 100644 --- a/index.js +++ b/index.js @@ -47,6 +47,9 @@ const Twilio = { } return result }, + initWithAccessTokenUrl (url) { + return TwilioVoice.initWithAccessTokenUrl(url) + }, connect(params = {}) { TwilioVoice.connect(params) }, @@ -88,6 +91,7 @@ const Twilio = { unregister() { if (Platform.OS === IOS) { TwilioVoice.unregister() + TwilioVoice.clearAccessTokenUrl() } }, addEventListener(type, handler) { diff --git a/ios/RNTwilioVoice/RNTwilioVoice.h b/ios/RNTwilioVoice/RNTwilioVoice.h index f48861f1..8f90306a 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.h +++ b/ios/RNTwilioVoice/RNTwilioVoice.h @@ -4,6 +4,6 @@ @interface RNTwilioVoice : RCTEventEmitter - (void)initPushRegistry; -- (void)configureCallKit: (NSDictionary *)params; +- (void)initWithCachedAccessTokenUrl: (NSDictionary *)params; @end diff --git a/ios/RNTwilioVoice/RNTwilioVoice.m b/ios/RNTwilioVoice/RNTwilioVoice.m index df4c3810..d09bc03a 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.m +++ b/ios/RNTwilioVoice/RNTwilioVoice.m @@ -7,6 +7,7 @@ @import TwilioVoice; NSString * const kCachedDeviceToken = @"CachedDeviceToken"; +NSString * const kCachedTokenUrl = @"CachedTokenUrl"; NSString * const kCallerNameCustomParameter = @"CallerName"; @interface RNTwilioVoice () @@ -61,6 +62,32 @@ - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } +RCT_EXPORT_METHOD(initWithAccessTokenUrl:(NSString *)tokenUrl) { + _tokenUrl = tokenUrl; + if (tokenUrl && [tokenUrl length] > 0) { + [[NSUserDefaults standardUserDefaults] setObject:tokenUrl forKey:kCachedTokenUrl]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAppTerminateNotification) name:UIApplicationWillTerminateNotification object:nil]; + [self initPushRegistry]; + } else { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCachedTokenUrl]; + } +} + +RCT_EXPORT_METHOD(clearAccessTokenUrl) { + [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCachedTokenUrl]; +} + +- (void)initWithCachedAccessTokenUrl: (NSDictionary *)params { + NSString *cachedAccessTokenUrl = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedTokenUrl]; + if ([cachedAccessTokenUrl length] > 0) { + _tokenUrl = cachedAccessTokenUrl; + [self initPushRegistry]; + [self configureCallKit:(params)]; + } +} + + + RCT_EXPORT_METHOD(initWithAccessToken:(NSString *)token) { _token = token; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAppTerminateNotification) name:UIApplicationWillTerminateNotification object:nil]; @@ -157,6 +184,7 @@ - (void)dealloc { NSLog(@"Successfully unregistered for VoIP push notifications."); } }]; + [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCachedDeviceToken]; } } @@ -232,10 +260,10 @@ - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPush ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]), ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]), ntohl(tokenBytes[6]), ntohl(tokenBytes[7])]; - NSString *accessToken = [self fetchAccessToken]; NSString *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; if (![cachedDeviceToken isEqualToString:deviceTokenString]) { cachedDeviceToken = deviceTokenString; + NSString *accessToken = [self fetchAccessToken]; /* * Perform registration if a new device token is detected. From 25b1f82f3801430d92df7a1875d1614bc543ed09 Mon Sep 17 00:00:00 2001 From: Arthur Islamov Date: Tue, 16 Nov 2021 15:07:51 -0800 Subject: [PATCH 4/7] VoIP push notification handling --- RNTwilioVoice.podspec | 2 +- index.js | 4 - ios/RNTwilioVoice/RNTwilioVoice.h | 3 +- ios/RNTwilioVoice/RNTwilioVoice.m | 641 ++++++++++++++++-------------- 4 files changed, 344 insertions(+), 306 deletions(-) diff --git a/RNTwilioVoice.podspec b/RNTwilioVoice.podspec index 651ec3c1..5af4a02a 100644 --- a/RNTwilioVoice.podspec +++ b/RNTwilioVoice.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.source = { git: 'https://github.com/hoxfon/react-native-twilio-programmable-voice', tag: s.version } s.dependency 'React-Core' - s.dependency 'TwilioVoice', '~> 5.5.2' + s.dependency 'TwilioVoice', '~> 6.3.0' s.xcconfig = { 'FRAMEWORK_SEARCH_PATHS' => '${PODS_ROOT}/TwilioVoice/Build/iOS' } s.frameworks = 'TwilioVoice' s.preserve_paths = 'LICENSE', 'README.md', 'package.json', 'index.js' diff --git a/index.js b/index.js index 05b7d489..b84b91bf 100644 --- a/index.js +++ b/index.js @@ -47,9 +47,6 @@ const Twilio = { } return result }, - initWithAccessTokenUrl (url) { - return TwilioVoice.initWithAccessTokenUrl(url) - }, connect(params = {}) { TwilioVoice.connect(params) }, @@ -91,7 +88,6 @@ const Twilio = { unregister() { if (Platform.OS === IOS) { TwilioVoice.unregister() - TwilioVoice.clearAccessTokenUrl() } }, addEventListener(type, handler) { diff --git a/ios/RNTwilioVoice/RNTwilioVoice.h b/ios/RNTwilioVoice/RNTwilioVoice.h index 8f90306a..d898038b 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.h +++ b/ios/RNTwilioVoice/RNTwilioVoice.h @@ -3,7 +3,6 @@ @interface RNTwilioVoice : RCTEventEmitter -- (void)initPushRegistry; -- (void)initWithCachedAccessTokenUrl: (NSDictionary *)params; +- (void) initPushKitIfTokenCached: (NSDictionary *)callKitParams; @end diff --git a/ios/RNTwilioVoice/RNTwilioVoice.m b/ios/RNTwilioVoice/RNTwilioVoice.m index d09bc03a..a8f0413f 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.m +++ b/ios/RNTwilioVoice/RNTwilioVoice.m @@ -8,8 +8,11 @@ NSString * const kCachedDeviceToken = @"CachedDeviceToken"; NSString * const kCachedTokenUrl = @"CachedTokenUrl"; +NSString * const kCachedBindingTime = @"CachedBindingTime"; NSString * const kCallerNameCustomParameter = @"CallerName"; +static NSInteger const kRegistrationTTLInDays = 365; + @interface RNTwilioVoice () @property (nonatomic, strong) PKPushRegistry *voipRegistry; @@ -29,10 +32,12 @@ @interface RNTwilioVoice () *)supportedEvents { - return @[@"connectionDidConnect", @"connectionDidDisconnect", @"callRejected", @"deviceReady", @"deviceNotReady", @"deviceDidReceiveIncoming", @"callInviteCancelled", @"callStateRinging", @"connectionIsReconnecting", @"connectionDidReconnect"]; + return @[@"connectionDidConnect", @"connectionDidDisconnect", @"callRejected", @"deviceReady", @"deviceNotReady", @"deviceDidReceiveIncoming", @"callInviteCancelled", @"callStateRinging", @"connectionIsReconnecting", @"connectionDidReconnect"]; } @synthesize bridge = _bridge; - (void)dealloc { - if (self.callKitProvider) { - [self.callKitProvider invalidate]; - } - - [[NSNotificationCenter defaultCenter] removeObserver:self]; -} - -RCT_EXPORT_METHOD(initWithAccessTokenUrl:(NSString *)tokenUrl) { - _tokenUrl = tokenUrl; - if (tokenUrl && [tokenUrl length] > 0) { - [[NSUserDefaults standardUserDefaults] setObject:tokenUrl forKey:kCachedTokenUrl]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAppTerminateNotification) name:UIApplicationWillTerminateNotification object:nil]; - [self initPushRegistry]; - } else { - [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCachedTokenUrl]; + if (self.callKitProvider) { + [self.callKitProvider invalidate]; } -} -RCT_EXPORT_METHOD(clearAccessTokenUrl) { - [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCachedTokenUrl]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; } -- (void)initWithCachedAccessTokenUrl: (NSDictionary *)params { - NSString *cachedAccessTokenUrl = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedTokenUrl]; - if ([cachedAccessTokenUrl length] > 0) { - _tokenUrl = cachedAccessTokenUrl; +/* + We need to init push kit immediately at start. But it might be first start of the app + so in that case lets pass initialization to RN code + */ +- (void) initPushKitIfTokenCached: (NSDictionary *)callKitParams { + _deregisterQueued = false; + NSData *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; + if (cachedDeviceToken && [cachedDeviceToken length] > 0) { [self initPushRegistry]; - [self configureCallKit:(params)]; + [self configureCallKit:callKitParams]; } } - - RCT_EXPORT_METHOD(initWithAccessToken:(NSString *)token) { - _token = token; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAppTerminateNotification) name:UIApplicationWillTerminateNotification object:nil]; - [self initPushRegistry]; + _token = token; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAppTerminateNotification) name:UIApplicationWillTerminateNotification object:nil]; + [self initPushRegistry]; + // if push kit is in init state and new device token waits to be registered/deregistered with our access token + [self registerNewDeviceToken]; + [self deregisterDeviceToken]; } RCT_EXPORT_METHOD(configureCallKit: (NSDictionary *)params) { - if (self.callKitCallController == nil) { - /* - * The important thing to remember when providing a TVOAudioDevice is that the device must be set - * before performing any other actions with the SDK (such as connecting a Call, or accepting an incoming Call). - * In this case we've already initialized our own `TVODefaultAudioDevice` instance which we will now set. - */ - self.audioDevice = [TVODefaultAudioDevice audioDevice]; - TwilioVoice.audioDevice = self.audioDevice; - - self.activeCallInvites = [NSMutableDictionary dictionary]; - self.activeCalls = [NSMutableDictionary dictionary]; - - _settings = [[NSMutableDictionary alloc] initWithDictionary:params]; - CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] initWithLocalizedName:params[@"appName"]]; - configuration.maximumCallGroups = 1; - configuration.maximumCallsPerCallGroup = 1; - if (_settings[@"imageName"]) { - configuration.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:_settings[@"imageName"]]); - } - if (_settings[@"ringtoneSound"]) { - configuration.ringtoneSound = _settings[@"ringtoneSound"]; - } + if (self.callKitCallController == nil) { + /* + * The important thing to remember when providing a TVOAudioDevice is that the device must be set + * before performing any other actions with the SDK (such as connecting a Call, or accepting an incoming Call). + * In this case we've already initialized our own `TVODefaultAudioDevice` instance which we will now set. + */ + self.audioDevice = [TVODefaultAudioDevice audioDevice]; + TwilioVoiceSDK.audioDevice = self.audioDevice; + + self.activeCallInvites = [NSMutableDictionary dictionary]; + self.activeCalls = [NSMutableDictionary dictionary]; + + _settings = [[NSMutableDictionary alloc] initWithDictionary:params]; + CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] initWithLocalizedName:params[@"appName"]]; + configuration.maximumCallGroups = 1; + configuration.maximumCallsPerCallGroup = 1; + if (_settings[@"imageName"]) { + configuration.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:_settings[@"imageName"]]); + } + if (_settings[@"ringtoneSound"]) { + configuration.ringtoneSound = _settings[@"ringtoneSound"]; + } - _callKitProvider = [[CXProvider alloc] initWithConfiguration:configuration]; - [_callKitProvider setDelegate:self queue:nil]; + _callKitProvider = [[CXProvider alloc] initWithConfiguration:configuration]; + [_callKitProvider setDelegate:self queue:nil]; - NSLog(@"CallKit Initialized"); + NSLog(@"CallKit Initialized"); - self.callKitCallController = [[CXCallController alloc] init]; - } + self.callKitCallController = [[CXCallController alloc] init]; + } } RCT_EXPORT_METHOD(connect: (NSDictionary *)params) { - NSLog(@"Calling phone number %@", [params valueForKey:@"To"]); + NSLog(@"Calling phone number %@", [params valueForKey:@"To"]); - UIDevice* device = [UIDevice currentDevice]; - device.proximityMonitoringEnabled = YES; + UIDevice* device = [UIDevice currentDevice]; + device.proximityMonitoringEnabled = YES; - if (self.activeCall && self.activeCall.state == TVOCallStateConnected) { - [self performEndCallActionWithUUID:self.activeCall.uuid]; - } else { - NSUUID *uuid = [NSUUID UUID]; - NSString *handle = [params valueForKey:@"To"]; - _callParams = [[NSMutableDictionary alloc] initWithDictionary:params]; - [self performStartCallActionWithUUID:uuid handle:handle]; - } + if (self.activeCall && self.activeCall.state == TVOCallStateConnected) { + [self performEndCallActionWithUUID:self.activeCall.uuid]; + } else { + NSUUID *uuid = [NSUUID UUID]; + NSString *handle = [params valueForKey:@"To"]; + _callParams = [[NSMutableDictionary alloc] initWithDictionary:params]; + [self performStartCallActionWithUUID:uuid handle:handle]; + } } RCT_EXPORT_METHOD(disconnect) { @@ -150,12 +145,12 @@ - (void)initWithCachedAccessTokenUrl: (NSDictionary *)params { } RCT_EXPORT_METHOD(setMuted: (BOOL *)muted) { - NSLog(@"Mute/UnMute call"); + NSLog(@"Mute/UnMute call"); self.activeCall.muted = muted ? YES : NO; } RCT_EXPORT_METHOD(setOnHold: (BOOL *)isOnHold) { - NSLog(@"Hold/Unhold call"); + NSLog(@"Hold/Unhold call"); self.activeCall.onHold = isOnHold ? YES : NO; } @@ -164,28 +159,29 @@ - (void)initWithCachedAccessTokenUrl: (NSDictionary *)params { } RCT_EXPORT_METHOD(sendDigits: (NSString *)digits) { - if (self.activeCall && self.activeCall.state == TVOCallStateConnected) { - NSLog(@"SendDigits %@", digits); - [self.activeCall sendDigits:digits]; - } + if (self.activeCall && self.activeCall.state == TVOCallStateConnected) { + NSLog(@"SendDigits %@", digits); + [self.activeCall sendDigits:digits]; + } } RCT_EXPORT_METHOD(unregister) { - NSLog(@"unregister"); - NSString *accessToken = [self fetchAccessToken]; - NSString *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; - if ([cachedDeviceToken length] > 0) { - [TwilioVoice unregisterWithAccessToken:accessToken - deviceToken:cachedDeviceToken - completion:^(NSError * _Nullable error) { - if (error) { - NSLog(@"An error occurred while unregistering: %@", [error localizedDescription]); - } else { - NSLog(@"Successfully unregistered for VoIP push notifications."); - } - }]; - [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCachedDeviceToken]; - } + NSLog(@"unregister"); + NSData *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; + if ([cachedDeviceToken length] > 0) { + NSString *accessToken = [self fetchAccessToken]; + [TwilioVoiceSDK unregisterWithAccessToken:accessToken + deviceToken:cachedDeviceToken + completion:^(NSError * _Nullable error) { + if (error) { + NSLog(@"An error occurred while unregistering: %@", [error localizedDescription]); + } else { + NSLog(@"Successfully unregistered for VoIP push notifications."); + } + }]; + // lets remove cached device token so we can register with new twilio access token (e.g. after login with another user) + [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCachedDeviceToken]; + } } RCT_REMAP_METHOD(getActiveCall, @@ -234,97 +230,140 @@ - (void)initWithCachedAccessTokenUrl: (NSDictionary *)params { } - (void)initPushRegistry { - self.voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()]; - self.voipRegistry.delegate = self; - self.voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP]; + self.voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()]; + self.voipRegistry.delegate = self; + self.voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP]; } - (NSString *)fetchAccessToken { - if (_tokenUrl) { - NSString *accessToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:_tokenUrl] - encoding:NSUTF8StringEncoding - error:nil]; - return accessToken; - } else { - return _token; - } + if (_tokenUrl) { + NSString *accessToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:_tokenUrl] + encoding:NSUTF8StringEncoding + error:nil]; + return accessToken; + } else { + return _token; + } } -#pragma mark - PKPushRegistryDelegate -- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type { - NSLog(@"pushRegistry:didUpdatePushCredentials:forType"); - - if ([type isEqualToString:PKPushTypeVoIP]) { - const unsigned *tokenBytes = [credentials.token bytes]; - NSString *deviceTokenString = [NSString stringWithFormat:@"<%08x %08x %08x %08x %08x %08x %08x %08x>", - ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]), - ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]), - ntohl(tokenBytes[6]), ntohl(tokenBytes[7])]; - NSString *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; - if (![cachedDeviceToken isEqualToString:deviceTokenString]) { - cachedDeviceToken = deviceTokenString; +/** + * The TTL of a registration is 1 year. The TTL for registration for this device/identity pair is reset to + * 1 year whenever a new registration occurs or a push notification is sent to this device/identity pair. + * This method checks if binding exists in UserDefaults, and if half of TTL has been passed then the method + * will return true, else false. + */ +- (BOOL)registrationRequired { + BOOL registrationRequired = YES; + NSDate *lastBindingCreated = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedBindingTime]; + + if (lastBindingCreated) { + NSDateComponents *dayComponent = [[NSDateComponents alloc] init]; + + // Register upon half of the TTL + dayComponent.day = kRegistrationTTLInDays / 2; + + NSDate *bindingExpirationDate = [[NSCalendar currentCalendar] dateByAddingComponents:dayComponent toDate:lastBindingCreated options:0]; + NSDate *currentDate = [NSDate date]; + if ([bindingExpirationDate compare:currentDate] == NSOrderedDescending) { + registrationRequired = NO; + } + } + return registrationRequired; +} + +- (void) registerNewDeviceToken { + if (!_newDeviceToken || !_token) { + return; + } + + NSData *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; + if ([self registrationRequired] || ![cachedDeviceToken isEqualToData:_newDeviceToken]) { + cachedDeviceToken = _newDeviceToken; NSString *accessToken = [self fetchAccessToken]; - /* - * Perform registration if a new device token is detected. - */ - [TwilioVoice registerWithAccessToken:accessToken - deviceToken:cachedDeviceToken - completion:^(NSError *error) { + [TwilioVoiceSDK registerWithAccessToken:accessToken + deviceToken:cachedDeviceToken + completion:^(NSError *error) { if (error) { NSLog(@"An error occurred while registering: %@", [error localizedDescription]); NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; [params setObject:[error localizedDescription] forKey:@"err"]; [self sendEventWithName:@"deviceNotReady" body:params]; - } - else { + } else { NSLog(@"Successfully registered for VoIP push notifications."); - /* - * Save the device token after successfully registered. - */ + // Save the device token after successfully registered. [[NSUserDefaults standardUserDefaults] setObject:cachedDeviceToken forKey:kCachedDeviceToken]; + + /** + * The TTL of a registration is 1 year. The TTL for registration for this device/identity + * pair is reset to 1 year whenever a new registration occurs or a push notification is + * sent to this device/identity pair. + */ + [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:kCachedBindingTime]; [self sendEventWithName:@"deviceReady" body:nil]; } - }]; + }]; } - } + _newDeviceToken = NULL; } -- (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type { - NSLog(@"pushRegistry:didInvalidatePushTokenForType"); +- (void) deregisterDeviceToken { + if (!_deregisterQueued || !_token) { + return; + } - if ([type isEqualToString:PKPushTypeVoIP]) { NSString *accessToken = [self fetchAccessToken]; - NSString *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; + NSData *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; if ([cachedDeviceToken length] > 0) { - [TwilioVoice unregisterWithAccessToken:accessToken - deviceToken:cachedDeviceToken - completion:^(NSError * _Nullable error) { - if (error) { - NSLog(@"An error occurred while unregistering: %@", [error localizedDescription]); - } else { - NSLog(@"Successfully unregistered for VoIP push notifications."); - } - }]; + [TwilioVoiceSDK unregisterWithAccessToken:accessToken + deviceToken:cachedDeviceToken + completion:^(NSError * _Nullable error) { + if (error) { + NSLog(@"An error occurred while unregistering: %@", [error localizedDescription]); + } else { + NSLog(@"Successfully unregistered for VoIP push notifications."); + } + }]; + } + _deregisterQueued = false; +} + +#pragma mark - PKPushRegistryDelegate +- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type { + NSLog(@"pushRegistry:didUpdatePushCredentials:forType"); + + if ([type isEqualToString:PKPushTypeVoIP]) { + // we might get updated credentials before RN code calls initWithAccessToken so we cannot register new credentials + _newDeviceToken = credentials.token; + [self registerNewDeviceToken]; + } +} + +- (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type { + NSLog(@"pushRegistry:didInvalidatePushTokenForType"); + + if ([type isEqualToString:PKPushTypeVoIP]) { + // we might get updated credentials before RN code calls initWithAccessToken so we cannot register new credentials + _deregisterQueued = true; + [self deregisterDeviceToken]; } - } } /** -* Try using the `pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:` method if -* your application is targeting iOS 11. According to the docs, this delegate method is deprecated by Apple. -*/ + * Try using the `pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:` method if + * your application is targeting iOS 11. According to the docs, this delegate method is deprecated by Apple. + */ - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type { - NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType"); - if ([type isEqualToString:PKPushTypeVoIP]) { - // The Voice SDK will use main queue to invoke `cancelledCallInviteReceived:error` when delegate queue is not passed - if (![TwilioVoice handleNotification:payload.dictionaryPayload delegate:self delegateQueue: nil]) { - NSLog(@"This is not a valid Twilio Voice notification."); - } - } + NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType"); + if ([type isEqualToString:PKPushTypeVoIP]) { + // The Voice SDK will use main queue to invoke `cancelledCallInviteReceived:error` when delegate queue is not passed + if (![TwilioVoiceSDK handleNotification:payload.dictionaryPayload delegate:self delegateQueue: nil]) { + NSLog(@"This is not a valid Twilio Voice notification."); + } + } } /** @@ -343,7 +382,7 @@ - (void)pushRegistry:(PKPushRegistry *)registry if ([type isEqualToString:PKPushTypeVoIP]) { // The Voice SDK will use main queue to invoke `cancelledCallInviteReceived:error` when delegate queue is not passed - if (![TwilioVoice handleNotification:payload.dictionaryPayload delegate:self delegateQueue: nil]) { + if (![TwilioVoiceSDK handleNotification:payload.dictionaryPayload delegate:self delegateQueue: nil]) { NSLog(@"This is not a valid Twilio Voice notification."); } } @@ -352,9 +391,9 @@ - (void)pushRegistry:(PKPushRegistry *)registry self.incomingPushCompletionCallback = completion; } else { /** - * The Voice SDK processes the call notification and returns the call invite synchronously. Report the incoming call to - * CallKit and fulfill the completion before exiting this callback method. - */ + * The Voice SDK processes the call notification and returns the call invite synchronously. Report the incoming call to + * CallKit and fulfill the completion before exiting this callback method. + */ completion(); } } @@ -373,15 +412,16 @@ - (void)callInviteReceived:(TVOCallInvite *)callInvite { * provide you a `TVOCallInvite` object. Report the incoming call to CallKit upon receiving this callback. */ NSLog(@"callInviteReceived"); + NSString *callerCustomName = NULL; NSString *from = @"Unknown"; if (callInvite.from) { from = [callInvite.from stringByReplacingOccurrencesOfString:@"client:" withString:@""]; } if (callInvite.customParameters[kCallerNameCustomParameter]) { - from = callInvite.customParameters[kCallerNameCustomParameter]; + callerCustomName = callInvite.customParameters[kCallerNameCustomParameter]; } // Always report to CallKit - [self reportIncomingCallFrom:from withUUID:callInvite.uuid]; + [self reportIncomingCallFrom:from withUUID:callInvite.uuid withCallerCustomName:callerCustomName]; self.activeCallInvites[[callInvite.uuid UUIDString]] = callInvite; if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) { [self incomingPushHandled]; @@ -389,23 +429,23 @@ - (void)callInviteReceived:(TVOCallInvite *)callInvite { NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; if (callInvite.callSid) { - [params setObject:callInvite.callSid forKey:@"call_sid"]; + [params setObject:callInvite.callSid forKey:@"call_sid"]; } if (callInvite.from) { - [params setObject:callInvite.from forKey:@"call_from"]; + [params setObject:callInvite.from forKey:@"call_from"]; } if (callInvite.to) { - [params setObject:callInvite.to forKey:@"call_to"]; + [params setObject:callInvite.to forKey:@"call_to"]; } [self sendEventWithName:@"deviceDidReceiveIncoming" body:params]; } - (void)cancelledCallInviteReceived:(nonnull TVOCancelledCallInvite *)cancelledCallInvite { /** - * The SDK may call `[TVONotificationDelegate callInviteReceived:error:]` asynchronously on the dispatch queue - * with a `TVOCancelledCallInvite` if the caller hangs up or the client encounters any other error before the called - * party could answer or reject the call. - */ + * The SDK may call `[TVONotificationDelegate callInviteReceived:error:]` asynchronously on the dispatch queue + * with a `TVOCancelledCallInvite` if the caller hangs up or the client encounters any other error before the called + * party could answer or reject the call. + */ NSLog(@"cancelledCallInviteReceived"); TVOCallInvite *callInvite; for (NSString *activeCallInviteId in self.activeCallInvites) { @@ -434,10 +474,10 @@ - (void)cancelledCallInviteReceived:(nonnull TVOCancelledCallInvite *)cancelledC - (void)cancelledCallInviteReceived:(TVOCancelledCallInvite *)cancelledCallInvite error:(NSError *)error { /** - * The SDK may call `[TVONotificationDelegate callInviteReceived:error:]` asynchronously on the dispatch queue - * with a `TVOCancelledCallInvite` if the caller hangs up or the client encounters any other error before the called - * party could answer or reject the call. - */ + * The SDK may call `[TVONotificationDelegate callInviteReceived:error:]` asynchronously on the dispatch queue + * with a `TVOCancelledCallInvite` if the caller hangs up or the client encounters any other error before the called + * party could answer or reject the call. + */ NSLog(@"cancelledCallInviteReceived with error"); TVOCallInvite *callInvite; for (NSString *activeCallInviteId in self.activeCallInvites) { @@ -464,7 +504,7 @@ - (void)cancelledCallInviteReceived:(TVOCancelledCallInvite *)cancelledCallInvit } - (void)notificationError:(NSError *)error { - NSLog(@"notificationError: %@", [error localizedDescription]); + NSLog(@"notificationError: %@", [error localizedDescription]); } #pragma mark - TVOCallDelegate @@ -487,24 +527,24 @@ - (void)callDidStartRinging:(TVOCall *)call { #pragma mark - TVOCallDelegate - (void)callDidConnect:(TVOCall *)call { - NSLog(@"callDidConnect"); - self.callKitCompletionCallback(YES); - - NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; - [callParams setObject:call.sid forKey:@"call_sid"]; - if (call.state == TVOCallStateConnecting) { - [callParams setObject:StateConnecting forKey:@"call_state"]; - } else if (call.state == TVOCallStateConnected) { - [callParams setObject:StateConnected forKey:@"call_state"]; - } - - if (call.from) { - [callParams setObject:call.from forKey:@"call_from"]; - } - if (call.to) { - [callParams setObject:call.to forKey:@"call_to"]; - } - [self sendEventWithName:@"connectionDidConnect" body:callParams]; + NSLog(@"callDidConnect"); + self.callKitCompletionCallback(YES); + + NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; + [callParams setObject:call.sid forKey:@"call_sid"]; + if (call.state == TVOCallStateConnecting) { + [callParams setObject:StateConnecting forKey:@"call_state"]; + } else if (call.state == TVOCallStateConnected) { + [callParams setObject:StateConnected forKey:@"call_state"]; + } + + if (call.from) { + [callParams setObject:call.from forKey:@"call_from"]; + } + if (call.to) { + [callParams setObject:call.to forKey:@"call_to"]; + } + [self sendEventWithName:@"connectionDidConnect" body:callParams]; } - (void)call:(TVOCall *)call isReconnectingWithError:(NSError *)error { @@ -512,10 +552,10 @@ - (void)call:(TVOCall *)call isReconnectingWithError:(NSError *)error { NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; [callParams setObject:call.sid forKey:@"call_sid"]; if (call.from) { - [callParams setObject:call.from forKey:@"call_from"]; + [callParams setObject:call.from forKey:@"call_from"]; } if (call.to) { - [callParams setObject:call.to forKey:@"call_to"]; + [callParams setObject:call.to forKey:@"call_to"]; } [self sendEventWithName:@"connectionIsReconnecting" body:callParams]; } @@ -525,18 +565,18 @@ - (void)callDidReconnect:(TVOCall *)call { NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; [callParams setObject:call.sid forKey:@"call_sid"]; if (call.from) { - [callParams setObject:call.from forKey:@"call_from"]; + [callParams setObject:call.from forKey:@"call_from"]; } if (call.to) { - [callParams setObject:call.to forKey:@"call_to"]; + [callParams setObject:call.to forKey:@"call_to"]; } [self sendEventWithName:@"connectionDidReconnect" body:callParams]; } - (void)call:(TVOCall *)call didFailToConnectWithError:(NSError *)error { - NSLog(@"Call failed to connect: %@", error); + NSLog(@"Call failed to connect: %@", error); - self.callKitCompletionCallback(NO); + self.callKitCompletionCallback(NO); [self performEndCallActionWithUUID:call.uuid]; [self callDisconnected:call error:error]; } @@ -574,7 +614,7 @@ - (void)callDisconnected:(TVOCall *)call error:(NSError *)error { if (error) { NSString* errMsg = [error localizedDescription]; if (error.localizedFailureReason) { - errMsg = [error localizedFailureReason]; + errMsg = [error localizedFailureReason]; } [params setObject:errMsg forKey:@"err"]; } @@ -619,66 +659,66 @@ - (void)toggleAudioRoute:(BOOL)toSpeaker { #pragma mark - CXProviderDelegate - (void)providerDidReset:(CXProvider *)provider { - NSLog(@"providerDidReset"); + NSLog(@"providerDidReset"); self.audioDevice.enabled = YES; } - (void)providerDidBegin:(CXProvider *)provider { - NSLog(@"providerDidBegin"); + NSLog(@"providerDidBegin"); } - (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession { - NSLog(@"provider:didActivateAudioSession"); + NSLog(@"provider:didActivateAudioSession"); self.audioDevice.enabled = YES; } - (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession { - NSLog(@"provider:didDeactivateAudioSession"); + NSLog(@"provider:didDeactivateAudioSession"); self.audioDevice.enabled = NO; } - (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action { - NSLog(@"provider:timedOutPerformingAction"); + NSLog(@"provider:timedOutPerformingAction"); } - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action { - NSLog(@"provider:performStartCallAction"); + NSLog(@"provider:performStartCallAction"); self.audioDevice.enabled = NO; self.audioDevice.block(); - [self.callKitProvider reportOutgoingCallWithUUID:action.callUUID startedConnectingAtDate:[NSDate date]]; + [self.callKitProvider reportOutgoingCallWithUUID:action.callUUID startedConnectingAtDate:[NSDate date]]; - __weak typeof(self) weakSelf = self; - [self performVoiceCallWithUUID:action.callUUID client:nil completion:^(BOOL success) { - __strong typeof(self) strongSelf = weakSelf; - if (success) { - [strongSelf.callKitProvider reportOutgoingCallWithUUID:action.callUUID connectedAtDate:[NSDate date]]; - [action fulfill]; - } else { - [action fail]; - } - }]; + __weak typeof(self) weakSelf = self; + [self performVoiceCallWithUUID:action.callUUID client:nil completion:^(BOOL success) { + __strong typeof(self) strongSelf = weakSelf; + if (success) { + [strongSelf.callKitProvider reportOutgoingCallWithUUID:action.callUUID connectedAtDate:[NSDate date]]; + [action fulfill]; + } else { + [action fail]; + } + }]; } - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action { - NSLog(@"provider:performAnswerCallAction"); + NSLog(@"provider:performAnswerCallAction"); - self.audioDevice.enabled = NO; - self.audioDevice.block(); - [self performAnswerVoiceCallWithUUID:action.callUUID completion:^(BOOL success) { - if (success) { - [action fulfill]; - } else { - [action fail]; - } - }]; + self.audioDevice.enabled = NO; + self.audioDevice.block(); + [self performAnswerVoiceCallWithUUID:action.callUUID completion:^(BOOL success) { + if (success) { + [action fulfill]; + } else { + [action fail]; + } + }]; - [action fulfill]; + [action fulfill]; } - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action { - NSLog(@"provider:performEndCallAction"); + NSLog(@"provider:performEndCallAction"); TVOCallInvite *callInvite = self.activeCallInvites[action.callUUID.UUIDString]; TVOCall *call = self.activeCalls[action.callUUID.UUIDString]; @@ -694,17 +734,17 @@ - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *) } self.audioDevice.enabled = YES; - [action fulfill]; + [action fulfill]; } - (void)provider:(CXProvider *)provider performSetHeldCallAction:(CXSetHeldCallAction *)action { TVOCall *call = self.activeCalls[action.callUUID.UUIDString]; - if (call) { - [call setOnHold:action.isOnHold]; - [action fulfill]; - } else { - [action fail]; - } + if (call) { + [call setOnHold:action.isOnHold]; + [action fulfill]; + } else { + [action fail]; + } } - (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action { @@ -718,92 +758,95 @@ - (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCal } - (void)provider:(CXProvider *)provider performPlayDTMFCallAction:(CXPlayDTMFCallAction *)action { - TVOCall *call = self.activeCalls[action.callUUID.UUIDString]; - if (call && call.state == TVOCallStateConnected) { - NSLog(@"SendDigits %@", action.digits); - [call sendDigits:action.digits]; - } + TVOCall *call = self.activeCalls[action.callUUID.UUIDString]; + if (call && call.state == TVOCallStateConnected) { + NSLog(@"SendDigits %@", action.digits); + [call sendDigits:action.digits]; + } } #pragma mark - CallKit Actions - (void)performStartCallActionWithUUID:(NSUUID *)uuid handle:(NSString *)handle { - if (uuid == nil || handle == nil) { - return; - } + if (uuid == nil || handle == nil) { + return; + } - CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle]; - CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle]; - CXTransaction *transaction = [[CXTransaction alloc] initWithAction:startCallAction]; + CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle]; + CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle]; + CXTransaction *transaction = [[CXTransaction alloc] initWithAction:startCallAction]; - [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) { - if (error) { - NSLog(@"StartCallAction transaction request failed: %@", [error localizedDescription]); - } else { - NSLog(@"StartCallAction transaction request successful"); + [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) { + if (error) { + NSLog(@"StartCallAction transaction request failed: %@", [error localizedDescription]); + } else { + NSLog(@"StartCallAction transaction request successful"); - CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; - callUpdate.remoteHandle = callHandle; - callUpdate.supportsDTMF = YES; - callUpdate.supportsHolding = YES; - callUpdate.supportsGrouping = NO; - callUpdate.supportsUngrouping = NO; - callUpdate.hasVideo = NO; + CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + callUpdate.remoteHandle = callHandle; + callUpdate.supportsDTMF = YES; + callUpdate.supportsHolding = YES; + callUpdate.supportsGrouping = NO; + callUpdate.supportsUngrouping = NO; + callUpdate.hasVideo = NO; - [self.callKitProvider reportCallWithUUID:uuid updated:callUpdate]; - } - }]; + [self.callKitProvider reportCallWithUUID:uuid updated:callUpdate]; + } + }]; } -- (void)reportIncomingCallFrom:(NSString *)from withUUID:(NSUUID *)uuid { - CXHandleType type = [[from substringToIndex:1] isEqual:@"+"] ? CXHandleTypePhoneNumber : CXHandleTypeGeneric; - // lets replace 'client:' with '' - CXHandle *callHandle = [[CXHandle alloc] initWithType:type value:[from stringByReplacingOccurrencesOfString:@"client:" withString:@""]]; +- (void)reportIncomingCallFrom:(NSString *)from withUUID:(NSUUID *)uuid withCallerCustomName:(NSString *)name { + CXHandleType type = [[from substringToIndex:1] isEqual:@"+"] ? CXHandleTypePhoneNumber : CXHandleTypeGeneric; + // lets replace 'client:' with '' + CXHandle *callHandle = [[CXHandle alloc] initWithType:type value:[from stringByReplacingOccurrencesOfString:@"client:" withString:@""]]; - CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; - callUpdate.remoteHandle = callHandle; - callUpdate.supportsDTMF = YES; - callUpdate.supportsHolding = YES; - callUpdate.supportsGrouping = NO; - callUpdate.supportsUngrouping = NO; - callUpdate.hasVideo = NO; - - [self.callKitProvider reportNewIncomingCallWithUUID:uuid update:callUpdate completion:^(NSError *error) { - if (!error) { - NSLog(@"Incoming call successfully reported"); - } else { - NSLog(@"Failed to report incoming call successfully: %@.", [error localizedDescription]); + CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init]; + if (name) { + callUpdate.localizedCallerName = name; } - }]; + callUpdate.remoteHandle = callHandle; + callUpdate.supportsDTMF = YES; + callUpdate.supportsHolding = YES; + callUpdate.supportsGrouping = NO; + callUpdate.supportsUngrouping = NO; + callUpdate.hasVideo = NO; + + [self.callKitProvider reportNewIncomingCallWithUUID:uuid update:callUpdate completion:^(NSError *error) { + if (!error) { + NSLog(@"Incoming call successfully reported"); + } else { + NSLog(@"Failed to report incoming call successfully: %@.", [error localizedDescription]); + } + }]; } - (void)performEndCallActionWithUUID:(NSUUID *)uuid { - if (uuid == nil) { - return; - } + if (uuid == nil) { + return; + } - CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:uuid]; - CXTransaction *transaction = [[CXTransaction alloc] initWithAction:endCallAction]; + CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:uuid]; + CXTransaction *transaction = [[CXTransaction alloc] initWithAction:endCallAction]; - [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) { - if (error) { - NSLog(@"EndCallAction transaction request failed: %@", [error localizedDescription]); - } - }]; + [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) { + if (error) { + NSLog(@"EndCallAction transaction request failed: %@", [error localizedDescription]); + } + }]; } - (void)performVoiceCallWithUUID:(NSUUID *)uuid client:(NSString *)client completion:(void(^)(BOOL success))completionHandler { - __weak typeof(self) weakSelf = self; + __weak typeof(self) weakSelf = self; TVOConnectOptions *connectOptions = [TVOConnectOptions optionsWithAccessToken:[self fetchAccessToken] block:^(TVOConnectOptionsBuilder *builder) { - __strong typeof(self) strongSelf = weakSelf; - builder.params = strongSelf->_callParams; - builder.uuid = uuid; + __strong typeof(self) strongSelf = weakSelf; + builder.params = strongSelf->_callParams; + builder.uuid = uuid; }]; - TVOCall *call = [TwilioVoice connectWithOptions:connectOptions delegate:self]; + TVOCall *call = [TwilioVoiceSDK connectWithOptions:connectOptions delegate:self]; if (call) { - self.activeCall = call; - self.activeCalls[call.uuid.UUIDString] = call; + self.activeCall = call; + self.activeCalls[call.uuid.UUIDString] = call; } self.callKitCompletionCallback = completionHandler; } @@ -835,12 +878,12 @@ - (void)performAnswerVoiceCallWithUUID:(NSUUID *)uuid } - (void)handleAppTerminateNotification { - NSLog(@"handleAppTerminateNotification called"); + NSLog(@"handleAppTerminateNotification called"); - if (self.activeCall) { - NSLog(@"handleAppTerminateNotification disconnecting an active call"); - [self.activeCall disconnect]; - } + if (self.activeCall) { + NSLog(@"handleAppTerminateNotification disconnecting an active call"); + [self.activeCall disconnect]; + } } @end From 9930c4589f5bbdce1419e19719cf2169c9053730 Mon Sep 17 00:00:00 2001 From: Arthur Islamov Date: Tue, 16 Nov 2021 15:18:34 -0800 Subject: [PATCH 5/7] Updated README.md --- README.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 68b75bf8..971453de 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,12 @@ To pass caller's name to CallKit via Voip push notification add custom parameter ``` -If your app gets killed after receiving push notification you must initialize CallKit on start +Your app must initialize PKPushRegistry with PushKit push type VoIP at the launch time. As mentioned in the +[PushKit guidelines](https://developer.apple.com/documentation/pushkit/supporting_pushkit_notifications_in_your_app), +the system can't deliver push notifications to your app until you create a PKPushRegistry object for VoIP push type and set the delegate. If your app delays the initialization of PKPushRegistry, your app may receive outdated +PushKit push notifications, and if your app decides not to report the received outdated push notifications to CallKit, iOS may terminate your app. + +We will initialize push kit only if RN code had called TwilioVoice.initWithAccessToken(token) and we've cached device token. You can pass same arguments to initPushKitIfTokenCached as you would pass to configureCallKit ```obj-c // add import @@ -170,11 +175,10 @@ If your app gets killed after receiving push notification you must initialize Ca RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; // ... - // add these three lines - RNTwilioVoice *voice = [bridge moduleForName:@"RNTwilioVoice"]; - [voice initPushRegistry]; - // you can pass same arguments as from your JS code - [voice configureCallKit:@{ @"appName" : @"YOUR FANCY APP NAME" }]; + // add these two lines + RNTwilioVoice *voice = [bridge moduleForClass:RNTwilioVoice.class]; + [voice initPushKitIfTokenCached:@{ @"appName" : @"YOUR FANCY APP NAME" }]; + return YES; } ``` From 3a6ed77624c635320468bf3c3f170da46c113e19 Mon Sep 17 00:00:00 2001 From: Arthur Islamov Date: Wed, 17 Nov 2021 07:00:30 -0800 Subject: [PATCH 6/7] Added restoration handler on iOS --- README.md | 25 ++++++++++++++++++++---- index.js | 1 + ios/RNTwilioVoice/RNTwilioVoice.h | 1 + ios/RNTwilioVoice/RNTwilioVoice.m | 32 ++++++++++++++++++++++++++++++- 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 971453de..42a4e4b8 100644 --- a/README.md +++ b/README.md @@ -168,17 +168,28 @@ We will initialize push kit only if RN code had called TwilioVoice.initWithAcces // add import #import -@implementation AppDelegate +@implementation AppDelegate { // <-- add bracket and next two lines + RCTBridge* bridge; +} - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; + bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; // REMOVE RCTBridge* // ... // add these two lines - RNTwilioVoice *voice = [bridge moduleForClass:RNTwilioVoice.class]; - [voice initPushKitIfTokenCached:@{ @"appName" : @"YOUR FANCY APP NAME" }]; + _voice = [bridge moduleForClass:RNTwilioVoice.class]; + [_voice initPushKitIfTokenCached:@{ @"appName" : @"YOUR FANCY APP NAME" }]; + + return YES; +} +// add this method to handle taps in call log +- (BOOL)application:(UIApplication *)application +continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void(^)(NSArray> *restorableObjects))restorationHandler { + RNTwilioVoice* _voice = [_reactBridge moduleForClass:RNTwilioVoice.class]; + [_voice handleRestoration:userActivity]; return YES; } ``` @@ -377,6 +388,12 @@ TwilioVoice.addEventListener('deviceDidReceiveIncoming', function(data) { // } }) +TwilioVoice.addEventListener('iosCallHistoryTap', function(data) { + // { + // call_to: string, // "+441234567890" + // } +}) + // Android Only TwilioVoice.addEventListener('proximity', function(data) { // { diff --git a/index.js b/index.js index b84b91bf..acaf09b1 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,7 @@ const _eventHandlers = { callStateRinging: new Map(), callInviteCancelled: new Map(), callRejected: new Map(), + iosCallHistoryTap: new Map(), } const Twilio = { diff --git a/ios/RNTwilioVoice/RNTwilioVoice.h b/ios/RNTwilioVoice/RNTwilioVoice.h index d898038b..54054a8d 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.h +++ b/ios/RNTwilioVoice/RNTwilioVoice.h @@ -4,5 +4,6 @@ @interface RNTwilioVoice : RCTEventEmitter - (void) initPushKitIfTokenCached: (NSDictionary *)callKitParams; +- (BOOL) handleRestoration: (NSUserActivity *)userActivity; @end diff --git a/ios/RNTwilioVoice/RNTwilioVoice.m b/ios/RNTwilioVoice/RNTwilioVoice.m index a8f0413f..ba23bbea 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.m +++ b/ios/RNTwilioVoice/RNTwilioVoice.m @@ -5,6 +5,8 @@ @import PushKit; @import CallKit; @import TwilioVoice; +@import Intents; + NSString * const kCachedDeviceToken = @"CachedDeviceToken"; NSString * const kCachedTokenUrl = @"CachedTokenUrl"; @@ -54,7 +56,19 @@ - (dispatch_queue_t)methodQueue - (NSArray *)supportedEvents { - return @[@"connectionDidConnect", @"connectionDidDisconnect", @"callRejected", @"deviceReady", @"deviceNotReady", @"deviceDidReceiveIncoming", @"callInviteCancelled", @"callStateRinging", @"connectionIsReconnecting", @"connectionDidReconnect"]; + return @[ + @"connectionDidConnect", + @"connectionDidDisconnect", + @"callRejected", + @"deviceReady", + @"deviceNotReady", + @"deviceDidReceiveIncoming", + @"callInviteCancelled", + @"callStateRinging", + @"connectionIsReconnecting", + @"connectionDidReconnect", + @"iosCallHistoryTap" + ]; } @synthesize bridge = _bridge; @@ -80,6 +94,21 @@ - (void) initPushKitIfTokenCached: (NSDictionary *)callKitParams { } } +- (BOOL) handleRestoration: (NSUserActivity *)userActivity { + INStartAudioCallIntent *callIntent = (INStartAudioCallIntent *)userActivity.interaction.intent; + if (callIntent.contacts[0]) { + INPersonHandle *handle = callIntent.contacts[0].personHandle; + if ([handle.value length] > 0) { + // Start a new call with CallKit + NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; + [callParams setObject:callIntent.contacts[0].personHandle.value forKey:@"call_to"]; + [self sendEventWithName:@"iosCallHistoryTap" body:callParams]; + } + } + + return YES; +} + RCT_EXPORT_METHOD(initWithAccessToken:(NSString *)token) { _token = token; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAppTerminateNotification) name:UIApplicationWillTerminateNotification object:nil]; @@ -112,6 +141,7 @@ - (void) initPushKitIfTokenCached: (NSDictionary *)callKitParams { if (_settings[@"ringtoneSound"]) { configuration.ringtoneSound = _settings[@"ringtoneSound"]; } + configuration.supportedHandleTypes = [NSSet setWithArray:@[@(CXHandleTypeGeneric), @(CXHandleTypePhoneNumber)]]; _callKitProvider = [[CXProvider alloc] initWithConfiguration:configuration]; [_callKitProvider setDelegate:self queue:nil]; From 1cd5a7fa6eaf897ac3eededba6f7b1e6c09b17be Mon Sep 17 00:00:00 2001 From: Arthur Islamov Date: Wed, 17 Nov 2021 09:20:45 -0800 Subject: [PATCH 7/7] Readme fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 42a4e4b8..3bb9d42d 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ We will initialize push kit only if RN code had called TwilioVoice.initWithAcces - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void(^)(NSArray> *restorableObjects))restorationHandler { - RNTwilioVoice* _voice = [_reactBridge moduleForClass:RNTwilioVoice.class]; + RNTwilioVoice* _voice = [bridge moduleForClass:RNTwilioVoice.class]; [_voice handleRestoration:userActivity]; return YES; }