feat: Add link to hosted sign-up page (#2045)

Resolves https://github.com/webrecorder/browsertrix/issues/2043

<!-- Fixes #issue_number -->

### Changes

- Shows link to sign up in UI if `sign_up_url` is configured.
- Expires settings in session storage (for now)
This commit is contained in:
sua yoo 2024-08-26 17:26:25 -07:00 committed by GitHub
parent c0725599b2
commit 337454f8c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 108 additions and 21 deletions

View File

@ -115,6 +115,8 @@ class SettingsResponse(BaseModel):
billingEnabled: bool billingEnabled: bool
signUpUrl: str = ""
salesEmail: str = "" salesEmail: str = ""
supportEmail: str = "" supportEmail: str = ""
@ -143,6 +145,7 @@ def main() -> None:
maxPagesPerCrawl=int(os.environ.get("MAX_PAGES_PER_CRAWL", 0)), maxPagesPerCrawl=int(os.environ.get("MAX_PAGES_PER_CRAWL", 0)),
maxScale=int(os.environ.get("MAX_CRAWL_SCALE", 3)), maxScale=int(os.environ.get("MAX_CRAWL_SCALE", 3)),
billingEnabled=is_bool(os.environ.get("BILLING_ENABLED")), billingEnabled=is_bool(os.environ.get("BILLING_ENABLED")),
signUpUrl=os.environ.get("SIGN_UP_URL", ""),
salesEmail=os.environ.get("SALES_EMAIL", ""), salesEmail=os.environ.get("SALES_EMAIL", ""),
supportEmail=os.environ.get("EMAIL_SUPPORT", ""), supportEmail=os.environ.get("EMAIL_SUPPORT", ""),
) )

View File

@ -46,6 +46,7 @@ def test_api_settings():
"maxScale": 3, "maxScale": 3,
"defaultPageLoadTimeSeconds": 120, "defaultPageLoadTimeSeconds": 120,
"billingEnabled": True, "billingEnabled": True,
"signUpUrl": "",
"salesEmail": "", "salesEmail": "",
"supportEmail": "", "supportEmail": "",
} }

View File

@ -62,6 +62,8 @@ data:
BILLING_ENABLED: "{{ .Values.billing_enabled }}" BILLING_ENABLED: "{{ .Values.billing_enabled }}"
SIGN_UP_URL: "{{ .Values.sign_up_url }}"
SALES_EMAIL: "{{ .Values.sales_email }}" SALES_EMAIL: "{{ .Values.sales_email }}"
LOG_SENT_EMAILS: "{{ .Values.email.log_sent_emails }}" LOG_SENT_EMAILS: "{{ .Values.email.log_sent_emails }}"

View File

@ -60,9 +60,10 @@ volume_storage_class:
# if set, set the node selector 'nodeType' to this crawling pods # if set, set the node selector 'nodeType' to this crawling pods
# crawler_node_type: # crawler_node_type:
# if set to "1", enables open registration
registration_enabled: "0" registration_enabled: "0"
# if set, along with 'registration_enabled', will add registrated users to this org # if set, along with 'registration_enabled', will add registered users to this org
# registration_org_id: "" # registration_org_id: ""
jwt_token_lifetime_minutes: 1440 jwt_token_lifetime_minutes: 1440
@ -113,6 +114,11 @@ profile_browser_idle_seconds: 60
# set to true to enable subscriptions API and Billing tab # set to true to enable subscriptions API and Billing tab
billing_enabled: false billing_enabled: false
# set URL to external sign-up page
# the internal sign-up page will take precedence if
# `registration_enabled` is set to `"1"``
sign_up_url: ""
# set e-mail to show for subscriptions related info # set e-mail to show for subscriptions related info
sales_email: "" sales_email: ""

View File

