Skip to content

Free premium slots: 2 premium features for signed-in users#213

Merged
lawrencehook merged 15 commits intomainfrom
free-premium-slots
Apr 20, 2026
Merged

Free premium slots: 2 premium features for signed-in users#213
lawrencehook merged 15 commits intomainfrom
free-premium-slots

Conversation

@lawrencehook
Copy link
Copy Markdown
Owner

@lawrencehook lawrencehook commented Apr 17, 2026

Closes #212

Summary

Lets signed-in non-premium users activate up to 2 premium features (FREE_PREMIUM_SLOTS = 2 in src/shared/config.js) as freebies. Any excess is clamped at the storage-write layer, so both direct toggles and cascaded effects writes behave the same.

Tier model

New frozen TIER constant in shared/config.js:

  • TIER.PREMIUM — valid paid license
  • TIER.FREE_SIGNED_IN — signed in, no paid license (gets the 2 slots)
  • TIER.FREE — signed out

License.getTierSync(licenseToken, sessionToken) resolves the tier. An html[tier] attribute drives CSS + JS gates and is set synchronously from local storage before the options page renders, then refined after the async license check.

Enforcement

Single clamp point per runtime:

  • src/options/main.js#updateSetting — on any premium-setting write
  • src/content-script/main.js#logStorageChange — on any premium-setting storage change

Both reuse shared helpers in src/shared/main.js:

  • PREMIUM_FEATURE_IDS / PREMIUM_FEATURE_ID_SET (includes schedule & password)
  • countActivePremium(cache)
  • clearAllPremium(settings) — in-memory only (storage preserved so premium state returns on re-upgrade)
  • enforceSlotBudget(settings, slotLimit) — mutates + returns writeBack map

UI

  • Slot indicator: two dots in the options header (right of the search bar). Filled blue for used slots, hollow for free. Tooltip shows N/2 free premium features used. Visible only on TIER.FREE_SIGNED_IN.
  • Blue-dot marker on every premium option (replaces the padlock) for non-premium tiers, scoped via html:not([tier='premium']). Same treatment applied to menu-level premium options (Schedule, Password, Edit Schedule). Premium users see no marker.
  • Upgrade modal takes a { reason: 'slot_limit' } option: when the slot budget is full, shows a short note ("Free tier allows 2 premium features. Upgrade or deselect one.") above the checkout plans. Modal width capped to prevent the note from stretching the layout.
  • Premium-required modal (signed-out users): "Sign in to enable any 2 premium features. Upgrade to unlock them all."
  • "free" sidebar tab filters out premium options and hides sections that contain only premium options. Default selection for non-premium tiers; premium users still get the URL-based default (Basic / Homepage / etc.).
  • Current-selection indicator in the top-right of #primary_options: sticky on scroll, shows the active sidebar label or the search query in quotes.
  • Typography: modal descriptions split into premium-note-main / premium-note-sub spans for softer hierarchy.
  • Password modal buttons repainted to match the other modal buttons (blue bg + white text + disabled fade) — previously the disabled Confirm button was near-invisible in dark mode.

Tier transitions

  • Sign-in: provisional TIER.FREE_SIGNED_IN immediately, refined by refreshLicense().
  • Sign-out (explicit or 401): disableAllPremiumFeatures() loops PREMIUM_FEATURE_IDS (including schedule/password) via updateSetting, which cascades through storage to open content-script tabs.
  • Premium-to-free downgrade (subscription lapsed while options open): pruneToSlotBudget() keeps the first 2 active premium features and disables the rest. Delegates the "keep first N" rule to shared enforceSlotBudget().

Menu-level features

Schedule and Password live in OTHER_SETTINGS (not SECTIONS) but are premium-gated. They're added to PREMIUM_FEATURE_IDS so they count toward the slot budget. A new canUsePremiumFeature(settingId) helper gates their menu-click handlers: at 2/2 slots (and not already active) it opens the upgrade modal with slot-limit copy; already-active features can always be opened for management.

Grandfathered / expired premium

Grandfathered users are unaffected — they show as premium via the existing license flow. Expired-premium users fall back to TIER.FREE_SIGNED_IN, and the initial load calls enforceSlotBudget so their first 2 premium settings stay on and the rest are cleared (both in-memory and persisted).

Firefox install handler fix

src/background/events.js now awaits browser.management.getSelf() instead of passing a callback. Chrome MV3 returns a promise; Firefox MV2 silently ignored the callback — the welcome tab previously never opened on real Firefox installs after the dev-install skip was added on main.

Manifests

shared/config.js is now loaded into the content-script context in both chrome_manifest.json and firefox_manifest.json (required for PREMIUM_CONFIG.FREE_PREMIUM_SLOTS and TIER).

🤖 Generated with Claude Code

lawrencehook and others added 15 commits April 17, 2026 19:15
Signed-in non-premium users can now activate up to 2 premium features
(FREE_PREMIUM_SLOTS). Enforcement lives in updateSetting / logStorageChange
so direct toggles and effect-chained writes are clamped consistently.
Toggle clicks at the slot limit reuse the upgrade modal with slot-limit
copy. New html[tier] attribute drives styling; header shows "N/2 free
premium" when applicable.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
chrome.management.getSelf() / browser.management.getSelf() reports
installType 'development' for unpacked/temporary installs (Chrome
--load-extension, Firefox web-ext run). Guard the first-install welcome
tab on that so local reloads and the UI test harness do not pop a new
tab every time the profile is fresh.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
YouTube migrated the subscriptions feed to yt-lockup-view-model with
yt-thumbnail-view-model inside, so `ytd-thumbnail` no longer matches
the feed thumbnails there. Add the new element to the
remove_video_thumbnails hide rule and the blur_video_thumbnails filter
rule so both features work on /feed/subscriptions again.

