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

Begin porting the web POC to react native #10

Merged
merged 45 commits into from
Aug 10, 2020
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
bef1ed1
Remove all async and clean up pages
tgolen Aug 8, 2020
7a8f83b
Fix an error with merge
tgolen Aug 8, 2020
d9cb5b3
Add some style to the header
tgolen Aug 8, 2020
f55a6d4
Add styles and display a header
tgolen Aug 8, 2020
07db03a
Add the personal details action
tgolen Aug 8, 2020
4496f10
Add personal details to the header
tgolen Aug 8, 2020
ae9f529
WIP on direct binding
tgolen Aug 8, 2020
b0a1575
Implement direct binding
tgolen Aug 8, 2020
f9a094a
Fix unsubscribing
tgolen Aug 9, 2020
0366c4f
Fix infinite session redirect
tgolen Aug 9, 2020
f153fb5
Build an HOC for subscribing the state to the store
tgolen Aug 9, 2020
307dfd2
Add a little more comment
tgolen Aug 9, 2020
e31528b
Add initial sidebar and main views and lay them out
tgolen Aug 9, 2020
a5cbde6
List reports in the sidebar
tgolen Aug 9, 2020
2490b9c
Update src/page/HomePage/HeaderView.js
tgolen Aug 9, 2020
a02d992
Update src/style/StyleSheet.js
tgolen Aug 9, 2020
a3de49e
Fix typo
tgolen Aug 9, 2020
743db5f
Merge branch 'tgolen-port-webpoc' of https://github.com/AndrewGable/R…
tgolen Aug 9, 2020
2f8016b
Use Link instead of NavLink
tgolen Aug 9, 2020
67ee483
Use a simple subscription ID
tgolen Aug 9, 2020
e42283d
Rename subscribeToState to bind
tgolen Aug 9, 2020
b2a9b7e
Trigger key changed after storage is set
tgolen Aug 9, 2020
8df561f
Move the sidebar link into it's own component
tgolen Aug 9, 2020
74d5455
remove calc() from styles
tgolen Aug 9, 2020
da8177c
Show the list of report names as decent links
tgolen Aug 9, 2020
f2ed6a8
Remvoe subscribe and unsubscribe for now
tgolen Aug 9, 2020
04b5751
Only create regex object when binding
tgolen Aug 9, 2020
cbc7368
Rename unsubscribeFromState to unbind
tgolen Aug 9, 2020
9b8a3b8
Reorder the params for binding
tgolen Aug 9, 2020
9a26bc3
Remove persistent storage
tgolen Aug 9, 2020
af2333f
Use merge in as many places as possible
tgolen Aug 9, 2020
cad8207
Remove weird code
tgolen Aug 9, 2020
2130a64
Improve the styles in the sidebar
tgolen Aug 9, 2020
99e3a73
Add a report view and show the report name
tgolen Aug 9, 2020
3b8fcaf
Show the report name on the header
tgolen Aug 9, 2020
1843eb7
Load the report history when looking at a report
tgolen Aug 9, 2020
fb063ce
Rename the HOC to WithStore
tgolen Aug 9, 2020
732ac89
Do better at prefilling data from the right keys
tgolen Aug 9, 2020
c9e45c1
Improve the signature of bind
tgolen Aug 9, 2020
545be1f
Add a view for displaying history items
tgolen Aug 9, 2020
1807767
Add some more views for displaying history items
tgolen Aug 9, 2020
22aa79e
Display history fragments
tgolen Aug 9, 2020
c8384ab
Better comment
tgolen Aug 9, 2020
a813cde
Fix a semi-broken home page
tgolen Aug 9, 2020
798135c
Apply mobile fixes
AndrewGable Aug 10, 2020
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
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;
63 changes: 27 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,33 @@ 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>
<Beforeunload onBeforeunload={ActiveClientManager.removeClient}>
Copy link
Contributor

Choose a reason for hiding this comment

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

Mobile doesn't play well with this, it seems to use window quite a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dang, this was supposed to be compatible :(

Copy link
Contributor

@AndrewGable AndrewGable Aug 9, 2020

Choose a reason for hiding this comment

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

I am not an expert on this event, but could we use AppState instead? It is supported by RN and RN4W out of the box: https://github.com/necolas/react-native-web#modules

Copy link
Contributor

Choose a reason for hiding this comment

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

I just tested this and yes it does change to inactive when the tab is changed, then back to active when it comes back. This sounds like possibly we need to handle it using a native/web solution (like Router).

<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