feature (prompt): proper prompt system

This commit is contained in:
Mickael KERJEAN
2018-04-06 13:18:38 +10:00
parent 4b06b8a802
commit 0eb59521ab
5 changed files with 93 additions and 86 deletions

View File

@ -8,48 +8,61 @@ export class ModalPrompt extends React.Component {
constructor(props){ constructor(props){
super(props); super(props);
this.state = { this.state = {
appear: false appear: false,
value: ''
}; };
this.onCancel = this.onCancel.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.onEscapeKeyPress = this.onEscapeKeyPress.bind(this);
} }
componentDidMount(){ componentDidMount(){
prompt.subscribe((text, okCallback, cancelCallback, type) => { prompt.subscribe((text, okCallback, cancelCallback, type) => {
console.log("REQUEST FOR PROMPT");
this.setState({ this.setState({
appear: true, appear: true,
value: '',
error: null, error: null,
type: type || 'text', type: type || 'text',
text: text || '', text: text || '',
fns: {ok: okCallback, cancel: cancelCallback} fns: {ok: okCallback, cancel: cancelCallback}
}); });
}); });
window.addEventListener('keydown', this.onEscapeKeyPress);
}
componentDidUmount(){
window.removeEventListener('keydown', this.onEscapeKeyPress);
} }
onCancel(){ onCancel(){
this.setState({appear: false}); this.setState({appear: false});
this.state.fns.cancelCallback(); this.state.fns.cancel();
} }
onSubmit(e){ onSubmit(e){
e && e.preventDefault && e.preventDefault(); e && e.preventDefault && e.preventDefault();
this.state.fns.okCallback(this.state.value) this.state.fns.ok(this.state.value)
.then(() => this.setState({appear: false})) .then(() => this.setState({appear: false}))
.catch((message) => this.setState({error: message})); .catch((message) => this.setState({error: message}));
} }
onEscapeKeyPress(e){
if(e.keyCode === 27 && this.state.fns){ this.onCancel(); }
}
render() { render() {
return ( return (
<Modal isActive={this.state.appear} onQuit={this.onCancel.bind(this)}> <Modal isActive={this.state.appear} onQuit={this.onCancel}>
<div className="component_prompt"> <div className="component_prompt">
<p className="modal-message"> <p className="modal-message">
{this.state.text} {this.state.text}
</p> </p>
<form onSubmit={this.onSubmit.bind(this)}> <form onSubmit={this.onSubmit}>
<Input autoFocus={true} value={this.state.value} type={this.state.type} autoComplete="new-password" onChange={(e) => this.setState({value: e.target.value})} /> <Input autoFocus={true} value={this.state.value} type={this.state.type} autoComplete="new-password" onChange={(e) => this.setState({value: e.target.value})} />
<div className="modal-error-message">{this.state.error}&nbsp;</div> <div className="modal-error-message">{this.state.error}&nbsp;</div>
<div className="buttons"> <div className="buttons">
<Button type="button" onClick={this.onCancel.bind(this)}>CANCEL</Button> <Button type="button" onClick={this.onCancel}>CANCEL</Button>
<Button type="submit" theme="secondary" onClick={this.onSubmit.bind(this)}>OK</Button> <Button type="submit" theme="secondary" onClick={this.onSubmit}>OK</Button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,20 +1,15 @@
import { Observable } from 'rxjs/Observable';
const Prompt = function (){ const Prompt = function (){
let obs = null; let fn = null;
return { return {
emit: function(text, okCallback, cancelCallbck, type){ now: function(text, okCallback, cancelCallback, type){
console.log(obs); if(!fn){ return window.setTimeout(() => this.now(text, okCallback, cancelCallback, type), 50); }
obs.emit(text, okCallback, cancelcallBack, type); fn(text, okCallback, cancelCallback, type);
}, },
subscribe: function(){ subscribe: function(_fn){
console.log("> SUBSCRIBE") fn = _fn;
return new Observable((_obs) => {
console.log(_obs);
obs = _obs;
});
}
}
} }
};
};
export const prompt = new Prompt(); export const prompt = new Prompt();

View File

@ -3,20 +3,32 @@ import PropTypes from 'prop-types';
import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group';
import { ModalPrompt } from '../../components/'; import { ModalPrompt } from '../../components/';
import { encrypt, decrypt, memory } from '../../helpers/'; import { encrypt, decrypt, memory, prompt } from '../../helpers/';
export class Credentials extends React.Component { export class Credentials extends React.Component {
constructor(props){ constructor(props){
super(props); super(props);
const key = memory.get('credentials_key') || ''; const key = memory.get('credentials_key') || ''; // we use a clojure for the "key" because we
// want to persist it in memory, not just in the
// state which is kill whenever react decide
this.state = { this.state = {
modal_appear: key ? false : this.props.remember_me,
key: key || '', key: key || '',
message: '', message: '',
error: null error: null
}; };
// we use a clojure for the "key" because we want to persist it in memory
// not just in the state which is kill whenever react decide if(this.props.remember_me === true){
if(key === ""){
let raw = window.localStorage.hasOwnProperty('credentials');
if(raw){
this.promptForExistingPassword();
}else{
this.promptForNewPassword();
}
}else{
this.hidrate_credentials(key);
}
}
} }
componentWillReceiveProps(new_props){ componentWillReceiveProps(new_props){
@ -27,25 +39,58 @@ export class Credentials extends React.Component {
} }
if(new_props.remember_me === true && this.props.remember_me === false){ if(new_props.remember_me === true && this.props.remember_me === false){
this.setState({modal_appear: true}); this.promptForNewPassword();
this.init();
}else if(new_props.remember_me === false && this.props.remember_me === true){ }else if(new_props.remember_me === false && this.props.remember_me === true){
memory.set('credentials_key', ''); memory.set('credentials_key', '');
this.setState({modal_appear: false, key: ''}); this.setState({key: ''});
} }
} }
componentDidMount(){ promptForExistingPassword(){
this.init(); prompt.now(
if(this.state.key) this.onSubmit(this.state.key); "Your Master Password",
(key) => {
if(!key.trim()) return Promise.reject("Password can\'t be empty");
this.setState({key: key});
memory.set('credentials_key', key);
return this.hidrate_credentials(key);
},
() => {
memory.set('credentials_key', '');
this.setState({key: ''});
},
'password'
);
}
promptForNewPassword(){
prompt.now(
"Pick a Master Password",
(key) => {
if(!key.trim()) return Promise.reject("Password can\'t be empty");
memory.set('credentials_key', key);
this.setState({key: key}, () => {
this.saveCreds(this.props.credentials);
});
return Promise.resolve();
},
() => {},
'password'
);
} }
init(){ hidrate_credentials(key){
let raw = window.localStorage.hasOwnProperty('credentials'); let raw = window.localStorage.getItem('credentials');
if(raw){ if(raw){
this.setState({message: "Your Master Password:"}); try{
let credentials = decrypt(raw, key);
this.props.onCredentialsFound(credentials);
return Promise.resolve();
}catch(e){
return Promise.reject('Incorrect password');
}
}else{ }else{
this.setState({message: "Pick a Master Password:"}); this.saveCreds(this.props.credentials);
return Promise.resolve();
} }
} }
@ -56,52 +101,7 @@ export class Credentials extends React.Component {
} }
} }
onCancel(should_clear){
memory.set('credentials_key', '');
this.setState({modal_appear: false, key: ''});
}
onSubmit(key){
this.setState({key: key});
memory.set('credentials_key', key);
/*
* 2 differents use cases:
* - a user is creating a new master password
* - a user want to unlock existing credentials
*/
if(key !== ''){
let raw = window.localStorage.getItem('credentials');
if(raw){
try{
let credentials = decrypt(raw, key);
this.setState({modal_appear: false});
this.props.onCredentialsFound(credentials);
}catch(e){
this.setState({error: 'Incorrect password'});
}
}else{
this.saveCreds(this.props.credentials);
this.setState({modal_appear: false});
}
}else{
this.setState({error: 'Password can\'t be empty'});
window.setTimeout(() => this.setState({error: null}), 1500);
}
}
render() { render() {
return ( return null;
<ModalPrompt
type="password"
appear={this.state.modal_appear}
error={this.state.error}
message={this.state.message}
onCancel={this.onCancel.bind(this)}
onSubmit={this.onSubmit.bind(this)}
/>
);
} }
} }
Credentials.propTypes = {
};