Caught by the rys-test Playwright harness.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- New "free" sidebar (left of all/active) filters to non-premium options
  and hides sections that have none. Default for non-premium tiers.
- Sticky label in the top-right of #primary_options shows the current
  sidebar selection or search query. Uses margin-bottom: -28px so it
  overlays without pushing content down; left+bottom border gives it a
  tab-cutout look flush with the top/right edges.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- Fix: content-script referenced PREMIUM_CONFIG.FREE_PREMIUM_SLOTS but
  config.js was not loaded by either manifest's content_scripts.js —
  would have thrown for free_signed_in users on YouTube pages. Added
  /shared/config.js to the chrome + firefox content-script manifests.
- Extract clearAllPremium / enforceSlotBudget helpers to shared/main.js;
  both content-script and options/main.js now use them at load.
- Simplify the token-change handler in content-script's logStorageChange.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
"Sign in to pick any 2 premium features. Upgrade to unlock them all."
Frames the signed-in free tier around user choice rather than a
"premium required" gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
… tier

- Schedule and Password are now in PREMIUM_FEATURE_IDS, so signed-in free
  users can activate them as one of their 2 freebies. New
  canUsePremiumFeature(settingId) helper in settings-menu.js gates the
  menu clicks: at 2/2 slots (and not already active) it opens the upgrade
  modal with slot-limit copy, otherwise it opens the feature modal.
- Remove html[is_premium] attribute. Tier covers the same info
  (premium | free_signed_in | free). Updated ~6 CSS selectors and ~4 JS
  checks from is_premium to tier='premium'. No behavior change.
- Scope the blue premium-marker dot to non-premium tiers (was visible to
  all users, now only shown when the dot carries signal).
- Typography: split the premium-required modal and slot-limit note into
  main/sub spans for softer hierarchy.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ontrast

- Replace the padlock on Schedule/Password/Edit Schedule menu items with
  the same blue dot used on main-list premium options. Opacity bumped
  from 0.25 to 0.65 to match the main list and reflect that these are
  now activatable as freebies by signed-in free users.
- Password modal buttons (Confirm/Remove Password/Unlock) were using
  color: var(--body-color) with a browser-default light background, so
  in dark mode the text became near-invisible (light on light). Apply
  the same blue bg + white text + disabled fade used by the other modal
  buttons.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- Set html[tier] synchronously in options/main.js right after
  License.getTierSync(), before populateOptions() renders or wires click
  handlers. Avoids a window where premium clicks saw tier===null during
  initAccountState()'s async init and misrouted to the signed-out modal.
- New pruneToSlotBudget() in settings-menu.js runs when updatePremiumUI
  transitions into free_signed_in (subscription lapsed while options is
  open). It delegates the "keep first N" rule to shared enforceSlotBudget
  and routes over-budget IDs through updateSetting so DOM, cache, slot
  indicator, storage, and open content-script tabs stay in sync.
- disableAllPremiumFeatures() replaces the sign-out handler's SECTIONS
  loop (which missed schedule/password) and is also called from
  updatePremiumUI({ signedOut: true }) so a 401 prunes state the same
  way an explicit sign-out does.
- Switch background install handler to await browser.management.getSelf().
  Chrome MV3 returns a Promise when no callback is passed; Firefox MV2
  is promise-native and silently ignores callbacks. Previous callback
  form dropped the welcome-tab open on real Firefox installs.
  management.getSelf is exempt from the management permission, so no
  manifest change needed.
- Remove unused handlePremiumFeatureClick() (dead code since the switch
  to canUsePremiumFeature()).
- Document the clearAllPremium() / enforceSlotBudget() API asymmetry in
  shared/main.js: clearAllPremium is in-memory only so premium state
  returns on re-upgrade; enforceSlotBudget returns a writeBack map so
  over-budget settings don't re-surface on next load.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Replace raw 'premium' / 'free_signed_in' / 'free' string literals in JS
with TIER.PREMIUM / TIER.FREE_SIGNED_IN / TIER.FREE. The new constant
lives in shared/config.js as a frozen object — loaded by both options
and content-script via the existing script order.

CSS selectors (html[tier='premium']) keep string literals since they
target the underlying HTML attribute, which still holds the string
value. UI labels like the "free" sidebar tab text and data-status
attributes are separate concerns and untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
@lawrencehook lawrencehook merged commit ad3d634 into main Apr 20, 2026
4 checks passed
@lawrencehook lawrencehook deleted the free-premium-slots branch April 20, 2026 00:19
lawrencehook added a commit that referenced this pull request Apr 20, 2026
Lets signed-in free-tier users enable up to 2 premium features at no cost.
lawrencehook added a commit that referenced this pull request Apr 20, 2026
Lets signed-in free-tier users enable up to 2 premium features at no cost.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

allow 2 active premium features on free tier when signed in

1 participant