From 337454f8c9941c27b19e9b13bbd1e19bb390098b Mon Sep 17 00:00:00 2001 From: sua yoo Date: Mon, 26 Aug 2024 17:26:25 -0700 Subject: [PATCH] feat: Add link to hosted sign-up page (#2045) Resolves https://github.com/webrecorder/browsertrix/issues/2043 ### Changes - Shows link to sign up in UI if `sign_up_url` is configured. - Expires settings in session storage (for now) --- backend/btrixcloud/main.py | 3 ++ backend/test/test_api.py | 1 + chart/templates/configmap.yaml | 2 ++ chart/values.yaml | 8 ++++- frontend/src/controllers/navigate.ts | 28 ++++++++++------ frontend/src/index.ts | 26 ++++++++++++--- frontend/src/types/app.ts | 1 + frontend/src/utils/LiteElement.ts | 7 ++++ frontend/src/utils/persist.ts | 48 +++++++++++++++++++++++++--- frontend/src/utils/state.ts | 5 +-- 10 files changed, 108 insertions(+), 21 deletions(-) diff --git a/backend/btrixcloud/main.py b/backend/btrixcloud/main.py index d161d75d..3678f49e 100644 --- a/backend/btrixcloud/main.py +++ b/backend/btrixcloud/main.py @@ -115,6 +115,8 @@ class SettingsResponse(BaseModel): billingEnabled: bool + signUpUrl: str = "" + salesEmail: str = "" supportEmail: str = "" @@ -143,6 +145,7 @@ def main() -> None: maxPagesPerCrawl=int(os.environ.get("MAX_PAGES_PER_CRAWL", 0)), maxScale=int(os.environ.get("MAX_CRAWL_SCALE", 3)), billingEnabled=is_bool(os.environ.get("BILLING_ENABLED")), + signUpUrl=os.environ.get("SIGN_UP_URL", ""), salesEmail=os.environ.get("SALES_EMAIL", ""), supportEmail=os.environ.get("EMAIL_SUPPORT", ""), ) diff --git a/backend/test/test_api.py b/backend/test/test_api.py index 5cc0fd74..439bfbff 100644 --- a/backend/test/test_api.py +++ b/backend/test/test_api.py @@ -46,6 +46,7 @@ def test_api_settings(): "maxScale": 3, "defaultPageLoadTimeSeconds": 120, "billingEnabled": True, + "signUpUrl": "", "salesEmail": "", "supportEmail": "", } diff --git a/chart/templates/configmap.yaml b/chart/templates/configmap.yaml index 3f8e2c6c..fa1c7db6 100644 --- a/chart/templates/configmap.yaml +++ b/chart/templates/configmap.yaml @@ -62,6 +62,8 @@ data: BILLING_ENABLED: "{{ .Values.billing_enabled }}" + SIGN_UP_URL: "{{ .Values.sign_up_url }}" + SALES_EMAIL: "{{ .Values.sales_email }}" LOG_SENT_EMAILS: "{{ .Values.email.log_sent_emails }}" diff --git a/chart/values.yaml b/chart/values.yaml index 5e7406bc..5966aee7 100644 --- a/chart/values.yaml +++ b/chart/values.yaml @@ -60,9 +60,10 @@ volume_storage_class: # if set, set the node selector 'nodeType' to this crawling pods # crawler_node_type: +# if set to "1", enables open registration 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: "" jwt_token_lifetime_minutes: 1440 @@ -113,6 +114,11 @@ profile_browser_idle_seconds: 60 # set to true to enable subscriptions API and Billing tab 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 sales_email: "" diff --git a/frontend/src/controllers/navigate.ts b/frontend/src/controllers/navigate.ts index 5fa64e43..c826b284 100644 --- a/frontend/src/controllers/navigate.ts +++ b/frontend/src/controllers/navigate.ts @@ -51,15 +51,7 @@ export class NavigateController implements ReactiveController { this.host.dispatchEvent(evt); }; - /** - * Bind to anchor tag to prevent full page navigation - * @example - * ```ts - * go - * ``` - * @param event Click event - */ - link = (event: MouseEvent, _href?: string, resetScroll = true): void => { + handleAnchorClick = (event: MouseEvent) => { if ( // Detect keypress for opening in a new tab event.ctrlKey || @@ -69,11 +61,27 @@ export class NavigateController implements ReactiveController { // Account for event prevented on anchor tag event.defaultPrevented ) { - return; + return false; } event.preventDefault(); + return true; + }; + + /** + * Bind to anchor tag to prevent full page navigation + * @example + * ```ts + * go + * ``` + * @param event Click event + */ + link = (event: MouseEvent, _href?: string, resetScroll = true): void => { + if (!this.handleAnchorClick(event)) { + return; + } + const el = event.currentTarget as HTMLAnchorElement | null; if (el?.ariaDisabled === "true") { diff --git a/frontend/src/index.ts b/frontend/src/index.ts index 46db02bf..9fa8a970 100644 --- a/frontend/src/index.ts +++ b/frontend/src/index.ts @@ -196,6 +196,7 @@ export class App extends LiteElement { maxPagesPerCrawl: 0, maxScale: 0, billingEnabled: false, + signUpUrl: "", salesEmail: "", supportEmail: "", }; @@ -450,11 +451,28 @@ export class App extends LiteElement { } private renderSignUpLink() { - if (!this.appState.settings) return; + const { registrationEnabled, signUpUrl } = this.appState.settings || {}; - if (this.appState.settings.registrationEnabled) { + if (registrationEnabled) { return html` - + + ${msg("Sign Up")} + + `; + } + + if (signUpUrl) { + return html` + ${msg("Sign Up")} `; @@ -947,7 +965,7 @@ export class App extends LiteElement { private clearUser() { this.authService.logout(); this.authService = new AuthService(); - AppStateService.resetUser(); + AppStateService.resetAll(); } private showDialog(content: DialogContent) { diff --git a/frontend/src/types/app.ts b/frontend/src/types/app.ts index 5844ed02..3078afea 100644 --- a/frontend/src/types/app.ts +++ b/frontend/src/types/app.ts @@ -6,6 +6,7 @@ export type AppSettings = { maxPagesPerCrawl: number; maxScale: number; billingEnabled: boolean; + signUpUrl: string; salesEmail: string; supportEmail: string; }; diff --git a/frontend/src/utils/LiteElement.ts b/frontend/src/utils/LiteElement.ts index 87a1cd25..c79168f1 100644 --- a/frontend/src/utils/LiteElement.ts +++ b/frontend/src/utils/LiteElement.ts @@ -51,6 +51,13 @@ export default class LiteElement extends LitElement { return this; } + /** + * @deprecated New components should use NavigateController directly + */ + navHandleAnchorClick = ( + ...args: Parameters + ) => this.navigateController.handleAnchorClick(...args); + /** * @deprecated New components should use NavigateController directly */ diff --git a/frontend/src/utils/persist.ts b/frontend/src/utils/persist.ts index e126499f..bd8ee0bc 100644 --- a/frontend/src/utils/persist.ts +++ b/frontend/src/utils/persist.ts @@ -9,14 +9,54 @@ import type { const STORAGE_KEY_PREFIX = "btrix.app"; -export const persist = (storage: Storage): StateOptions => ({ - set(stateVar: StateVar, v: string) { - storage.setItem(`${STORAGE_KEY_PREFIX}.${stateVar.key}`, JSON.stringify(v)); +type ExpiringValue = { + value: unknown; + 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; }, get(stateVar: ReadonlyStateVar) { 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) { return stateVar.options.get(stateVar) || valueInit; diff --git a/frontend/src/utils/state.ts b/frontend/src/utils/state.ts index 070c68c2..8c954936 100644 --- a/frontend/src/utils/state.ts +++ b/frontend/src/utils/state.ts @@ -22,7 +22,8 @@ export function makeAppStateService() { @state() class AppState { - @options(persist(window.localStorage)) + // @TODO Persist in local storage with expiry + @options(persist(window.sessionStorage)) settings: AppSettings | null = null; @options(persist(window.sessionStorage)) @@ -140,7 +141,6 @@ export function makeAppStateService() { @unlock() resetAll() { appState.settings = null; - appState.org = undefined; this._resetUser(); } @@ -154,6 +154,7 @@ export function makeAppStateService() { appState.auth = null; appState.userInfo = null; appState.orgSlug = null; + appState.org = undefined; } }