From c13a436d186b752ba460f487c539421169df846d Mon Sep 17 00:00:00 2001 From: enahum Date: Thu, 9 Mar 2017 14:07:18 -0300 Subject: [PATCH] Enable SAML authentication (#325) * enable saml authentication * Fix unit tests * Fix android cookies * Feedback review --- android/app/build.gradle | 1 + .../java/com/mattermost/MainApplication.java | 2 + android/settings.gradle | 2 + app/actions/navigation/index.js | 18 ++++ app/actions/storage/index.js | 2 +- app/actions/views/select_server.js | 23 ++++- app/navigation/routes.js | 18 ++++ app/scenes/index.js | 6 +- app/scenes/login/login.js | 9 +- app/scenes/login/login_container.js | 5 +- app/scenes/login_options/index.js | 29 ++++++ app/scenes/login_options/login_options.js | 93 +++++++++++++++++++ app/scenes/saml/index.js | 30 ++++++ app/scenes/saml/saml.js | 63 +++++++++++++ app/scenes/select_server/select_server.js | 54 +++++++---- .../select_server/select_server_container.js | 16 +++- assets/base/i18n/en.json | 3 + ios/Mattermost.xcodeproj/project.pbxproj | 36 ++++++- package.json | 1 + service/actions/general.js | 6 ++ service/client/client.js | 1 + service/constants/general.js | 1 + service/reducers/requests/general.js | 4 + test/setup.js | 6 ++ 24 files changed, 390 insertions(+), 39 deletions(-) create mode 100644 app/scenes/login_options/index.js create mode 100644 app/scenes/login_options/login_options.js create mode 100644 app/scenes/saml/index.js create mode 100644 app/scenes/saml/saml.js diff --git a/android/app/build.gradle b/android/app/build.gradle index e3a9fd640..3a0c7bb43 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -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"]) diff --git a/android/app/src/main/java/com/mattermost/MainApplication.java b/android/app/src/main/java/com/mattermost/MainApplication.java index dcabea6ee..762667882 100644 --- a/android/app/src/main/java/com/mattermost/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/MainApplication.java @@ -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 getPackages() { return Arrays.asList( new MainReactPackage(), + new CookieManagerPackage(), new VectorIconsPackage(), new RNSvgPackage() ); diff --git a/android/settings.gradle b/android/settings.gradle index 66adc1efc..6e1445e11 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -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') diff --git a/app/actions/navigation/index.js b/app/actions/navigation/index.js index 88ff1c070..2c32ee497 100644 --- a/app/actions/navigation/index.js +++ b/app/actions/navigation/index.js @@ -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({ diff --git a/app/actions/storage/index.js b/app/actions/storage/index.js index 5c016387c..88199755d 100644 --- a/app/actions/storage/index.js +++ b/app/actions/storage/index.js @@ -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} ]; diff --git a/app/actions/views/select_server.js b/app/actions/views/select_server.js index 667c18fe4..0b095e4ea 100644 --- a/app/actions/views/select_server.js +++ b/app/actions/views/select_server.js @@ -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 }; diff --git a/app/navigation/routes.js b/app/navigation/routes.js index 0260631bd..4c43f4c40 100644 --- a/app/navigation/routes.js +++ b/app/navigation/routes.js @@ -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, diff --git a/app/scenes/index.js b/app/scenes/index.js index 6b5829850..6e95cc6ff 100644 --- a/app/scenes/index.js +++ b/app/scenes/index.js @@ -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 }; diff --git a/app/scenes/login/login.js b/app/scenes/login/login.js index ffcdd5144..42a06e985 100644 --- a/app/scenes/login/login.js +++ b/app/scenes/login/login.js @@ -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 ; - } - return ( { + const config = this.props.config; + if (config.EnableSignInWithEmail === 'true' || config.EnableSignInWithUsername === 'true') { + return ( + + ); + } + + return null; + }; + + renderSamlOption = () => { + const {config, license} = this.props; + if (config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true') { + return ( + + ); + } + + return null; + }; + + render() { + return ( + + + + {this.props.config.SiteName} + + + + {this.renderEmailOption()} + {this.renderSamlOption()} + + ); + } +} diff --git a/app/scenes/saml/index.js b/app/scenes/saml/index.js new file mode 100644 index 000000000..10ca6c3ae --- /dev/null +++ b/app/scenes/saml/index.js @@ -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); diff --git a/app/scenes/saml/saml.js b/app/scenes/saml/saml.js new file mode 100644 index 000000000..7fbd4c9df --- /dev/null +++ b/app/scenes/saml/saml.js @@ -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 ( + + ); + } +} diff --git a/app/scenes/select_server/select_server.js b/app/scenes/select_server/select_server.js index 3c60926bb..9bc521288 100644 --- a/app/scenes/select_server/select_server.js +++ b/app/scenes/select_server/select_server.js @@ -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 ; + } + + const error = pingRequest.error || configRequest.error || licenseRequest.error; + return ( - + diff --git a/app/scenes/select_server/select_server_container.js b/app/scenes/select_server/select_server_container.js index cff3e2494..a9dcbad31 100644 --- a/app/scenes/select_server/select_server_container.js +++ b/app/scenes/select_server/select_server_container.js @@ -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) }; } diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 5f369923a..5c9e2aac0 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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", diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index b0099f8c5..b4931e97a 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -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 = ""; }; 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 = ""; }; 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 = ""; }; + 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 = ""; }; 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 = ""; }; 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 = ""; }; 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 = ""; }; @@ -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 = ""; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; 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 = ""; }; + CE45EB909FB44F3597EF3C60 /* libRNCookieManagerIOS.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCookieManagerIOS.a; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; F81F6DC42D394831B4549928 /* libRNSearchBar.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNSearchBar.a; sourceTree = ""; }; @@ -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 = ""; }; + 7F906D7A1E6A1ADF00B8F49A /* Products */ = { + isa = PBXGroup; + children = ( + 7F906D971E6A1ADF00B8F49A /* libRNCookieManagerIOS.a */, + ); + name = Products; + sourceTree = ""; + }; 7FDF28C41E1F4B1F00DBBE56 /* Products */ = { isa = PBXGroup; children = ( @@ -497,6 +516,7 @@ 2CBE9C0FB56E4FDA96C30792 /* RNSVG.xcodeproj */, 2B25899FDAC149EB96ED3305 /* RNVectorIcons.xcodeproj */, 59954D479A89488091EB588F /* RNSearchBar.xcodeproj */, + 31A0780E2F224AC8AF8E6930 /* RNCookieManagerIOS.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -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)"; diff --git a/package.json b/package.json index e8e4d57d3..a3ce02678 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/service/actions/general.js b/service/actions/general.js index 92de72be1..29beb594c 100644 --- a/service/actions/general.js +++ b/service/actions/general.js @@ -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, diff --git a/service/client/client.js b/service/client/client.js index a1b1816bd..7cfdae539 100644 --- a/service/client/client.js +++ b/service/client/client.js @@ -240,6 +240,7 @@ export default class Client { if (response.ok) { this.token = ''; } + this.serverVersion = ''; return response; }; diff --git a/service/constants/general.js b/service/constants/general.js index 53c877ae2..de40343da 100644 --- a/service/constants/general.js +++ b/service/constants/general.js @@ -11,6 +11,7 @@ const GeneralTypes = keyMirror({ PING_REQUEST: null, PING_SUCCESS: null, PING_FAILURE: null, + PING_RESET: null, RECEIVED_SERVER_VERSION: null, diff --git a/service/reducers/requests/general.js b/service/reducers/requests/general.js index 6d8867377..03b289db5 100644 --- a/service/reducers/requests/general.js +++ b/service/reducers/requests/general.js @@ -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, diff --git a/test/setup.js b/test/setup.js index 7d1c7aed3..01887bb31 100644 --- a/test/setup.js +++ b/test/setup.js @@ -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) {