diff --git a/src/Expensify.js b/src/Expensify.js index 797363e7ebdc..5277d4a3e7dc 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -1,5 +1,6 @@ import React, {Component} from 'react'; import {View} from 'react-native'; +import PropTypes from 'prop-types'; // import {Beforeunload} from 'react-beforeunload'; import SignInPage from './page/SignInPage'; @@ -21,6 +22,17 @@ import { // Initialize the store when the app loads for the first time Ion.init(); +const propTypes = { + /* Ion Props */ + + // A route set by Ion that we will redirect to if present. Always empty on app init. + redirectTo: PropTypes.string, +}; + +const defaultProps = { + redirectTo: '', +}; + class Expensify extends Component { constructor(props) { super(props); @@ -62,7 +74,7 @@ class Expensify extends Component { // We can only have a redirectTo if this is not the initial render so if we have one we'll // always navigate to it. If we are not authenticated by this point then we'll force navigate to sign in. - const redirectTo = this.state.redirectTo || (!this.state.authToken && '/signin'); + const redirectTo = this.props.redirectTo || (!this.state.authToken && '/signin'); return ( @@ -84,6 +96,9 @@ class Expensify extends Component { } } +Expensify.propTypes = propTypes; +Expensify.defaultProps = defaultProps; + export default WithIon({ redirectTo: { key: IONKEYS.APP_REDIRECT_TO, diff --git a/src/components/WithIon.js b/src/components/WithIon.js index 9da95d77c5c7..0e1ec3bbe29f 100644 --- a/src/components/WithIon.js +++ b/src/components/WithIon.js @@ -92,13 +92,12 @@ export default function (mapIonToState) { * @param {boolean} [mapping.initWithStoredValues] If set to false, then no data will be prefilled into the * component * @param {string} statePropertyName the name of the state property that Ion will add the data to - * @param {object} reactComponent a reference to the react component whose state needs updated by Ion */ - connectMappingToIon(mapping, statePropertyName, reactComponent) { + connectMappingToIon(mapping, statePropertyName) { const ionConnectionConfig = { ...mapping, statePropertyName, - reactComponent, + withIonInstance: this, }; // Connect to Ion and keep track of the connectionID @@ -120,7 +119,7 @@ export default function (mapIonToState) { // Pre-fill the state with any data already in the store if (mapping.initWithStoredValues !== false) { Ion.get(ionConnectionConfig.key, mapping.path, mapping.defaultValue) - .then(data => reactComponent.setState({[statePropertyName]: data})); + .then(data => this.setState({[statePropertyName]: data})); } // Load the data from an API request if necessary diff --git a/src/lib/Ion.js b/src/lib/Ion.js index f64c0e7f58b6..03c84e41a815 100644 --- a/src/lib/Ion.js +++ b/src/lib/Ion.js @@ -33,7 +33,7 @@ const callbackToStateMapping = {}; * @param {string} mapping.statePropertyName the name of the property in the state to connect the data to * @param {boolean} [mapping.addAsCollection] rather than setting a single state value, this will add things to an array * @param {string} [mapping.collectionID] the name of the ID property to use for the collection - * @param {object} mapping.reactComponent whose setState() method will be called with any changed data + * @param {object} mapping.withIonInstance whose setState() method will be called with any changed data * @returns {number} an ID to use when calling disconnect */ function connect(mapping) { @@ -75,7 +75,7 @@ function keyChanged(key, data) { // Set the state of the react component with either the pathed data, or the data if (mappedComponent.addAsCollection) { // Add the data to an array of existing items - mappedComponent.reactComponent.setState((prevState) => { + mappedComponent.withIonInstance.setState((prevState) => { const collection = prevState[mappedComponent.statePropertyName] || {}; collection[newValue[mappedComponent.collectionID]] = newValue; const newState = { @@ -84,7 +84,7 @@ function keyChanged(key, data) { return newState; }); } else { - mappedComponent.reactComponent.setState({ + mappedComponent.withIonInstance.setState({ [mappedComponent.statePropertyName]: newValue, }); } diff --git a/src/page/HomePage/HeaderView.js b/src/page/HomePage/HeaderView.js index bf33a7f30bd6..9d6b93bf02a4 100644 --- a/src/page/HomePage/HeaderView.js +++ b/src/page/HomePage/HeaderView.js @@ -14,39 +14,47 @@ const propTypes = { // Decides whether we should show the hamburger menu button shouldShowHamburgerButton: PropTypes.bool.isRequired, + + /* Ion Props */ + + // Name of the report (if we have one) + reportName: PropTypes.string, }; -class HeaderView extends React.Component { - render() { - return ( - - - {this.props.shouldShowHamburgerButton && ( - - - - )} - {this.state && this.state.reportName && ( - - {this.state.reportName} - - )} - - - ); - } -} +const defaultProps = { + reportName: '', +}; + +const HeaderView = props => ( + + + {props.shouldShowHamburgerButton && ( + + + + )} + {props.reportName && ( + + {props.reportName} + + )} + + +); + HeaderView.propTypes = propTypes; +HeaderView.displayName = 'HeaderView'; +HeaderView.defaultProps = defaultProps; export default withRouter(WithIon({ - // Map this.state.reportName to the data for a specific report in the store, and bind it to the reportName property + // Map this.props.reportName to the data for a specific report in the store, and bind it to the reportName property // It uses the data returned from the props path (ie. the reportID) to replace %DATAFROMPROPS% in the key it // binds to reportName: { diff --git a/src/page/HomePage/MainView.js b/src/page/HomePage/MainView.js index 07e638a18592..c8007386692d 100644 --- a/src/page/HomePage/MainView.js +++ b/src/page/HomePage/MainView.js @@ -12,17 +12,22 @@ const propTypes = { // This comes from withRouter // eslint-disable-next-line react/forbid-prop-types match: PropTypes.object.isRequired, -}; -class MainView extends React.Component { - constructor(props) { - super(props); + /* Ion Props */ - this.state = {}; - } + // List of reports to display + reports: PropTypes.arrayOf(PropTypes.shape({ + reportID: PropTypes.number, + })), +}; +const defaultProps = { + reports: [], +}; + +class MainView extends React.Component { render() { - if (!this.state || !this.state.reports || this.state.reports.length === 0) { + if (this.props.reports.length === 0) { return null; } @@ -30,7 +35,7 @@ class MainView extends React.Component { // The styles for each of our reports. Basically, they are all hidden except for the one matching the // reportID in the URL - const reportStyles = _.reduce(this.state.reports, (memo, report) => { + const reportStyles = _.reduce(this.props.reports, (memo, report) => { const finalData = {...memo}; const reportStyle = reportIDInURL === report.reportID ? [styles.dFlex, styles.flex1] @@ -41,7 +46,7 @@ class MainView extends React.Component { return ( <> - {_.map(this.state.reports, report => ( + {_.map(this.props.reports, report => ( Be the first person to comment! @@ -137,11 +146,11 @@ class ReportHistoryView extends React.Component { paddingVertical: 8 }} > - {_.chain(reportHistory).sortBy('sequenceNumber').map((item, index) => ( + {_.chain(this.props.reportHistory).sortBy('sequenceNumber').map((item, index) => ( )).value()} @@ -149,7 +158,9 @@ class ReportHistoryView extends React.Component { ); } } + ReportHistoryView.propTypes = propTypes; +ReportHistoryView.defaultProps = defaultProps; const key = `${IONKEYS.REPORT_HISTORY}_%DATAFROMPROPS%`; export default withRouter(WithIon({ diff --git a/src/page/HomePage/SidebarLink.js b/src/page/HomePage/SidebarLink.js index c46f3256c33f..7af619445c31 100644 --- a/src/page/HomePage/SidebarLink.js +++ b/src/page/HomePage/SidebarLink.js @@ -21,31 +21,41 @@ const propTypes = { // Toggles the hamburger menu open and closed onLinkClick: PropTypes.func.isRequired, + + /* Ion Props */ + + // Does the report for this link have unread comments? + isUnread: PropTypes.bool, }; -class SidebarLink extends React.Component { - render() { - const paramsReportID = parseInt(this.props.match.params.reportID, 10); - const isReportActive = paramsReportID === this.props.reportID; - const linkWrapperActiveStyle = isReportActive && styles.sidebarLinkWrapperActive; - const linkActiveStyle = isReportActive ? styles.sidebarLinkActive : styles.sidebarLink; - const textActiveStyle = isReportActive ? styles.sidebarLinkActiveText : styles.sidebarLinkText; - const textActiveUnreadStyle = this.state && this.state.isUnread - ? [textActiveStyle, styles.sidebarLinkTextUnread] : [textActiveStyle]; - return ( - - - - - {this.props.reportName} - - - - - ); - } -} +const defaultProps = { + isUnread: false, +}; + +const SidebarLink = (props) => { + const paramsReportID = parseInt(props.match.params.reportID, 10); + const isReportActive = paramsReportID === props.reportID; + const linkWrapperActiveStyle = isReportActive && styles.sidebarLinkWrapperActive; + const linkActiveStyle = isReportActive ? styles.sidebarLinkActive : styles.sidebarLink; + const textActiveStyle = isReportActive ? styles.sidebarLinkActiveText : styles.sidebarLinkText; + const textActiveUnreadStyle = props.isUnread + ? [textActiveStyle, styles.sidebarLinkTextUnread] : [textActiveStyle]; + return ( + + + + + {props.reportName} + + + + + ); +}; + +SidebarLink.displayName = 'SidebarLink'; SidebarLink.propTypes = propTypes; +SidebarLink.defaultProps = defaultProps; export default withRouter(WithIon({ isUnread: { diff --git a/src/page/HomePage/SidebarView.js b/src/page/HomePage/SidebarView.js index 71cd0b9a5800..dd68c26d42a8 100644 --- a/src/page/HomePage/SidebarView.js +++ b/src/page/HomePage/SidebarView.js @@ -25,7 +25,32 @@ const propTypes = { // Safe area insets required for mobile devices margins // eslint-disable-next-line react/forbid-prop-types - insets: PropTypes.object.isRequired + insets: PropTypes.object.isRequired, + + /* Ion Props */ + + // Display name of the current user from their personal details + userDisplayName: PropTypes.string, + + // Avatar URL of the current user from their personal details + avatarURL: PropTypes.string, + + // List of reports + reports: PropTypes.arrayOf(PropTypes.shape({ + hasUnread: PropTypes.bool, + reportName: PropTypes.string, + reportID: PropTypes.number, + })), + + // Is this person offline? + isOffline: PropTypes.bool, +}; + +const defaultProps = { + userDisplayName: '', + avatarURL: '', + reports: [], + isOffline: false, }; class SidebarView extends React.Component { @@ -33,10 +58,8 @@ class SidebarView extends React.Component { * Updates the page title to indicate there are unread reports */ updateUnreadReportIndicator() { - if (this.state) { - const hasUnreadReports = _.any(this.state.reports, report => report.hasUnread); - PageTitleUpdater(hasUnreadReports); - } + const hasUnreadReports = _.any(this.props.reports, report => report.hasUnread); + PageTitleUpdater(hasUnreadReports); } alertInstallInstructions() { @@ -51,7 +74,7 @@ class SidebarView extends React.Component { } render() { - const reports = this.state && this.state.reports; + const reports = this.props.reports; this.updateUnreadReportIndicator(); return ( @@ -80,17 +103,17 @@ class SidebarView extends React.Component { - {this.state && this.state.isOffline && ( + {this.props.isOffline && ( )} - {this.state && this.state.userDisplayName && ( + {this.props.userDisplayName && ( - {this.state.userDisplayName} + {this.props.userDisplayName} )} @@ -122,9 +145,10 @@ class SidebarView extends React.Component { } SidebarView.propTypes = propTypes; +SidebarView.defaultProps = defaultProps; export default WithIon({ - // Map this.state.userDisplayName to the personal details key in the store and bind it to the displayName property + // Map this.props.userDisplayName to the personal details key in the store and bind it to the displayName property // and load it with data from getPersonalDetails() userDisplayName: { key: IONKEYS.MY_PERSONAL_DETAILS, diff --git a/src/page/SignInPage.js b/src/page/SignInPage.js index 19d45a0d66b9..52714cfc9ca2 100644 --- a/src/page/SignInPage.js +++ b/src/page/SignInPage.js @@ -20,6 +20,15 @@ const propTypes = { // These are from withRouter // eslint-disable-next-line react/forbid-prop-types match: PropTypes.object.isRequired, + + /* Ion Props */ + + // Error to display when there is a session error returned + error: PropTypes.string, +}; + +const defaultProps = { + error: '', }; class App extends Component { @@ -103,9 +112,9 @@ class App extends Component { > Log In - {this.state.error && ( + {this.props.error && ( - {this.state.error} + {this.props.error} )} @@ -117,8 +126,9 @@ class App extends Component { } App.propTypes = propTypes; +App.defaultProps = defaultProps; export default withRouter(WithIon({ - // Bind this.state.error to the error in the session object + // Bind this.props.error to the error in the session object error: {key: IONKEYS.SESSION, path: 'error', defaultValue: null}, })(App));