diff --git a/Libraries/Components/Intent/IntentAndroid.android.js b/Libraries/Components/Intent/IntentAndroid.android.js index cebe69948209e3..48877dd6b1eef0 100644 --- a/Libraries/Components/Intent/IntentAndroid.android.js +++ b/Libraries/Components/Intent/IntentAndroid.android.js @@ -7,13 +7,16 @@ * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule IntentAndroid + * @flow */ 'use strict'; -var IntentAndroidModule = require('NativeModules').IntentAndroid; +var Linking = require('Linking'); var invariant = require('invariant'); /** + * NOTE: `IntentAndroid` is being deprecated. Use `Linking` instead. + * * `IntentAndroid` gives you a general interface to handle external links. * * ### Basic Usage @@ -89,10 +92,12 @@ class IntentAndroid { * If you're passing in a non-http(s) URL, it's best to check {@code canOpenURL} first. * * NOTE: For web URLs, the protocol ("http://", "https://") must be set accordingly! + * + * @deprecated */ static openURL(url: string) { - this._validateURL(url); - IntentAndroidModule.openURL(url); + console.warn('"IntentAndroid.openURL" is deprecated. Use the promise based "Linking.openURL" instead.'); + Linking.openURL(url); } /** @@ -104,14 +109,16 @@ class IntentAndroid { * NOTE: For web URLs, the protocol ("http://", "https://") must be set accordingly! * * @param URL the URL to open + * + * @deprecated */ static canOpenURL(url: string, callback: Function) { - this._validateURL(url); + console.warn('"IntentAndroid.canOpenURL" is deprecated. Use the promise based "Linking.canOpenURL" instead.'); invariant( typeof callback === 'function', 'A valid callback function is required' ); - IntentAndroidModule.canOpenURL(url, callback); + Linking.canOpenURL(url).then(callback); } /** @@ -119,24 +126,16 @@ class IntentAndroid { * it will give the link url, otherwise it will give `null` * * Refer http://developer.android.com/training/app-indexing/deep-linking.html#handling-intents + * + * @deprecated */ static getInitialURL(callback: Function) { + console.warn('"IntentAndroid.getInitialURL" is deprecated. Use the promise based "Linking.getInitialURL" instead.'); invariant( typeof callback === 'function', 'A valid callback function is required' ); - IntentAndroidModule.getInitialURL(callback); - } - - static _validateURL(url: string) { - invariant( - typeof url === 'string', - 'Invalid URL: should be a string. Was: ' + url - ); - invariant( - url, - 'Invalid URL: cannot be empty' - ); + Linking.getInitialURL().then(callback); } } diff --git a/Libraries/Linking/Linking.js b/Libraries/Linking/Linking.js new file mode 100644 index 00000000000000..97282ab663918b --- /dev/null +++ b/Libraries/Linking/Linking.js @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule Linking + * @flow + */ +'use strict'; + +const Platform = require('Platform'); +const RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +const { + IntentAndroid, + LinkingManager: LinkingManagerIOS +} = require('NativeModules'); +const LinkingManager = Platform.OS === 'android' ? IntentAndroid : LinkingManagerIOS; +const invariant = require('invariant'); +const Map = require('Map'); + +const _notifHandlers = new Map(); + +const DEVICE_NOTIF_EVENT = 'openURL'; + +/** + * `Linking` gives you a general interface to interact with both incoming + * and outgoing app links. + * + * ### Basic Usage + * + * #### Handling deep links + * + * If your app was launched from an external url registered to your app you can + * access and handle it from any component you want with + * + * ``` + * componentDidMount() { + * var url = Linking.getInitialURL().then(url) => { + * if (url) { + * console.log('Initial url is: ' + url); + * } + * }).catch(err => console.error('An error occurred', err)); + * } + * ``` + * + * NOTE: For instructions on how to add support for deep linking on Android, + * refer [Enabling Deep Links for App Content - Add Intent Filters for Your Deep Links](http://developer.android.com/training/app-indexing/deep-linking.html#adding-filters). + * + * NOTE: For iOS, in case you also want to listen to incoming app links during your app's + * execution you'll need to add the following lines to you `*AppDelegate.m`: + * + * ``` + * - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url + * sourceApplication:(NSString *)sourceApplication annotation:(id)annotation + * { + * return [LinkingManager application:application openURL:url + * sourceApplication:sourceApplication annotation:annotation]; + * } + * + * // Only if your app is using [Universal Links](https://developer.apple.com/library/prerelease/ios/documentation/General/Conceptual/AppSearch/UniversalLinks.html). + * - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity + * restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler + * { + * return [LinkingManager application:application + * continueUserActivity:userActivity + * restorationHandler:restorationHandler]; + * } + * + * ``` + * + * And then on your React component you'll be able to listen to the events on + * `Linking` as follows + * + * ``` + * componentDidMount() { + * Linking.addEventListener('url', this._handleOpenURL); + * }, + * componentWillUnmount() { + * Linking.removeEventListener('url', this._handleOpenURL); + * }, + * _handleOpenURL(event) { + * console.log(event.url); + * } + * ``` + * Note that this is only supported on iOS. + * + * #### Opening external links + * + * To start the corresponding activity for a link (web URL, email, contact etc.), call + * + * ``` + * Linking.openURL(url).catch(err => console.error('An error occurred', err)); + * ``` + * + * If you want to check if any installed app can handle a given URL beforehand you can call + * ``` + * Linking.canOpenURL(url).then(supported => { + * if (!supported) { + * console.log('Can\'t handle url: ' + url); + * } else { + * return Linking.openURL(url); + * } + * }).catch(err => console.error('An error occurred', err)); + * ``` + */ +class Linking { + /** + * Add a handler to Linking changes by listening to the `url` event type + * and providing the handler + * + * @platform ios + */ + static addEventListener(type: string, handler: Function) { + if (Platform.OS === 'android') { + console.warn('Linking.addEventListener is not supported on Android'); + } else { + invariant( + type === 'url', + 'Linking only supports `url` events' + ); + var listener = RCTDeviceEventEmitter.addListener( + DEVICE_NOTIF_EVENT, + handler + ); + _notifHandlers.set(handler, listener); + } + } + + /** + * Remove a handler by passing the `url` event type and the handler + * + * @platform ios + */ + static removeEventListener(type: string, handler: Function ) { + if (Platform.OS === 'android') { + console.warn('Linking.removeEventListener is not supported on Android'); + } else { + invariant( + type === 'url', + 'Linking only supports `url` events' + ); + var listener = _notifHandlers.get(handler); + if (!listener) { + return; + } + listener.remove(); + _notifHandlers.delete(handler); + } + } + + /** + * Try to open the given `url` with any of the installed apps. + * + * You can use other URLs, like a location (e.g. "geo:37.484847,-122.148386"), a contact, + * or any other URL that can be opened with the installed apps. + * + * NOTE: This method will fail if the system doesn't know how to open the specified URL. + * If you're passing in a non-http(s) URL, it's best to check {@code canOpenURL} first. + * + * NOTE: For web URLs, the protocol ("http://", "https://") must be set accordingly! + */ + static openURL(url: string): Promise { + this._validateURL(url); + return LinkingManager.openURL(url); + } + + /** + * Determine whether or not an installed app can handle a given URL. + * + * NOTE: For web URLs, the protocol ("http://", "https://") must be set accordingly! + * + * NOTE: As of iOS 9, your app needs to provide the `LSApplicationQueriesSchemes` key + * inside `Info.plist`. + * + * @param URL the URL to open + */ + static canOpenURL(url: string): Promise { + this._validateURL(url); + return LinkingManager.canOpenURL(url); + } + + /** + * If the app launch was triggered by an app link with, + * it will give the link url, otherwise it will give `null` + * + * NOTE: To support deep linking on Android, refer http://developer.android.com/training/app-indexing/deep-linking.html#handling-intents + */ + static getInitialURL(): Promise { + if (Platform.OS === 'android') { + return IntentAndroid.getInitialURL(); + } else { + return Promise.resolve(LinkingManagerIOS.initialURL); + } + } + + static _validateURL(url: string) { + invariant( + typeof url === 'string', + 'Invalid URL: should be a string. Was: ' + url + ); + invariant( + url, + 'Invalid URL: cannot be empty' + ); + } +} + +module.exports = Linking; diff --git a/Libraries/LinkingIOS/LinkingIOS.js b/Libraries/LinkingIOS/LinkingIOS.js index a4832f2683040d..d223b0d8b729a1 100644 --- a/Libraries/LinkingIOS/LinkingIOS.js +++ b/Libraries/LinkingIOS/LinkingIOS.js @@ -11,17 +11,15 @@ */ 'use strict'; -var RCTDeviceEventEmitter = require('RCTDeviceEventEmitter'); +var Linking = require('Linking'); var RCTLinkingManager = require('NativeModules').LinkingManager; var invariant = require('invariant'); -var _notifHandlers = new Map(); -var _initialURL = RCTLinkingManager && - RCTLinkingManager.initialURL; - -var DEVICE_NOTIF_EVENT = 'openURL'; +var _initialURL = RCTLinkingManager.initialURL; /** + * NOTE: `LinkingIOS` is being deprecated. Use `Linking` instead. + * * `LinkingIOS` gives you a general interface to interact with both incoming * and outgoing app links. * @@ -98,44 +96,32 @@ class LinkingIOS { /** * Add a handler to LinkingIOS changes by listening to the `url` event type * and providing the handler + * + * @deprecated */ static addEventListener(type: string, handler: Function) { - invariant( - type === 'url', - 'LinkingIOS only supports `url` events' - ); - var listener = RCTDeviceEventEmitter.addListener( - DEVICE_NOTIF_EVENT, - handler - ); - _notifHandlers.set(handler, listener); + console.warn('"LinkingIOS.addEventListener" is deprecated. Use "Linking.addEventListener" instead.'); + Linking.addEventListener(type, handler); } /** * Remove a handler by passing the `url` event type and the handler + * + * @deprecated */ static removeEventListener(type: string, handler: Function ) { - invariant( - type === 'url', - 'LinkingIOS only supports `url` events' - ); - var listener = _notifHandlers.get(handler); - if (!listener) { - return; - } - listener.remove(); - _notifHandlers.delete(handler); + console.warn('"LinkingIOS.removeEventListener" is deprecated. Use "Linking.removeEventListener" instead.'); + Linking.removeEventListener(type, handler); } /** * Try to open the given `url` with any of the installed apps. + * + * @deprecated */ static openURL(url: string) { - invariant( - typeof url === 'string', - 'Invalid url: should be a string' - ); - RCTLinkingManager.openURL(url); + console.warn('"LinkingIOS.openURL" is deprecated. Use the promise based "Linking.openURL" instead.'); + Linking.openURL(url); } /** @@ -144,24 +130,26 @@ class LinkingIOS { * * NOTE: As of iOS 9, your app needs to provide the `LSApplicationQueriesSchemes` key * inside `Info.plist`. + * + * @deprecated */ static canOpenURL(url: string, callback: Function) { - invariant( - typeof url === 'string', - 'Invalid url: should be a string' - ); + console.warn('"LinkingIOS.canOpenURL" is deprecated. Use the promise based "Linking.canOpenURL" instead.'); invariant( typeof callback === 'function', 'A valid callback function is required' ); - RCTLinkingManager.canOpenURL(url, callback); + Linking.canOpenURL(url).then(callback); } /** * If the app launch was triggered by an app link, it will pop the link url, * otherwise it will return `null` + * + * @deprecated */ static popInitialURL(): ?string { + console.warn('"LinkingIOS.popInitialURL" is deprecated. Use the promise based "Linking.getInitialURL" instead.'); var initialURL = _initialURL; _initialURL = null; return initialURL; diff --git a/Libraries/LinkingIOS/RCTLinkingManager.m b/Libraries/LinkingIOS/RCTLinkingManager.m index 749d2c27ad3095..d720631674c939 100644 --- a/Libraries/LinkingIOS/RCTLinkingManager.m +++ b/Libraries/LinkingIOS/RCTLinkingManager.m @@ -73,26 +73,30 @@ - (void)handleOpenURLNotification:(NSNotification *)notification body:notification.userInfo]; } -RCT_EXPORT_METHOD(openURL:(NSURL *)URL) +RCT_EXPORT_METHOD(openURL:(NSURL *)URL + resolve:(RCTPromiseResolveBlock)resolve + reject:(__unused RCTPromiseRejectBlock)reject) { - // TODO: we should really return success/failure via a callback here + // TODO: we should really report success/failure via the promise here // Doesn't really matter what thread we call this on since it exits the app [RCTSharedApplication() openURL:URL]; + resolve(@[@YES]); } RCT_EXPORT_METHOD(canOpenURL:(NSURL *)URL - callback:(RCTResponseSenderBlock)callback) + resolve:(RCTPromiseResolveBlock)resolve + reject:(__unused RCTPromiseRejectBlock)reject) { if (RCTRunningInAppExtension()) { // Technically Today widgets can open urls, but supporting that would require // a reference to the NSExtensionContext - callback(@[@NO]); + resolve(@[@NO]); return; } // This can be expensive, so we deliberately don't call on main thread BOOL canOpen = [RCTSharedApplication() canOpenURL:URL]; - callback(@[@(canOpen)]); + resolve(@[@(canOpen)]); } @end diff --git a/Libraries/react-native/react-native.js b/Libraries/react-native/react-native.js index 1768b5707ccdb0..e731e27ac52dc6 100644 --- a/Libraries/react-native/react-native.js +++ b/Libraries/react-native/react-native.js @@ -72,6 +72,7 @@ var ReactNative = { get IntentAndroid() { return require('IntentAndroid'); }, get InteractionManager() { return require('InteractionManager'); }, get LayoutAnimation() { return require('LayoutAnimation'); }, + get Linking() { return require('Linking'); }, get LinkingIOS() { return require('LinkingIOS'); }, get NetInfo() { return require('NetInfo'); }, get PanResponder() { return require('PanResponder'); }, diff --git a/Libraries/react-native/react-native.js.flow b/Libraries/react-native/react-native.js.flow index 197e9095664e9c..6e761b70de3887 100644 --- a/Libraries/react-native/react-native.js.flow +++ b/Libraries/react-native/react-native.js.flow @@ -84,6 +84,7 @@ var ReactNative = Object.assign(Object.create(require('React')), { IntentAndroid: require('IntentAndroid'), InteractionManager: require('InteractionManager'), LayoutAnimation: require('LayoutAnimation'), + Linking: require('Linking'), LinkingIOS: require('LinkingIOS'), NetInfo: require('NetInfo'), PanResponder: require('PanResponder'), diff --git a/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.java b/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.java index ce4681264193da..8f57f570ea7337 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.java +++ b/ReactAndroid/src/main/java/com/facebook/react/modules/intent/IntentModule.java @@ -13,8 +13,8 @@ import android.content.Intent; import android.net.Uri; -import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; @@ -36,10 +36,10 @@ public String getName() { /** * Return the URL the activity was started with * - * @param callback a callback which is called with the initial URL + * @param promise a promise which is resolved with the initial URL */ @ReactMethod - public void getInitialURL(Callback callback) { + public void getInitialURL(Promise promise) { try { Activity currentActivity = getCurrentActivity(); String initialURL = null; @@ -54,10 +54,10 @@ public void getInitialURL(Callback callback) { } } - callback.invoke(initialURL); + promise.resolve(initialURL); } catch (Exception e) { - throw new JSApplicationIllegalArgumentException( - "Could not get the initial URL : " + e.getMessage()); + promise.reject(new JSApplicationIllegalArgumentException( + "Could not get the initial URL : " + e.getMessage())); } } @@ -70,9 +70,10 @@ public void getInitialURL(Callback callback) { * @param url the URL to open */ @ReactMethod - public void openURL(String url) { + public void openURL(String url, Promise promise) { if (url == null || url.isEmpty()) { - throw new JSApplicationIllegalArgumentException("Invalid URL: " + url); + promise.reject(new JSApplicationIllegalArgumentException("Invalid URL: " + url)); + return; } try { @@ -85,9 +86,11 @@ public void openURL(String url) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getReactApplicationContext().startActivity(intent); } + + promise.resolve(true); } catch (Exception e) { - throw new JSApplicationIllegalArgumentException( - "Could not open URL '" + url + "': " + e.getMessage()); + promise.reject(new JSApplicationIllegalArgumentException( + "Could not open URL '" + url + "': " + e.getMessage())); } } @@ -95,12 +98,13 @@ public void openURL(String url) { * Determine whether or not an installed app can handle a given URL. * * @param url the URL to open - * @param callback a callback that is always called with a boolean argument + * @param promise a promise that is always resolved with a boolean argument */ @ReactMethod - public void canOpenURL(String url, Callback callback) { + public void canOpenURL(String url, Promise promise) { if (url == null || url.isEmpty()) { - throw new JSApplicationIllegalArgumentException("Invalid URL: " + url); + promise.reject(new JSApplicationIllegalArgumentException("Invalid URL: " + url)); + return; } try { @@ -110,10 +114,10 @@ public void canOpenURL(String url, Callback callback) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); boolean canOpen = intent.resolveActivity(getReactApplicationContext().getPackageManager()) != null; - callback.invoke(canOpen); + promise.resolve(canOpen); } catch (Exception e) { - throw new JSApplicationIllegalArgumentException( - "Could not check if URL '" + url + "' can be opened: " + e.getMessage()); + promise.reject(new JSApplicationIllegalArgumentException( + "Could not check if URL '" + url + "' can be opened: " + e.getMessage())); } } }