diff --git a/frontend/src/index.ts b/frontend/src/index.ts
index 964a9104..02c6e42e 100644
--- a/frontend/src/index.ts
+++ b/frontend/src/index.ts
@@ -288,13 +288,25 @@ export class App extends LiteElement {
case "join":
return html``;
+ case "acceptInvite":
+ return html``;
+
case "login":
+ case "loginWithRedirect":
case "forgotPassword":
return html``;
case "resetPassword":
@@ -413,7 +426,7 @@ export class App extends LiteElement {
});
if (!detail.api) {
- this.navigate(DASHBOARD_ROUTE);
+ this.navigate(detail.redirectUrl || DASHBOARD_ROUTE);
}
if (detail.firstLogin) {
diff --git a/frontend/src/pages/accept-invite.ts b/frontend/src/pages/accept-invite.ts
new file mode 100644
index 00000000..5bfac194
--- /dev/null
+++ b/frontend/src/pages/accept-invite.ts
@@ -0,0 +1,129 @@
+import { state, property } from "lit/decorators.js";
+import { msg, localized } from "@lit/localize";
+
+import LiteElement, { html } from "../utils/LiteElement";
+import type { AuthState, LoggedInEvent } from "../utils/AuthService";
+import AuthService from "../utils/AuthService";
+import { DASHBOARD_ROUTE } from "../routes";
+
+@localized()
+export class AcceptInvite extends LiteElement {
+ @property({ type: Object })
+ authState?: AuthState;
+
+ @property({ type: String })
+ token?: string;
+
+ @property({ type: String })
+ email?: string;
+
+ @state()
+ serverError?: string;
+
+ connectedCallback(): void {
+ if (this.token && this.email) {
+ super.connectedCallback();
+ } else {
+ throw new Error("Missing email or token");
+ }
+ }
+
+ private get isLoggedIn(): boolean {
+ return Boolean(
+ this.authState && this.email && this.authState.username === this.email
+ );
+ }
+
+ firstUpdated() {
+ if (!this.isLoggedIn) {
+ this.dispatchEvent(
+ new CustomEvent("notify", {
+ detail: {
+ message: msg("Log in to continue."),
+ type: "success",
+ icon: "check2-circle",
+ duration: 10000,
+ },
+ })
+ );
+
+ this.navTo(
+ `/log-in?redirectUrl=${encodeURIComponent(
+ `${window.location.pathname}${window.location.search}`
+ )}`
+ );
+ }
+ }
+
+ render() {
+ let serverError;
+
+ if (this.serverError) {
+ serverError = html`
+
+ ${this.serverError}
+
+ `;
+ }
+
+ return html`
+
+ ${serverError}
+
+
+
+ ${msg("Join archive")}
+
+
+
+
+
+ ${msg("Accept invitation")}
+
+
+
+ `;
+ }
+
+ private async onAccept() {
+ if (!this.authState || !this.isLoggedIn) {
+ // TODO handle error
+ this.serverError = msg("Something unexpected went wrong");
+
+ return;
+ }
+
+ try {
+ await this.apiFetch(
+ `/archives/invite-accept/${this.token}`,
+ this.authState,
+ {
+ method: "POST",
+ }
+ );
+
+ this.dispatchEvent(
+ new CustomEvent("notify", {
+ detail: {
+ // TODO archive details
+ message: msg("You've joined the archive."),
+ type: "success",
+ icon: "check2-circle",
+ },
+ })
+ );
+
+ this.navTo(DASHBOARD_ROUTE);
+ } catch (err: any) {
+ if (err.isApiError && err.message === "Invalid Invite Code") {
+ this.serverError = msg("This invitation is not valid.");
+ } else {
+ this.serverError = msg("Something unexpected went wrong");
+ }
+ }
+ }
+}
diff --git a/frontend/src/pages/archives.ts b/frontend/src/pages/archives.ts
index 61fa54a0..38219644 100644
--- a/frontend/src/pages/archives.ts
+++ b/frontend/src/pages/archives.ts
@@ -36,12 +36,12 @@ export class Archives extends LiteElement {
return html`
${msg("Archives")}
-
+
${this.archiveList.map(
- (archive) =>
+ (archive, i) =>
html`
-
diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts
index ebde3dbc..86a1cf84 100644
--- a/frontend/src/pages/index.ts
+++ b/frontend/src/pages/index.ts
@@ -26,3 +26,8 @@ import(/* webpackChunkName: "users-invite" */ "./users-invite").then(
customElements.define("btrix-users-invite", UsersInvite);
}
);
+import(/* webpackChunkName: "accept-invite" */ "./accept-invite").then(
+ ({ AcceptInvite }) => {
+ customElements.define("btrix-accept-invite", AcceptInvite);
+ }
+);
diff --git a/frontend/src/pages/join.ts b/frontend/src/pages/join.ts
index d3c1d6d8..e73e65c3 100644
--- a/frontend/src/pages/join.ts
+++ b/frontend/src/pages/join.ts
@@ -1,99 +1,12 @@
import { state, property } from "lit/decorators.js";
-import { msg, localized, str } from "@lit/localize";
-import { createMachine, interpret, assign } from "@xstate/fsm";
+import { msg, localized } from "@lit/localize";
-import { DASHBOARD_ROUTE } from "../routes";
-import type { AuthState } from "../utils/AuthService";
import LiteElement, { html } from "../utils/LiteElement";
import type { LoggedInEvent } from "../utils/AuthService";
import AuthService from "../utils/AuthService";
-type JoinContext = {
- serverError?: string;
-};
-type JoinErrorEvent = {
- type: "ERROR";
- detail: {
- serverError?: JoinContext["serverError"];
- };
-};
-type JoinEvent =
- | { type: "SUBMIT_SIGN_UP" }
- | { type: "ACCEPT_INVITE" }
- | { type: "SIGN_UP_SUCCESS" }
- | JoinErrorEvent;
-type JoinTypestate =
- | {
- value: "initial";
- context: JoinContext;
- }
- | {
- value: "submittingForm";
- context: JoinContext;
- }
- | {
- value: "acceptInvite";
- context: JoinContext;
- }
- | {
- value: "acceptingInvite";
- context: JoinContext;
- };
-
-const initialContext = {};
-
-const machine = createMachine(
- {
- id: "join",
- initial: "initial",
- context: initialContext,
- states: {
- ["initial"]: {
- on: {
- SUBMIT_SIGN_UP: "submittingForm",
- },
- },
- ["submittingForm"]: {
- on: {
- SIGN_UP_SUCCESS: "acceptInvite",
- ERROR: {
- target: "initial",
- actions: "setError",
- },
- },
- },
- ["acceptInvite"]: {
- on: {
- ACCEPT_INVITE: "acceptingInvite",
- },
- },
- ["acceptingInvite"]: {
- on: {
- ERROR: {
- target: "acceptInvite",
- actions: "setError",
- },
- },
- },
- },
- },
- {
- actions: {
- setError: assign((context, event) => ({
- ...context,
- ...(event as JoinErrorEvent).detail,
- })),
- },
- }
-);
-
@localized()
export class Join extends LiteElement {
- private joinStateService = interpret(machine);
-
- @property({ type: Object })
- authState?: AuthState;
-
@property({ type: String })
token?: string;
@@ -101,7 +14,7 @@ export class Join extends LiteElement {
email?: string;
@state()
- private joinState = machine.initialState;
+ serverError?: string;
connectedCallback(): void {
if (this.token && this.email) {
@@ -111,151 +24,33 @@ export class Join extends LiteElement {
}
}
- firstUpdated() {
- // Enable state machine
- this.joinStateService.subscribe((state) => {
- this.joinState = state;
- });
-
- this.joinStateService.start();
- }
-
- disconnectedCallback() {
- this.joinStateService.stop();
- }
-
render() {
- const isSignUp =
- this.joinState.value === "initial" ||
- this.joinState.value === "submittingForm";
- const isAcceptInvite =
- this.joinState.value === "acceptInvite" ||
- this.joinState.value === "acceptingInvite";
-
- let content;
-
- if (isSignUp) {
- content = this.renderSignUp();
- } else if (isAcceptInvite) {
- content = this.renderAccept();
- }
-
return html`
-
${msg("Join archive")}
-
-
-
-
-
- 1. Create account
-
-
-
- 2. Accept invite
-
-
-
- ${content}
+
+ ${msg("Join Browsertrix Cloud")}
+
+
+
+
+
`;
}
- private renderSignUp() {
- return html`
- this.joinStateService.send("ERROR")}
- @authenticated=${this.onAuthenticated}
- >
- `;
- }
-
- private renderAccept() {
- let serverError;
-
- if (this.joinState.context.serverError) {
- serverError = html`
-
- ${this.joinState.context.serverError}
-
- `;
- }
-
- return html`
- ${serverError}
-
-
- Accept invitation
-
- `;
- }
-
- private onSignUp() {
- this.joinStateService.send("SUBMIT_SIGN_UP");
- }
-
private onAuthenticated(event: LoggedInEvent) {
- this.joinStateService.send("SIGN_UP_SUCCESS");
-
this.dispatchEvent(
AuthService.createLoggedInEvent({
...event.detail,
- api: true,
+ // TODO separate logic for confirmation message
+ // firstLogin: true,
})
);
}
-
- private async onAccept() {
- this.joinStateService.send("ACCEPT_INVITE");
-
- if (!this.authState) {
- this.joinStateService.send({
- type: "ERROR",
- detail: {
- serverError: msg("Something unexpected went wrong"),
- },
- });
-
- return;
- }
-
- try {
- await this.apiFetch(`/invite/accept/${this.token}`, this.authState);
-
- this.navTo(DASHBOARD_ROUTE);
- } catch (err: any) {
- if (err.isApiError && err.message === "Invalid Invite Code") {
- this.joinStateService.send({
- type: "ERROR",
- detail: {
- serverError: msg("This invitation is not valid."),
- },
- });
- } else {
- this.joinStateService.send({
- type: "ERROR",
- detail: {
- serverError: msg("Something unexpected went wrong"),
- },
- });
- }
- }
- }
}
diff --git a/frontend/src/pages/log-in.ts b/frontend/src/pages/log-in.ts
index c4865e8e..b016ccea 100644
--- a/frontend/src/pages/log-in.ts
+++ b/frontend/src/pages/log-in.ts
@@ -4,7 +4,9 @@ import { createMachine, interpret, assign } from "@xstate/fsm";
import type { ViewState } from "../utils/APIRouter";
import LiteElement, { html } from "../utils/LiteElement";
+import type { LoggedInEventDetail } from "../utils/AuthService";
import AuthService from "../utils/AuthService";
+import { DASHBOARD_ROUTE } from "../routes";
type FormContext = {
successMessage?: string;
@@ -126,6 +128,9 @@ export class LogInPage extends LiteElement {
@property({ type: Object })
viewState!: ViewState;
+ @property({ type: String })
+ redirectUrl: string = DASHBOARD_ROUTE;
+
private formStateService = interpret(machine);
@state()
@@ -308,6 +313,8 @@ export class LogInPage extends LiteElement {
try {
const data = await AuthService.login({ email: username, password });
+ (data as LoggedInEventDetail).redirectUrl = this.redirectUrl;
+
this.dispatchEvent(AuthService.createLoggedInEvent(data));
// no state update here, since "logged-in" event
diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts
index 4d2f2278..44292bae 100644
--- a/frontend/src/routes.ts
+++ b/frontend/src/routes.ts
@@ -2,8 +2,10 @@ export const ROUTES = {
home: "/",
join: "/join/:token?email",
signUp: "/sign-up",
+ acceptInvite: "/invite/accept/:token?email",
verify: "/verify?token",
login: "/log-in",
+ loginWithRedirect: "/log-in?redirectUrl",
forgotPassword: "/log-in/forgot-password",
resetPassword: "/reset-password?token",
myAccount: "/my-account",
diff --git a/frontend/src/utils/AuthService.ts b/frontend/src/utils/AuthService.ts
index c479ed31..c316c383 100644
--- a/frontend/src/utils/AuthService.ts
+++ b/frontend/src/utils/AuthService.ts
@@ -14,9 +14,10 @@ type Session = {
export type AuthState = (Auth & Session) | null;
-type LoggedInEventDetail = Auth & {
+export type LoggedInEventDetail = Auth & {
api?: boolean;
firstLogin?: boolean;
+ redirectUrl?: string;
};
export interface LoggedInEvent extends CustomEvent {