GladysProject/Gladys

View on GitHub
front/src/routes/locked/index.js

Summary

Maintainability
A
3 hrs
Test Coverage
import { Component } from 'preact';
import { connect } from 'unistore/preact';
import { Text, Localizer } from 'preact-i18n';
import cx from 'classnames';
import { route } from 'preact-router';
import get from 'get-value';

import style from './style.css';
import { WEBSOCKET_MESSAGE_TYPES } from '../../../../server/utils/constants';

const BUTTON_ARRAY = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9]
];

const KeyPadComponent = ({ currentCode, typeLetter, clearPreviousLetter }) => (
  <div>
    <div class="input-group mb-3">
      <input id="fakepasswordfield" class="d-none" type="password" name="fakepasswordfield" />
      <Localizer>
        <input
          type="password"
          class={cx('form-control', style.lockedInput)}
          value={currentCode}
          autocomplete="off"
          readonly="readonly"
          placeholder={<Text id="locked.codePlaceholder" />}
        />
      </Localizer>
      <div class="input-group-append">
        <button class={cx('btn btn-outline-secondary', style.lockedDeleteButton)} onClick={clearPreviousLetter}>
          <i class="fe fe-delete" />
        </button>
      </div>
    </div>
    {BUTTON_ARRAY.map(row => (
      <div class="row">
        {row.map(cell => (
          <div class="col mt-4">
            <button onClick={e => typeLetter(e, cell)} class={cx('btn btn-secondary btn-block', style.lockedButton)}>
              {cell}
            </button>
          </div>
        ))}
      </div>
    ))}

    <div class="row">
      <div class="col mt-4" />
      <div class="col mt-4">
        <button onClick={e => typeLetter(e, 0)} class={cx('btn btn-secondary btn-block', style.lockedButton)}>
          0
        </button>
      </div>
      <div class="col mt-4" />
    </div>
  </div>
);

class Locked extends Component {
  clearPreviousLetter = e => {
    e.preventDefault();
    if (this.state.currentCode.length > 0) {
      this.setState(prevState => {
        return { ...prevState, currentCode: prevState.currentCode.slice(0, -1) };
      });
    }
  };
  typeLetter = (e, letter) => {
    e.preventDefault();
    this.setState(prevState => {
      return { ...prevState, currentCode: prevState.currentCode + letter };
    });
  };
  redirectToDashboard = () => {
    route(`/dashboard${window.location.search}`);
  };
  init = async () => {
    try {
      // We make a dumb request just to verify if our token is valid
      await this.props.httpClient.post('/api/v1/access_token', {
        refresh_token: this.props.session.getRefreshToken(),
        scope: ['dashboard:write']
      });

      // if this resolves, we redirect to dashboard
      this.redirectToDashboard();
    } catch (e) {
      this.props.httpClient.setApiScopes(['alarm:write']);
      await this.props.httpClient.refreshAccessToken();
    }
  };
  constructor(props) {
    super(props);
    this.props = props;
    this.state = {
      currentCode: ''
    };
  }
  disarmed = async event => {
    try {
      const houseSelector = this.props.session.getTabletModeCurrentHouseSelector();
      // If the same house was disarmed, redirect to dashboard
      if (event.house === houseSelector) {
        this.props.httpClient.resetApiScopes();
        await this.props.httpClient.refreshAccessToken();
        this.redirectToDashboard();
      }
    } catch (e) {
      console.error(e);
    }
  };
  validateCode = async e => {
    e.preventDefault();
    try {
      await this.setState({
        error: false,
        wrongCode: false,
        tooManyRequests: false,
        errorMessage: null,
        errorStatus: null
      });
      const houseSelector = this.props.session.getTabletModeCurrentHouseSelector();
      await this.props.httpClient.refreshAccessToken();
      await this.props.httpClient.post(`/api/v1/house/${houseSelector}/disarm_with_code`, {
        code: this.state.currentCode
      });
    } catch (e) {
      console.error(e);
      const message = get(e, 'response.data.message');
      const status = get(e, 'response.status');
      this.setState({ currentCode: '' });
      if (status === 429) {
        this.setState({
          tooManyRequests: true,
          waitTimeInMinute: Math.round(get(e, 'response.data.properties.time_before_next', 5 * 60 * 1000) / 1000 / 60)
        });
      } else if (message === 'INVALID_CODE') {
        this.setState({ wrongCode: true });
      } else {
        this.setState({ error: true, errorMessage: e.toString(), errorStatus: status });
      }
    }
  };
  componentDidMount() {
    this.init();
    this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.ALARM.DISARMED, this.disarmed);
  }
  componentWillUnmount() {
    this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.ALARM.DISARMED, this.disarmed);
  }
  render({}, { currentCode, error, wrongCode, tooManyRequests, waitTimeInMinute, errorMessage, errorStatus }) {
    return (
      <div class={cx('container', style.lockedContainer)}>
        <div class="row">
          <div class={cx('col col-login mx-auto', style.lockedMainCol)}>
            <div class="text-center mb-6">
              <h2>
                <Localizer>
                  <img
                    src="/assets/icons/favicon-96x96.png"
                    class="header-brand-img"
                    alt={<Text id="global.logoAlt" />}
                  />
                </Localizer>
                <Text id="login.title" />
              </h2>
            </div>

            <form class="card">
              <div class="card-body p-6">
                <div class={cx('card-title mb-2', style.cardTitle)}>
                  <Text id="locked.cardTitle" />
                </div>
                <p>
                  <Text id="locked.description" />
                </p>
                {error && (
                  <div class="alert alert-danger">
                    <Text id="locked.error" />
                  </div>
                )}
                {errorMessage && (
                  <div class="alert alert-danger">
                    {errorStatus} {errorMessage}
                  </div>
                )}
                {wrongCode && (
                  <div class="alert alert-warning">
                    <Text id="locked.wrongCodeError" />
                  </div>
                )}
                {tooManyRequests && (
                  <div class="alert alert-danger">
                    <Text id="locked.tooManyRequests" fields={{ count: waitTimeInMinute }} />
                  </div>
                )}
                <div class="form" autocomplete="off">
                  <KeyPadComponent
                    currentCode={currentCode}
                    typeLetter={this.typeLetter}
                    clearPreviousLetter={this.clearPreviousLetter}
                  />
                  <button
                    class={cx('mt-4 btn btn-block btn-outline-success', style.lockedButton)}
                    onClick={this.validateCode}
                  >
                    <Text id="locked.validateButton" />
                  </button>
                </div>
              </div>
            </form>
          </div>
        </div>
      </div>
    );
  }
}

export default connect('httpClient,session', {})(Locked);