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`
${passwordFieldError}
`
: ""}
${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"),
},
});
}
}
}