Skip to content

Commit

Permalink
feat: implement UI for logging out (#638)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
gorjan5sk authored Sep 21, 2021
1 parent a9610fd commit 77525a5
Show file tree
Hide file tree
Showing 19 changed files with 342 additions and 70 deletions.
15 changes: 9 additions & 6 deletions app/assets/javascripts/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
8 changes: 4 additions & 4 deletions app/assets/javascripts/components/ConfirmSignoutModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => {
const [deleteLocalBackups, setDeleteLocalBackups] = useState(false);

const cancelRef = useRef<HTMLButtonElement>();
function close() {
function closeDialog() {
appState.accountMenu.setSigningOut(false);
}

Expand All @@ -37,7 +37,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => {
}, [appState.accountMenu.signingOut, application.bridge]);

return (
<AlertDialog onDismiss={close} leastDestructiveRef={cancelRef}>
<AlertDialog onDismiss={closeDialog} leastDestructiveRef={cancelRef}>
<div className="sk-modal-content">
<div className="sn-component">
<div className="sk-panel">
Expand Down Expand Up @@ -83,7 +83,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => {
<button
className="sn-button small neutral"
ref={cancelRef}
onClick={close}
onClick={closeDialog}
>
Cancel
</button>
Expand All @@ -95,7 +95,7 @@ const ConfirmSignoutModal = observer(({ application, appState }: Props) => {
} else {
application.signOut();
}
close();
closeDialog();
}}
>
{application.hasAccount()
Expand Down
35 changes: 35 additions & 0 deletions app/assets/javascripts/components/ConfirmationDialog.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLButtonElement>();

return (
<AlertDialog leastDestructiveRef={ldRef}>
{/* 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 */}
<div tabIndex={-1} className="sn-component">
<div
tabIndex={0}
className="max-w-89 bg-default rounded shadow-overlay focus:padded-ring-info px-9 py-9 flex flex-col items-center"
>
<AlertDialogLabel>{title}</AlertDialogLabel>
<div className="min-h-2" />

<AlertDialogDescription className="flex flex-col items-center">
{children}
</AlertDialogDescription>
</div>
</div>
</AlertDialog>
);
};
80 changes: 80 additions & 0 deletions app/assets/javascripts/components/OtherSessionsLogout.tsx
Original file line number Diff line number Diff line change
@@ -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 <ConfirmOtherSessionsLogout {...props} />;
});

const ConfirmOtherSessionsLogout = observer(
({ application, appState }: Props) => {

const cancelRef = useRef<HTMLButtonElement>();
function closeDialog() {
appState.accountMenu.setOtherSessionsLogout(false);
}

return (
<AlertDialog onDismiss={closeDialog} leastDestructiveRef={cancelRef}>
<div className="sk-modal-content">
<div className="sn-component">
<div className="sk-panel">
<div className="sk-panel-content">
<div className="sk-panel-section">
<AlertDialogLabel className="sk-h3 sk-panel-section-title capitalize">
End all other sessions?
</AlertDialogLabel>
<AlertDialogDescription className="sk-panel-row">
<p className="color-foreground">
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.
</p>
</AlertDialogDescription>
<div className="flex my-1 mt-4">
<button
className="sn-button small neutral"
ref={cancelRef}
onClick={closeDialog}
>
Cancel
</button>
<button
className="sn-button small danger ml-2"
onClick={() => {
application.revokeAllOtherSessions();
closeDialog();
application.alertService.alert(
"You have successfully revoked your sessions from other devices.",
undefined,
"Finish"
);
}}
>
End Sessions
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</AlertDialog>
);
}
);
14 changes: 7 additions & 7 deletions app/assets/javascripts/components/SessionsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ type Session = RemoteSession & {
function useSessions(
application: SNApplication
): [
Session[],
() => void,
boolean,
(uuid: UuidString) => Promise<void>,
string
] {
Session[],
() => void,
boolean,
(uuid: UuidString) => Promise<void>,
string
] {
const [sessions, setSessions] = useState<Session[]>([]);
const [lastRefreshDate, setLastRefreshDate] = useState(Date.now());
const [refreshing, setRefreshing] = useState(true);
Expand Down Expand Up @@ -240,7 +240,7 @@ const SessionsModal: FunctionComponent<{
);
};

const Sessions: FunctionComponent<{
export const Sessions: FunctionComponent<{
appState: AppState;
application: WebApplication;
}> = observer(({ appState, application }) => {
Expand Down
9 changes: 8 additions & 1 deletion app/assets/javascripts/preferences/PreferencesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -20,7 +22,12 @@ const PaneSelector: FunctionComponent<
case 'general':
return null;
case 'account':
return <AccountPreferences application={props.application} />;
return (
<AccountPreferences
application={props.application}
appState={props.appState}
/>
);
case 'appearance':
return null;
case 'security':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -18,6 +19,7 @@ export const PreferencesViewWrapper: FunctionComponent<PreferencesViewWrapperPro
<PreferencesView
closePreferences={() => appState.preferences.closePreferences()}
application={application}
appState={appState}
mfaProvider={application}
userProvider={application}
/>
Expand Down
24 changes: 24 additions & 0 deletions app/assets/javascripts/preferences/components/PreferencesGroup.tsx
Original file line number Diff line number Diff line change
@@ -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 ? <HorizontalSeparator classes="my-4" /> : null);

export const PreferencesGroup: FunctionComponent = ({ children }) => (
<div className="bg-default border-1 border-solid rounded border-gray-300 px-6 py-6 flex flex-col">
{Array.isArray(children)
? children
.filter(
(child) => child != undefined && child !== '' && child !== false
)
.map((child, i, arr) => (
<>
{child}
<HorizontalLine index={i} length={arr.length} />
</>
))
: children}
</div>
);
41 changes: 8 additions & 33 deletions app/assets/javascripts/preferences/components/PreferencesPane.tsx
Original file line number Diff line number Diff line change
@@ -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 ? <HorizontalSeparator classes="my-4" /> : null);

export const PreferencesSegment: FunctionComponent = ({ children }) => (
<div className="flex flex-col">{children}</div>
);

export const PreferencesGroup: FunctionComponent = ({ children }) => (
<div className="bg-default border-1 border-solid rounded border-gray-300 px-6 py-6 flex flex-col">
{Array.isArray(children)
? children
.filter(
(child) => child != undefined && child !== '' && child !== false
)
.map((child, i, arr) => (
<>
{child}
<HorizontalLine index={i} length={arr.length} />
</>
))
: children}
</div>
);

export const PreferencesPane: FunctionComponent = ({ children }) => (
<div className="color-black flex-grow flex flex-row overflow-y-auto min-h-0">
<div className="flex-grow flex flex-col py-6 items-center">
<div className="w-125 max-w-125 flex flex-col">
{children != undefined && Array.isArray(children)
? children.map((child, idx, arr) => (
<>
{child}
{idx < arr.length - 1 ? <div className="min-h-3" /> : undefined}
</>
))
? children
.filter((child) => child != undefined)
.map((child) => (
<>
{child}
<div className="min-h-3" />
</>
))
: children}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { FunctionComponent } from 'preact';

export const PreferencesSegment: FunctionComponent = ({ children }) => (
<div className="flex flex-col">{children}</div>
);
2 changes: 2 additions & 0 deletions app/assets/javascripts/preferences/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './Content';
export * from './MenuItem';
export * from './PreferencesPane';
export * from './PreferencesGroup';
export * from './PreferencesSegment';
33 changes: 22 additions & 11 deletions app/assets/javascripts/preferences/panes/AccountPreferences.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<PreferencesPane>
<Credentials application={application} />
<Sync application={application} />
<SubscriptionWrapper application={application} />
</PreferencesPane>
);
});
appState: AppState;
};

export const AccountPreferences = observer(
({ application, appState }: Props) => {
return (
<PreferencesPane>
<Credentials application={application} />
<Sync application={application} />
<SubscriptionWrapper application={application} />
<LogOutWrapper application={application} appState={appState} />
</PreferencesPane>
);
}
);
Loading

0 comments on commit 77525a5

Please sign in to comment.