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()}. *