diff --git a/src/pages/Background/index.ts b/src/pages/Background/index.ts index 3d86bae..e341d44 100644 --- a/src/pages/Background/index.ts +++ b/src/pages/Background/index.ts @@ -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 => { diff --git a/src/pages/Popup/Popup.css b/src/pages/Popup/Popup.css index 4905531..dd7f166 100644 --- a/src/pages/Popup/Popup.css +++ b/src/pages/Popup/Popup.css @@ -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; } diff --git a/src/pages/Popup/Popup.tsx b/src/pages/Popup/Popup.tsx index 8a11090..f07443f 100644 --- a/src/pages/Popup/Popup.tsx +++ b/src/pages/Popup/Popup.tsx @@ -5,6 +5,7 @@ import React, { ButtonHTMLAttributes, DetailedHTMLProps, ReactNode, + ReactElement, } from 'react'; import ICloudClient, { PremiumMailSettings, @@ -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 = (action: T) => void; // The iCloud API requires the Origin and Referer HTTP headers of a request // to be set to https://www.icloud.com. @@ -78,7 +77,10 @@ if (browser.webRequest !== undefined) { setupWebRequestListeners(); } -const SignInForm = (props: { callback: Callback; client: ICloudClient }) => { +const SignInForm = (props: { + callback: TransitionCallback; + client: ICloudClient; +}) => { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); @@ -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); @@ -170,7 +172,10 @@ const SignInForm = (props: { callback: Callback; client: ICloudClient }) => { ); }; -const TwoFaForm = (props: { callback: Callback; client: ICloudClient }) => { +const TwoFaForm = (props: { + callback: TransitionCallback; + client: ICloudClient; +}) => { const [code, setCode] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(); @@ -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( @@ -286,13 +291,16 @@ const FooterButton = ( ); }; -const SignOutButton = (props: { callback: Callback; client: ICloudClient }) => { +const SignOutButton = (props: { + callback: TransitionCallback<'SUCCESSFUL_SIGN_OUT'>; + client: ICloudClient; +}) => { return ( { await props.client.logOut(); - props.callback(PopupTransition.SuccessfulSignOut); + props.callback('SUCCESSFUL_SIGN_OUT'); }} label="Sign out" icon={faSignOut} @@ -300,7 +308,10 @@ const SignOutButton = (props: { callback: Callback; client: ICloudClient }) => { ); }; -const HmeGenerator = (props: { callback: Callback; client: ICloudClient }) => { +const HmeGenerator = (props: { + callback: TransitionCallback; + client: ICloudClient; +}) => { const [hmeEmail, setHmeEmail] = useState(); const [hmeError, setHmeError] = useState(); @@ -480,7 +491,7 @@ const HmeGenerator = (props: { callback: Callback; client: ICloudClient }) => {
props.callback(PopupTransition.List)} + onClick={() => props.callback('MANAGE')} icon={faList} label="Manage emails" /> @@ -650,7 +661,10 @@ const searchHmeEmails = ( return searchResults.map((result) => result.item); }; -const HmeManager = (props: { callback: Callback; client: ICloudClient }) => { +const HmeManager = (props: { + callback: TransitionCallback; + client: ICloudClient; +}) => { const [fetchedHmeEmails, setFetchedHmeEmails] = useState(); const [hmeEmailsError, setHmeEmailsError] = useState(); const [isFetching, setIsFetching] = useState(false); @@ -709,7 +723,7 @@ const HmeManager = (props: { callback: Callback; client: ICloudClient }) => {
{ @@ -801,7 +815,7 @@ const HmeManager = (props: { callback: Callback; client: ICloudClient }) => {
props.callback(PopupTransition.Generate)} + onClick={() => props.callback('GENERATE')} icon={faPlus} label="Generate new email" /> @@ -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, 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 ; +): ReactElement => { + switch (state) { + case PopupState.SignedOut: { + const callback = (action: SignedOutAction) => + setState(STATE_MACHINE_TRANSITIONS[state][action]); + return ; + } + case PopupState.SignedIn: { + const callback = (action: SignedInAction) => + setState(STATE_MACHINE_TRANSITIONS[state][action]); + return ; + } + case PopupState.Verified: { + const callback = (action: VerifiedAction) => + setState(STATE_MACHINE_TRANSITIONS[state][action]); + return ; + } + case PopupState.VerifiedAndManaging: { + const callback = (action: VerifiedAndManagingAction) => + setState(STATE_MACHINE_TRANSITIONS[state][action]); + return ; + } + default: { + const exhaustivenessCheck: never = state; + throw new Error(`Unhandled PopupState case: ${exhaustivenessCheck}`); + } + } }; const Popup = () => { diff --git a/src/pages/Popup/stateMachine.ts b/src/pages/Popup/stateMachine.ts new file mode 100644 index 0000000..569bfe7 --- /dev/null +++ b/src/pages/Popup/stateMachine.ts @@ -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 = { + [key in Actions]: PopupState; +}; + +type SignedOutTransitions = GenericTranstitions; +type SignedInTransitions = GenericTranstitions; +type VerifiedTransitions = GenericTranstitions; +type VerifiedAndManagingTransition = + GenericTranstitions; + +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, + }, +};