diff --git a/package.json b/package.json index 46e2d70..91bb703 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,13 @@ "react": "^16.2.0", "react-big-calendar": "^0.17.0", "react-dom": "^16.2.0", + "react-redux": "^5.0.6", "react-router-dom": "^4.2.2", "react-scripts-ts": "2.8.0", + "redux": "^3.7.2", + "redux-logger": "^3.0.6", + "redux-persist": "^5.4.0", + "redux-thunk": "^2.2.0", "sjcl": "git+https://github.com/etesync/sjcl", "urijs": "^1.16.1" }, @@ -29,8 +34,10 @@ "@types/react": "^16.0.25", "@types/react-big-calendar": "^0.15.0", "@types/react-dom": "^16.0.3", + "@types/react-redux": "^5.0.14", "@types/react-router": "^4.0.19", "@types/react-router-dom": "^4.2.3", + "@types/redux-logger": "^3.0.5", "@types/sjcl": "^1.0.28", "@types/urijs": "^1.15.34" } diff --git a/src/EteSyncContext.tsx b/src/EteSyncContext.tsx index 8b9f740..480d4b6 100644 --- a/src/EteSyncContext.tsx +++ b/src/EteSyncContext.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { Switch, Route, Redirect } from 'react-router'; +import { connect } from 'react-redux'; +import { Switch, Route, Redirect, withRouter } from 'react-router'; import Paper from 'material-ui/Paper'; import RaisedButton from 'material-ui/RaisedButton'; import TextField from 'material-ui/TextField'; @@ -11,17 +12,10 @@ import { JournalView } from './JournalView'; import * as EteSync from './api/EteSync'; import { routeResolver, getPalette } from './App'; +import * as store from './store'; import * as C from './Constants'; -const CONTEXT_SESSION_KEY = 'EteSyncContext'; - -enum LoadState { - Initial = 'INIT', - Working = 'WORKING', - Done = 'DONE', -} - export interface EteSyncContextType { serviceApiUrl: string; credentials: EteSync.Credentials; @@ -35,12 +29,35 @@ interface FormErrors { errorServer?: string; } -export class EteSyncContext extends React.Component { +function fetchCredentials(username: string, password: string, encryptionPassword: string, server: string) { + const authenticator = new EteSync.Authenticator(server); + + return (dispatch: any) => { + dispatch(store.credentialsRequest()); + + authenticator.getAuthToken(username, password).then( + (authToken) => { + const credentials = new EteSync.Credentials(username, authToken); + const derived = EteSync.deriveKey(username, encryptionPassword); + + const context = { + serviceApiUrl: server, + credentials, + encryptionKey: derived, + }; + + dispatch(store.credentialsSuccess(context)); + }, + (error) => { + dispatch(store.credentialsFailure(error)); + } + ); + }; +} + +export class EteSyncContextInner extends React.Component { state: { - context?: EteSyncContextType; - loadState: LoadState; showAdvanced?: boolean; - error?: Error; errors: FormErrors; server: string; @@ -49,10 +66,13 @@ export class EteSyncContext extends React.Component { encryptionPassword: string; }; + props: { + credentials: store.CredentialsType; + }; + constructor(props: any) { super(props); this.state = { - loadState: LoadState.Initial, errors: {}, server: '', username: '', @@ -62,17 +82,6 @@ export class EteSyncContext extends React.Component { this.generateEncryption = this.generateEncryption.bind(this); this.toggleAdvancedSettings = this.toggleAdvancedSettings.bind(this); this.handleInputChange = this.handleInputChange.bind(this); - - const contextStr = sessionStorage.getItem(CONTEXT_SESSION_KEY); - - if (contextStr !== null) { - const context: EteSyncContextType = JSON.parse(contextStr); - - this.state = Object.assign({}, this.state, { - loadState: LoadState.Done, - context - }); - } } handleInputChange(event: any) { @@ -87,8 +96,6 @@ export class EteSyncContext extends React.Component { e.preventDefault(); const server = this.state.showAdvanced ? this.state.server : C.serviceApiBase; - let authenticator = new EteSync.Authenticator(server); - const username = this.state.username; const password = this.state.password; const encryptionPassword = this.state.encryptionPassword; @@ -109,32 +116,7 @@ export class EteSyncContext extends React.Component { return; } - this.setState({ - loadState: LoadState.Working - }); - - authenticator.getAuthToken(username, password).then((authToken) => { - const credentials = new EteSync.Credentials(username, authToken); - const derived = EteSync.deriveKey(username, encryptionPassword); - - const context = { - serviceApiUrl: server, - credentials, - encryptionKey: derived, - }; - - sessionStorage.setItem(CONTEXT_SESSION_KEY, JSON.stringify(context)); - - this.setState({ - loadState: LoadState.Done, - context - }); - }).catch((error) => { - this.setState({ - loadState: LoadState.Initial, - error - }); - }); + store.store.dispatch(fetchCredentials(username, password, encryptionPassword, server)); } toggleAdvancedSettings() { @@ -142,7 +124,10 @@ export class EteSyncContext extends React.Component { } render() { - if (this.state.loadState === LoadState.Initial) { + if (((this.props.credentials.status === store.FetchStatus.Initial) && + (this.props.credentials.credentials === undefined)) || + (this.props.credentials.status === store.FetchStatus.Failure)) { + let advancedSettings = null; if (this.state.showAdvanced) { advancedSettings = ( @@ -187,7 +172,7 @@ export class EteSyncContext extends React.Component { return (
- {(this.state.error !== undefined) && (
Error! {this.state.error.message}
)} + {(this.props.credentials.error !== undefined) && (
Error! {this.props.credentials.error.message}
)}

Please Log In

); - } else if ((this.state.context === undefined) || - (this.state.loadState === LoadState.Working)) { + } else if (this.props.credentials.status === store.FetchStatus.Request) { return (
Loading
); } - let context: EteSyncContextType = this.state.context; + let context = this.props.credentials.credentials as store.CredentialsData; return (
@@ -260,3 +244,13 @@ export class EteSyncContext extends React.Component { ); } } + +const mapStateToProps = (state: store.StoreState) => { + return { + credentials: state.credentials, + }; +}; + +export const EteSyncContext = withRouter(connect( + mapStateToProps +)(EteSyncContextInner)); diff --git a/src/index.tsx b/src/index.tsx index 1c66245..6dbd0d0 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,11 +1,19 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { Provider } from 'react-redux'; +import { PersistGate } from 'redux-persist/es/integration/react'; import App from './App'; import registerServiceWorker from './registerServiceWorker'; import './index.css'; +import { store, persistor } from './store'; + ReactDOM.render( - , + + + + + , document.getElementById('root') as HTMLElement ); registerServiceWorker(); diff --git a/src/redux-persist.d.ts b/src/redux-persist.d.ts new file mode 100644 index 0000000..6d6e080 --- /dev/null +++ b/src/redux-persist.d.ts @@ -0,0 +1,3 @@ +declare module 'redux-persist'; +declare module 'redux-persist/lib/storage/session'; +declare module 'redux-persist/es/integration/react'; diff --git a/src/store.tsx b/src/store.tsx new file mode 100644 index 0000000..3c5b26a --- /dev/null +++ b/src/store.tsx @@ -0,0 +1,113 @@ +import { createStore, combineReducers, applyMiddleware } from 'redux'; +import { persistReducer, persistStore } from 'redux-persist'; +import session from 'redux-persist/lib/storage/session'; +import thunkMiddleware from 'redux-thunk'; +import { createLogger } from 'redux-logger'; + +import * as EteSync from './api/EteSync'; + +const loggerMiddleware = createLogger(); + +enum Actions { + FETCH_CREDENTIALS = 'FETCH_CREDENTIALS', +} + +export enum FetchStatus { + Initial = 'INITIAL', + Request = 'REQUEST', + Failure = 'FAILURE', + Success = 'SUCCESS', +} + +export interface CredentialsData { + serviceApiUrl: string; + credentials: EteSync.Credentials; + encryptionKey: string; +} + +export interface CredentialsType { + status: FetchStatus; + error?: Error; + credentials?: CredentialsData; +} + +export interface StoreState { + fetchCount: number; + credentials: CredentialsData; +} + +export function credentialsSuccess(creds: CredentialsData) { + return { + type: Actions.FETCH_CREDENTIALS, + status: FetchStatus.Success, + credentials: creds, + }; +} + +export function credentialsRequest() { + return { + type: Actions.FETCH_CREDENTIALS, + status: FetchStatus.Request, + }; +} + +export function credentialsFailure(error: Error) { + return { + type: Actions.FETCH_CREDENTIALS, + status: FetchStatus.Failure, + error + }; +} + +function credentials(state: CredentialsType = {status: FetchStatus.Initial}, action: any) { + switch (action.type) { + case Actions.FETCH_CREDENTIALS: + if (action.status === FetchStatus.Success) { + return { + status: action.status, + credentials: action.credentials, + }; + } else { + return { + status: action.status, + }; + } + default: + return state; + } +} + +function fetchCount(state: number = 0, action: any) { + // FIXME: Make it automatic by action properties. + switch (action.type) { + case Actions.FETCH_CREDENTIALS: + if (action.status === FetchStatus.Request) { + return state + 1; + } else { + return state - 1; + } + default: + return state; + } +} + +const credentialsPersistConfig = { + key: 'credentials', + storage: session, + whitelist: ['credentials'], +}; + +const reducers = combineReducers({ + fetchCount, + credentials: persistReducer(credentialsPersistConfig, credentials), +}); + +export const store = createStore( + reducers, + applyMiddleware( + thunkMiddleware, + loggerMiddleware + ) +); + +export const persistor = persistStore(store); diff --git a/yarn.lock b/yarn.lock index 0416016..97063d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -48,6 +48,13 @@ "@types/node" "*" "@types/react" "*" +"@types/react-redux@^5.0.14": + version "5.0.14" + resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-5.0.14.tgz#f3fc30dcbb2d20455a714f591cc27f77b4df09bb" + dependencies: + "@types/react" "*" + redux "^3.6.0" + "@types/react-router-dom@^4.2.3": version "4.2.3" resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.2.3.tgz#06e0b67ff536adc0681dffdbe592ae91fb85887d" @@ -67,6 +74,12 @@ version "16.0.25" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.0.25.tgz#bf696b83fe480c5e0eff4335ee39ebc95884a1ed" +"@types/redux-logger@^3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/redux-logger/-/redux-logger-3.0.5.tgz#d1a02758f90845899cd304aa0912daeba2028eb6" + dependencies: + redux "^3.6.0" + "@types/sjcl@^1.0.28": version "1.0.28" resolved "https://registry.yarnpkg.com/@types/sjcl/-/sjcl-1.0.28.tgz#4693eb6943e385e844a70fb25b4699db286c7214" @@ -1310,6 +1323,10 @@ decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +deep-diff@^0.3.5: + version "0.3.8" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84" + deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -2297,7 +2314,7 @@ hoek@4.x.x: version "4.2.0" resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" -hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.3.1: +hoist-non-react-statics@^2.2.1, hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0" @@ -2526,7 +2543,7 @@ interpret@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614" -invariant@^2.1.0, invariant@^2.2.1, invariant@^2.2.2: +invariant@^2.0.0, invariant@^2.1.0, invariant@^2.2.1, invariant@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" dependencies: @@ -3240,6 +3257,10 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" +lodash-es@^4.2.0, lodash-es@^4.2.1: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7" + lodash._reinterpolate@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" @@ -3281,7 +3302,7 @@ lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" -"lodash@>=3.5 <5", lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.3.0: +"lodash@>=3.5 <5", lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -4513,6 +4534,17 @@ react-prop-types@^0.4.0: dependencies: warning "^3.0.0" +react-redux@^5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.6.tgz#23ed3a4f986359d68b5212eaaa681e60d6574946" + dependencies: + hoist-non-react-statics "^2.2.1" + invariant "^2.0.0" + lodash "^4.2.0" + lodash-es "^4.2.0" + loose-envify "^1.1.0" + prop-types "^15.5.10" + react-router-dom@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-4.2.2.tgz#c8a81df3adc58bba8a76782e946cbd4eae649b8d" @@ -4694,6 +4726,29 @@ reduce-function-call@^1.0.1: dependencies: balanced-match "^0.4.2" +redux-logger@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" + dependencies: + deep-diff "^0.3.5" + +redux-persist@^5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-5.4.0.tgz#a1062313546a9d4ca6f9271464d18f736e8ca394" + +redux-thunk@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.2.0.tgz#e615a16e16b47a19a515766133d1e3e99b7852e5" + +redux@^3.6.0, redux@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b" + dependencies: + lodash "^4.2.1" + lodash-es "^4.2.1" + loose-envify "^1.1.0" + symbol-observable "^1.0.3" + regenerate@^1.2.1: version "1.3.3" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.3.3.tgz#0c336d3980553d755c39b586ae3b20aa49c82b7f" @@ -5355,7 +5410,7 @@ sw-toolbox@^3.4.0: path-to-regexp "^1.0.1" serviceworker-cache-polyfill "^4.0.0" -symbol-observable@^1.0.4: +symbol-observable@^1.0.3, symbol-observable@^1.0.4: version "1.1.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.1.0.tgz#5c68fd8d54115d9dfb72a84720549222e8db9b32"