mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-29 09:23:16 -06:00
feat(config): HIDE_BILLING flag to hide the Subscription/billing UI (#116)
Opt-in, default-off UI gate (per strobe's spec; verified his file refs first).
When set, hides the Subscription sidebar item + billing view and bounces
#/billing to the dashboard. Billing shown by default -> existing deployments
unchanged. UI-only: /api/subscription/* untouched (internal usage reads stay).
- config.js: config.hideBilling from HIDE_BILLING (mirrors selfHosted).
- auth.js: surface hide_billing on GET /api/auth/me (client already fetches it
at boot, stored on the user object).
- index.html: id="billingNavItem" on the Subscription <li> (mirrors adminNavItem).
- app.js: toggle billingNavItem in updateSidebarUser (next to the admin toggle);
guard #/billing -> history.replaceState('#/') + render dashboard (replaceState
so the back button doesn't loop into the guard).
- .env.example + README documented.
Spec assumptions verified against code: adminNavItem toggle pattern exists;
/me is fetched at boot and updateSidebarUser runs both at boot (cached user)
and post-/me, so no-flash holds on warm loads (one-time flash possible on the
first load after the flag flips — same as the admin nav, minor); route dispatch
is an if/else chain. Nav label is static (no data-i18n) so no i18n change.
Validated (headless Chrome, both states):
- flag unset -> Subscription tab present, #/billing renders (backward-compat).
- HIDE_BILLING=true -> tab hidden, #/billing redirects to #/.
- config maps HIDE_BILLING both ways; live /me default hide_billing=false.
- 149 server tests green. Default-off = zero change for existing deployments.
Known cosmetic (harmless): after the redirect the billing nav LINK keeps its
'active' class, but the nav item is display:none so it's never visible.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5b13254de3
commit
674a34ba45
|
|
@ -11,6 +11,12 @@
|
||||||
# instance never emits mail from a domain that isn't yours.
|
# instance never emits mail from a domain that isn't yours.
|
||||||
SELF_HOSTED=true
|
SELF_HOSTED=true
|
||||||
|
|
||||||
|
# Hide the Subscription/billing UI (nav item + pricing cards) and bounce #/billing to
|
||||||
|
# the dashboard. Opt-in; default off (billing shown). For instances that bill customers
|
||||||
|
# externally and don't sell plans through the app. UI-only — does not change SELF_HOSTED
|
||||||
|
# or disable any /api/subscription endpoints.
|
||||||
|
HIDE_BILLING=true
|
||||||
|
|
||||||
# Close public self-service registration — for instances where all accounts are
|
# Close public self-service registration — for instances where all accounts are
|
||||||
# provisioned by your team (admin "Add user" / invites). When true, the public
|
# provisioned by your team (admin "Add user" / invites). When true, the public
|
||||||
# signup route is blocked (OAuth auto-signup with it) AND the login page hides
|
# signup route is blocked (OAuth auto-signup with it) AND the login page hides
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ Schema migrations run automatically on first boot — no manual migration comman
|
||||||
| `HTTPS_PORT` | HTTPS port (used when SSL certs are present) | `3443` |
|
| `HTTPS_PORT` | HTTPS port (used when SSL certs are present) | `3443` |
|
||||||
| `NODE_ENV` | Runtime env (`production` enables Express production optimizations + stricter error handling) | _(none)_ |
|
| `NODE_ENV` | Runtime env (`production` enables Express production optimizations + stricter error handling) | _(none)_ |
|
||||||
| `SELF_HOSTED` | First user gets all features unlocked | `false` |
|
| `SELF_HOSTED` | First user gets all features unlocked | `false` |
|
||||||
|
| `HIDE_BILLING` | Hide the Subscription nav item + billing view; `#/billing` redirects to the dashboard (UI-only, opt-in) | `false` |
|
||||||
| `DISABLE_REGISTRATION` | Block new account creation (including OAuth auto-signup). First-user setup on an empty DB is still allowed. | `false` |
|
| `DISABLE_REGISTRATION` | Block new account creation (including OAuth auto-signup). First-user setup on an empty DB is still allowed. | `false` |
|
||||||
| `DISABLE_HOMEPAGE` | Redirect `/` to `/app` instead of serving the marketing landing page. For internal-only self-hosted deployments. | `false` |
|
| `DISABLE_HOMEPAGE` | Redirect `/` to `/app` instead of serving the marketing landing page. For internal-only self-hosted deployments. | `false` |
|
||||||
| `APP_URL` | Your public URL (used for Stripe callbacks and invite-accept URLs in emailed invites) | _(none)_ |
|
| `APP_URL` | Your public URL (used for Stripe callbacks and invite-accept URLs in emailed invites) | _(none)_ |
|
||||||
|
|
|
||||||
|
|
@ -132,7 +132,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
<span>Settings</span>
|
<span>Settings</span>
|
||||||
</a></li>
|
</a></li>
|
||||||
<li><a href="#/billing" class="nav-link" data-view="billing">
|
<li id="billingNavItem"><a href="#/billing" class="nav-link" data-view="billing">
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/>
|
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"/>
|
||||||
<line x1="1" y1="10" x2="23" y2="10"/>
|
<line x1="1" y1="10" x2="23" y2="10"/>
|
||||||
|
|
|
||||||
|
|
@ -458,8 +458,17 @@ function route() {
|
||||||
currentView = settings;
|
currentView = settings;
|
||||||
settings.render(app);
|
settings.render(app);
|
||||||
} else if (hash === '#/billing') {
|
} else if (hash === '#/billing') {
|
||||||
currentView = billing;
|
// #116: when HIDE_BILLING is set, a direct #/billing navigation is bounced to the
|
||||||
billing.render(app);
|
// dashboard. replaceState (not a hash assignment) so it doesn't add a history entry
|
||||||
|
// — the back button skips over it instead of looping back into the guard.
|
||||||
|
if (getCurrentUser()?.hide_billing) {
|
||||||
|
history.replaceState(null, '', window.location.pathname + '#/');
|
||||||
|
currentView = dashboard;
|
||||||
|
dashboard.render(app);
|
||||||
|
} else {
|
||||||
|
currentView = billing;
|
||||||
|
billing.render(app);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
currentView = dashboard;
|
currentView = dashboard;
|
||||||
dashboard.render(app);
|
dashboard.render(app);
|
||||||
|
|
@ -474,6 +483,11 @@ function updateSidebarUser() {
|
||||||
const adminNav = document.getElementById('adminNavItem');
|
const adminNav = document.getElementById('adminNavItem');
|
||||||
if (adminNav) adminNav.style.display = isPlatformAdmin(user) ? '' : 'none';
|
if (adminNav) adminNav.style.display = isPlatformAdmin(user) ? '' : 'none';
|
||||||
|
|
||||||
|
// #116: hide the Subscription nav item when HIDE_BILLING is set (surfaced on /me).
|
||||||
|
// Runs at boot from the cached user (no flash on warm loads) and again after /me.
|
||||||
|
const billingNav = document.getElementById('billingNavItem');
|
||||||
|
if (billingNav) billingNav.style.display = user.hide_billing ? 'none' : '';
|
||||||
|
|
||||||
let userEl = document.getElementById('sidebarUser');
|
let userEl = document.getElementById('sidebarUser');
|
||||||
if (!userEl) {
|
if (!userEl) {
|
||||||
const footer = document.querySelector('.sidebar-footer');
|
const footer = document.querySelector('.sidebar-footer');
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,10 @@ module.exports = {
|
||||||
graphDevRestrictTo: process.env.GRAPH_DEV_RESTRICT_TO || '',
|
graphDevRestrictTo: process.env.GRAPH_DEV_RESTRICT_TO || '',
|
||||||
// Self-hosted mode: if true, first user gets enterprise plan and no billing
|
// Self-hosted mode: if true, first user gets enterprise plan and no billing
|
||||||
selfHosted: process.env.SELF_HOSTED === 'true',
|
selfHosted: process.env.SELF_HOSTED === 'true',
|
||||||
|
// #116: opt-in UI gate. When true, hides the Subscription nav item + billing view
|
||||||
|
// and bounces #/billing to the dashboard. Default off, so existing deployments are
|
||||||
|
// unchanged. UI-only — /api/subscription/* stays in place (internal usage reads).
|
||||||
|
hideBilling: process.env.HIDE_BILLING === 'true',
|
||||||
// Disable public registration (OAuth auto-signup is also blocked when set).
|
// Disable public registration (OAuth auto-signup is also blocked when set).
|
||||||
// First-user setup is still allowed so a fresh install can be initialized.
|
// First-user setup is still allowed so a fresh install can be initialized.
|
||||||
disableRegistration: ['true', '1'].includes(String(process.env.DISABLE_REGISTRATION || '').toLowerCase()),
|
disableRegistration: ['true', '1'].includes(String(process.env.DISABLE_REGISTRATION || '').toLowerCase()),
|
||||||
|
|
|
||||||
|
|
@ -522,6 +522,7 @@ router.get('/me', requireAuth, resolveTenancy, (req, res) => {
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
...req.user,
|
...req.user,
|
||||||
|
hide_billing: config.hideBilling, // #116: client hides the Subscription nav + guards #/billing
|
||||||
current_workspace_id: req.workspaceId,
|
current_workspace_id: req.workspaceId,
|
||||||
current_workspace: req.workspace ? { id: req.workspace.id, name: req.workspace.name, organization_id: req.workspace.organization_id } : null,
|
current_workspace: req.workspace ? { id: req.workspace.id, name: req.workspace.name, organization_id: req.workspace.organization_id } : null,
|
||||||
current_organization: currentOrg,
|
current_organization: currentOrg,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue