diff --git a/ios/app/src/AppDelegate.m b/ios/app/src/AppDelegate.m index 3de9b1417..d263cb0ae 100644 --- a/ios/app/src/AppDelegate.m +++ b/ios/app/src/AppDelegate.m @@ -19,11 +19,10 @@ #import "FIRUtilities.h" #import "Types.h" -#import - @import Crashlytics; @import Fabric; @import Firebase; +@import JitsiMeet; @implementation AppDelegate @@ -40,10 +39,10 @@ // Set the conference activity type defined in this application. // This cannot be defined by the SDK. - JitsiMeetView.conferenceActivityType = JitsiMeetConferenceActivityType; + [JitsiMeet sharedInstance].conferenceActivityType = JitsiMeetConferenceActivityType; + [[JitsiMeet sharedInstance] application:application didFinishLaunchingWithOptions:launchOptions]; - return [JitsiMeetView application:application - didFinishLaunchingWithOptions:launchOptions]; + return YES; } #pragma mark Linking delegate methods @@ -63,9 +62,9 @@ NSURL *dynamicLinkURL = dynamicLink.url; if (dynamicLinkURL) { userActivity.webpageURL = dynamicLinkURL; - [JitsiMeetView application:application - continueUserActivity:userActivity - restorationHandler:restorationHandler]; + [[JitsiMeet sharedInstance] application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; } }]; @@ -75,9 +74,9 @@ } // 2. Default to plain old, non-Firebase-assisted Universal Links. - return [JitsiMeetView application:application - continueUserActivity:userActivity - restorationHandler:restorationHandler]; + return [[JitsiMeet sharedInstance] application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; } - (BOOL)application:(UIApplication *)app @@ -100,9 +99,9 @@ } } - return [JitsiMeetView application:app - openURL:openUrl - options:options]; + return [[JitsiMeet sharedInstance] application:app + openURL:openUrl + options:options]; } @end diff --git a/ios/app/src/ViewController.m b/ios/app/src/ViewController.m index eb5463f70..ffde23a42 100644 --- a/ios/app/src/ViewController.m +++ b/ios/app/src/ViewController.m @@ -1,5 +1,6 @@ /* - * Copyright @ 2017-present Atlassian Pty Ltd + * Copyright @ 2018-present 8x8, Inc. + * Copyright @ 2017-2018 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. @@ -15,18 +16,14 @@ */ #import -#import -#import + +@import CoreSpotlight; +@import MobileCoreServices; +@import Intents; // Needed for NSUserActivity suggestedInvocationPhrase #import "Types.h" #import "ViewController.h" -// Needed for NSUserActivity suggestedInvocationPhrase -@import Intents; - - -@interface ViewController () -@end @implementation ViewController @@ -42,11 +39,9 @@ // anyway. view.welcomePageEnabled = YES; - [view loadURL:nil]; + [view loadURL:[[JitsiMeet sharedInstance] getInitialURL]]; } - - // JitsiMeetViewDelegate - (void)_onJitsiMeetViewDelegateEvent:(NSString *)name diff --git a/ios/sdk/sdk.xcodeproj/project.pbxproj b/ios/sdk/sdk.xcodeproj/project.pbxproj index 93a79065b..ab1858d37 100644 --- a/ios/sdk/sdk.xcodeproj/project.pbxproj +++ b/ios/sdk/sdk.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 0B44A0191F902126009D1D64 /* MPVolumeViewManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */; }; 0B49424520AD8DBD00BD2DE0 /* outgoingStart.wav in Resources */ = {isa = PBXBuildFile; fileRef = 0B49424320AD8DBD00BD2DE0 /* outgoingStart.wav */; }; 0B49424620AD8DBD00BD2DE0 /* outgoingRinging.wav in Resources */ = {isa = PBXBuildFile; fileRef = 0B49424420AD8DBD00BD2DE0 /* outgoingRinging.wav */; }; - 0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B7C2CFC200F51D60060D076 /* LaunchOptions.m */; }; 0B93EF7E1EC9DDCD0030D24D /* RCTBridgeWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */; }; 0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */; }; 0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */; }; @@ -45,6 +44,8 @@ C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */; }; C6F99C15204DB63E0001F710 /* JitsiMeetView+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */; }; DEFC743F21B178FA00E4DD96 /* LocaleDetector.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */; }; + DEFE535421FB1BF800011A3A /* JitsiMeet.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE535321FB1BF800011A3A /* JitsiMeet.m */; }; + DEFE535621FB2E8300011A3A /* ReactUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = DEFE535521FB2E8300011A3A /* ReactUtils.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -55,7 +56,6 @@ 0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPVolumeViewManager.m; sourceTree = ""; }; 0B49424320AD8DBD00BD2DE0 /* outgoingStart.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = outgoingStart.wav; path = ../../sounds/outgoingStart.wav; sourceTree = ""; }; 0B49424420AD8DBD00BD2DE0 /* outgoingRinging.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; name = outgoingRinging.wav; path = ../../sounds/outgoingRinging.wav; 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 = ""; }; @@ -93,6 +93,10 @@ C6CC49AE207412CF000DFA42 /* PiPViewCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiPViewCoordinator.swift; sourceTree = ""; }; C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "JitsiMeetView+Private.h"; sourceTree = ""; }; DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LocaleDetector.m; sourceTree = ""; }; + DEFE535321FB1BF800011A3A /* JitsiMeet.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JitsiMeet.m; sourceTree = ""; }; + DEFE535521FB2E8300011A3A /* ReactUtils.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ReactUtils.m; sourceTree = ""; }; + DEFE535721FB2E9E00011A3A /* ReactUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ReactUtils.h; sourceTree = ""; }; + DEFE535821FB311F00011A3A /* JitsiMeet+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "JitsiMeet+Private.h"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -158,16 +162,19 @@ 0BA13D301EE83FF8007BEF7F /* ExternalAPI.m */, 0BD906E91EC0C00300C8C18E /* Info.plist */, 0BD906E81EC0C00300C8C18E /* JitsiMeet.h */, + DEFE535821FB311F00011A3A /* JitsiMeet+Private.h */, + DEFE535321FB1BF800011A3A /* JitsiMeet.m */, 0B412F161EDEC65D00B1A0A6 /* JitsiMeetView.h */, 0B412F171EDEC65D00B1A0A6 /* JitsiMeetView.m */, C6F99C13204DB63D0001F710 /* JitsiMeetView+Private.h */, 0B412F1B1EDEC80100B1A0A6 /* JitsiMeetViewDelegate.h */, - 0B7C2CFC200F51D60060D076 /* LaunchOptions.m */, DEFC743D21B178FA00E4DD96 /* LocaleDetector.m */, 0B44A0181F902126009D1D64 /* MPVolumeViewManager.m */, C6A3426B204F127900E062DD /* picture-in-picture */, 0BCA495D1EC4B6C600B793EE /* POSIX.m */, 0BCA495E1EC4B6C600B793EE /* Proximity.m */, + DEFE535721FB2E9E00011A3A /* ReactUtils.h */, + DEFE535521FB2E8300011A3A /* ReactUtils.m */, 0B93EF7C1EC9DDCD0030D24D /* RCTBridgeWrapper.h */, 0B93EF7D1EC9DDCD0030D24D /* RCTBridgeWrapper.m */, ); @@ -447,7 +454,6 @@ 0B93EF7F1EC9DDCD0030D24D /* RCTBridgeWrapper.m in Sources */, 0BA13D311EE83FF8007BEF7F /* ExternalAPI.m in Sources */, 0BCA49601EC4B6C600B793EE /* POSIX.m in Sources */, - 0B7C2CFD200F51D60060D076 /* LaunchOptions.m in Sources */, C6CC49AF207412CF000DFA42 /* PiPViewCoordinator.swift in Sources */, DEFC743F21B178FA00E4DD96 /* LocaleDetector.m in Sources */, 0BCA495F1EC4B6C600B793EE /* AudioMode.m in Sources */, @@ -455,11 +461,13 @@ 0BCA49611EC4B6C600B793EE /* Proximity.m in Sources */, A480429C21EE335600289B73 /* AmplitudeModule.m in Sources */, C69EFA0C209A0F660027712B /* JMCallKitEmitter.swift in Sources */, + DEFE535621FB2E8300011A3A /* ReactUtils.m in Sources */, C6A34261204EF76800E062DD /* DragGestureController.swift in Sources */, A4A934E9212F3ADB001E9388 /* Dropbox.m in Sources */, C69EFA0D209A0F660027712B /* JMCallKitProxy.swift in Sources */, C69EFA0E209A0F660027712B /* JMCallKitListener.swift in Sources */, 0B412F191EDEC65D00B1A0A6 /* JitsiMeetView.m in Sources */, + DEFE535421FB1BF800011A3A /* JitsiMeet.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/sdk/src/JitsiMeet+Private.h b/ios/sdk/src/JitsiMeet+Private.h new file mode 100644 index 000000000..31dca550e --- /dev/null +++ b/ios/sdk/src/JitsiMeet+Private.h @@ -0,0 +1,23 @@ +/* + * Copyright @ 2019-present 8x8, Inc. + * + * 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 "JitsiMeet.h" + +@interface JitsiMeet () + +- (RCTBridge *)getReactBridge; + +@end diff --git a/ios/sdk/src/JitsiMeet.h b/ios/sdk/src/JitsiMeet.h index cad0a6bdf..a31670967 100644 --- a/ios/sdk/src/JitsiMeet.h +++ b/ios/sdk/src/JitsiMeet.h @@ -1,5 +1,6 @@ /* - * Copyright @ 2017-present Atlassian Pty Ltd + * Copyright @ 2018-present 8x8, Inc. + * Copyright @ 2017-2018 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. @@ -14,6 +15,32 @@ * limitations under the License. */ -// JitsiMeetView #import #import + +@interface JitsiMeet : NSObject + +@property (copy, nonatomic, nullable) NSString *conferenceActivityType; + +#pragma mak - This class is a singleton + ++ (instancetype)sharedInstance; + +#pragma mark - Methods that the App delegate must call + +- (BOOL)application:(UIApplication *_Nonnull)application + didFinishLaunchingWithOptions:(NSDictionary *_Nonnull)launchOptions; + +- (BOOL)application:(UIApplication * _Nonnull)application + continueUserActivity:(NSUserActivity * _Nonnull)userActivity + restorationHandler:(void (^ _Nullable)(NSArray * _Nullable))restorationHandler; + +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options; + +#pragma mark - Utility methods + +- (NSDictionary *)getInitialURL; + +@end diff --git a/ios/sdk/src/JitsiMeet.m b/ios/sdk/src/JitsiMeet.m new file mode 100644 index 000000000..69761e0a9 --- /dev/null +++ b/ios/sdk/src/JitsiMeet.m @@ -0,0 +1,161 @@ +/* + * Copyright @ 2019-present 8x8, Inc. + * + * 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 "Dropbox.h" +#import "JitsiMeet+Private.h" +#import "JitsiMeetView+Private.h" +#import "RCTBridgeWrapper.h" +#import "ReactUtils.h" + +#import + + +@implementation JitsiMeet { + RCTBridgeWrapper *_bridgeWrapper; + NSDictionary *_launchOptions; +} + +#pragma mak - This class is a singleton + ++ (instancetype)sharedInstance { + static JitsiMeet *sharedInstance = nil; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + sharedInstance = [[self alloc] init]; + }); + + return sharedInstance; +} + +- (instancetype)init { + if (self = [super init]) { + // Initialize the on and only bridge for interfacing with React Native. + _bridgeWrapper = [[RCTBridgeWrapper alloc] init]; + + // Register a fatal error handler for React. + registerReactFatalErrorHandler(); + } + + return self; +} + +#pragma mark - Methods that the App delegate must call + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + + _launchOptions = [launchOptions copy]; + + [Dropbox setAppKey]; + + return YES; +} + +- (BOOL)application:(UIApplication *)application + continueUserActivity:(NSUserActivity *)userActivity + restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler { + + id url = [self urlFromUserActivity:userActivity]; + + return url && [JitsiMeetView loadURLInViews:url]; +} + +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options { + + if ([Dropbox application:app openURL:url options:options]) { + return YES; + } + + if ([RNGoogleSignin application:app + openURL:url + sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey] + annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]) { + return YES; + } + + return [JitsiMeetView loadURLInViews:@{ @"url" : url.absoluteString }]; +} + +#pragma mark - Utility methods + +- (NSDictionary *)getInitialURL { + if (_launchOptions[UIApplicationLaunchOptionsURLKey]) { + NSURL *url = _launchOptions[UIApplicationLaunchOptionsURLKey]; + return @{ @"url" : url.absoluteString }; + } else { + NSDictionary *userActivityDictionary + = _launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; + NSUserActivity *userActivity + = [userActivityDictionary objectForKey:@"UIApplicationLaunchOptionsUserActivityKey"]; + if (userActivity != nil) { + return [self urlFromUserActivity:userActivity]; + } + } + + return nil; +} + +- (NSDictionary *)urlFromUserActivity:(NSUserActivity *)userActivity { + NSString *activityType = userActivity.activityType; + + if ([activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { + // App was started by opening a URL in the browser + return @{ @"url" : 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)) { + return @{ + @"config": @{@"startAudioOnly":@(startAudioOnly)}, + @"url": url + }; + } + } else if (self.conferenceActivityType && [activityType isEqualToString:self.conferenceActivityType]) { + // App was started by continuing a registered NSUserActivity (SiriKit, Handoff, ...) + NSString *url; + + if ((url = userActivity.userInfo[@"url"])) { + return @{ @"url" : url }; + } + } + + return nil; +} + +#pragma mark - Private API methods + +- (RCTBridge *)getReactBridge { + return _bridgeWrapper.bridge; +} + +@end diff --git a/ios/sdk/src/JitsiMeetView+Private.h b/ios/sdk/src/JitsiMeetView+Private.h index b62501746..89f6cc0e2 100644 --- a/ios/sdk/src/JitsiMeetView+Private.h +++ b/ios/sdk/src/JitsiMeetView+Private.h @@ -1,5 +1,6 @@ /* - * Copyright @ 2017-present Atlassian Pty Ltd + * Copyright @ 2018-present 8x8, Inc. + * Copyright @ 2017-2018 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. @@ -18,7 +19,7 @@ @interface JitsiMeetView () -+ (NSDictionary *)conferenceURLFromUserActivity:(NSUserActivity *)userActivity; + (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope; ++ (BOOL)loadURLInViews:(NSDictionary *)urlObject; @end diff --git a/ios/sdk/src/JitsiMeetView.h b/ios/sdk/src/JitsiMeetView.h index be1b2e89c..e1dcf33e8 100644 --- a/ios/sdk/src/JitsiMeetView.h +++ b/ios/sdk/src/JitsiMeetView.h @@ -1,5 +1,6 @@ /* - * Copyright @ 2017-present Atlassian Pty Ltd + * Copyright @ 2018-present 8x8, Inc. + * Copyright @ 2017-2018 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. @@ -33,26 +34,6 @@ @property (nonatomic) BOOL welcomePageEnabled; -+ (BOOL)application:(UIApplication *_Nonnull)application - didFinishLaunchingWithOptions:(NSDictionary *_Nonnull)launchOptions; - -+ (BOOL)application:(UIApplication * _Nonnull)application - continueUserActivity:(NSUserActivity * _Nonnull)userActivity - restorationHandler:(void (^ _Nullable)(NSArray * _Nullable))restorationHandler; - -+ (BOOL)application:(UIApplication *)app - openURL:(NSURL *)url - options:(NSDictionary *)options; - -+ (BOOL)application:(UIApplication * _Nonnull)application - openURL:(NSURL * _Nonnull)URL - sourceApplication:(NSString * _Nullable)sourceApplication - annotation:(id _Nullable)annotation __deprecated; - -- (void)loadURL:(NSURL * _Nullable)url; - -- (void)loadURLObject:(NSDictionary * _Nullable)urlObject; - -- (void)loadURLString:(NSString * _Nullable)urlString; +- (void)loadURL:(NSDictionary * _Nullable)urlObject; @end diff --git a/ios/sdk/src/JitsiMeetView.m b/ios/sdk/src/JitsiMeetView.m index 7379a0bb1..90457dbd6 100644 --- a/ios/sdk/src/JitsiMeetView.m +++ b/ios/sdk/src/JitsiMeetView.m @@ -1,5 +1,6 @@ /* - * Copyright @ 2017-present Atlassian Pty Ltd + * Copyright @ 2018-present 8x8, Inc. + * Copyright @ 2017-2018 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. @@ -14,62 +15,15 @@ * limitations under the License. */ -#import - #include -#import -#import #import -#import - -#import "Dropbox.h" +#import "JitsiMeet+Private.h" #import "JitsiMeetView+Private.h" -#import "RCTBridgeWrapper.h" -/** - * A `RCTFatalHandler` implementation which swallows JavaScript errors. In the - * Release configuration, React Native will (intentionally) raise an unhandled - * `NSException` for an unhandled JavaScript error. This will effectively kill - * the application. `_RCTFatal` is suitable to be in accord with the Web i.e. - * not kill the application. - */ -RCTFatalHandler _RCTFatal = ^(NSError *error) { - id jsStackTrace = error.userInfo[RCTJSStackTraceKey]; - @try { - NSString *name - = [NSString stringWithFormat:@"%@: %@", - RCTFatalExceptionName, - error.localizedDescription]; - NSString *message - = RCTFormatError(error.localizedDescription, jsStackTrace, 75); - [NSException raise:name format:@"%@", message]; - } @catch (NSException *e) { - if (!jsStackTrace) { - @throw; - } - } -}; -/** - * Helper function to register a fatal error handler for React. Our handler - * won't kill the process, it will swallow JS errors and print stack traces - * instead. - */ -void registerFatalErrorHandler() { -#if !DEBUG - // In the Release configuration, React Native will (intentionally) raise an - // unhandled `NSException` for an unhandled JavaScript error. This will - // effectively kill the application. In accord with the Web, do not kill the - // application. - if (!RCTGetFatalHandler()) { - RCTSetFatalHandler(_RCTFatal); - } -#endif -} - -@interface JitsiMeetView() { +@implementation JitsiMeetView { /** * The unique identifier of this `JitsiMeetView` within the process for the * purposes of `ExternalAPI`. The name scope was inspired by postis which we @@ -78,98 +32,24 @@ void registerFatalErrorHandler() { NSString *externalAPIScope; RCTRootView *rootView; -} -@end - -@implementation JitsiMeetView { NSNumber *_pictureInPictureEnabled; } @dynamic pictureInPictureEnabled; -static NSString *_conferenceActivityType; - -static RCTBridgeWrapper *bridgeWrapper; - -/** - * Copy of the `launchOptions` dictionary that the application was started with. - * It is required for the initial URL to be used if a (Universal) link was used - * to launch a new instance of the application. - */ -static NSDictionary *_launchOptions; - /** * The `JitsiMeetView`s associated with their `ExternalAPI` scopes (i.e. unique * identifiers within the process). */ static NSMapTable *views; -+ (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - // Store launch options, will be used when we create the bridge. - _launchOptions = [launchOptions copy]; - - [Dropbox setAppKey]; - - return YES; -} - -#pragma mark Linking delegate helpers -// https://facebook.github.io/react-native/docs/linking.html - -+ (BOOL)application:(UIApplication *)application - continueUserActivity:(NSUserActivity *)userActivity - restorationHandler:(void (^)(NSArray *restorableObjects))restorationHandler -{ - // XXX At least twice we received bug reports about malfunctioning loadURL - // in the Jitsi Meet SDK while the Jitsi Meet app seemed to functioning as - // expected in our testing. But that was to be expected because the app does - // not exercise loadURL. In order to increase the test coverage of loadURL, - // channel Universal linking through loadURL. - - id url = [self conferenceURLFromUserActivity:userActivity]; - - if (url && [self loadURLObjectInViews:url]) { - return YES; - } - - return [RCTLinkingManager application:application - continueUserActivity:userActivity - restorationHandler:restorationHandler]; -} - -+ (BOOL)application:(UIApplication *)app - openURL:(NSURL *)url - options:(NSDictionary *)options { - if ([Dropbox application:app openURL:url options:options]) { - return YES; - } - - if ([RNGoogleSignin application:app - openURL:url - sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey] - annotation:options[UIApplicationOpenURLOptionsAnnotationKey]]) { - return YES; - } - - // XXX At least twice we received bug reports about malfunctioning loadURL - // in the Jitsi Meet SDK while the Jitsi Meet app seemed to functioning as - // expected in our testing. But that was to be expected because the app does - // not exercise loadURL. In order to increase the test coverage of loadURL, - // channel Universal linking through loadURL. - if ([self loadURLInViews:url]) { - return YES; - } - - return [RCTLinkingManager application:app openURL:url options:options]; -} - -+ (BOOL)application:(UIApplication *)application - openURL:(NSURL *)url - sourceApplication:(NSString *)sourceApplication - annotation:(id)annotation { - return [self application:application openURL:url options:@{}]; +/** + * This gets called automagically when the program starts. + */ +__attribute__((constructor)) +static void initializeViewsMap() { + views = [NSMapTable strongToWeakObjectsMapTable]; } #pragma mark Initializers @@ -201,19 +81,26 @@ static NSMapTable *views; return self; } -#pragma mark API - /** - * Loads a specific `NSURL` which may identify a conference to join. If the - * specified `NSURL` is `nil` and the Welcome page is enabled, the Welcome page - * is displayed instead. + * Internal initialization: * - * @param url The `NSURL` to load which may identify a conference to join. + * - sets the background color + * - initializes the external API scope */ -- (void)loadURL:(NSURL *)url { - [self loadURLString:url ? url.absoluteString : nil]; +- (void)initWithXXX { + // Hook this JitsiMeetView into ExternalAPI. + externalAPIScope = [NSUUID UUID].UUIDString; + [views setObject:self forKey:externalAPIScope]; + + // Set a background color which is in accord with the JavaScript and Android + // parts of the application and causes less perceived visual flicker than + // the default background color. + self.backgroundColor + = [UIColor colorWithRed:.07f green:.07f blue:.07f alpha:1]; } +#pragma mark API + /** * Loads a specific URL which may identify a conference to join. The URL is * specified in the form of an `NSDictionary` of properties which (1) @@ -224,7 +111,7 @@ static NSMapTable *views; * * @param urlObject The URL to load which may identify a conference to join. */ -- (void)loadURLObject:(NSDictionary *)urlObject { +- (void)loadURL:(NSDictionary *_Nullable)urlObject { NSMutableDictionary *props = [[NSMutableDictionary alloc] init]; if (self.defaultURL) { @@ -242,14 +129,14 @@ static NSMapTable *views; props[@"url"] = urlObject; } - // XXX The method loadURLObject: is supposed to be imperative i.e. a second + // XXX The method loadURL: is supposed to be imperative i.e. a second // invocation with one and the same URL is expected to join the respective // conference again if the first invocation was followed by leaving the // conference. However, React and, respectively, // appProperties/initialProperties are declarative expressions i.e. one and // the same URL will not trigger an automatic re-render in the JavaScript // source code. The workaround implemented bellow introduces imperativeness - // in React Component props by defining a unique value per loadURLObject: + // in React Component props by defining a unique value per loadURL: // invocation. props[@"timestamp"] = @(mach_absolute_time()); @@ -257,8 +144,9 @@ static NSMapTable *views; // Update props with the new URL. rootView.appProperties = props; } else { + RCTBridge *bridge = [[JitsiMeet sharedInstance] getReactBridge]; rootView - = [[RCTRootView alloc] initWithBridge:bridgeWrapper.bridge + = [[RCTRootView alloc] initWithBridge:bridge moduleName:@"App" initialProperties:props]; rootView.backgroundColor = self.backgroundColor; @@ -272,28 +160,6 @@ static NSMapTable *views; } } -/** - * Loads a specific URL `NSString` which may identify a conference to - * join. If the specified URL `NSString` is `nil` and the Welcome page is - * enabled, the Welcome page is displayed instead. - * - * @param urlString The URL `NSString` to load which may identify a conference - * to join. - */ -- (void)loadURLString:(NSString *)urlString { - [self loadURLObject:urlString ? @{ @"url": urlString } : nil]; -} - -#pragma conferenceActivityType getter / setter - -+ (NSString *)conferenceActivityType { - return _conferenceActivityType; -} - -+ (void) setConferenceActivityType:(NSString *)conferenceActivityType { - _conferenceActivityType = conferenceActivityType; -} - #pragma pictureInPictureEnabled getter / setter - (void) setPictureInPictureEnabled:(BOOL)pictureInPictureEnabled { @@ -318,19 +184,7 @@ static NSMapTable *views; #pragma mark Private methods -/** - * Loads a specific `NSURL` in all existing `JitsiMeetView`s. - * - * @param url The `NSURL` to load in all existing `JitsiMeetView`s. - * @return `YES` if the specified `url` was submitted for loading in at least - * one `JitsiMeetView`; otherwise, `NO`. - */ -+ (BOOL)loadURLInViews:(NSURL *)url { - return - [self loadURLObjectInViews:url ? @{ @"url": url.absoluteString } : nil]; -} - -+ (BOOL)loadURLObjectInViews:(NSDictionary *)urlObject { ++ (BOOL)loadURLInViews:(NSDictionary *)urlObject { BOOL handled = NO; if (views) { @@ -339,7 +193,7 @@ static NSMapTable *views; = [self viewForExternalAPIScope:externalAPIScope]; if (view) { - [view loadURLObject:urlObject]; + [view loadURL:urlObject]; handled = YES; } } @@ -348,79 +202,8 @@ static NSMapTable *views; return handled; } -+ (NSDictionary *)conferenceURLFromUserActivity:(NSUserActivity *)userActivity { - NSString *activityType = userActivity.activityType; - - if ([activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { - // App was started by opening a URL in the browser - return @{ @"url" : 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)) { - return @{ - @"config": @{@"startAudioOnly":@(startAudioOnly)}, - @"url": url - }; - } - } else if (_conferenceActivityType && [activityType isEqualToString:_conferenceActivityType]) { - // App was started by continuing a registered NSUserActivity (SiriKit, Handoff, ...) - NSString *url; - - if ((url = userActivity.userInfo[@"url"])) { - return @{ @"url" : url }; - } - } - - return nil; -} - + (instancetype)viewForExternalAPIScope:(NSString *)externalAPIScope { return [views objectForKey:externalAPIScope]; } -/** - * Internal initialization: - * - * - sets the background color - * - creates the React bridge - * - loads the necessary custom fonts - * - registers a custom fatal error error handler for React - */ -- (void)initWithXXX { - static dispatch_once_t dispatchOncePredicate; - - dispatch_once(&dispatchOncePredicate, ^{ - // Initialize the static state of JitsiMeetView. - bridgeWrapper - = [[RCTBridgeWrapper alloc] initWithLaunchOptions:_launchOptions]; - views = [NSMapTable strongToWeakObjectsMapTable]; - - // Register a fatal error handler for React. - registerFatalErrorHandler(); - }); - - // Hook this JitsiMeetView into ExternalAPI. - externalAPIScope = [NSUUID UUID].UUIDString; - [views setObject:self forKey:externalAPIScope]; - - // Set a background color which is in accord with the JavaScript and Android - // parts of the application and causes less perceived visual flicker than - // the default background color. - self.backgroundColor - = [UIColor colorWithRed:.07f green:.07f blue:.07f alpha:1]; -} - @end diff --git a/ios/sdk/src/LaunchOptions.m b/ios/sdk/src/LaunchOptions.m deleted file mode 100644 index decf2dd68..000000000 --- a/ios/sdk/src/LaunchOptions.m +++ /dev/null @@ -1,56 +0,0 @@ -/* - * 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 "JitsiMeetView+Private.h" - -#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) { - initialURL = [JitsiMeetView conferenceURLFromUserActivity:userActivity]; - } - } - - resolve(initialURL != nil ? initialURL : (id)kCFNull); -} - -@end diff --git a/ios/sdk/src/RCTBridgeWrapper.h b/ios/sdk/src/RCTBridgeWrapper.h index 249377be0..995bd4fd2 100644 --- a/ios/sdk/src/RCTBridgeWrapper.h +++ b/ios/sdk/src/RCTBridgeWrapper.h @@ -34,6 +34,4 @@ @property (nonatomic, readonly, strong) RCTBridge *bridge; -- (instancetype)initWithLaunchOptions:(NSDictionary *)launchOptions; - @end diff --git a/ios/sdk/src/RCTBridgeWrapper.m b/ios/sdk/src/RCTBridgeWrapper.m index 4fc5b03aa..24ce96032 100644 --- a/ios/sdk/src/RCTBridgeWrapper.m +++ b/ios/sdk/src/RCTBridgeWrapper.m @@ -22,12 +22,12 @@ */ @implementation RCTBridgeWrapper -- (instancetype)initWithLaunchOptions:(NSDictionary *)launchOptions { +- (instancetype)init { self = [super init]; if (self) { _bridge = [[RCTBridge alloc] initWithDelegate:self - launchOptions:launchOptions]; + launchOptions:nil]; } return self; diff --git a/ios/sdk/src/ReactUtils.h b/ios/sdk/src/ReactUtils.h new file mode 100644 index 000000000..163f1ade7 --- /dev/null +++ b/ios/sdk/src/ReactUtils.h @@ -0,0 +1,22 @@ +/* + * Copyright @ 2019-present 8x8, Inc. + * + * 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. + */ + +#ifndef ReactUtils_h +#define ReactUtils_h + +void registerReactFatalErrorHandler(void); + +#endif /* ReactUtils_h */ diff --git a/ios/sdk/src/ReactUtils.m b/ios/sdk/src/ReactUtils.m new file mode 100644 index 000000000..52e3e3216 --- /dev/null +++ b/ios/sdk/src/ReactUtils.m @@ -0,0 +1,60 @@ +/* + * Copyright @ 2019-present 8x8, Inc. + * + * 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 "ReactUtils.h" + +/** + * A `RCTFatalHandler` implementation which swallows JavaScript errors. In the + * Release configuration, React Native will (intentionally) raise an unhandled + * `NSException` for an unhandled JavaScript error. This will effectively kill + * the application. `_RCTFatal` is suitable to be in accord with the Web i.e. + * not kill the application. + */ +RCTFatalHandler _RCTFatal = ^(NSError *error) { + id jsStackTrace = error.userInfo[RCTJSStackTraceKey]; + @try { + NSString *name + = [NSString stringWithFormat:@"%@: %@", + RCTFatalExceptionName, + error.localizedDescription]; + NSString *message + = RCTFormatError(error.localizedDescription, jsStackTrace, 75); + [NSException raise:name format:@"%@", message]; + } @catch (NSException *e) { + if (!jsStackTrace) { + @throw; + } + } +}; + +/** + * Helper function to register a fatal error handler for React. Our handler + * won't kill the process, it will swallow JS errors and print stack traces + * instead. + */ +void registerReactFatalErrorHandler() { +#if !DEBUG + // In the Release configuration, React Native will (intentionally) raise an + // unhandled `NSException` for an unhandled JavaScript error. This will + // effectively kill the application. In accord with the Web, do not kill the + // application. + if (!RCTGetFatalHandler()) { + RCTSetFatalHandler(_RCTFatal); + } +#endif +}