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} ?disabled=${this.isRequesting}
@click=${this.requestVerification} @click=${this.requestVerification}
> >
${msg("Resend verification email")} ${this.isRequesting
? msg("Sending...")
: msg("Resend verification email")}
</span> </span>
`; `;
} }

View File

@ -32,6 +32,14 @@ const ROUTES = {
"archive-info-tab": "/archive/:aid/:tab", "archive-info-tab": "/archive/:aid/:tab",
} as const; } as const;
/**
* @event navigate
* @event notify
* @event need-login
* @event logged-in
* @event log-out
* @event user-info-change
*/
@localized() @localized()
export class App extends LiteElement { export class App extends LiteElement {
private router: APIRouter = new APIRouter(ROUTES); private router: APIRouter = new APIRouter(ROUTES);
@ -230,6 +238,11 @@ export class App extends LiteElement {
return html`<btrix-verify return html`<btrix-verify
class="w-full flex items-center justify-center" class="w-full flex items-center justify-center"
token="${this.viewState.params.token}" 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>`; ></btrix-verify>`;
case "login": case "login":
@ -295,8 +308,8 @@ export class App extends LiteElement {
} }
} }
onLogOut(event: CustomEvent<{ redirect?: boolean }>) { onLogOut(event: CustomEvent<{ redirect?: boolean } | null>) {
const { detail } = event; const detail = event.detail || {};
const redirect = detail.redirect !== false; const redirect = detail.redirect !== false;
this.clearAuthState(); this.clearAuthState();
@ -334,6 +347,61 @@ export class App extends LiteElement {
this.navigate(event.detail); 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() { clearAuthState() {
this.authState = null; this.authState = null;
window.localStorage.setItem("authState", ""); window.localStorage.setItem("authState", "");

View File

@ -1,10 +1,14 @@
import { state, property } from "lit/decorators.js"; import { state, property } from "lit/decorators.js";
import { msg, localized } from "@lit/localize"; import { msg, localized } from "@lit/localize";
import { AuthState } from "../types/auth";
import LiteElement, { html } from "../utils/LiteElement"; import LiteElement, { html } from "../utils/LiteElement";
@localized() @localized()
export class Verify extends LiteElement { export class Verify extends LiteElement {
@property({ type: Object })
authState?: AuthState;
@property({ type: String }) @property({ type: String })
token?: string; token?: string;
@ -24,7 +28,7 @@ export class Verify extends LiteElement {
return html` <div class="text-4xl"><sl-spinner></sl-spinner></div> `; 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", { const resp = await fetch("/api/auth/verify", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -33,19 +37,61 @@ export class Verify extends LiteElement {
}), }),
}); });
const data = await resp.json();
switch (resp.status) { switch (resp.status) {
case 200: case 200:
this.navTo("/log-in"); return this.onVerificationComplete(data);
break;
case 400: case 400:
const { detail } = await resp.json(); const { detail } = data;
if (detail === "VERIFY_USER_BAD_TOKEN") { if (detail === "VERIFY_USER_BAD_TOKEN") {
this.serverError = msg("This verification email is not valid."); this.serverError = msg("This verification email is not valid.");
break; break;
} }
if (detail === "VERIFY_USER_ALREADY_VERIFIED") {
return this.onVerificationComplete(data);
}
default: default:
this.serverError = msg("Something unexpected went wrong"); this.serverError = msg("Something unexpected went wrong");
break; 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 { setBasePath } from "@shoelace-style/shoelace/dist/utilities/base-path.js";
import "@shoelace-style/shoelace/dist/themes/light.css"; 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/button/button";
import "@shoelace-style/shoelace/dist/components/form/form"; import "@shoelace-style/shoelace/dist/components/form/form";
import "@shoelace-style/shoelace/dist/components/icon/icon"; 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-medium: var(--sl-font-size-small);
--sl-input-label-font-size-large: var(--sl-font-size-medium); --sl-input-label-font-size-large: var(--sl-font-size-medium);
} }
.sl-toast-stack {
bottom: 0;
top: auto;
}
`; `;
export default theme; export default theme;