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}
|
?disabled=${this.isRequesting}
|
||||||
@click=${this.requestVerification}
|
@click=${this.requestVerification}
|
||||||
>
|
>
|
||||||
${msg("Resend verification email")}
|
${this.isRequesting
|
||||||
|
? msg("Sending...")
|
||||||
|
: msg("Resend verification email")}
|
||||||
</span>
|
</span>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
@ -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", "");
|
||||||
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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";
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user