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:
parent
6e193b1157
commit
8bcdc8877f
@ -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>
|
||||
`;
|
||||
}
|
||||
|
@ -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", "");
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user