import { LitElement } from "lit"; import { state, query, property } from "lit/decorators.js"; import { msg, localized } from "@lit/localize"; import { createMachine, interpret, assign } from "@xstate/fsm"; import type { CurrentUser } from "../types/user"; import LiteElement, { html } from "../utils/LiteElement"; import { needLogin } from "../utils/auth"; import type { AuthState, Auth } from "../utils/AuthService"; import AuthService from "../utils/AuthService"; @localized() class RequestVerify extends LitElement { @property({ type: String }) email!: string; @state() private isRequesting: boolean = false; @state() private requestSuccess: boolean = false; createRenderRoot() { return this; } render() { if (this.requestSuccess) { return html`
${msg("Sent", { desc: "Status message after sending verification email", })}
`; } return html` ${this.isRequesting ? msg("Sending...") : msg("Resend verification email")} `; } private async requestVerification() { this.isRequesting = true; const resp = await fetch("/api/auth/request-verify-token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: this.email, }), }); switch (resp.status) { case 202: this.requestSuccess = true; break; default: // TODO generic toast error break; } this.isRequesting = false; } } customElements.define("btrix-request-verify", RequestVerify); type FormContext = { successMessage?: string; serverError?: string; fieldErrors: { [fieldName: string]: string }; }; type FormSuccessEvent = { type: "SUCCESS"; detail: { successMessage?: FormContext["successMessage"]; }; }; type FormErrorEvent = { type: "ERROR"; detail: { serverError?: FormContext["serverError"]; fieldErrors?: FormContext["fieldErrors"]; }; }; type FormEvent = | { type: "EDIT" } | { type: "CANCEL" } | { type: "SUBMIT" } | FormSuccessEvent | FormErrorEvent; type FormTypestate = | { value: "readOnly"; context: FormContext; } | { value: "editingForm"; context: FormContext; } | { value: "submittingForm"; context: FormContext; }; const initialContext = { fieldErrors: {}, }; const machine = createMachine( { id: "changePasswordForm", initial: "readOnly", context: initialContext, states: { ["readOnly"]: { on: { EDIT: { target: "editingForm", actions: "reset", }, }, }, ["editingForm"]: { on: { CANCEL: "readOnly", SUBMIT: "submittingForm" }, }, ["submittingForm"]: { on: { SUCCESS: { target: "readOnly", actions: "setSucessMessage", }, ERROR: { target: "editingForm", actions: "setError", }, }, }, }, }, { actions: { reset: assign(() => initialContext), setSucessMessage: assign((context, event) => ({ ...context, ...(event as FormSuccessEvent).detail, })), setError: assign((context, event) => ({ ...context, ...(event as FormErrorEvent).detail, })), }, } ); @needLogin @localized() export class AccountSettings extends LiteElement { private formStateService = interpret(machine); @property({ type: Object }) authState?: AuthState; @property({ type: Object }) userInfo?: CurrentUser; @state() private formState = machine.initialState; firstUpdated() { // Enable state machine this.formStateService.subscribe((state) => { this.formState = state; }); this.formStateService.start(); } disconnectedCallback() { this.formStateService.stop(); super.disconnectedCallback(); } render() { const showForm = this.formState.value === "editingForm" || this.formState.value === "submittingForm"; let successMessage; let verificationMessage; if (this.formState.context.successMessage) { successMessage = html`
${this.formState.context.successMessage}
`; } if (this.userInfo) { if (this.userInfo.isVerified) { verificationMessage = html` ${msg("verified", { desc: "Status text when user email is verified", })} `; } else { verificationMessage = html` ${msg("unverified", { desc: "Status text when user email is not yet verified", })} `; } } return html`

${msg("Account settings")}

${successMessage}
${msg("Email")}
${this.userInfo?.email} ${verificationMessage}
${showForm ? this.renderChangePasswordForm() : html`
this.formStateService.send("EDIT")} >${msg("Change password")}
`}
`; } renderChangePasswordForm() { const passwordFieldError = this.formState.context.fieldErrors.password; let formError; if (this.formState.context.serverError) { formError = html`
${this.formState.context.serverError}
`; } return html`

${msg("Change password")}

${passwordFieldError ? html`` : ""}
${formError}
${msg("Update password")} this.formStateService.send("CANCEL")} >${msg("Cancel")}
`; } async onSubmit(event: { detail: { formData: FormData } }) { if (!this.authState) return; this.formStateService.send("SUBMIT"); const { formData } = event.detail; let nextAuthState: Auth | null = null; try { nextAuthState = await AuthService.login({ email: this.authState.username, password: formData.get("password") as string, }); this.dispatchEvent(AuthService.createLoggedInEvent(nextAuthState)); } catch (e: any) { console.debug(e); } if (!nextAuthState) { this.formStateService.send({ type: "ERROR", detail: { fieldErrors: { password: msg("Wrong password"), }, }, }); return; } const params = { password: formData.get("newPassword"), }; try { await this.apiFetch("/users/me", nextAuthState, { method: "PATCH", body: JSON.stringify(params), }); this.formStateService.send({ type: "SUCCESS", detail: { successMessage: "Successfully updated password", }, }); } catch (e) { console.error(e); this.formStateService.send({ type: "ERROR", detail: { serverError: msg("Something went wrong changing password"), }, }); } } }