Skip to content

Commit

Permalink
Fixing minor state machine bug in SignInForm (#21)
Browse files Browse the repository at this point in the history
* Ability to search for existing HME addresses

* Linting

* Last item borderless

* Fix borders, error refresh

* Fixing minor state machine bug in SignInForm
  • Loading branch information
dedoussis authored Jan 21, 2023
1 parent 05583a7 commit fcded91
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 70 deletions.
2 changes: 1 addition & 1 deletion src/pages/Background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
ReservationRequestData,
sendMessageToActiveTab,
} from '../../messages';
import { PopupState } from '../Popup/Popup';
import { PopupState } from '../Popup/stateMachine';
import browser from 'webextension-polyfill';

const getClient = async (withTokenValidation = true): Promise<ICloudClient> => {
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Popup/Popup.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ input[type='search']::-webkit-search-cancel-button {
padding-left: 5%;
}
input[type='search']:focus::-webkit-search-cancel-button {
opacity: 0.3;
opacity: 0.25;
pointer-events: all;
}

Expand Down
133 changes: 65 additions & 68 deletions src/pages/Popup/Popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import React, {
ButtonHTMLAttributes,
DetailedHTMLProps,
ReactNode,
ReactElement,
} from 'react';
import ICloudClient, {
PremiumMailSettings,
Expand Down Expand Up @@ -45,19 +46,17 @@ import browser from 'webextension-polyfill';
import { setupWebRequestListeners } from '../../webRequestUtils';
import Fuse from 'fuse.js';
import isEqual from 'lodash.isequal';
import {
PopupAction,
PopupState,
SignedInAction,
SignedOutAction,
STATE_MACHINE_TRANSITIONS,
VerifiedAction,
VerifiedAndManagingAction,
} from './stateMachine';

enum PopupTransition {
SuccessfulSignIn,
FailedSignIn,
SuccessfulVerification,
FailedVerification,
SuccessfulSignOut,
FailedSignOut,
List,
Generate,
}

type Callback = (transition: PopupTransition) => void;
type TransitionCallback<T extends PopupAction> = (action: T) => void;

// The iCloud API requires the Origin and Referer HTTP headers of a request
// to be set to https://www.icloud.com.
Expand All @@ -78,7 +77,10 @@ if (browser.webRequest !== undefined) {
setupWebRequestListeners();
}

const SignInForm = (props: { callback: Callback; client: ICloudClient }) => {
const SignInForm = (props: {
callback: TransitionCallback<SignedOutAction>;
client: ICloudClient;
}) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
Expand All @@ -94,9 +96,9 @@ const SignInForm = (props: { callback: Callback; client: ICloudClient }) => {
await props.client.accountLogin();
setIsSubmitting(false);
if (props.client.requires2fa) {
props.callback(PopupTransition.SuccessfulSignIn);
props.callback('SUCCESSFUL_SIGN_IN');
} else {
props.callback(PopupTransition.SuccessfulVerification);
props.callback('SUCCESSFUL_VERIFICATION');
}
} catch (e) {
setIsSubmitting(false);
Expand Down Expand Up @@ -170,7 +172,10 @@ const SignInForm = (props: { callback: Callback; client: ICloudClient }) => {
);
};

const TwoFaForm = (props: { callback: Callback; client: ICloudClient }) => {
const TwoFaForm = (props: {
callback: TransitionCallback<SignedInAction>;
client: ICloudClient;
}) => {
const [code, setCode] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string>();
Expand All @@ -187,7 +192,7 @@ const TwoFaForm = (props: { callback: Callback; client: ICloudClient }) => {

setIsSubmitting(false);

props.callback(PopupTransition.SuccessfulVerification);
props.callback('SUCCESSFUL_VERIFICATION');
} catch (e) {
setIsSubmitting(false);
setError(
Expand Down Expand Up @@ -286,21 +291,27 @@ const FooterButton = (
);
};

const SignOutButton = (props: { callback: Callback; client: ICloudClient }) => {
const SignOutButton = (props: {
callback: TransitionCallback<'SUCCESSFUL_SIGN_OUT'>;
client: ICloudClient;
}) => {
return (
<FooterButton
className="text-sky-400 hover:text-sky-500 focus:outline-sky-400"
onClick={async () => {
await props.client.logOut();
props.callback(PopupTransition.SuccessfulSignOut);
props.callback('SUCCESSFUL_SIGN_OUT');
}}
label="Sign out"
icon={faSignOut}
/>
);
};

const HmeGenerator = (props: { callback: Callback; client: ICloudClient }) => {
const HmeGenerator = (props: {
callback: TransitionCallback<VerifiedAction>;
client: ICloudClient;
}) => {
const [hmeEmail, setHmeEmail] = useState<string>();
const [hmeError, setHmeError] = useState<string>();

Expand Down Expand Up @@ -480,7 +491,7 @@ const HmeGenerator = (props: { callback: Callback; client: ICloudClient }) => {
<div className="grid grid-cols-2">
<div>
<FooterButton
onClick={() => props.callback(PopupTransition.List)}
onClick={() => props.callback('MANAGE')}
icon={faList}
label="Manage emails"
/>
Expand Down Expand Up @@ -650,7 +661,10 @@ const searchHmeEmails = (
return searchResults.map((result) => result.item);
};

const HmeManager = (props: { callback: Callback; client: ICloudClient }) => {
const HmeManager = (props: {
callback: TransitionCallback<VerifiedAndManagingAction>;
client: ICloudClient;
}) => {
const [fetchedHmeEmails, setFetchedHmeEmails] = useState<HmeEmail[]>();
const [hmeEmailsError, setHmeEmailsError] = useState<string>();
const [isFetching, setIsFetching] = useState(false);
Expand Down Expand Up @@ -709,7 +723,7 @@ const HmeManager = (props: { callback: Callback; client: ICloudClient }) => {
</div>
<input
type="search"
className="pl-9 p-2 w-full rounded placeholder-gray-400 focus:outline-none focus:ring-1 focus:ring-sky-400"
className="pl-9 p-2 w-full rounded placeholder-gray-400 border border-gray-200 focus:outline-none focus:border-sky-400"
placeholder="Search"
aria-label="Search through your HideMyEmail addresses"
onChange={(e) => {
Expand Down Expand Up @@ -801,7 +815,7 @@ const HmeManager = (props: { callback: Callback; client: ICloudClient }) => {
<div className="grid grid-cols-2">
<div>
<FooterButton
onClick={() => props.callback(PopupTransition.Generate)}
onClick={() => props.callback('GENERATE')}
icon={faPlus}
label="Generate new email"
/>
Expand All @@ -814,54 +828,37 @@ const HmeManager = (props: { callback: Callback; client: ICloudClient }) => {
);
};

export enum PopupState {
SignedIn,
Verified,
SignedOut,
VerifiedAndManaging,
}

const STATE_ELEMENTS: {
[key in PopupState]: React.FC<{ callback: Callback; client: ICloudClient }>;
} = {
[PopupState.SignedOut]: SignInForm,
[PopupState.SignedIn]: TwoFaForm,
[PopupState.Verified]: HmeGenerator,
[PopupState.VerifiedAndManaging]: HmeManager,
};

const STATE_MACHINE_TRANSITIONS: {
[key in PopupState]: { [key in PopupTransition]?: PopupState };
} = {
[PopupState.SignedOut]: {
[PopupTransition.SuccessfulSignIn]: PopupState.SignedIn,
},
[PopupState.SignedIn]: {
[PopupTransition.SuccessfulVerification]: PopupState.Verified,
[PopupTransition.SuccessfulSignOut]: PopupState.SignedOut,
},
[PopupState.Verified]: {
[PopupTransition.SuccessfulSignOut]: PopupState.SignedOut,
[PopupTransition.List]: PopupState.VerifiedAndManaging,
},
[PopupState.VerifiedAndManaging]: {
[PopupTransition.SuccessfulSignOut]: PopupState.SignedOut,
[PopupTransition.Generate]: PopupState.Verified,
},
};

const transitionToNextStateElement = (
state: PopupState,
setState: Dispatch<PopupState>,
client: ICloudClient
) => {
const callback = (transition: PopupTransition) => {
const currStateTransitions = STATE_MACHINE_TRANSITIONS[state];
const nextState = currStateTransitions[transition];
nextState !== undefined && setState(nextState);
};
const StateElement = STATE_ELEMENTS[state];
return <StateElement callback={callback} client={client} />;
): ReactElement => {
switch (state) {
case PopupState.SignedOut: {
const callback = (action: SignedOutAction) =>
setState(STATE_MACHINE_TRANSITIONS[state][action]);
return <SignInForm callback={callback} client={client} />;
}
case PopupState.SignedIn: {
const callback = (action: SignedInAction) =>
setState(STATE_MACHINE_TRANSITIONS[state][action]);
return <TwoFaForm callback={callback} client={client} />;
}
case PopupState.Verified: {
const callback = (action: VerifiedAction) =>
setState(STATE_MACHINE_TRANSITIONS[state][action]);
return <HmeGenerator callback={callback} client={client} />;
}
case PopupState.VerifiedAndManaging: {
const callback = (action: VerifiedAndManagingAction) =>
setState(STATE_MACHINE_TRANSITIONS[state][action]);
return <HmeManager callback={callback} client={client} />;
}
default: {
const exhaustivenessCheck: never = state;
throw new Error(`Unhandled PopupState case: ${exhaustivenessCheck}`);
}
}
};

const Popup = () => {
Expand Down
52 changes: 52 additions & 0 deletions src/pages/Popup/stateMachine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export enum PopupState {
SignedIn,
Verified,
SignedOut,
VerifiedAndManaging,
}

export type SignedOutAction = 'SUCCESSFUL_SIGN_IN' | 'SUCCESSFUL_VERIFICATION';
export type SignedInAction = 'SUCCESSFUL_VERIFICATION' | 'SUCCESSFUL_SIGN_OUT';
export type VerifiedAction = 'MANAGE' | 'SUCCESSFUL_SIGN_OUT';
export type VerifiedAndManagingAction = 'GENERATE' | 'SUCCESSFUL_SIGN_OUT';
export type PopupAction =
| SignedOutAction
| SignedInAction
| VerifiedAction
| VerifiedAndManagingAction;

type GenericTranstitions<Actions extends PopupAction> = {
[key in Actions]: PopupState;
};

type SignedOutTransitions = GenericTranstitions<SignedOutAction>;
type SignedInTransitions = GenericTranstitions<SignedInAction>;
type VerifiedTransitions = GenericTranstitions<VerifiedAction>;
type VerifiedAndManagingTransition =
GenericTranstitions<VerifiedAndManagingAction>;

type Transitions = {
[PopupState.SignedOut]: SignedOutTransitions;
[PopupState.SignedIn]: SignedInTransitions;
[PopupState.Verified]: VerifiedTransitions;
[PopupState.VerifiedAndManaging]: VerifiedAndManagingTransition;
} & { [key in PopupState]: unknown };

export const STATE_MACHINE_TRANSITIONS: Transitions = {
[PopupState.SignedOut]: {
SUCCESSFUL_SIGN_IN: PopupState.SignedIn,
SUCCESSFUL_VERIFICATION: PopupState.Verified,
},
[PopupState.SignedIn]: {
SUCCESSFUL_VERIFICATION: PopupState.Verified,
SUCCESSFUL_SIGN_OUT: PopupState.SignedOut,
},
[PopupState.Verified]: {
MANAGE: PopupState.VerifiedAndManaging,
SUCCESSFUL_SIGN_OUT: PopupState.SignedOut,
},
[PopupState.VerifiedAndManaging]: {
GENERATE: PopupState.Verified,
SUCCESSFUL_SIGN_OUT: PopupState.SignedOut,
},
};

0 comments on commit fcded91

Please sign in to comment.