Frontend verification UX fixes (#40)

- Show toast alert when user is verified
- Redirect to correct page on verified
- Update already-logged in user info on verify
- Adds new toast component

closes #39
This commit is contained in:
sua yoo 2021-12-01 11:56:09 -08:00 committed by GitHub
parent 6e193b1157
commit 8bcdc8877f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 129 additions and 7 deletions

View File

@ -40,7 +40,9 @@ class RequestVerify extends LitElement {
?disabled=${this.isRequesting}
@click=${this.requestVerification}
>
${msg("Resend verification email")}
${this.isRequesting
? msg("Sending...")
: msg("Resend verification email")}
</span>
`;
}

View File

@ -32,6 +32,14 @@ const ROUTES = {
"archive-info-tab": "/archive/:aid/:tab",
} as const;
/**
* @event navigate
* @event notify
* @event need-login
* @event logged-in
* @event log-out
* @event user-info-change
*/
@localized()
export class App extends LiteElement {
private router: APIRouter = new APIRouter(ROUTES);
@ -230,6 +238,11 @@ export class App extends LiteElement {
return html`<btrix-verify
class="w-full flex items-center justify-center"
token="${this.viewState.params.token}"
@navigate="${this.onNavigateTo}"
@notify="${this.onNotify}"
@log-out="${this.onLogOut}"
@user-info-change="${this.onUserInfoChange}"
.authState="${this.authState}"
></btrix-verify>`;
case "login":
@ -295,8 +308,8 @@ export class App extends LiteElement {
}
}
onLogOut(event: CustomEvent<{ redirect?: boolean }>) {
const { detail } = event;
onLogOut(event: CustomEvent<{ redirect?: boolean } | null>) {
const detail = event.detail || {};
const redirect = detail.redirect !== false;
this.clearAuthState();
@ -334,6 +347,61 @@ export class App extends LiteElement {
this.navigate(event.detail);
}
onUserInfoChange(event: CustomEvent<Partial<CurrentUser>>) {
// @ts-ignore
this.userInfo = {
...this.userInfo,
...event.detail,
};
}
onNotify(
event: CustomEvent<{
title?: string;
message?: string;
type?: "success" | "warning" | "danger" | "primary";
icon?: string;
duration?: number;
}>
) {
const {
title,
message,
type = "primary",
icon = "info-circle",
duration = 5000,
} = event.detail;
const escapeHtml = (html: any) => {
const div = document.createElement("div");
div.textContent = html;
return div.innerHTML;
};
const alert = Object.assign(document.createElement("sl-alert"), {
type: type,
closable: true,
duration: duration,
style: [
"--sl-panel-background-color: var(--sl-color-neutral-1000)",
"--sl-color-neutral-700: var(--sl-color-neutral-0)",
// "--sl-panel-border-width: 0px",
"--sl-spacing-large: var(--sl-spacing-medium)",
].join(";"),
innerHTML: `
<sl-icon name="${icon}" slot="icon"></sl-icon>
<span>
${title ? `<strong>${escapeHtml(title)}</strong>` : ""}
${message ? `<div>${escapeHtml(message)}</div>` : ""}
</span>
`,
});
document.body.append(alert);
alert.toast();
}
clearAuthState() {
this.authState = null;
window.localStorage.setItem("authState", "");

View File

@ -1,10 +1,14 @@
import { state, property } from "lit/decorators.js";
import { msg, localized } from "@lit/localize";
import { AuthState } from "../types/auth";
import LiteElement, { html } from "../utils/LiteElement";
@localized()
export class Verify extends LiteElement {
@property({ type: Object })
authState?: AuthState;
@property({ type: String })
token?: string;
@ -24,7 +28,7 @@ export class Verify extends LiteElement {
return html` <div class="text-4xl"><sl-spinner></sl-spinner></div> `;
}
private async verify() {
private async verify(): Promise<void> {
const resp = await fetch("/api/auth/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
@ -33,19 +37,61 @@ export class Verify extends LiteElement {
}),
});
const data = await resp.json();
switch (resp.status) {
case 200:
this.navTo("/log-in");
break;
return this.onVerificationComplete(data);
case 400:
const { detail } = await resp.json();
const { detail } = data;
if (detail === "VERIFY_USER_BAD_TOKEN") {
this.serverError = msg("This verification email is not valid.");
break;
}
if (detail === "VERIFY_USER_ALREADY_VERIFIED") {
return this.onVerificationComplete(data);
}
default:
this.serverError = msg("Something unexpected went wrong");
break;
}
}
private onVerificationComplete(data: {
email: string;
is_verified: boolean;
}) {
const isLoggedIn = Boolean(this.authState);
const shouldLogOut = isLoggedIn && this.authState?.username !== data.email;
this.dispatchEvent(
new CustomEvent("notify", {
detail: {
title: msg("Email address verified"),
message:
isLoggedIn && !shouldLogOut ? "" : msg("Log in to continue."),
type: "success",
icon: "check2-circle",
duration: 10000,
},
})
);
if (shouldLogOut) {
this.dispatchEvent(new CustomEvent("log-out"));
} else {
if (isLoggedIn) {
this.dispatchEvent(
new CustomEvent("user-info-change", {
detail: {
isVerified: data.is_verified,
},
})
);
}
this.navTo("/log-in");
}
}
}

View File

@ -4,6 +4,7 @@
*/
import { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
import "@shoelace-style/shoelace/dist/themes/light.css";
import "@shoelace-style/shoelace/dist/components/alert/alert";
import "@shoelace-style/shoelace/dist/components/button/button";
import "@shoelace-style/shoelace/dist/components/form/form";
import "@shoelace-style/shoelace/dist/components/icon/icon";

View File

@ -50,6 +50,11 @@ const theme = css`
--sl-input-label-font-size-medium: var(--sl-font-size-small);
--sl-input-label-font-size-large: var(--sl-font-size-medium);
}
.sl-toast-stack {
bottom: 0;
top: auto;
}
`;
export default theme;