View File

@ -100,8 +100,7 @@ export class ExistingThing extends React.Component {
} }
onDeleteRequest(filename){ onDeleteRequest(filename){
console.log(prompt); prompt.now(
prompt.emit(
"Confirm by tapping \""+this._confirm_delete_text()+"\"", "Confirm by tapping \""+this._confirm_delete_text()+"\"",
(answer) => { // click on ok (answer) => { // click on ok
if(answer === this._confirm_delete_text()){ if(answer === this._confirm_delete_text()){

View File

@ -2,7 +2,7 @@ import React from 'react';
import { BrowserRouter, Route, IndexRoute, Switch } from 'react-router-dom'; import { BrowserRouter, Route, IndexRoute, Switch } from 'react-router-dom';
import { NotFoundPage, ConnectPage, HomePage, LogoutPage, FilesPage, ViewerPage } from './pages/'; import { NotFoundPage, ConnectPage, HomePage, LogoutPage, FilesPage, ViewerPage } from './pages/';
import { Bundle, URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from './helpers/'; import { Bundle, URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from './helpers/';
import { ModalPrompt, Audio, Video } from './components/'; import { ModalPrompt, Notification, Audio, Video } from './components/';
export default class AppRouter extends React.Component { export default class AppRouter extends React.Component {
render() { render() {