Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Share module #5904

Closed
wants to merge 49 commits into from
Closed
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
a76c6c0
Add Share module
deminoth Feb 13, 2016
424a665
change the way handling promise reject
deminoth Feb 13, 2016
5da16c7
buck file added
deminoth Feb 13, 2016
8c3bece
added buck target
deminoth Feb 13, 2016
1f5f0c0
add share module to flow file
deminoth Feb 13, 2016
fb5f6ea
Merge branch 'master' into share-module
deminoth Feb 13, 2016
f21d709
options handling fixed
deminoth Feb 13, 2016
8917788
Keyword "if" must be followed by whitespace.
deminoth Feb 14, 2016
767f914
Merge branch 'master' into share-module
deminoth Feb 15, 2016
de58c94
Merge branch 'master' into share-module
deminoth Apr 7, 2016
68804ca
refactoring
deminoth Apr 7, 2016
fb88c94
fix lint errors
deminoth Apr 7, 2016
9917041
Merge branch 'master' into share-module
deminoth Apr 7, 2016
72c6d50
improves platform check
deminoth Apr 8, 2016
2678aee
show chooser when could not get current activity
deminoth Apr 8, 2016
32847bb
logs when exception
deminoth Apr 8, 2016
0371750
fix CI errors
deminoth Apr 8, 2016
dfe1f63
change invariant module path
deminoth Apr 8, 2016
772523e
Merge branch 'share-module' of https://github.com/dobbit/react-native…
deminoth Apr 8, 2016
43209dc
update example (+3 squashed commits)
deminoth Apr 19, 2016
74e6966
uses legacy http library for robolectric 3.0 (see https://github.com/…
deminoth Apr 20, 2016
dc7fdaa
add a unit test for share module
deminoth Apr 20, 2016
7f39ae3
fix flow errors
deminoth Apr 20, 2016
08c29bd
Merge branch 'master' into share-module
deminoth Apr 20, 2016
375ee94
better invariant
deminoth Apr 26, 2016
851c4ea
Merge branch 'master' into share-module
deminoth May 2, 2016
4d6add8
indent fix
deminoth May 2, 2016
e398fd9
Fix up this pattern var React = require('react-native');
deminoth May 2, 2016
71a67fb
It's enough to use a mock Activity
deminoth May 2, 2016
4351eec
promise check and test case for call with an invalid content
deminoth May 2, 2016
92a793b
import missed
deminoth May 2, 2016
68ca817
replace `shadowOf` with `ShadowExtractor.extract`
deminoth May 2, 2016
d3e13ed
Merge branch 'master' into share-module
deminoth May 6, 2016
ae290f0
add jsr dep for @Nullable annotation
deminoth May 6, 2016
918aaf6
Merge branch 'master' into share-module
deminoth Jul 11, 2016
f4d7d3a
replace `Intent.createChooser` with custom share dialog
deminoth Jul 12, 2016
29b5e3b
android integration tests
deminoth Jul 13, 2016
db99ac4
lint fix
deminoth Jul 13, 2016
d0f6fb8
Block launching activity for next test cases
deminoth Jul 13, 2016
bc14456
back to `Intent.createChooser`
deminoth Jul 14, 2016
a404f49
dep for the `@Nullable` annotation
deminoth Jul 14, 2016
9c896e5
documentation update
deminoth Jul 14, 2016
de1ab8f
Merge branch 'master' into share-module
deminoth Jul 15, 2016
5127f30
move example file (#2f73ca8)
deminoth Jul 15, 2016
0834fc7
Merge branch 'master' into share-module
deminoth Jul 18, 2016
994cda0
Merge branch 'master' into share-module
deminoth Jul 22, 2016
2b52ff5
fix unit tests
deminoth Jul 22, 2016
dc1c57c
do not log the error if we're already rejecting
deminoth Jul 25, 2016
94023f9
`url` in `content` prop is iOS only now
deminoth Jul 25, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions Examples/UIExplorer/js/ShareExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/**
* The examples provided by Facebook are for non-commercial testing and
* evaluation purposes only.
*
* Facebook reserves all rights not expressly granted.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* @flow
*/
'use strict';

var React = require('react');
var ReactNative = require('react-native');
var {
StyleSheet,
View,
Text,
TouchableHighlight,
Share,
} = ReactNative;

exports.framework = 'React';
exports.title = 'Share';
exports.description = 'Share data with other Apps.';
exports.examples = [{
title: 'Share Text Content',
render() {
return <ShareMessageExample />;
}
}];

class ShareMessageExample extends React.Component {
_shareMessage: Function;
_shareText: Function;
_showResult: Function;
state: any;

constructor(props) {
super(props);

this._shareMessage = this._shareMessage.bind(this);
this._shareText = this._shareText.bind(this);
this._showResult = this._showResult.bind(this);

this.state = {
result: ''
};
}

render() {
return (
<View>
<TouchableHighlight style={styles.wrapper}
onPress={this._shareMessage}>
<View style={styles.button}>
<Text>Click to share message</Text>
</View>
</TouchableHighlight>
<TouchableHighlight style={styles.wrapper}
onPress={this._shareText}>
<View style={styles.button}>
<Text>Click to share message, URL and title</Text>
</View>
</TouchableHighlight>
<Text>{this.state.result}</Text>
</View>
);
}

_shareMessage() {
Share.share({
message: 'React Native | A framework for building native apps using React'
})
.then(this._showResult)
.catch((error) => this.setState({result: 'error: ' + error.message}));
}

_shareText() {
Share.share({
message: 'A framework for building native apps using React',
url: 'http://facebook.github.io/react-native/',
title: 'React Native'
}, {
dialogTitle: 'Share React Native website',
excludedActivityTypes: [
'com.apple.UIKit.activity.PostToTwitter'
],
tintColor: 'green'
})
.then(this._showResult)
.catch((error) => this.setState({result: 'error: ' + error.message}));
}

_showResult(result) {
if (result.action === Share.sharedAction) {
if (result.activityType) {
this.setState({result: 'shared with an activityType: ' + result.activityType});
} else {
this.setState({result: 'shared'});
}
} else if (result.action === Share.dismissedAction) {
this.setState({result: 'dismissed'});
}
}

}


var styles = StyleSheet.create({
wrapper: {
borderRadius: 5,
marginBottom: 5,
},
button: {
backgroundColor: '#eeeeee',
padding: 10,
},
});
4 changes: 4 additions & 0 deletions Examples/UIExplorer/js/UIExplorerList.android.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,10 @@ const APIExamples = [
key: 'PointerEventsExample',
module: require('./PointerEventsExample'),
},
{
key: 'ShareExample',
module: require('./ShareExample'),
},
{
key: 'TimePickerAndroidExample',
module: require('./TimePickerAndroidExample'),
Expand Down
4 changes: 4 additions & 0 deletions Examples/UIExplorer/js/UIExplorerList.ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,10 @@ const APIExamples: Array<UIExplorerExample> = [
key: 'RCTRootViewIOSExample',
module: require('./RCTRootViewIOSExample'),
},
{
key: 'ShareExample',
module: require('./ShareExample'),
},
{
key: 'SnapshotExample',
module: require('./SnapshotExample'),
Expand Down
114 changes: 114 additions & 0 deletions Libraries/Share/Share.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

* 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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

* of patent rights can be found in the PATENTS file in the same directory.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

*

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

* @providesModule Share

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

* @flow
*/
'use strict';

const Platform = require('Platform');
const {
ActionSheetManager,
ShareModule
} = require('NativeModules');
const invariant = require('fbjs/lib/invariant');
const processColor = require('processColor');

type Content = { title?: string, message: string } | { title?: string, url: string };
type Options = { dialogTitle?: string, excludeActivityTypes?: Array<string>, tintColor?: string };

class Share {

/**
* Open a dialog to share text content.
*
* In iOS, Returns a Promise which will be invoked an object containing `action`, `activityType`.
* If the user dismissed the dialog, the Promise will still be resolved with action being `Share.dismissedAction`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about a isCancelled property instead? cc @grabbou we discussed about this.

Copy link
Contributor Author

@deminoth deminoth Jul 25, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fyi, this pattern(resolve with constant action) comes from DatePickerAndroid and TimePickerAndroid.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@deminoth Interesting. We've so many patterns. Let's keep this for now then.

* and all the other keys being undefined.
*
* In Android, Returns a Promise which always be resolved with action being `Share.sharedAction`.
*
* ### Content
*
* - `message` - a message to share
* - `url` - an URL to share. In Android, this will overwrite message
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better just to ignore this prop on Android instead of overwriting message. It becomes more explicit then, and the user can decide what to do. For example, I would usually prefer to combine the message and url into a single text on Android.

* - `title` - title of the message

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially unsafe get/set usage Getters and setters with side effects are potentially unsafe and disabled by default. You may opt-in to using them anyway by putting unsafe.enable_getters_and_setters=true into the [options] section of your .flowconfig.

*
* At least one of URL and message is required.
*
* ### Options
*
* #### iOS
*
* - `excludedActivityTypes`
* - `tintColor`
*
* #### Android
*
* - `dialogTitle`
*
*/
static share(content: Content, options: Options = {}): Promise<Object> {
invariant(
typeof content === 'object' && content !== null,
'Content must a valid object'
);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no-trailing-spaces: Trailing spaces not allowed.

invariant(
typeof content.url === 'string' || typeof content.message === 'string',
'At least one of URL and message is required'
);
invariant(
typeof options === 'object' && options !== null,
'Options must be a valid object'
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this invariant should go under android specific code since iOS does not even accept such parameter.


if (Platform.OS === 'android') {
invariant(
!content.title || typeof content.title === 'string',
'Invalid title: title should be a string.'
);
return ShareModule.share(content, options.dialogTitle);
} else if (Platform.OS === 'ios') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we skip this else if ? Should reduce the indentation. I am not sure if we need to warn about unsupported platform given we don't do it anywhere else (e.g. https://github.com/facebook/react-native/blob/master/Libraries/Linking/Linking.js#L123)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grabbou this else if was a request of @andreicoman11. I guess it's for RN on Windows platform and furthermore.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok - then probably something to remember for other files :) thanks.

return new Promise((resolve, reject) => {
ActionSheetManager.showShareActionSheetWithOptions(
{...content, ...options, tintColor: processColor(options.tintColor)},
(error) => reject(error),
(success, activityType) => {
if (success) {
resolve({
'action': 'sharedAction',
'activityType': activityType
});
} else {
resolve({
'action': 'dismissedAction'
});
}
}
);
});
} else {
console.warn('Share.share is not supported on this platform');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to both console.warn and reject. If the user is not handling the rejection, it'll show a redbox and if he is already handling the rejection the warning is unnecessary.

return Promise.reject(new Error('Unsupported platform'));
}
}

/**
* The content was successfully shared.
*/
static get sharedAction() { return 'sharedAction'; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just have plain strings instead of constants.


/**
* The dialog has been dismissed.
* @platform ios
*/
static get dismissedAction() { return 'dismissedAction'; }

}

module.exports = Share;
1 change: 1 addition & 0 deletions Libraries/react-native/react-native.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ const ReactNative = {
get PixelRatio() { return require('PixelRatio'); },
get PushNotificationIOS() { return require('PushNotificationIOS'); },
get Settings() { return require('Settings'); },
get Share() { return require('Share'); },
get StatusBarIOS() { return require('StatusBarIOS'); },
get StyleSheet() { return require('StyleSheet'); },
get Systrace() { return require('Systrace'); },
Expand Down
1 change: 1 addition & 0 deletions Libraries/react-native/react-native.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ var ReactNative = {
PixelRatio: require('PixelRatio'),
PushNotificationIOS: require('PushNotificationIOS'),
Settings: require('Settings'),
Share: require('Share'),
StatusBarIOS: require('StatusBarIOS'),
StyleSheet: require('StyleSheet'),
Systrace: require('Systrace'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ deps = [
react_native_target('java/com/facebook/react/common:common'),
react_native_target('java/com/facebook/react/modules/core:core'),
react_native_target('java/com/facebook/react/modules/datepicker:datepicker'),
react_native_target('java/com/facebook/react/modules/share:share'),
react_native_target('java/com/facebook/react/modules/systeminfo:systeminfo'),
react_native_target('java/com/facebook/react/modules/timepicker:timepicker'),
react_native_target('java/com/facebook/react/touch:touch'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* Copyright (c) 2014-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.
*/

package com.facebook.react.tests;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Instrumentation.ActivityMonitor;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentFilter.MalformedMimeTypeException;
import android.support.v4.app.DialogFragment;

import com.facebook.react.bridge.BaseJavaModule;
import com.facebook.react.testing.ReactInstanceSpecForTest;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.modules.share.ShareModule;
import com.facebook.react.testing.ReactAppInstrumentationTestCase;

/**
* Test case for {@link ShareModule}.
*/
public class ShareTestCase extends ReactAppInstrumentationTestCase {

private static interface ShareTestModule extends JavaScriptModule {
public void showShareDialog(WritableMap content, WritableMap options);
}

private static class ShareRecordingModule extends BaseJavaModule {

private int mOpened = 0;
private int mErrors = 0;

@Override
public String getName() {
return "ShareRecordingModule";
}

@ReactMethod
public void recordOpened() {
mOpened++;
}

@ReactMethod
public void recordError() {
mErrors++;
}

public int getOpened() {
return mOpened;
}

public int getErrors() {
return mErrors;
}

}

final ShareRecordingModule mRecordingModule = new ShareRecordingModule();

@Override
protected ReactInstanceSpecForTest createReactInstanceSpecForTest() {
return super.createReactInstanceSpecForTest()
.addNativeModule(mRecordingModule)
.addJSModule(ShareTestModule.class);
}

@Override
protected String getReactApplicationKeyUnderTest() {
return "ShareTestApp";
}

private ShareTestModule getTestModule() {
return getReactContext().getCatalystInstance().getJSModule(ShareTestModule.class);
}

public void testShowBasicShareDialog() {
final WritableMap content = new WritableNativeMap();
content.putString("message", "Hello, ReactNative!");
final WritableMap options = new WritableNativeMap();

IntentFilter intentFilter = new IntentFilter(Intent.ACTION_CHOOSER);
intentFilter.addCategory(Intent.CATEGORY_DEFAULT);
ActivityMonitor monitor = getInstrumentation().addMonitor(intentFilter, null, true);

getTestModule().showShareDialog(content, options);

waitForBridgeAndUIIdle();
getInstrumentation().waitForIdleSync();

assertEquals(1, monitor.getHits());
assertEquals(1, mRecordingModule.getOpened());
assertEquals(0, mRecordingModule.getErrors());

}

}
Loading