@ -51,15 +51,7 @@ export class NavigateController implements ReactiveController {
this.host.dispatchEvent(evt); this.host.dispatchEvent(evt);
}; };
/** handleAnchorClick = (event: MouseEvent) => {
* Bind to anchor tag to prevent full page navigation
* @example
* ```ts
* <a href="/" @click=${this.navigate.link}>go</a>
* ```
* @param event Click event
*/
link = (event: MouseEvent, _href?: string, resetScroll = true): void => {
if ( if (
// Detect keypress for opening in a new tab // Detect keypress for opening in a new tab
event.ctrlKey || event.ctrlKey ||
@ -69,11 +61,27 @@ export class NavigateController implements ReactiveController {
// Account for event prevented on anchor tag // Account for event prevented on anchor tag
event.defaultPrevented event.defaultPrevented
) { ) {
return; return false;
} }
event.preventDefault(); event.preventDefault();
return true;
};
/**
* Bind to anchor tag to prevent full page navigation
* @example
* ```ts
* <a href="/" @click=${this.navigate.link}>go</a>
* ```
* @param event Click event
*/
link = (event: MouseEvent, _href?: string, resetScroll = true): void => {
if (!this.handleAnchorClick(event)) {
return;
}
const el = event.currentTarget as HTMLAnchorElement | null; const el = event.currentTarget as HTMLAnchorElement | null;
if (el?.ariaDisabled === "true") { if (el?.ariaDisabled === "true") {

View File

@ -196,6 +196,7 @@ export class App extends LiteElement {
maxPagesPerCrawl: 0, maxPagesPerCrawl: 0,
maxScale: 0, maxScale: 0,
billingEnabled: false, billingEnabled: false,
signUpUrl: "",
salesEmail: "", salesEmail: "",
supportEmail: "", supportEmail: "",
}; };
@ -450,11 +451,28 @@ export class App extends LiteElement {
} }
private renderSignUpLink() { private renderSignUpLink() {
if (!this.appState.settings) return; const { registrationEnabled, signUpUrl } = this.appState.settings || {};
if (this.appState.settings.registrationEnabled) { if (registrationEnabled) {
return html` return html`
<sl-button variant="text" @click="${() => this.navigate("/sign-up")}"> <sl-button
href="/sign-up"
size="small"
@click="${(e: MouseEvent) => {
if (!this.navHandleAnchorClick(e)) {
return;
}
this.navigate("/sign-up");
}}"
>
${msg("Sign Up")}
</sl-button>
`;
}
if (signUpUrl) {
return html`
<sl-button href=${signUpUrl} size="small">
${msg("Sign Up")} ${msg("Sign Up")}
</sl-button> </sl-button>
`; `;
@ -947,7 +965,7 @@ export class App extends LiteElement {
private clearUser() { private clearUser() {
this.authService.logout(); this.authService.logout();
this.authService = new AuthService(); this.authService = new AuthService();
AppStateService.resetUser(); AppStateService.resetAll();
} }
private showDialog(content: DialogContent) { private showDialog(content: DialogContent) {

View File

@ -6,6 +6,7 @@ export type AppSettings = {
maxPagesPerCrawl: number; maxPagesPerCrawl: number;
maxScale: number; maxScale: number;
billingEnabled: boolean; billingEnabled: boolean;
signUpUrl: string;
salesEmail: string; salesEmail: string;
supportEmail: string; supportEmail: string;
}; };

View File

@ -51,6 +51,13 @@ export default class LiteElement extends LitElement {
return this; return this;
} }
/**
* @deprecated New components should use NavigateController directly
*/
navHandleAnchorClick = (
...args: Parameters<NavigateController["handleAnchorClick"]>
) => this.navigateController.handleAnchorClick(...args);
/** /**
* @deprecated New components should use NavigateController directly * @deprecated New components should use NavigateController directly
*/ */

View File

@ -9,14 +9,54 @@ import type {
const STORAGE_KEY_PREFIX = "btrix.app"; const STORAGE_KEY_PREFIX = "btrix.app";
export const persist = (storage: Storage): StateOptions => ({ type ExpiringValue = {
set(stateVar: StateVar, v: string) { value: unknown;
storage.setItem(`${STORAGE_KEY_PREFIX}.${stateVar.key}`, JSON.stringify(v)); expiry: number;
};
export const persist = (
storage: Storage,
ttlMinutes?: number,
): StateOptions => ({
set(stateVar: StateVar, v: string | null | undefined) {
if (v === null || v === undefined) {
storage.removeItem(`${STORAGE_KEY_PREFIX}.${stateVar.key}`);
} else {
storage.setItem(
`${STORAGE_KEY_PREFIX}.${stateVar.key}`,
JSON.stringify(
ttlMinutes
? ({
value: v,
expiry: Date.now() + ttlMinutes * 1000 * 60,
} as ExpiringValue)
: v,
),
);
}
stateVar.value = v; stateVar.value = v;
}, },
get(stateVar: ReadonlyStateVar) { get(stateVar: ReadonlyStateVar) {
const stored = storage.getItem(`${STORAGE_KEY_PREFIX}.${stateVar.key}`); const stored = storage.getItem(`${STORAGE_KEY_PREFIX}.${stateVar.key}`);
return stored ? (JSON.parse(stored) as unknown) : undefined; if (stored) {
const data = JSON.parse(stored) as unknown;
if (
data !== null &&
typeof data === "object" &&
Object.prototype.hasOwnProperty.call(data, "expiry") &&
Object.prototype.hasOwnProperty.call(data, "value")
) {
if (Date.now() > (data as ExpiringValue).expiry) {
storage.removeItem(`${STORAGE_KEY_PREFIX}.${stateVar.key}`);
return undefined;
}
return (data as ExpiringValue).value;
}
return data;
}
return undefined;
}, },
init(stateVar: ReadonlyStateVar, valueInit?: unknown) { init(stateVar: ReadonlyStateVar, valueInit?: unknown) {
return stateVar.options.get(stateVar) || valueInit; return stateVar.options.get(stateVar) || valueInit;

View File

@ -22,7 +22,8 @@ export function makeAppStateService() {
@state() @state()
class AppState { class AppState {
@options(persist(window.localStorage)) // @TODO Persist in local storage with expiry
@options(persist(window.sessionStorage))
settings: AppSettings | null = null; settings: AppSettings | null = null;
@options(persist(window.sessionStorage)) @options(persist(window.sessionStorage))
@ -140,7 +141,6 @@ export function makeAppStateService() {
@unlock() @unlock()
resetAll() { resetAll() {
appState.settings = null; appState.settings = null;
appState.org = undefined;
this._resetUser(); this._resetUser();
} }
@ -154,6 +154,7 @@ export function makeAppStateService() {
appState.auth = null; appState.auth = null;
appState.userInfo = null; appState.userInfo = null;
appState.orgSlug = null; appState.orgSlug = null;
appState.org = undefined;
} }
} }