Skip to content

Commit

Permalink
Merge pull request #10 from AndrewGable/tgolen-port-webpoc
Browse files Browse the repository at this point in the history
Begin porting the web POC to react native
  • Loading branch information
AndrewGable authored Aug 10, 2020
2 parents fd60e71 + 798135c commit 9c479ff
Show file tree
Hide file tree
Showing 36 changed files with 1,270 additions and 313 deletions.
25 changes: 23 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,13 @@
},
"dependencies": {
"@react-native-community/async-storage": "^1.11.0",
"html-entities": "^1.3.1",
"jquery": "^3.5.1",
"lodash.get": "^4.4.2",
"moment": "^2.27.0",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-beforeunload": "^2.2.2",
"react-dom": "^16.13.1",
"react-native": "0.63.2",
"react-native-web": "^0.13.5",
Expand Down
2 changes: 2 additions & 0 deletions src/CONFIG.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {Platform} from 'react-native';

// eslint-disable-next-line no-undef
const IS_IN_PRODUCTION = Platform.OS === 'web' ? process.env.NODE_ENV === 'production' : !__DEV__;

export default {
IS_IN_PRODUCTION,
PUSHER: {
APP_KEY: IS_IN_PRODUCTION ? '268df511a204fbb60884' : 'ac6d22b891daae55283a',
AUTH_URL: IS_IN_PRODUCTION ? 'https://www.expensify.com' : 'https://www.expensify.com.dev',
Expand Down
5 changes: 5 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const CONST = {
CLOUDFRONT_URL: 'https://d2k5nsl2zxldvw.cloudfront.net',
};

export default CONST;
64 changes: 28 additions & 36 deletions src/Expensify.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, {Component} from 'react';
import {Beforeunload} from 'react-beforeunload';
import SignInPage from './page/SignInPage';
import HomePage from './page/HomePage/HomePage';
import * as Store from './store/Store';
import * as ActiveClientManager from './lib/ActiveClientManager';
import {verifyAuthToken} from './store/actions/SessionActions';
import STOREKEYS from './store/STOREKEYS';
import WithStore from './components/WithStore';
import {
Route,
Router,
Expand All @@ -15,44 +17,34 @@ import {
// Initialize the store when the app loads for the first time
Store.init();

export default class Expensify extends Component {
constructor(props) {
super(props);

this.state = {
redirectTo: null,
};
}

async componentDidMount() {
// Listen for when the app wants to redirect to a specific URL
Store.subscribe(STOREKEYS.APP_REDIRECT_TO, (redirectTo) => {
this.setState({redirectTo});
});

// Verify that our authToken is OK to use
verifyAuthToken();

// Initialize this client as being an active client
await ActiveClientManager.init();

// TODO: Refactor window events
// window.addEventListener('beforeunload', () => {
// ActiveClientManager.removeClient();
// });
}

class Expensify extends Component {
render() {
return (
<Router>
{/* If there is ever a property for redirecting, we do the redirect here */}
{this.state.redirectTo && <Redirect to={this.state.redirectTo} />}

<Switch>
<Route path="/signin" component={SignInPage} />
<Route path="/" component={HomePage} />
</Switch>
</Router>
// TODO: Mobile does not support Beforeunload
// <Beforeunload onBeforeunload={ActiveClientManager.removeClient}>
<Router>
{/* If there is ever a property for redirecting, we do the redirect here */}
{this.state && this.state.redirectTo && <Redirect to={this.state.redirectTo} />}

<Switch>
<Route path="/signin" component={SignInPage} />
<Route path="/" component={HomePage} />
</Switch>
</Router>
// </Beforeunload>
);
}
}

export default WithStore({
redirectTo: {
key: STOREKEYS.APP_REDIRECT_TO,
loader: () => {
// Verify that our authToken is OK to use
verifyAuthToken();

// Initialize this client as being an active client
ActiveClientManager.init();
},
},
})(Expensify);
94 changes: 94 additions & 0 deletions src/components/WithStore.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* This is a higher order component that provides the ability to map a state property directly to
* something in the store. That way, as soon as the store changes, the state will be set and the view
* will automatically change to reflect the new data.
*/
import React from 'react';
import _ from 'underscore';
import * as Store from '../store/Store';

export default function (mapStoreToStates) {
return WrappedComponent => class WithStore extends React.Component {
constructor(props) {
super(props);

this.subscriptionIDs = [];
this.bind = this.bind.bind(this);
this.unbind = this.unbind.bind(this);

// Initialize the state with each of our property names
this.state = _.reduce(_.keys(mapStoreToStates), (finalResult, propertyName) => ({
...finalResult,
[propertyName]: null,
}), {});
}

componentDidMount() {
this.bindStoreToStates(mapStoreToStates, this.wrappedComponent);
}

componentWillUnmount() {
this.unbind();
}

/**
* A method that is convenient to bind the state to the store. Typically used when you can't pass
* mapStoreToStates to this HOC. For example: if the key that you want to subscribe to has a piece of
* information that can only come from the component's props, then you want to use bind() directly from inside
* componentDidMount(). All subscriptions will automatically be unbound when unmounted.
*
* The options passed to bind are the exact same that you would pass to the HOC.
*
* @param {object} mapping
* @param {object} component
*/
bind(mapping, component) {
this.bindStoreToStates(mapping, component);
}

bindStoreToStates(statesToStoreMap, component) {
// Subscribe each of the state properties to the proper store key
_.each(statesToStoreMap, (stateToStoreMap, propertyName) => {
const {
key,
path,
prefillWithKey,
loader,
loaderParams,
defaultValue,
} = stateToStoreMap;

this.subscriptionIDs.push(Store.bind(key, path, defaultValue, propertyName, component));
if (prefillWithKey) {
Store.get(prefillWithKey, path, defaultValue)
.then(data => component.setState({[propertyName]: data}));
}
if (loader) {
loader(...loaderParams || []);
}
});
}

/**
* Unsubscribe from any subscriptions
*/
unbind() {
_.each(this.subscriptionIDs, Store.unbind);
}

render() {
// Spreading props and state is necessary in an HOC where the data cannot be predicted
return (
<WrappedComponent
// eslint-disable-next-line react/jsx-props-no-spreading
{...this.props}
// eslint-disable-next-line react/jsx-props-no-spreading
{...this.state}
ref={el => this.wrappedComponent = el}
bind={this.bind}
unbind={this.unbind}
/>
);
}
};
}
32 changes: 17 additions & 15 deletions src/lib/ActiveClientManager.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import _ from 'underscore';
import Guid from './Guid';
import * as Store from '../store/Store';
import STOREKEYS from '../store/STOREKEYS';
Expand All @@ -6,33 +7,34 @@ const clientID = Guid();

/**
* Add our client ID to the list of active IDs
*
* @returns {Promise}
*/
const init = async () => {
const activeClientIDs = (await Store.get(STOREKEYS.ACTIVE_CLIENT_IDS)) || [];
activeClientIDs.push(clientID);
Store.set(STOREKEYS.ACTIVE_CLIENT_IDS, activeClientIDs);
};
const init = () => Store.merge(STOREKEYS.ACTIVE_CLIENT_IDS, {clientID});

/**
* Remove this client ID from the array of active client IDs when this client is exited
*
* @returns {Promise}
*/
function removeClient() {
const activeClientIDs = Store.get(STOREKEYS.ACTIVE_CLIENT_IDS) || [];
const newActiveClientIDs = activeClientIDs.filter(activeClientID => activeClientID !== clientID);
Store.set(STOREKEYS.ACTIVE_CLIENT_IDS, newActiveClientIDs);
return Store.get(STOREKEYS.ACTIVE_CLIENT_IDS)
.then(activeClientIDs => _.omit(activeClientIDs, clientID))
.then(newActiveClientIDs => Store.set(STOREKEYS.ACTIVE_CLIENT_IDS, newActiveClientIDs));
}

/**
* Checks if the current client is the leader (the first one in the list of active clients)
*
* @returns {boolean}
* @returns {Promise}
*/
function isClientTheLeader() {
const activeClientIDs = Store.get(STOREKEYS.ACTIVE_CLIENT_IDS) || [];
if (!activeClientIDs.length) {
return false;
}
return activeClientIDs[0] === clientID;
return Store.get(STOREKEYS.ACTIVE_CLIENT_IDS)
.then(activeClientIDs => _.first(activeClientIDs) === clientID);
}

export {init, removeClient, isClientTheLeader};
export {
init,
removeClient,
isClientTheLeader
};
15 changes: 9 additions & 6 deletions src/lib/ExpensiMark.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,19 @@ export default class ExpensiMark {
},
{
/**
* Use \b in this case because it will match on words, letters, and _: https://www.rexegg.com/regex-boundaries.html#wordboundary
* Use \b in this case because it will match on words, letters, and _:
* https://www.rexegg.com/regex-boundaries.html#wordboundary
* The !_blank is to prevent the `target="_blank">` section of the link replacement from being captured
* Additionally, something like `\b\_([^<>]*?)\_\b` doesn't work because it won't replace `_https://www.test.com_`
* Additionally, something like `\b\_([^<>]*?)\_\b` doesn't work because it won't replace
* `_https://www.test.com_`
*/
name: 'italic',
regex: '(?!_blank">)\\b\\_(.*?)\\_\\b',
replacement: '<em>$1</em>',
},
{
// Use \B in this case because \b doesn't match * or ~. \B will match everything that \b doesn't, so it works for * and ~: https://www.rexegg.com/regex-boundaries.html#notb
// Use \B in this case because \b doesn't match * or ~. \B will match everything that \b doesn't, so it
// works for * and ~: https://www.rexegg.com/regex-boundaries.html#notb
name: 'bold',
regex: '\\B\\*(.*?)\\*\\B',
replacement: '<strong>$1</strong>',
Expand All @@ -52,12 +55,12 @@ export default class ExpensiMark {
*/
replace(text) {
// This ensures that any html the user puts into the comment field shows as raw html
text = Str.safeEscape(text);
let safeText = Str.safeEscape(text);

this.rules.forEach((rule) => {
text = text.replace(new RegExp(rule.regex, 'g'), rule.replacement);
safeText = safeText.replace(new RegExp(rule.regex, 'g'), rule.replacement);
});

return text;
return safeText;
}
}
6 changes: 6 additions & 0 deletions src/lib/Network.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ function request(command, data, type = 'post') {
body: formData,
}))
.then(response => response.json())
.then((responseData) => {
if (responseData.jsonCode === 200) {
return responseData;
}
console.error('[API] Error', responseData);
})
// eslint-disable-next-line no-unused-vars
.catch(() => isAppOffline = true);
}
Expand Down
Loading

0 comments on commit 9c479ff

Please sign in to comment.