padloc/packages/app/src/elements/invite-view.ts

245 lines
8.5 KiB
TypeScript

import { until } from "lit/directives/until.js";
import { translate as $l } from "@padloc/locale/src/translate";
import { formatDateFromNow } from "../lib/util";
import { shared } from "../styles";
import { app } from "../globals";
import { alert, confirm } from "../lib/dialog";
import { Routing } from "../mixins/routing";
import { StateMixin } from "../mixins/state";
import { Button } from "./button";
import "./icon";
import "./scroller";
import "./spinner";
import { UnlockedOrg } from "@padloc/core/src/org";
import { UnlockedAccount } from "@padloc/core/src/account";
import { customElement, property, query, state } from "lit/decorators.js";
import { css, html, LitElement } from "lit";
import { checkFeatureDisabled } from "../lib/provisioning";
@customElement("pl-invite-view")
export class InviteView extends Routing(StateMixin(LitElement)) {
readonly routePattern = /^orgs\/([^\/]+)\/invites(?:\/([^\/]+))?/;
@property()
inviteId: string;
@property()
orgId: string;
@state()
_secret?: Promise<string>;
private get _org() {
return app.getOrg(this.orgId);
}
private get _invite() {
return this._org && this._org.getInvite(this.inviteId);
}
@query("#resendButton")
private _resendButton: Button;
@query("#deleteButton")
private _deleteButton: Button;
@query("#confirmButton")
private _confirmButton: Button;
handleRoute([orgId, inviteId]: [string, string]) {
this.orgId = orgId;
this.inviteId = inviteId;
if (!this._org?.isOwner(this.app.account!)) {
this.redirect("");
return;
}
this._secret = (async () => {
if (!this._invite || !this._org || !app.account || app.account.locked) {
return "";
}
await this._org.unlock(app.account as UnlockedAccount);
await this._invite.unlock((this._org as UnlockedOrg).invitesKey);
return this._invite.secret!;
})();
}
static styles = [
shared,
css`
:host {
position: relative;
background: var(--color-background);
}
`,
];
private async _delete() {
if (this._deleteButton.state === "loading") {
return;
}
if (
!(await confirm($l("Are you sure you want to delete this invite?"), $l("Delete"), $l("Cancel"), {
title: $l("Delete Invite"),
type: "destructive",
}))
) {
return;
}
this._deleteButton.start();
try {
await app.deleteInvite(this._invite!);
this._deleteButton && this._deleteButton.success();
this.go(`orgs/${this.orgId}/invites`);
} catch (e) {
this._deleteButton && this._deleteButton.fail();
alert(e.message, { type: "warning" });
}
}
private async _resend() {
if (!this._invite || !this._org || this._resendButton.state === "loading") {
return;
}
this._resendButton.start();
let org = this._org!;
try {
const newInvite = (await app.createInvites(org, [this._invite.email], this._invite.purpose))[0];
this.go(`orgs/${this.orgId}/invites/${newInvite.id}`, undefined, true);
this._resendButton.success();
} catch (e) {
this._resendButton.fail();
alert(e.message, { type: "warning" });
}
}
private async _confirm() {
if (checkFeatureDisabled(app.getOrgFeatures(this._org!).addMember, this._org!.isOwner(app.account!))) {
return;
}
if (this._confirmButton.state === "loading") {
return;
}
this._confirmButton.start();
try {
const member = await app.confirmInvite(this._invite!);
this._confirmButton && this._confirmButton.success();
alert($l("{0} was successfully added to your organisation!", member.name || member.email), {
title: "Member Added",
type: "success",
});
this.go(`orgs/${this.orgId}/members/${member.id}`);
} catch (e) {
this._confirmButton && this._confirmButton.fail();
alert(e.message, { type: "warning" });
throw e;
}
}
render() {
if (!this._invite) {
return html`
<div class="fullbleed centering double-padded text-centering vertical layout subtle">
<pl-icon icon="mail" class="enormous thin"></pl-icon>
<div>${$l("No invite selected.")}</div>
</div>
`;
}
const { email, expires, expired, accepted, purpose } = this._invite!;
const status = expired
? { icon: "time", class: "warning", text: $l("This invite has expired") }
: accepted
? { icon: "check", class: "", text: $l("Accepted") }
: {
icon: "time",
class: "",
text: until(
(async () => {
return $l("expires {0}", await formatDateFromNow(expires));
})()
),
};
return html`
<div class="fullbleed vertical layout">
<header class="padded horizontal center-aligning layout">
<pl-button
class="transparent slim back-button"
@click=${() => this.go(`orgs/${this.orgId}/invites`)}
>
<pl-icon icon="backward"></pl-icon>
</pl-button>
<div class="stretch large padded">${$l("Invite")}</div>
<div class="small tag ${status.class}">
<pl-icon icon="${status.icon}" class="inline"></pl-icon>
${status.text}
</div>
</header>
<pl-scroller class="stretch">
<div class="large spacer"></div>
<div class="margined text-centering">
${$l(
purpose === "confirm_membership"
? "A membership confirmation request was sent to:"
: "An invite was sent to:"
)}
</div>
<div class="bold large text-centering">${email}</div>
<div class="double-margined text-centering">
${$l(
"They will also need the following confirmation code, which " +
"you should communicate to them separately:"
)}
</div>
<div class="giant centering double-margined padded layout mono card">
<div>${until(this._secret, html` <pl-spinner active></pl-spinner> `)}</div>
</div>
<div class="horziontal margined evenly stretching spacing horizontal layout">
${accepted
? html`
<pl-button
?disabled=${expired}
id="confirmButton"
class="tap primary"
@click=${() => this._confirm()}
>
<pl-icon icon="invite" class="right-margined"></pl-icon>
<div>${$l(purpose === "confirm_membership" ? "Confirm" : "Add Member")}</div>
</pl-button>
`
: html`
<pl-button id="resendButton" class="tap" @click=${() => this._resend()}>
<pl-icon icon="mail" class="right-margined"></pl-icon>
<div>${$l("Resend")}</div>
</pl-button>
`}
<pl-button id="deleteButton" class="tap negative" @click=${() => this._delete()}>
<pl-icon icon="delete" class="right-margined"></pl-icon>
<div>${$l("Delete")}</div>
</pl-button>
</div>
</pl-scroller>
</div>
`;
}
}