Commit c8373cba authored by David Burke's avatar David Burke

Merge branch '191-add-login-style-server-url-input-to-forgot-password' into 'dev'

Resolve "Add login style server url input to forgot password"

Closes #192 and #191

See merge request !162
parents 6c0c3161 13f13bd5
Pipeline #36136186 passed with stages
in 5 minutes and 31 seconds
import { tap, withLatestFrom, map, exhaustMap } from "rxjs/operators";
import { of as observableOf } from "rxjs";
import {
tap,
withLatestFrom,
map,
exhaustMap,
catchError
} from "rxjs/operators";
import { of as observableOf, of, from } from "rxjs";
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
......@@ -24,7 +30,7 @@ import { UserService } from "./user";
import { Store } from "@ngrx/store";
import { IState } from "../app.reducers";
import { HttpErrorResponse } from "@angular/common/http";
import { SetPasswordActionTypes } from "./forgot-password/set-password/set-password.actions";
import { SetPasswordActionTypes } from "./reset-password/set-password/set-password.actions";
@Injectable()
export class LoginEffects {
......@@ -35,20 +41,22 @@ export class LoginEffects {
map(([action, form]) => form.value),
exhaustMap(auth => {
const callLogin = () => {
return this.userService
.login(
return from(
this.userService.login(
auth.email,
auth.password,
auth.rememberMe ? auth.rememberMe : false
)
.then(resp => new LoginSuccessAction(resp))
.catch(err => new LoginFailureAction(err));
).pipe(
map(resp => new LoginSuccessAction(resp)),
catchError(err => of(new LoginFailureAction(err)))
);
};
const callCheckAndSetUrl = (url: string) => {
return this.userService
.checkAndSetUrl(url)
.then(() => callLogin())
.catch(err => new LoginFailureAction(err));
return this.userService.checkAndSetUrl(url).pipe(
exhaustMap(() => callLogin()),
catchError(err => of(new LoginFailureAction(err)))
);
};
if (auth.url) {
......@@ -60,15 +68,20 @@ export class LoginEffects {
);
@Effect({ dispatch: false })
loginSuccess$ = this.actions$.ofType(AccountActionTypes.LOGIN_SUCCESS, SetPasswordActionTypes.SET_PASSWORD_SUCCESS).pipe(
tap(() => {
if (IS_EXTENSION) {
this.router.navigate(["/popup"]);
} else {
this.router.navigate(["/list"]);
}
})
);
loginSuccess$ = this.actions$
.ofType(
AccountActionTypes.LOGIN_SUCCESS,
SetPasswordActionTypes.SET_PASSWORD_SUCCESS
)
.pipe(
tap(() => {
if (IS_EXTENSION) {
this.router.navigate(["/popup"]);
} else {
this.router.navigate(["/list"]);
}
})
);
@Effect({ dispatch: false })
loginRedirect$ = this.actions$
......
......@@ -19,18 +19,18 @@ import * as fromRegister from "./register/register.reducer";
import * as fromChangePassword from "./change-password/change-password.reducer";
import * as fromDeleteAccount from "./delete/delete.reducer";
import * as fromErrorReporting from "./error-reporting/error-reporting.reducer";
import * as fromForgotPassword from "./forgot-password/forgot-password.reducer";
import * as fromResetPasswordVerify from "./forgot-password/reset-password-verify/reset-password-verify.reducer";
import * as fromSetPassword from "./forgot-password/set-password/set-password.reducer";
import * as fromResetPassword from "./reset-password/reset-password.reducer";
import * as fromResetPasswordVerify from "./reset-password/reset-password-verify/reset-password-verify.reducer";
import * as fromSetPassword from "./reset-password/set-password/set-password.reducer";
import {
ResetPasswordVerifyActionTypes,
ResetPasswordVerifyActionsUnion
} from "./forgot-password/reset-password-verify/reset-password-verify.actions";
} from "./reset-password/reset-password-verify/reset-password-verify.actions";
import {
SetPasswordSuccess,
SetPasswordActionTypes,
ForceSetPassword
} from "./forgot-password/set-password/set-password.actions";
} from "./reset-password/set-password/set-password.actions";
export interface IAuthState {
email: string | null;
......@@ -125,7 +125,7 @@ export interface IAccountState {
changePassword: fromChangePassword.IChangePasswordState;
deleteAccount: fromDeleteAccount.IDeleteAccountState;
errorReporting: fromErrorReporting.IState;
forgotPassword: fromForgotPassword.IForgotPasswordState;
resetPassword: fromResetPassword.IResetPasswordState;
resetPasswordVerify: fromResetPasswordVerify.IResetPasswordVerifyState;
setPassword: fromSetPassword.ISetPasswordState;
}
......@@ -138,7 +138,7 @@ export const reducers: ActionReducerMap<IAccountState> = {
changePassword: fromChangePassword.reducer,
deleteAccount: fromDeleteAccount.reducer,
errorReporting: fromErrorReporting.reducer,
forgotPassword: fromForgotPassword.reducer,
resetPassword: fromResetPassword.reducer,
resetPasswordVerify: fromResetPasswordVerify.reducer,
setPassword: fromSetPassword.reducer
};
......@@ -324,25 +324,25 @@ export const getErrorReportingHasStarted = createSelector(
fromErrorReporting.getHasStarted
);
export const selectForgotPasswordState = createSelector(
export const selectResetPasswordState = createSelector(
selectAccountState,
(state: IAccountState) => state.forgotPassword
(state: IAccountState) => state.resetPassword
);
export const getForgotPasswordForm = createSelector(
selectForgotPasswordState,
fromForgotPassword.getForm
export const getResetPasswordForm = createSelector(
selectResetPasswordState,
fromResetPassword.getForm
);
export const getForgotPasswordHasStarted = createSelector(
selectForgotPasswordState,
fromForgotPassword.getHasStarted
export const getResetPasswordHasStarted = createSelector(
selectResetPasswordState,
fromResetPassword.getHasStarted
);
export const getForgotPasswordHasFinished = createSelector(
selectForgotPasswordState,
fromForgotPassword.getHasFinished
export const getResetPasswordHasFinished = createSelector(
selectResetPasswordState,
fromResetPassword.getHasFinished
);
export const getForgotPasswordErrorMessage = createSelector(
selectForgotPasswordState,
fromForgotPassword.getErrorMessage
export const getResetPasswordErrorMessage = createSelector(
selectResetPasswordState,
fromResetPassword.getErrorMessage
);
export const selectResetPasswordVerifyState = createSelector(
......
......@@ -33,17 +33,18 @@ import { ErrorReportingComponent } from "./error-reporting/error-reporting.compo
import { ErrorReportingContainer } from "./error-reporting/error-reporting.container";
import { ErrorReportingEffects } from "./error-reporting/error-reporting.effects";
import { BackupCodeComponent } from "./backup-code/backup-code.component";
import { ForgotPasswordComponent } from "./forgot-password/forgot-password.component";
import { ResetPasswordComponent } from "./reset-password/reset-password.component";
import { MarketingFrameComponent } from "./marketing-frame/marketing-frame.component";
import { ForgotPasswordContainer } from "./forgot-password/forgot-password.container";
import { ForgotPasswordEffects } from "./forgot-password/forgot-password.effects";
import { ResetPasswordVerifyComponent } from "./forgot-password/reset-password-verify/reset-password-verify.component";
import { ResetPasswordVerifyContainer } from "./forgot-password/reset-password-verify/reset-password-verify.container";
import { ResetPasswordVerifyEffects } from "./forgot-password/reset-password-verify/reset-password-verify.effects";
import { ResetPasswordContainer } from "./reset-password/reset-password.container";
import { ResetPasswordEffects } from "./reset-password/reset-password.effects";
import { ResetPasswordVerifyComponent } from "./reset-password/reset-password-verify/reset-password-verify.component";
import { ResetPasswordVerifyContainer } from "./reset-password/reset-password-verify/reset-password-verify.container";
import { ResetPasswordVerifyEffects } from "./reset-password/reset-password-verify/reset-password-verify.effects";
import { PasswordInputComponent } from "./change-password/password-input/password-input.component";
import { SetPasswordComponent } from "./forgot-password/set-password/set-password.component";
import { SetPasswordContainer } from "./forgot-password/set-password/set-password.container";
import { SetPasswordEffects } from "./forgot-password/set-password/set-password.effects";
import { SetPasswordComponent } from "./reset-password/set-password/set-password.component";
import { SetPasswordContainer } from "./reset-password/set-password/set-password.container";
import { SetPasswordEffects } from "./reset-password/set-password/set-password.effects";
import { ServerSelectComponent } from "./shared/server-select.component";
export const COMPONENTS = [
AccountComponent,
......@@ -61,13 +62,14 @@ export const COMPONENTS = [
RegisterComponent,
RegisterContainer,
BackupCodeComponent,
ForgotPasswordComponent,
ForgotPasswordContainer,
ResetPasswordComponent,
ResetPasswordContainer,
ResetPasswordVerifyComponent,
ResetPasswordVerifyContainer,
SetPasswordComponent,
SetPasswordContainer,
MarketingFrameComponent
MarketingFrameComponent,
ServerSelectComponent
];
export const SERVICES = [UserService, ConfirmEmailGuard];
......@@ -89,7 +91,7 @@ export const SERVICES = [UserService, ConfirmEmailGuard];
RegisterEffects,
ConfirmEmailEffects,
ErrorReportingEffects,
ForgotPasswordEffects,
ResetPasswordEffects,
ResetPasswordVerifyEffects,
SetPasswordEffects
])
......
......@@ -57,39 +57,15 @@
[control]="form.controls.rememberMe"
tabindex="4"
></app-checkbox>
</div>
<div *ngIf="isExtension">
<app-checkbox
title="My company has its own Passit server"
subtext="If you signed up for Passit anywhere other than app.passit.io (e.g. passit.mycompany.com, or your self-hosted server), you’ll need to specify where you want to log&nbsp;in."
[control]="form.controls.showUrl"
tabindex="5"
></app-checkbox>
<div *ngIf="form.value.showUrl" class="form-field form-field--large">
<label [for]="form.controls.url.id" class="form-field__label">Server URL</label>
<input
type="url"
[ngrxFormControlState]="form.controls.url"
class="form-field__input"
[class.form-field__input--invalid]="form.errors._url"
[class.form-field__input--one-action]="form.controls.url.isValidationPending"
tabindex="6"
>
<div class="form-field__actions" *ngIf="form.controls.url.isValidationPending">
<progress-indicator
[inProgress]="form.controls.url.isValidationPending"
[inputAction]="true"></progress-indicator>
</div>
</div>
<ul class="form-field__error-list">
<li *ngIf="form.errors._url?.$exists" class="form-field__error">
Cannot connect to {{ form.errors._url?.$exists }}
</li>
</ul>
</div>
<app-server-select
*ngIf="isExtension"
[showUrlControl]="form.controls.showUrl"
[urlControl]="form.controls.url"
[formErrors]="form.errors"
></app-server-select>
<div class="auth-form__actions">
<button
id="loginSubmit"
......@@ -104,7 +80,8 @@
[inProgress]="hasLoginStarted"
inProgressText="Logging In"
[completed]="hasLoginFinished"
completedText="Logged In"></progress-indicator>
completedText="Logged In"
></progress-indicator>
</div>
</form>
......
......@@ -16,13 +16,20 @@ import { FormGroupState } from "ngrx-forms";
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoginComponent {
@Input() form: FormGroupState<ILoginForm>;
@Input() errorMessage: string;
@Input() hasLoginStarted: boolean;
@Input() hasLoginFinished: boolean;
@Input() isPopup: boolean;
@Output() login = new EventEmitter();
@Output() goToRegister = new EventEmitter();
@Input()
form: FormGroupState<ILoginForm>;
@Input()
errorMessage: string;
@Input()
hasLoginStarted: boolean;
@Input()
hasLoginFinished: boolean;
@Input()
isPopup: boolean;
@Output()
login = new EventEmitter();
@Output()
goToRegister = new EventEmitter();
isExtension = IS_EXTENSION;
constructor() {}
......
......@@ -14,6 +14,7 @@ import * as fromRoot from "../../app.reducers";
import { RouterTestingModule } from "@angular/router/testing";
import { SharedModule } from "../../shared";
import { MarketingFrameComponent } from "../marketing-frame/marketing-frame.component";
import { ServerSelectComponent } from "../shared/server-select.component";
describe("LoginComponent", () => {
let component: LoginContainer;
......@@ -25,6 +26,7 @@ describe("LoginComponent", () => {
ProgressIndicatorComponent,
LoginContainer,
LoginComponent,
ServerSelectComponent,
MarketingFrameComponent
],
imports: [
......
import { map, exhaustMap, filter, tap, withLatestFrom } from "rxjs/operators";
import {
map,
exhaustMap,
filter,
tap,
withLatestFrom,
catchError
} from "rxjs/operators";
import "rxjs/add/operator/do";
import "rxjs/add/operator/exhaustMap";
import "rxjs/add/operator/map";
......@@ -30,6 +37,7 @@ import { MoonMail } from "../moonmail/moonmail.service";
import { Store } from "@ngrx/store";
import { IState } from "../../app.reducers";
import { IPassitSDKError } from "../../ngsdk";
import { of } from "rxjs";
@Injectable()
export class RegisterEffects {
......@@ -69,14 +77,10 @@ export class RegisterEffects {
withLatestFrom(this.store.select(fromAccount.getUrlForm)),
map(([action, form]) => form),
exhaustMap(form => {
return this.userService
.checkAndSetUrl(form.value.url)
.then(() => {
return new CheckUrlSuccessAction();
})
.catch(() => {
return new CheckUrlFailureAction();
});
return this.userService.checkAndSetUrl(form.value.url).pipe(
map(() => new CheckUrlSuccessAction()),
catchError(() => of(new CheckUrlFailureAction()))
);
})
);
......
......@@ -23,7 +23,6 @@ import { BACKUP_CODE_CHARS, BACKUP_CODE_LENGTH } from "../constants";
export class ResetPasswordVerifyContainer implements OnInit {
form$ = this.store.select(fromAccount.getResetPasswordVerifyForm);
hasStarted$ = this.store.select(fromAccount.getResetPasswordVerifyHasStarted);
// hasFinished$ = this.store.select(fromAccount.getForgotPasswordHasFinished);
errorMessage$ = this.store.select(
fromAccount.getResetPasswordVerifyErrorMessage
);
......
......@@ -12,7 +12,7 @@ import {
ResetPasswordVerifyActionsUnion
} from "./reset-password-verify.actions";
const FORM_ID = "Forgot Password Form";
const FORM_ID = "Reset Password Form";
export interface IResetPasswordVerifyForm {
code: string;
......
import { Action } from "@ngrx/store";
export enum ForgotPasswordActionTypes {
SUBMIT_FORM = "[Forgot Password] Submit",
SUBMIT_FORM_SUCCESS = "[Forgot Password] Submit Success",
SUBMIT_FORM_FAILURE = "[Forgot Password] Submit Failure",
RESET_FORM = "[Forgot Password] Reset"
export enum ResetPasswordActionTypes {
SUBMIT_FORM = "[Reset Password] Submit",
SUBMIT_FORM_SUCCESS = "[Reset Password] Submit Success",
SUBMIT_FORM_FAILURE = "[Reset Password] Submit Failure",
RESET_FORM = "[Reset Password] Reset"
}
export class SubmitForm implements Action {
readonly type = ForgotPasswordActionTypes.SUBMIT_FORM;
readonly type = ResetPasswordActionTypes.SUBMIT_FORM;
}
export class SubmitFormSuccess implements Action {
readonly type = ForgotPasswordActionTypes.SUBMIT_FORM_SUCCESS;
readonly type = ResetPasswordActionTypes.SUBMIT_FORM_SUCCESS;
}
export class SubmitFormFailure implements Action {
readonly type = ForgotPasswordActionTypes.SUBMIT_FORM_FAILURE;
readonly type = ResetPasswordActionTypes.SUBMIT_FORM_FAILURE;
}
export class ResetForm implements Action {
readonly type = ForgotPasswordActionTypes.RESET_FORM;
readonly type = ResetPasswordActionTypes.RESET_FORM;
}
export type ForgotPasswordActionsUnion =
export type ResetPasswordActionsUnion =
| SubmitForm
| SubmitFormSuccess
| SubmitFormFailure
......
......@@ -33,24 +33,12 @@
</div>
<div *ngIf="!hasFinished">
<div *ngIf="isExtension">
<app-checkbox title="My company has its own Passit server" subtext="If you signed up for Passit anywhere other than app.passit.io (e.g. passit.mycompany.com, or your self-hosted server), you’ll need to specify where you want to log&nbsp;in."
[control]="form.controls.showUrl"></app-checkbox>
<div *ngIf="form.value.showUrl" class="form-field form-field--large">
<label [for]="form.controls.url.id" class="form-field__label">Server URL</label>
<input type="url" [ngrxFormControlState]="form.controls.url" class="form-field__input"
[class.form-field__input--invalid]="form.errors._url" [class.form-field__input--one-action]="form.controls.url.isValidationPending">
<div class="form-field__actions" *ngIf="form.controls.url.isValidationPending">
<progress-indicator [inProgress]="form.controls.url.isValidationPending" [inputAction]="true"></progress-indicator>
</div>
</div>
<ul class="form-field__error-list">
<li *ngIf="form.errors._url?.$exists" class="form-field__error">
Cannot connect to {{ form.errors._url?.$exists }}
</li>
</ul>
</div>
<app-server-select
*ngIf="isExtension"
[showUrlControl]="form.controls.showUrl"
[urlControl]="form.controls.url"
[formErrors]="form.errors"
></app-server-select>
</div>
<div *ngIf="!hasFinished" class="auth-form__actions">
......
......@@ -6,25 +6,24 @@ import {
EventEmitter,
ViewChild,
ElementRef,
OnInit,
OnInit
} from "@angular/core";
import { FormGroupState } from "ngrx-forms";
import { IForgotPasswordForm } from "./forgot-password.reducer";
import { IResetPasswordForm } from "./reset-password.reducer";
@Component({
selector: "app-forgot-password",
templateUrl: "./forgot-password.component.html",
styleUrls: ["../account.component.scss", "./forgot-password.styles.scss"],
selector: "app-reset-password",
templateUrl: "./reset-password.component.html",
styleUrls: ["../account.component.scss", "./reset-password.styles.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ForgotPasswordComponent implements OnInit {
export class ResetPasswordComponent implements OnInit {
@Input()
isExtension: boolean;
@Input()
form: FormGroupState<IForgotPasswordForm>;
form: FormGroupState<IResetPasswordForm>;
@Input()
errorMessage: string;
@Input()
......@@ -55,6 +54,6 @@ export class ForgotPasswordComponent implements OnInit {
onReset() {
this.reset.emit();
setTimeout(() => (this.emailInput.nativeElement.focus()), 0);
setTimeout(() => this.emailInput.nativeElement.focus(), 0);
}
}
import { Component, ChangeDetectionStrategy } from "@angular/core";
import * as fromAccount from "../account.reducer";
import { Store } from "@ngrx/store";
import { SubmitForm, ResetForm } from "./forgot-password.actions";
import { SubmitForm, ResetForm } from "./reset-password.actions";
import { IS_EXTENSION } from "../../constants";
@Component({
template: `
<app-forgot-password
<app-reset-password
[form]="form$ | async"
[isExtension]="isExtension"
[hasStarted]="hasStarted$ | async"
......@@ -14,15 +14,15 @@ import { IS_EXTENSION } from "../../constants";
[errorMessage]="errorMessage$ | async"
(submitEmail)="submitEmail()"
(reset)="reset()"
></app-forgot-password>
></app-reset-password>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ForgotPasswordContainer {
form$ = this.store.select(fromAccount.getForgotPasswordForm);
hasStarted$ = this.store.select(fromAccount.getForgotPasswordHasStarted);
hasFinished$ = this.store.select(fromAccount.getForgotPasswordHasFinished);
errorMessage$ = this.store.select(fromAccount.getForgotPasswordErrorMessage);
export class ResetPasswordContainer {
form$ = this.store.select(fromAccount.getResetPasswordForm);
hasStarted$ = this.store.select(fromAccount.getResetPasswordHasStarted);
hasFinished$ = this.store.select(fromAccount.getResetPasswordHasFinished);
errorMessage$ = this.store.select(fromAccount.getResetPasswordErrorMessage);
isExtension = IS_EXTENSION;
constructor(private store: Store<fromAccount.IAuthState>) {}
......
......@@ -2,34 +2,47 @@ import { Injectable } from "@angular/core";
import { Effect, Actions } from "@ngrx/effects";
import {
SubmitForm,
ForgotPasswordActionTypes,
ResetPasswordActionTypes,
SubmitFormSuccess,
SubmitFormFailure
} from "./forgot-password.actions";
import { withLatestFrom, map, exhaustMap, catchError } from "rxjs/operators";
} from "./reset-password.actions";
import {
withLatestFrom,
map,
exhaustMap,
catchError,
filter,
distinctUntilChanged,
switchMap
} from "rxjs/operators";
import { Store } from "@ngrx/store";
import { IState } from "../../app.reducers";
import { getForgotPasswordForm } from "../account.reducer";
import { getResetPasswordForm } from "../account.reducer";
import { UserService } from "../user";
import { of } from "rxjs";
import { of, concat, timer } from "rxjs";
import {
StartAsyncValidationAction,
ClearAsyncErrorAction,
SetAsyncErrorAction
} from "ngrx-forms";
@Injectable()
export class ForgotPasswordEffects {
export class ResetPasswordEffects {
@Effect()
submitForm$ = this.actions$
.ofType<SubmitForm>(ForgotPasswordActionTypes.SUBMIT_FORM)
.ofType<SubmitForm>(ResetPasswordActionTypes.SUBMIT_FORM)
.pipe(
withLatestFrom(this.store.select(getForgotPasswordForm)),
withLatestFrom(this.store.select(getResetPasswordForm)),
map(([action, form]) => form.value),
exhaustMap(form => {
const callForgotPassword = () => {
return this.userService.forgotPassword(form.email).pipe(
return this.userService.resetPassword(form.email).pipe(
map(() => new SubmitFormSuccess()),
catchError(err => of(new SubmitFormFailure()))
);
};
const callCheckAndSetUrl = (url: string) => {
return of(this.userService.checkAndSetUrl(url)).pipe(
return this.userService.checkAndSetUrl(url).pipe(
exhaustMap(() => callForgotPassword()),
catchError(err => of(new SubmitFormFailure()))
);
......@@ -43,6 +56,33 @@ export class ForgotPasswordEffects {
})
);
@Effect()
asyncServerUrlCheck$ = this.store.select(getResetPasswordForm).pipe(
filter(form => form.value.showUrl),
distinctUntilChanged(
(first, second) => first.value.url === second.value.url
),
switchMap(form =>
concat(
timer(300).pipe(
map(
() => new StartAsyncValidationAction(form.controls.url.id, "exists")
)
),
this.userService.checkUrl(form.value.url).pipe(
map(() => new ClearAsyncErrorAction(form.controls.url.id, "exists")),
catchError(() => [
new SetAsyncErrorAction(
form.controls.url.id,
"exists",
form.value.url
)
])
)
)
)
);
constructor(
private actions$: Actions,
private store: Store<IState>,
......
......@@ -9,68 +9,68 @@ import { required, pattern } from "ngrx-forms/validation";
import { IBaseFormState } from "../../utils/interfaces";
import {
ForgotPasswordActionsUnion,
ForgotPasswordActionTypes
} from "./forgot-password.actions";
ResetPasswordActionsUnion,
ResetPasswordActionTypes
} from "./reset-password.actions";
const FORM_ID = "Forgot Password Form";
const FORM_ID = "Reset Password Form";
export interface IForgotPasswordForm {
export interface IResetPasswordForm {
email: string;
showUrl: boolean;
url: string;
}
const validateAndUpdateFormState = updateGroup<IForgotPasswordForm>({
const validateAndUpdateFormState = updateGroup<IResetPasswordForm>({
email: validate(required, pattern(/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/))
});
export const initialFormState = validateAndUpdateFormState(
createFormGroupState<IForgotPasswordForm>(FORM_ID, {
createFormGroupState<IResetPasswordForm>(FORM_ID, {
email: "",
showUrl: false,
url: ""
})
);
export interface IForgotPasswordState extends IBaseFormState {
form: FormGroupState<IForgotPasswordForm>;
export interface IResetPasswordState extends IBaseFormState {
form: FormGroupState<IResetPasswordForm>;
errorMessage: string | null;
}
const initialState: IForgotPasswordState = {
const initialState: IResetPasswordState = {
form: initialFormState,
hasStarted: false,
hasFinished: false,
errorMessage: null
};