From bd301403c45dd2f20ad43449bec23c146d1e9b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sa=C3=BAl=20Ibarra=20Corretg=C3=A9?= Date: Wed, 17 Jan 2018 13:24:00 +0100 Subject: [PATCH] [RN] Fix app startup from a CallKit intent Story time. Currently the app can be started in 4 ways: - just tapping on the icon - via a deep link - via a universal link - via the phone's recent calls list The last 3 options will make the app join the specified room upon launch. React Native's Linking module implements the necessary bits to handle deep or universal linking, but CallKit is out of its scope. In order to blend any type of app startup mode, a new LaunchOptions module (iOS only) exports a getInitialURL function, akin to the one in the Linking module, but taking CallKit instents into consideration. This function is then used to make app startup with a URL consistent across all different modes. --- ios/sdk/sdk.xcodeproj/project.pbxproj | 4 ++ ios/sdk/src/LaunchOptions.m | 83 +++++++++++++++++++++++++++ react/index.native.js | 23 +++++++- 3 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 ios/sdk/src/LaunchOptions.m diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj index 5f0d60da4..71a23abff 100644 --- a/ios/sdk/sdk.xcodeproj/project.pbxproj +++ b/ios/sdk/sdk.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */; }; 0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */; }; + 0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B7C2CFC200F51D60060D076 /* LaunchOptions.m */; }; 0B93EF7B1EC608550030D24D /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B93EF7A1EC608550030D24D /* CoreText.framework */; }; 0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */; }; 0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */; }; @@ -34,6 +35,7 @@ 0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetView.m; sourceTree = ""; }; 0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetViewDelegate.h; sourceTree = ""; }; 0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPVolumeViewManager.m; sourceTree = ""; }; + 0B7C2CFC200F51D60060D076 /* LaunchOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LaunchOptions.m; sourceTree = ""; }; 0B93EF7A1EC608550030D24D /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; }; 0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBridgeWrapper.h; sourceTree = ""; }; 0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBridgeWrapper.m; sourceTree = ""; }; @@ -106,6 +108,7 @@ 0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */, 0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */, 0BD906E91EC0C00300C8C18E /* Info.plist */, + 0B7C2CFC200F51D60060D076 /* LaunchOptions.m */, 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */, 0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */, 0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */, @@ -305,6 +308,7 @@ 0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */, 0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */, 0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */, + 0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */, 0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */, 0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */, 0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */, diff --git a/ios/sdk/src/LaunchOptions.m b/ios/sdk/src/LaunchOptions.m new file mode 100644 index 000000000..578328c94 --- /dev/null +++ b/ios/sdk/src/LaunchOptions.m @@ -0,0 +1,83 @@ +/* + * Copyright @ 2018-present Atlassian Pty Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import +#import + +@interface LaunchOptions : NSObject + +@property (nonatomic, weak) RCTBridge *bridge; + +@end + +@implementation LaunchOptions + +RCT_EXPORT_MODULE(); + +- (dispatch_queue_t)methodQueue { + return dispatch_get_main_queue(); +} + +RCT_EXPORT_METHOD(getInitialURL:(RCTPromiseResolveBlock)resolve + reject:(__unused RCTPromiseRejectBlock)reject) { + id initialURL = nil; + if (self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]) { + NSURL *url = self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]; + initialURL = url.absoluteString; + } else { + NSDictionary *userActivityDictionary + = self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; + NSUserActivity *userActivity + = [userActivityDictionary objectForKey:@"UIApplicationLaunchOptionsUserActivityKey"]; + if (userActivity != nil) { + NSString *activityType = userActivity.activityType; + + if ([activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { + // App was started by opening a URL in the browser + initialURL = userActivity.webpageURL.absoluteString; + } else if ([activityType isEqualToString:@"INStartAudioCallIntent"] + || [activityType isEqualToString:@"INStartVideoCallIntent"]) { + // App was started by a CallKit Intent + INIntent *intent = userActivity.interaction.intent; + NSArray *contacts; + NSString *url; + BOOL startAudioOnly = NO; + + if ([intent isKindOfClass:[INStartAudioCallIntent class]]) { + contacts = ((INStartAudioCallIntent *) intent).contacts; + startAudioOnly = YES; + } else if ([intent isKindOfClass:[INStartVideoCallIntent class]]) { + contacts = ((INStartVideoCallIntent *) intent).contacts; + } + + if (contacts && (url = contacts.firstObject.personHandle.value)) { + initialURL + = @{ + @"config": @{@"startAudioOnly":@(startAudioOnly)}, + @"url": url + }; + } + } + } + } + + resolve(initialURL != nil ? initialURL : (id)kCFNull); +} + +@end + diff --git a/react/index.native.js b/react/index.native.js index 7c2d1e12e..1ce6b581b 100644 --- a/react/index.native.js +++ b/react/index.native.js @@ -14,7 +14,7 @@ import './features/base/react/prop-types-polyfill'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { AppRegistry, Linking } from 'react-native'; +import { AppRegistry, Linking, NativeModules } from 'react-native'; import { App } from './features/app'; import { equals } from './features/base/redux'; @@ -77,7 +77,7 @@ class Root extends Component { // Handle the URL, if any, with which the app was launched. But props // have precedence. if (typeof this.props.url === 'undefined') { - Linking.getInitialURL() + this._getInitialURL() .then(url => { if (typeof this.state.url === 'undefined') { this.setState({ url }); @@ -95,6 +95,25 @@ class Root extends Component { } } + /** + * Gets the initial URL the app was launched with. This can be a universal + * (or deep) link, or a CallKit intent in iOS. Since the native + * {@code Linking} module doesn't provide a way to access intents in iOS, + * those are handled with the {@code LaunchOptions} module, which + * essentially provides a replacement which takes that into consideration. + * + * @private + * @returns {Promise} - A promise which will be fulfilled with the URL that + * the app was launched with. + */ + _getInitialURL() { + if (NativeModules.LaunchOptions) { + return NativeModules.LaunchOptions.getInitialURL(); + } + + return Linking.getInitialURL(); + } + /** * Implements React's {@link Component#componentWillReceiveProps()}. *