From 77525a56cd48aed2617a2b5790655e1f8e19ffed Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Tue, 21 Sep 2021 19:01:32 +0200 Subject: [PATCH] feat: implement UI for logging out (#638) * feat: implement UI for logging out * feat: use old style dialogs for logout confirmation * feat: implement manage sessions * feat: implement session logout success dialog * feat: use snjs alert for revoking sessions confirmation * fix: make OtherSessionsLogout easier to read --- app/assets/javascripts/components/Button.tsx | 15 ++- .../components/ConfirmSignoutModal.tsx | 8 +- .../components/ConfirmationDialog.tsx | 35 ++++++ .../components/OtherSessionsLogout.tsx | 80 ++++++++++++++ .../javascripts/components/SessionsModal.tsx | 14 +-- .../preferences/PreferencesView.tsx | 9 +- .../preferences/PreferencesViewWrapper.tsx | 4 +- .../components/PreferencesGroup.tsx | 24 ++++ .../components/PreferencesPane.tsx | 41 ++----- .../components/PreferencesSegment.tsx | 5 + .../preferences/components/index.ts | 2 + .../preferences/panes/AccountPreferences.tsx | 33 ++++-- .../preferences/panes/account/LogOutView.tsx | 103 ++++++++++++++++++ .../preferences/panes/account/index.ts | 1 + .../ui_models/app_state/account_menu_state.ts | 7 ++ .../ui_models/app_state/app_state.ts | 4 +- app/assets/stylesheets/_sn.scss | 17 +++ package.json | 2 +- yarn.lock | 8 +- 19 files changed, 342 insertions(+), 70 deletions(-) create mode 100644 app/assets/javascripts/components/ConfirmationDialog.tsx create mode 100644 app/assets/javascripts/components/OtherSessionsLogout.tsx create mode 100644 app/assets/javascripts/preferences/components/PreferencesGroup.tsx create mode 100644 app/assets/javascripts/preferences/components/PreferencesSegment.tsx create mode 100644 app/assets/javascripts/preferences/panes/account/LogOutView.tsx diff --git a/app/assets/javascripts/components/Button.tsx b/app/assets/javascripts/components/Button.tsx index bf13b5509..a1176f6ba 100644 --- a/app/assets/javascripts/components/Button.tsx +++ b/app/assets/javascripts/components/Button.tsx @@ -2,19 +2,22 @@ import { FunctionComponent } from 'preact'; const baseClass = `rounded px-4 py-1.75 font-bold text-sm fit-content`; -const normalClass = `${baseClass} bg-default color-text border-solid border-gray-300 border-1 \ -focus:bg-contrast hover:bg-contrast`; -const primaryClass = `${baseClass} no-border bg-info color-info-contrast hover:brightness-130 \ -focus:brightness-130`; +type ButtonType = 'normal' | 'primary' | 'danger'; + +const buttonClasses: { [type in ButtonType]: string } = { + normal: `${baseClass} bg-default color-text border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`, + primary: `${baseClass} no-border bg-info color-info-contrast hover:brightness-130 focus:brightness-130`, + danger: `${baseClass} bg-default color-danger border-solid border-gray-300 border-1 focus:bg-contrast hover:bg-contrast`, +}; export const Button: FunctionComponent<{ className?: string; - type: 'normal' | 'primary'; + type: ButtonType; label: string; onClick: () => void; disabled?: boolean; }> = ({ type, label, className = '', onClick, disabled = false }) => { - const buttonClass = type === 'primary' ? primaryClass : normalClass; + const buttonClass = buttonClasses[type]; const cursorClass = disabled ? 'cursor-default' : 'cursor-pointer'; return ( diff --git a/app/assets/javascripts/components/ConfirmSignoutModal.tsx b/app/assets/javascripts/components/ConfirmSignoutModal.tsx index 6a9ae8226..708ff1d95 100644 --- a/app/assets/javascripts/components/ConfirmSignoutModal.tsx +++ b/app/assets/javascripts/components/ConfirmSignoutModal.tsx @@ -26,7 +26,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => { const [deleteLocalBackups, setDeleteLocalBackups] = useState(false); const cancelRef = useRef(); - function close() { + function closeDialog() { appState.accountMenu.setSigningOut(false); } @@ -37,7 +37,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => { }, [appState.accountMenu.signingOut, application.bridge]); return ( - +
@@ -83,7 +83,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => { @@ -95,7 +95,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => { } else { application.signOut(); } - close(); + closeDialog(); }} > {application.hasAccount() diff --git a/app/assets/javascripts/components/ConfirmationDialog.tsx b/app/assets/javascripts/components/ConfirmationDialog.tsx new file mode 100644 index 000000000..78114ed66 --- /dev/null +++ b/app/assets/javascripts/components/ConfirmationDialog.tsx @@ -0,0 +1,35 @@ +import { ComponentChildren, FunctionComponent } from 'preact'; +import { + AlertDialog, + AlertDialogDescription, + AlertDialogLabel, +} from '@reach/alert-dialog'; +import { useRef } from 'preact/hooks'; + +export const ConfirmationDialog: FunctionComponent<{ + title: string | ComponentChildren; +}> = ({ title, children }) => { + const ldRef = useRef(); + + return ( + + {/* sn-component is focusable by default, but doesn't stretch to child width + resulting in a badly focused dialog. Utility classes are not available + at the sn-component level, only below it. tabIndex -1 disables focus + and enables it on the child component */} +
+
+ {title} +
+ + + {children} + +
+
+ + ); +}; diff --git a/app/assets/javascripts/components/OtherSessionsLogout.tsx b/app/assets/javascripts/components/OtherSessionsLogout.tsx new file mode 100644 index 000000000..a6da7ef9f --- /dev/null +++ b/app/assets/javascripts/components/OtherSessionsLogout.tsx @@ -0,0 +1,80 @@ +import { useRef, useState } from 'preact/hooks'; +import { + AlertDialog, + AlertDialogDescription, + AlertDialogLabel, +} from '@reach/alert-dialog'; +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; + +type Props = { + application: WebApplication; + appState: AppState; +}; + +export const OtherSessionsLogoutContainer = observer((props: Props) => { + if (!props.appState.accountMenu.otherSessionsLogOut) { + return null; + } + return ; +}); + +const ConfirmOtherSessionsLogout = observer( + ({ application, appState }: Props) => { + + const cancelRef = useRef(); + function closeDialog() { + appState.accountMenu.setOtherSessionsLogout(false); + } + + return ( + +
+
+
+
+
+ + End all other sessions? + + +

+ This action will sign out all other devices signed into your account, + and remove your data from those devices when they next regain connection + to the internet. You may sign back in on those devices at any time. +

+
+
+ + +
+
+
+
+
+
+
+ ); + } +); diff --git a/app/assets/javascripts/components/SessionsModal.tsx b/app/assets/javascripts/components/SessionsModal.tsx index 875f25f9c..e06c7df7b 100644 --- a/app/assets/javascripts/components/SessionsModal.tsx +++ b/app/assets/javascripts/components/SessionsModal.tsx @@ -26,12 +26,12 @@ type Session = RemoteSession & { function useSessions( application: SNApplication ): [ - Session[], - () => void, - boolean, - (uuid: UuidString) => Promise, - string -] { + Session[], + () => void, + boolean, + (uuid: UuidString) => Promise, + string + ] { const [sessions, setSessions] = useState([]); const [lastRefreshDate, setLastRefreshDate] = useState(Date.now()); const [refreshing, setRefreshing] = useState(true); @@ -240,7 +240,7 @@ const SessionsModal: FunctionComponent<{ ); }; -const Sessions: FunctionComponent<{ +export const Sessions: FunctionComponent<{ appState: AppState; application: WebApplication; }> = observer(({ appState, application }) => { diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index 34ee084af..9ed5cf152 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -7,9 +7,11 @@ import { PreferencesMenu } from './PreferencesMenu'; import { PreferencesMenuView } from './PreferencesMenuView'; import { WebApplication } from '@/ui_models/application'; import { MfaProps } from './panes/two-factor-auth/MfaProps'; +import { AppState } from '@/ui_models/app_state'; interface PreferencesProps extends MfaProps { application: WebApplication; + appState: AppState; closePreferences: () => void; } @@ -20,7 +22,12 @@ const PaneSelector: FunctionComponent< case 'general': return null; case 'account': - return ; + return ( + + ); case 'appearance': return null; case 'security': diff --git a/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx b/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx index cd4007990..03cc8623d 100644 --- a/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx +++ b/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx @@ -2,9 +2,10 @@ import { FunctionComponent } from 'preact'; import { observer } from 'mobx-react-lite'; import { WebApplication } from '@/ui_models/application'; import { PreferencesView } from './PreferencesView'; +import { AppState } from '@/ui_models/app_state'; export interface PreferencesViewWrapperProps { - appState: { preferences: { isOpen: boolean; closePreferences: () => void } }; + appState: AppState; application: WebApplication; } @@ -18,6 +19,7 @@ export const PreferencesViewWrapper: FunctionComponent appState.preferences.closePreferences()} application={application} + appState={appState} mfaProvider={application} userProvider={application} /> diff --git a/app/assets/javascripts/preferences/components/PreferencesGroup.tsx b/app/assets/javascripts/preferences/components/PreferencesGroup.tsx new file mode 100644 index 000000000..51dbd196a --- /dev/null +++ b/app/assets/javascripts/preferences/components/PreferencesGroup.tsx @@ -0,0 +1,24 @@ +import { FunctionComponent } from 'preact'; +import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator'; + +const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({ + index, + length, +}) => (index < length - 1 ? : null); + +export const PreferencesGroup: FunctionComponent = ({ children }) => ( +
+ {Array.isArray(children) + ? children + .filter( + (child) => child != undefined && child !== '' && child !== false + ) + .map((child, i, arr) => ( + <> + {child} + + + )) + : children} +
+); diff --git a/app/assets/javascripts/preferences/components/PreferencesPane.tsx b/app/assets/javascripts/preferences/components/PreferencesPane.tsx index 722a02ec4..bfe2e4572 100644 --- a/app/assets/javascripts/preferences/components/PreferencesPane.tsx +++ b/app/assets/javascripts/preferences/components/PreferencesPane.tsx @@ -1,43 +1,18 @@ import { FunctionComponent } from 'preact'; -import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator'; - -const HorizontalLine: FunctionComponent<{ index: number; length: number }> = ({ - index, - length, -}) => (index < length - 1 ? : null); - -export const PreferencesSegment: FunctionComponent = ({ children }) => ( -
{children}
-); - -export const PreferencesGroup: FunctionComponent = ({ children }) => ( -
- {Array.isArray(children) - ? children - .filter( - (child) => child != undefined && child !== '' && child !== false - ) - .map((child, i, arr) => ( - <> - {child} - - - )) - : children} -
-); export const PreferencesPane: FunctionComponent = ({ children }) => (
{children != undefined && Array.isArray(children) - ? children.map((child, idx, arr) => ( - <> - {child} - {idx < arr.length - 1 ?
: undefined} - - )) + ? children + .filter((child) => child != undefined) + .map((child) => ( + <> + {child} +
+ + )) : children}
diff --git a/app/assets/javascripts/preferences/components/PreferencesSegment.tsx b/app/assets/javascripts/preferences/components/PreferencesSegment.tsx new file mode 100644 index 000000000..dad798f5c --- /dev/null +++ b/app/assets/javascripts/preferences/components/PreferencesSegment.tsx @@ -0,0 +1,5 @@ +import { FunctionComponent } from 'preact'; + +export const PreferencesSegment: FunctionComponent = ({ children }) => ( +
{children}
+); diff --git a/app/assets/javascripts/preferences/components/index.ts b/app/assets/javascripts/preferences/components/index.ts index 0c4046f06..04139c83d 100644 --- a/app/assets/javascripts/preferences/components/index.ts +++ b/app/assets/javascripts/preferences/components/index.ts @@ -1,3 +1,5 @@ export * from './Content'; export * from './MenuItem'; export * from './PreferencesPane'; +export * from './PreferencesGroup'; +export * from './PreferencesSegment'; diff --git a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx index 49eb88a7d..1af8bc6f3 100644 --- a/app/assets/javascripts/preferences/panes/AccountPreferences.tsx +++ b/app/assets/javascripts/preferences/panes/AccountPreferences.tsx @@ -1,17 +1,28 @@ -import { Sync, SubscriptionWrapper, Credentials } from '@/preferences/panes/account'; +import { + Sync, + SubscriptionWrapper, + Credentials, + LogOutWrapper, +} from '@/preferences/panes/account'; import { PreferencesPane } from '@/preferences/components'; import { observer } from 'mobx-react-lite'; import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; type Props = { application: WebApplication; -} -export const AccountPreferences = observer(({application}: Props) => { - return ( - - - - - - ); -}); + appState: AppState; +}; + +export const AccountPreferences = observer( + ({ application, appState }: Props) => { + return ( + + + + + + + ); + } +); diff --git a/app/assets/javascripts/preferences/panes/account/LogOutView.tsx b/app/assets/javascripts/preferences/panes/account/LogOutView.tsx new file mode 100644 index 000000000..a3f1219f9 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/account/LogOutView.tsx @@ -0,0 +1,103 @@ +import { Button } from '@/components/Button'; +import { ConfirmSignoutContainer } from '@/components/ConfirmSignoutModal'; +import { OtherSessionsLogoutContainer } from '@/components/OtherSessionsLogout'; +import { + PreferencesGroup, + PreferencesSegment, + Subtitle, + Text, + Title, +} from '@/preferences/components'; +import { WebApplication } from '@/ui_models/application'; +import { AppState } from '@/ui_models/app_state'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; + +const LogOutView: FunctionComponent<{ + application: WebApplication; + appState: AppState; +}> = observer(({ application, appState }) => { + + return ( + <> + + + Log out +
+ Other devices + Want to log out on all devices except this one? +
+
+
+ + + This device + This will delete all local items and preferences. +
+