Enable SAML authentication (#325)

* enable saml authentication

* Fix unit tests

* Fix android cookies

* Feedback review
This commit is contained in:
enahum 2017-03-09 14:07:18 -03:00 committed by GitHub
parent 13a1f8a9a8
commit c13a436d18
24 changed files with 390 additions and 39 deletions

View file

@ -143,6 +143,7 @@ android {
}
dependencies {
compile project(':react-native-cookies')
compile project(':react-native-vector-icons')
compile project(':react-native-svg')
compile fileTree(dir: "libs", include: ["*.jar"])

View file

@ -4,6 +4,7 @@ import android.app.Application;
import android.util.Log;
import com.facebook.react.ReactApplication;
import com.psykar.cookiemanager.CookieManagerPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.horcrux.svg.RNSvgPackage;
import com.facebook.react.ReactInstanceManager;
@ -27,6 +28,7 @@ public class MainApplication extends Application implements ReactApplication {
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new CookieManagerPackage(),
new VectorIconsPackage(),
new RNSvgPackage()
);

View file

@ -1,4 +1,6 @@
rootProject.name = 'Mattermost'
include ':react-native-cookies'
project(':react-native-cookies').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-cookies/android')
include ':react-native-vector-icons'
project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')

View file

@ -31,6 +31,15 @@ export function goToLogin() {
};
}
export function goToLoginOptions() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.LoginOptions
}, getState);
};
}
export function goToMfa() {
return async (dispatch, getState) => {
dispatch({
@ -40,6 +49,15 @@ export function goToMfa() {
};
}
export function goToSaml() {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_PUSH,
route: Routes.Saml
}, getState);
};
}
export function goToLoadTeam() {
return async (dispatch, getState) => {
dispatch({

View file

@ -20,7 +20,7 @@ export function loadStorage() {
const actions = [
{type: GeneralTypes.RECEIVED_APP_CREDENTIALS, data: credentials},
{type: GeneralTypes.RECEIVED_SERVER_VERSION, data: serverVersion},
{type: TeamsTypes.SELECT_TEAM, data: currentTeamId},
{type: TeamsTypes.SELECT_TEAM, data: (currentTeamId || '')},
{type: ChannelTypes.SELECT_CHANNEL, data: currentChannelId}
];

View file

@ -2,6 +2,7 @@
// See License.txt for license information.
import {ViewTypes} from 'app/constants';
import {goToLogin, goToLoginOptions} from 'app/actions/navigation';
export function handleServerUrlChanged(serverUrl) {
return async (dispatch, getState) => {
@ -12,6 +13,26 @@ export function handleServerUrlChanged(serverUrl) {
};
}
export function handleLoginOptions() {
return async (dispatch, getState) => {
const {config, license} = getState().entities.general;
const samlEnabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true';
let options = 0;
if (samlEnabled) {
options += 1;
}
if (options) {
await goToLoginOptions()(dispatch, getState);
} else {
await goToLogin()(dispatch, getState);
}
};
}
export default {
handleServerUrlChanged
handleServerUrlChanged,
handleLoginOptions
};

View file

@ -11,12 +11,14 @@ import {
CreateChannel,
LoadTeam,
Login,
LoginOptions,
Mfa,
MoreChannels,
MoreDirectMessages,
OptionsModal,
RightMenuDrawer,
Root,
Saml,
Search,
SelectServer,
SelectTeam,
@ -99,6 +101,14 @@ export const Routes = {
title: {id: 'mobile.routes.login', defaultMessage: 'Login'}
}
},
LoginOptions: {
key: 'LoginOptions',
transition: RouteTransitions.Horizontal,
component: LoginOptions,
navigationProps: {
title: {id: 'mobile.routes.loginOptions', defaultMessage: 'Login Chooser'}
}
},
Mfa: {
key: 'Mfa',
transition: RouteTransitions.Horizontal,
@ -136,6 +146,14 @@ export const Routes = {
key: 'Root',
component: Root
},
Saml: {
key: 'Saml',
transition: RouteTransitions.Horizontal,
component: Saml,
navigationProps: {
title: {id: 'mobile.routes.saml', defaultMessage: 'Single SignOn'}
}
},
Search: {
key: 'Search',
transition: RouteTransitions.Horizontal,

View file

@ -10,6 +10,7 @@ import ChannelAddMembers from './channel_add_members';
import CreateChannel from './create_channel';
import LoadTeam from './load_team';
import Login from './login/login_container.js';
import LoginOptions from './login_options';
import Mfa from './mfa';
import MoreChannels from './more_channels';
import MoreDirectMessages from './more_dms';
@ -21,6 +22,7 @@ import SelectServer from './select_server/select_server_container.js';
import SelectTeam from './select_team/select_team_container.js';
import Thread from './thread';
import UserProfile from './user_profile';
import Saml from './saml';
module.exports = {
AccountSettings,
@ -32,6 +34,7 @@ module.exports = {
CreateChannel,
LoadTeam,
Login,
LoginOptions,
Mfa,
MoreChannels,
MoreDirectMessages,
@ -42,5 +45,6 @@ module.exports = {
SelectServer,
SelectTeam,
Thread,
UserProfile
UserProfile,
Saml
};

View file

@ -16,7 +16,6 @@ import Button from 'react-native-button';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import Loading from 'app/components/loading';
import {GlobalStyles} from 'app/styles';
import logo from 'assets/images/logo.png';
@ -40,9 +39,7 @@ class Login extends Component {
loginId: PropTypes.string.isRequired,
password: PropTypes.string.isRequired,
checkMfaRequest: PropTypes.object.isRequired,
loginRequest: PropTypes.object.isRequired,
configRequest: PropTypes.object.isRequired,
licenseRequest: PropTypes.object.isRequired
loginRequest: PropTypes.object.isRequired
};
constructor(props) {
@ -205,10 +202,6 @@ class Login extends Component {
};
render() {
if (this.props.configRequest.status === RequestStatus.STARTED || this.props.licenseRequest.status === RequestStatus.STARTED) {
return <Loading/>;
}
return (
<KeyboardLayout
behavior='padding'

View file

@ -12,15 +12,12 @@ import {checkMfa, login} from 'service/actions/users';
import Login from './login.js';
function mapStateToProps(state) {
const {config, license} = state.entities.general;
const {config: configRequest, license: licenseRequest} = state.requests.general;
const {checkMfa: checkMfaRequest, login: loginRequest} = state.requests.users;
const {config, license} = state.entities.general;
return {
...state.views.login,
checkMfaRequest,
loginRequest,
configRequest,
licenseRequest,
config,
license
};

View file

@ -0,0 +1,29 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {goToLogin, goToSaml} from 'app/actions/navigation';
import LoginOptions from './login_options';
function mapStateToProps(state) {
const {config, license} = state.entities.general;
return {
config,
license
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToLogin,
goToSaml
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(LoginOptions);

View file

@ -0,0 +1,93 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent, PropTypes} from 'react';
import {
Image,
Text,
View
} from 'react-native';
import Button from 'react-native-button';
import FormattedText from 'app/components/formatted_text';
import {GlobalStyles} from 'app/styles';
import logo from 'assets/images/logo.png';
export default class LoginOptions extends PureComponent {
static propTypes = {
actions: React.PropTypes.shape({
goToLogin: PropTypes.func.isRequired,
goToSaml: PropTypes.func.isRequired
}).isRequired,
config: PropTypes.object.isRequired,
license: PropTypes.object.isRequired
};
renderEmailOption = () => {
const config = this.props.config;
if (config.EnableSignInWithEmail === 'true' || config.EnableSignInWithUsername === 'true') {
return (
<Button
key='email'
onPress={this.props.actions.goToLogin}
containerStyle={[GlobalStyles.signupButton, {backgroundColor: '#2389d7'}]}
>
<FormattedText
id='signup.email'
defaultMessage='Email and Password'
style={[GlobalStyles.signupButtonText, {color: 'white'}]}
/>
</Button>
);
}
return null;
};
renderSamlOption = () => {
const {config, license} = this.props;
if (config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true') {
return (
<Button
key='saml'
onPress={this.props.actions.goToSaml}
containerStyle={[GlobalStyles.signupButton, {backgroundColor: '#34a28b'}]}
>
<Text
style={[GlobalStyles.signupButtonText, {color: 'white'}]}
>
{config.SamlLoginButtonText}
</Text>
</Button>
);
}
return null;
};
render() {
return (
<View style={[GlobalStyles.container, GlobalStyles.signupContainer]}>
<Image
source={logo}
/>
<Text style={GlobalStyles.header}>
{this.props.config.SiteName}
</Text>
<FormattedText
style={GlobalStyles.subheader}
id='web.root.signup_info'
defaultMessage='All team communication in one place, searchable and accessible anywhere'
/>
<FormattedText
style={[GlobalStyles.subheader, {fontWeight: 'bold', marginTop: 10}]}
id='mobile.login_options.choose_title'
defaultMessage='Choose your login method'
/>
{this.renderEmailOption()}
{this.renderSamlOption()}
</View>
);
}
}

30
app/scenes/saml/index.js Normal file
View file

@ -0,0 +1,30 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {handleSuccessfulLogin} from 'app/actions/views/login';
import {goToLoadTeam} from 'app/actions/navigation';
import {setStoreFromLocalData} from 'app/actions/views/root';
import Saml from './saml';
function mapStateToProps(state) {
return {
...state.views.selectServer
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
handleSuccessfulLogin,
setStoreFromLocalData,
goToLoadTeam
}, dispatch)
};
}
export default navigationSceneConnect(mapStateToProps, mapDispatchToProps)(Saml);

63
app/scenes/saml/saml.js Normal file
View file

@ -0,0 +1,63 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes, PureComponent} from 'react';
import {
InteractionManager,
WebView
} from 'react-native';
import CookieManager from 'react-native-cookies';
export default class Saml extends PureComponent {
static propTypes = {
serverUrl: PropTypes.string.isRequired,
actions: PropTypes.shape({
handleSuccessfulLogin: PropTypes.func.isRequired,
setStoreFromLocalData: PropTypes.func.isRequired,
goToLoadTeam: PropTypes.func.isRequired
}).isRequired
};
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this.setState({renderWebview: true});
});
}
onNavigationStateChange = (navState) => {
const {url} = navState;
if (url.includes('/login/sso/saml') && url.includes('mobile')) {
CookieManager.get(this.props.serverUrl, (err, res) => {
const token = res.MMAUTHTOKEN;
const {
handleSuccessfulLogin,
setStoreFromLocalData,
goToLoadTeam
} = this.props.actions;
if (token) {
handleSuccessfulLogin().
then(setStoreFromLocalData.bind(null, {url: this.props.serverUrl, token})).
then(goToLoadTeam);
}
});
}
};
render() {
if (!this.state || !this.state.renderWebview) {
return null;
}
return (
<WebView
source={{uri: `${this.props.serverUrl}/login/sso/saml?action=mobile`}}
javaScriptEnabledAndroid={true}
automaticallyAdjustContentInsets={false}
scalesPageToFit={true}
onNavigationStateChange={this.onNavigationStateChange}
/>
);
}
}

View file

@ -13,21 +13,29 @@ import Button from 'react-native-button';
import ErrorText from 'app/components/error_text';
import FormattedText from 'app/components/formatted_text';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import Loading from 'app/components/loading';
import TextInputWithLocalizedPlaceholder from 'app/components/text_input_with_localized_placeholder';
import {GlobalStyles} from 'app/styles';
import {isValidUrl, stripTrailingSlashes} from 'app/utils/url';
import logo from 'assets/images/logo.png';
import {RequestStatus} from 'service/constants';
import Client from 'service/client';
import RequestStatus from 'service/constants/request_status';
import {isValidUrl, stripTrailingSlashes} from 'app/utils/url';
export default class SelectServer extends PureComponent {
static propTypes = {
transition: PropTypes.bool.isRequired,
serverUrl: PropTypes.string.isRequired,
server: PropTypes.object.isRequired,
actions: PropTypes.object.isRequired
pingRequest: PropTypes.object.isRequired,
configRequest: PropTypes.object.isRequired,
licenseRequest: PropTypes.object.isRequired,
actions: PropTypes.shape({
getPing: PropTypes.func.isRequired,
resetPing: PropTypes.func.isRequired,
handleLoginOptions: PropTypes.func.isRequired,
handleServerUrlChanged: PropTypes.func.isRequired
}).isRequired
};
constructor(props) {
@ -38,19 +46,23 @@ export default class SelectServer extends PureComponent {
};
}
onClick = () => {
componentWillReceiveProps(nextProps) {
if (!this.props.transition && nextProps.transition) {
this.props.actions.resetPing().then(() => {
this.props.actions.handleLoginOptions();
});
}
}
onClick = async () => {
const url = this.props.serverUrl;
let error = null;
Keyboard.dismiss();
if (isValidUrl(url)) {
Client.setUrl(stripTrailingSlashes(url));
this.props.actions.getPing().then(() => {
if (this.props.server.status === RequestStatus.SUCCESS) {
Keyboard.dismiss();
this.props.actions.goToLogin();
}
});
await this.props.actions.getPing();
} else {
error = {
intl: {
@ -72,6 +84,17 @@ export default class SelectServer extends PureComponent {
};
render() {
const {serverUrl, pingRequest, configRequest, licenseRequest} = this.props;
const isLoading = pingRequest.status === RequestStatus.STARTED ||
configRequest.status === RequestStatus.STARTED ||
licenseRequest.status === RequestStatus.STARTED;
if (isLoading) {
return <Loading/>;
}
const error = pingRequest.error || configRequest.error || licenseRequest.error;
return (
<KeyboardLayout
behavior='padding'
@ -92,7 +115,7 @@ export default class SelectServer extends PureComponent {
</View>
<TextInputWithLocalizedPlaceholder
ref={this.inputRef}
value={this.props.serverUrl}
value={serverUrl}
onChangeText={this.props.actions.handleServerUrlChanged}
onSubmitEditing={this.onClick}
style={GlobalStyles.inputBox}
@ -105,7 +128,6 @@ export default class SelectServer extends PureComponent {
/>
<Button
onPress={this.onClick}
loading={this.props.server.loading}
containerStyle={GlobalStyles.signupButton}
>
<FormattedText
@ -114,7 +136,7 @@ export default class SelectServer extends PureComponent {
defaultMessage='Proceed'
/>
</Button>
<ErrorText error={this.state.error || this.props.server.error}/>
<ErrorText error={this.state.error || error}/>
</View>
</TouchableWithoutFeedback>
</KeyboardLayout>

View file

@ -5,16 +5,24 @@ import {bindActionCreators} from 'redux';
import navigationSceneConnect from '../navigationSceneConnect';
import {getPing} from 'service/actions/general';
import {goToLogin} from 'app/actions/navigation';
import {getPing, resetPing} from 'service/actions/general';
import {RequestStatus} from 'service/constants';
import * as SelectServerActions from 'app/actions/views/select_server';
import SelectServer from './select_server';
function mapStateToProps(state) {
const {config: configRequest, license: licenseRequest, server: pingRequest} = state.requests.general;
const success = RequestStatus.SUCCESS;
const transition = (pingRequest.status === success && configRequest.status === success && licenseRequest.status === success);
return {
...state.views.selectServer,
server: state.requests.general.server
pingRequest,
configRequest,
licenseRequest,
transition
};
}
@ -23,7 +31,7 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
...SelectServerActions,
getPing,
goToLogin
resetPing
}, dispatch)
};
}

View file

@ -1511,9 +1511,11 @@
"mobile.loading_channels": "Loading Channels...",
"mobile.loading_members": "Loading Members...",
"mobile.request.invalid_response": "Received invalid response from the server.",
"mobile.login_options.choose_title": "Choose your login method",
"mobile.routes.channels": "Channels",
"mobile.routes.enterServerUrl": "Enter Server URL",
"mobile.routes.login": "Login",
"mobile.routes.loginOptions": "Login Chooser",
"mobile.routes.mfa": "Multi-factor Authentication",
"mobile.routes.postsList": "Posts List",
"mobile.routes.selectTeam": "Select Team",
@ -1530,6 +1532,7 @@
"mobile.routes.user_profile.send_message": "Send Message",
"mobile.server_ping_failed": "Cannot connect to the server. Please check your server URL and internet connection.",
"mobile.server_url.invalid_format": "URL must start with http:// or https://",
"mobile.routes.saml": "Single SignOn",
"more_channels.close": "Close",
"more_channels.create": "Create New Channel",
"more_channels.createClick": "Click 'Create New Channel' to make a new one",

View file

@ -33,6 +33,7 @@
7FBB5E9B1E1F5A4B000DE18A /* libRNVectorIcons.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7FDF290C1E1F4B4E00DBBE56 /* libRNVectorIcons.a */; };
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
895C9A56B94A45C1BAF568FE /* Entypo.ttf in Resources */ = {isa = PBXBuildFile; fileRef = AC6EB561E1F64C17A69D2FAD /* Entypo.ttf */; };
9906AC672C594EEB9D5EE503 /* libRNCookieManagerIOS.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CE45EB909FB44F3597EF3C60 /* libRNCookieManagerIOS.a */; };
B50561A109B4442299F82327 /* libRNSearchBar.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F81F6DC42D394831B4549928 /* libRNSearchBar.a */; };
C035DB50ED2045F09923FFAE /* MaterialCommunityIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 04AA5E8EF3B54735A11E3B95 /* MaterialCommunityIcons.ttf */; };
C337E8708D2845C6A8DBB47E /* Ionicons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 2DCFD31D3F4A4154822AB532 /* Ionicons.ttf */; };
@ -222,6 +223,13 @@
remoteGlobalIDString = 134814201AA4EA6300B7C361;
remoteInfo = RNSearchBar;
};
7F906D961E6A1ADF00B8F49A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 31A0780E2F224AC8AF8E6930 /* RNCookieManagerIOS.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 1BD725DA1CF77A8B005DBD79;
remoteInfo = RNCookieManagerIOS;
};
7FDF28E01E1F4B1F00DBBE56 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 2CBE9C0FB56E4FDA96C30792 /* RNSVG.xcodeproj */;
@ -270,6 +278,7 @@
2B25899FDAC149EB96ED3305 /* RNVectorIcons.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNVectorIcons.xcodeproj; path = "../node_modules/react-native-vector-icons/RNVectorIcons.xcodeproj"; sourceTree = "<group>"; };
2CBE9C0FB56E4FDA96C30792 /* RNSVG.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNSVG.xcodeproj; path = "../node_modules/react-native-svg/ios/RNSVG.xcodeproj"; sourceTree = "<group>"; };
2DCFD31D3F4A4154822AB532 /* Ionicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Ionicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf"; sourceTree = "<group>"; };
31A0780E2F224AC8AF8E6930 /* RNCookieManagerIOS.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNCookieManagerIOS.xcodeproj; path = "../node_modules/react-native-cookies/RNCookieManagerIOS.xcodeproj"; sourceTree = "<group>"; };
349FBA7338E74D9BBD709528 /* EvilIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = EvilIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"; sourceTree = "<group>"; };
356C9186FA374641A00EB2EA /* MaterialIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = MaterialIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialIcons.ttf"; sourceTree = "<group>"; };
51F5A483EA9F4A9A87ACFB59 /* Foundation.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Foundation.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Foundation.ttf"; sourceTree = "<group>"; };
@ -278,6 +287,7 @@
78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = "<group>"; };
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; };
AC6EB561E1F64C17A69D2FAD /* Entypo.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Entypo.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Entypo.ttf"; sourceTree = "<group>"; };
CE45EB909FB44F3597EF3C60 /* libRNCookieManagerIOS.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCookieManagerIOS.a; sourceTree = "<group>"; };
EDC04CBCF81642219D199CBB /* Octicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Octicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Octicons.ttf"; sourceTree = "<group>"; };
F1F071EE85494E269A50AE88 /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = "<group>"; };
F81F6DC42D394831B4549928 /* libRNSearchBar.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNSearchBar.a; sourceTree = "<group>"; };
@ -310,6 +320,7 @@
00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */,
139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */,
B50561A109B4442299F82327 /* libRNSearchBar.a in Frameworks */,
9906AC672C594EEB9D5EE503 /* libRNCookieManagerIOS.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -464,6 +475,14 @@
name = Products;
sourceTree = "<group>";
};
7F906D7A1E6A1ADF00B8F49A /* Products */ = {
isa = PBXGroup;
children = (
7F906D971E6A1ADF00B8F49A /* libRNCookieManagerIOS.a */,
);
name = Products;
sourceTree = "<group>";
};
7FDF28C41E1F4B1F00DBBE56 /* Products */ = {
isa = PBXGroup;
children = (
@ -497,6 +516,7 @@
2CBE9C0FB56E4FDA96C30792 /* RNSVG.xcodeproj */,
2B25899FDAC149EB96ED3305 /* RNVectorIcons.xcodeproj */,
59954D479A89488091EB588F /* RNSearchBar.xcodeproj */,
31A0780E2F224AC8AF8E6930 /* RNCookieManagerIOS.xcodeproj */,
);
name = Libraries;
sourceTree = "<group>";
@ -585,6 +605,7 @@
TestTargetID = 13B07F861A680F5B00A75B9A;
};
13B07F861A680F5B00A75B9A = {
DevelopmentTeam = UQ8HT4Q2XM;
ProvisioningStyle = Automatic;
};
};
@ -645,6 +666,10 @@
ProductGroup = 146834001AC3E56700842450 /* Products */;
ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */;
},
{
ProductGroup = 7F906D7A1E6A1ADF00B8F49A /* Products */;
ProjectRef = 31A0780E2F224AC8AF8E6930 /* RNCookieManagerIOS.xcodeproj */;
},
{
ProductGroup = 7F48904C1E3B7D3B008EDBEA /* Products */;
ProjectRef = 59954D479A89488091EB588F /* RNSearchBar.xcodeproj */;
@ -842,6 +867,13 @@
remoteRef = 7F4890681E3B7D3B008EDBEA /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
7F906D971E6A1ADF00B8F49A /* libRNCookieManagerIOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libRNCookieManagerIOS.a;
remoteRef = 7F906D961E6A1ADF00B8F49A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
7FDF28E11E1F4B1F00DBBE56 /* libRNSVG.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@ -967,8 +999,6 @@
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -988,8 +1018,6 @@
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
"\"$(SRCROOT)/$(TARGET_NAME)\"",
);
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
PRODUCT_NAME = "$(TARGET_NAME)";

View file

@ -12,6 +12,7 @@
"react-intl": "2.2.2",
"react-native": "0.40.0",
"react-native-button": "1.7.1",
"react-native-cookies": "2.0.0",
"react-native-drawer": "2.3.0",
"react-native-keyboard-aware-scroll-view": "0.2.7",
"react-native-keyboard-spacer": "0.3.1",

View file

@ -31,6 +31,12 @@ export function getPing() {
};
}
export function resetPing() {
return async (dispatch, getState) => {
dispatch({type: GeneralTypes.PING_RESET}, getState);
};
}
export function getClientConfig() {
return bindClientFunc(
Client.getClientConfig,

View file

@ -240,6 +240,7 @@ export default class Client {
if (response.ok) {
this.token = '';
}
this.serverVersion = '';
return response;
};

View file

@ -11,6 +11,7 @@ const GeneralTypes = keyMirror({
PING_REQUEST: null,
PING_SUCCESS: null,
PING_FAILURE: null,
PING_RESET: null,
RECEIVED_SERVER_VERSION: null,

View file

@ -6,6 +6,10 @@ import {GeneralTypes} from 'service/constants';
import {handleRequest, initialRequestState} from './helpers';
function server(state = initialRequestState(), action) {
if (action.type === GeneralTypes.PING_RESET) {
return initialRequestState();
}
return handleRequest(
GeneralTypes.PING_REQUEST,
GeneralTypes.PING_SUCCESS,

View file

@ -11,10 +11,16 @@ import path from 'path';
import register from 'babel-core/register';
import chai from 'chai';
import chaiEnzyme from 'chai-enzyme';
import ReactNative from 'react-native-mock';
const m = require('module');
const originalLoader = m._load;
const NativeModules = ReactNative.NativeModules;
NativeModules.RNCookieManagerIOS = {};
NativeModules.RNCookieManagerAndroid = {};
// Image file ignore setup from:
// http://valuemotive.com/2016/08/01/unit-testing-react-native-components-with-mocha-and-enzyme/
m._load = function hookedLoader(request, parent, isMain) {