Просмотр исходного кода

[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.
j8
Saúl Ibarra Corretgé 7 лет назад
Родитель
Сommit
bd301403c4
3 измененных файлов: 108 добавлений и 2 удалений
  1. 4
    0
      ios/sdk/sdk.xcodeproj/project.pbxproj
  2. 83
    0
      ios/sdk/src/LaunchOptions.m
  3. 21
    2
      react/index.native.js

+ 4
- 0
ios/sdk/sdk.xcodeproj/project.pbxproj Просмотреть файл

@@ -11,6 +11,7 @@
11 11
 		0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */; };
12 12
 		0B412F221EDEF6EA00B1A0A6 /* JitsiMeetViewDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; };
13 13
 		0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */; };
14
+		0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B7C2CFC200F51D60060D076 /* LaunchOptions.m */; };
14 15
 		0B93EF7B1EC608550030D24D /* CoreText.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B93EF7A1EC608550030D24D /* CoreText.framework */; };
15 16
 		0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */; };
16 17
 		0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */; };
@@ -34,6 +35,7 @@
34 35
 		0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JitsiMeetView.m; sourceTree = "<group>"; };
35 36
 		0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JitsiMeetViewDelegate.h; sourceTree = "<group>"; };
36 37
 		0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPVolumeViewManager.m; sourceTree = "<group>"; };
38
+		0B7C2CFC200F51D60060D076 /* LaunchOptions.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LaunchOptions.m; sourceTree = "<group>"; };
37 39
 		0B93EF7A1EC608550030D24D /* CoreText.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreText.framework; path = System/Library/Frameworks/CoreText.framework; sourceTree = SDKROOT; };
38 40
 		0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTBridgeWrapper.h; sourceTree = "<group>"; };
39 41
 		0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTBridgeWrapper.m; sourceTree = "<group>"; };
@@ -106,6 +108,7 @@
106 108
 				0BB9AD7A1F5EC8F4001C08DB /* CallKit.m */,
107 109
 				0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */,
108 110
 				0BD906E91EC0C00300C8C18E /* Info.plist */,
111
+				0B7C2CFC200F51D60060D076 /* LaunchOptions.m */,
109 112
 				0BD906E81EC0C00300C8C18E /* JitsiMeet.h */,
110 113
 				0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */,
111 114
 				0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */,
@@ -305,6 +308,7 @@
305 308
 				0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */,
306 309
 				0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */,
307 310
 				0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */,
311
+				0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */,
308 312
 				0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */,
309 313
 				0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */,
310 314
 				0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */,

+ 83
- 0
ios/sdk/src/LaunchOptions.m Просмотреть файл

@@ -0,0 +1,83 @@
1
+/*
2
+ * Copyright @ 2018-present Atlassian Pty Ltd
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ *     http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+#import <Intents/Intents.h>
18
+
19
+#import <React/RCTBridge.h>
20
+#import <React/RCTBridgeModule.h>
21
+
22
+@interface LaunchOptions : NSObject<RCTBridgeModule>
23
+
24
+@property (nonatomic, weak) RCTBridge *bridge;
25
+
26
+@end
27
+
28
+@implementation LaunchOptions
29
+
30
+RCT_EXPORT_MODULE();
31
+
32
+- (dispatch_queue_t)methodQueue {
33
+    return dispatch_get_main_queue();
34
+}
35
+
36
+RCT_EXPORT_METHOD(getInitialURL:(RCTPromiseResolveBlock)resolve
37
+                  reject:(__unused RCTPromiseRejectBlock)reject) {
38
+    id initialURL = nil;
39
+    if (self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey]) {
40
+        NSURL *url = self.bridge.launchOptions[UIApplicationLaunchOptionsURLKey];
41
+        initialURL = url.absoluteString;
42
+    } else {
43
+        NSDictionary *userActivityDictionary
44
+            = self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey];
45
+        NSUserActivity *userActivity
46
+            = [userActivityDictionary objectForKey:@"UIApplicationLaunchOptionsUserActivityKey"];
47
+        if (userActivity != nil) {
48
+            NSString *activityType = userActivity.activityType;
49
+
50
+            if ([activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
51
+                // App was started by opening a URL in the browser
52
+                initialURL = userActivity.webpageURL.absoluteString;
53
+            } else if ([activityType isEqualToString:@"INStartAudioCallIntent"]
54
+                       || [activityType isEqualToString:@"INStartVideoCallIntent"]) {
55
+                // App was started by a CallKit Intent
56
+                INIntent *intent = userActivity.interaction.intent;
57
+                NSArray<INPerson *> *contacts;
58
+                NSString *url;
59
+                BOOL startAudioOnly = NO;
60
+
61
+                if ([intent isKindOfClass:[INStartAudioCallIntent class]]) {
62
+                    contacts = ((INStartAudioCallIntent *) intent).contacts;
63
+                    startAudioOnly = YES;
64
+                } else if ([intent isKindOfClass:[INStartVideoCallIntent class]]) {
65
+                    contacts = ((INStartVideoCallIntent *) intent).contacts;
66
+                }
67
+
68
+                if (contacts && (url = contacts.firstObject.personHandle.value)) {
69
+                    initialURL
70
+                        = @{
71
+                            @"config": @{@"startAudioOnly":@(startAudioOnly)},
72
+                            @"url": url
73
+                            };
74
+                }
75
+            }
76
+        }
77
+    }
78
+
79
+    resolve(initialURL != nil ? initialURL : (id)kCFNull);
80
+}
81
+
82
+@end
83
+

+ 21
- 2
react/index.native.js Просмотреть файл

@@ -14,7 +14,7 @@ import './features/base/react/prop-types-polyfill';
14 14
 
15 15
 import PropTypes from 'prop-types';
16 16
 import React, { Component } from 'react';
17
-import { AppRegistry, Linking } from 'react-native';
17
+import { AppRegistry, Linking, NativeModules } from 'react-native';
18 18
 
19 19
 import { App } from './features/app';
20 20
 import { equals } from './features/base/redux';
@@ -77,7 +77,7 @@ class Root extends Component {
77 77
         // Handle the URL, if any, with which the app was launched. But props
78 78
         // have precedence.
79 79
         if (typeof this.props.url === 'undefined') {
80
-            Linking.getInitialURL()
80
+            this._getInitialURL()
81 81
                 .then(url => {
82 82
                     if (typeof this.state.url === 'undefined') {
83 83
                         this.setState({ url });
@@ -95,6 +95,25 @@ class Root extends Component {
95 95
         }
96 96
     }
97 97
 
98
+    /**
99
+     * Gets the initial URL the app was launched with. This can be a universal
100
+     * (or deep) link, or a CallKit intent in iOS. Since the native
101
+     * {@code Linking} module doesn't provide a way to access intents in iOS,
102
+     * those are handled with the {@code LaunchOptions} module, which
103
+     * essentially provides a replacement which takes that into consideration.
104
+     *
105
+     * @private
106
+     * @returns {Promise} - A promise which will be fulfilled with the URL that
107
+     * the app was launched with.
108
+     */
109
+    _getInitialURL() {
110
+        if (NativeModules.LaunchOptions) {
111
+            return NativeModules.LaunchOptions.getInitialURL();
112
+        }
113
+
114
+        return Linking.getInitialURL();
115
+    }
116
+
98 117
     /**
99 118
      * Implements React's {@link Component#componentWillReceiveProps()}.
100 119
      *

Загрузка…
Отмена
Сохранить