Skip to content

Commit 31b56a5

Browse files
committed
Free premium slots: 2 premium features for signed-in users (#213)
Lets signed-in free-tier users enable up to 2 premium features at no cost.
1 parent 310cb91 commit 31b56a5

12 files changed

Lines changed: 376 additions & 141 deletions

File tree

src/background/events.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,14 @@ if (typeof browser === 'undefined') {
88
const uninstallUrl = "http://lawrencehook.com/rys/👋";
99
browser.runtime.setUninstallURL(uninstallUrl);
1010

11-
browser.runtime.onInstalled.addListener(object => {
11+
browser.runtime.onInstalled.addListener(async object => {
1212
const url = "http://lawrencehook.com/rys/welcome";
13-
if (object.reason === browser.runtime.OnInstalledReason.INSTALL) {
14-
browser.tabs.create({ url }, tab => {});
15-
}
13+
if (object.reason !== browser.runtime.OnInstalledReason.INSTALL) return;
14+
// management.getSelf() returns a Promise in both Chrome MV3 and Firefox
15+
// (Firefox ignores callbacks), so use await for cross-browser consistency.
16+
const info = await browser.management.getSelf();
17+
if (info && info.installType === 'development') return; // skip on unpacked/dev loads
18+
browser.tabs.create({ url });
1619
});
1720

1821
// Change the browserAction icon if the extension is disabled

src/chrome_manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
{
2121
"js": [
2222
"/shared/main.js",
23+
"/shared/config.js",
2324
"/shared/https.js",
2425
"/shared/utils.js",
2526
"/shared/banners.js",

src/content-script/main.js

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,6 @@ let lastRedirect;
5353
const redirectInterval = 1_000; // 1 second
5454

5555

56-
// Get list of premium feature IDs
57-
const PREMIUM_FEATURE_IDS = SECTIONS.flatMap(section =>
58-
section.options.filter(opt => opt.premium).map(opt => opt.id)
59-
);
60-
6156
// Decode license token to check premium status (no signature verification - we trust our server)
6257
function decodeLicenseToken(token) {
6358
if (!token) return null;
@@ -77,21 +72,35 @@ function isPremiumFromToken(token) {
7772
return decoded.premium === true && decoded.exp > now;
7873
}
7974

75+
function getTier(licenseToken, sessionToken) {
76+
if (isPremiumFromToken(licenseToken)) return TIER.PREMIUM;
77+
if (sessionToken) return TIER.FREE_SIGNED_IN;
78+
return TIER.FREE;
79+
}
80+
8081
// Respond to changes in settings
8182
function logStorageChange(changes, area) {
8283
if (area !== 'local') return;
8384

84-
// Update premium status in cache if license token changed
85-
if ('license_token' in changes) {
86-
cache['_isPremium'] = isPremiumFromToken(changes['license_token'].newValue);
85+
if ('license_token' in changes) cache['_licenseToken'] = changes['license_token'].newValue;
86+
if ('session_token' in changes) cache['_sessionToken'] = changes['session_token'].newValue;
87+
if ('license_token' in changes || 'session_token' in changes) {
88+
cache['_tier'] = getTier(cache['_licenseToken'], cache['_sessionToken']);
8789
}
8890

8991
Object.entries(changes).forEach(([id, { oldValue, newValue }]) => {
9092
if (oldValue === newValue) return;
9193

92-
// Enforce premium features are false for non-premium users
93-
if (PREMIUM_FEATURE_IDS.includes(id) && cache['_isPremium'] !== true) {
94-
newValue = false;
94+
if (PREMIUM_FEATURE_ID_SET.has(id)) {
95+
const tier = cache['_tier'];
96+
if (tier === TIER.FREE) {
97+
newValue = false;
98+
} else if (tier === TIER.FREE_SIGNED_IN && newValue === true) {
99+
const alreadyOn = cache[id] === true;
100+
if (!alreadyOn && countActivePremium(cache) >= PREMIUM_CONFIG.FREE_PREMIUM_SLOTS) {
101+
newValue = false;
102+
}
103+
}
95104
}
96105

97106
HTML.setAttribute(id, newValue);
@@ -113,13 +122,15 @@ browser.storage.local.get(settings => {
113122
browser.storage.local.set(revealUpdates);
114123
}
115124

116-
// Enforce premium features are false for non-premium users
117-
const isPremium = isPremiumFromToken(settings['license_token']);
118-
cache['_isPremium'] = isPremium;
119-
if (!isPremium) {
120-
PREMIUM_FEATURE_IDS.forEach(id => {
121-
settings[id] = false;
122-
});
125+
const tier = getTier(settings['license_token'], settings['session_token']);
126+
cache['_tier'] = tier;
127+
cache['_licenseToken'] = settings['license_token'];
128+
cache['_sessionToken'] = settings['session_token'];
129+
if (tier === TIER.FREE) {
130+
clearAllPremium(settings);
131+
} else if (tier === TIER.FREE_SIGNED_IN) {
132+
const writeBack = enforceSlotBudget(settings, PREMIUM_CONFIG.FREE_PREMIUM_SLOTS);
133+
if (Object.keys(writeBack).length) browser.storage.local.set(writeBack);
123134
}
124135

125136
Object.entries({ ...DEFAULT_SETTINGS, ...settings}).forEach(([ id, value ]) => {

src/firefox_manifest.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
{
2424
"js": [
2525
"/shared/main.js",
26+
"/shared/config.js",
2627
"/shared/https.js",
2728
"/shared/utils.js",
2829
"/shared/banners.js",

src/options/body.css

Lines changed: 86 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,21 @@ html[foo=bar] {
318318
}
319319

320320
#password_container_background button {
321-
color: var(--body-color);
321+
padding: 8px 16px;
322+
font-size: 0.85rem;
323+
font-weight: 500;
324+
border: none;
325+
border-radius: var(--border-radius);
326+
cursor: pointer;
327+
background-color: var(--blue-color);
328+
color: white;
329+
}
330+
#password_container_background button:hover {
331+
opacity: 0.9;
332+
}
333+
#password_container_background button:disabled {
334+
opacity: 0.6;
335+
cursor: not-allowed;
322336
}
323337

324338
#enable_password_container, #enable_password_container * {
@@ -381,10 +395,6 @@ html[foo=bar] {
381395
justify-self: start;
382396
}
383397

384-
#password-button button {
385-
color: var(--body-color);
386-
}
387-
388398
#disable_password_container a:first-child {
389399
font-weight: bold;
390400
}
@@ -592,6 +602,23 @@ html[dark_mode='true'] #active_sidebar .active_count {
592602
gap: 1px;
593603
}
594604

605+
#current_selection_indicator {
606+
position: sticky;
607+
top: 0;
608+
align-self: flex-end;
609+
margin-bottom: -28px;
610+
font-size: 0.8rem;
611+
color: var(--label-color);
612+
opacity: 0.75;
613+
padding: 4px 10px;
614+
background-color: var(--body-background-color);
615+
border-left: 1px solid var(--box-shadow-color);
616+
border-bottom: 1px solid var(--box-shadow-color);
617+
border-bottom-left-radius: 6px;
618+
pointer-events: none;
619+
z-index: 5;
620+
}
621+
595622

596623
/*************************************
597624
* SIGN-IN / ACCOUNT / UPGRADE MODALS
@@ -926,6 +953,7 @@ html[dark_mode='true'] #active_sidebar .active_count {
926953
/* Upgrade Modal */
927954
#upgrade_container {
928955
width: auto;
956+
max-width: min(90vw, 380px);
929957
padding: 24px;
930958
gap: 18px;
931959
align-items: flex-start;
@@ -940,8 +968,29 @@ html[dark_mode='true'] #active_sidebar .active_count {
940968
}
941969

942970
#premium_required_description {
943-
font-size: 0.85rem;
944971
margin: 0;
972+
text-align: center;
973+
}
974+
975+
#upgrade_slot_limit_note {
976+
color: var(--body-color);
977+
text-align: center;
978+
padding: 0 8px;
979+
}
980+
981+
.premium-note-main,
982+
.premium-note-sub {
983+
display: block;
984+
line-height: 1.4;
985+
}
986+
.premium-note-main {
987+
font-size: 0.9rem;
988+
opacity: 0.85;
989+
}
990+
.premium-note-sub {
991+
font-size: 0.8rem;
992+
opacity: 0.55;
993+
margin-top: 4px;
945994
}
946995

947996
#upgrade_plans {
@@ -1026,53 +1075,55 @@ html[dark_mode='true'] .upgrade-plan[selected] {
10261075
align-self: flex-end;
10271076
}
10281077

1029-
/* Premium lock icon for options */
1030-
html:not([is_premium='true']) .option[data-premium='true'] {
1031-
opacity: 0.25;
1032-
position: relative;
1078+
/* Premium options: dim slightly for non-premium users, full opacity for premium */
1079+
html:not([tier='premium']) .option[data-premium='true'] {
1080+
opacity: 0.65;
1081+
}
1082+
html[tier='premium'] .option[data-premium='true'],
1083+
#active_section .option[data-premium='true'] {
1084+
opacity: 1;
10331085
}
10341086

1035-
html:not([is_premium='true']) .option[data-premium='true']::after {
1087+
/* Blue dot at the right edge of premium options (only shown for non-premium users). */
1088+
.option[data-premium='true'] {
1089+
position: relative;
1090+
}
1091+
html:not([tier='premium']) .option[data-premium='true']::after {
10361092
content: '';
10371093
position: absolute;
10381094
right: 10px;
10391095
top: 50%;
10401096
transform: translateY(-50%);
1041-
width: 16px;
1042-
height: 16px;
1043-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23666'%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z'/%3E%3C/svg%3E");
1044-
background-size: contain;
1045-
background-repeat: no-repeat;
1046-
}
1047-
1048-
/* Premium options unlocked */
1049-
html[is_premium='true'] .option[data-premium='true'] {
1050-
opacity: 1;
1097+
width: 6px;
1098+
height: 6px;
1099+
border-radius: 50%;
1100+
background-color: var(--blue-color, #4a6cf7);
1101+
opacity: 0.85;
10511102
}
10521103

1053-
/* Premium lock icon for settings menu items */
1054-
html:not([is_premium='true']) #settings-menu div[data-premium='true'],
1055-
html:not([is_premium='true']) #power-options div[data-premium='true'] {
1056-
opacity: 0.25;
1104+
/* Premium marker for settings-menu / power-options items (same dot as main list) */
1105+
html:not([tier='premium']) #settings-menu div[data-premium='true'],
1106+
html:not([tier='premium']) #power-options div[data-premium='true'] {
1107+
opacity: 0.65;
10571108
position: relative;
10581109
}
10591110

1060-
html:not([is_premium='true']) #settings-menu div[data-premium='true']::after,
1061-
html:not([is_premium='true']) #power-options div[data-premium='true']::after {
1111+
html:not([tier='premium']) #settings-menu div[data-premium='true']::after,
1112+
html:not([tier='premium']) #power-options div[data-premium='true']::after {
10621113
content: '';
10631114
position: absolute;
1064-
right: 8px;
1115+
right: 10px;
10651116
top: 50%;
10661117
transform: translateY(-50%);
1067-
width: 14px;
1068-
height: 14px;
1069-
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23666'%3E%3Cpath d='M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z'/%3E%3C/svg%3E");
1070-
background-size: contain;
1071-
background-repeat: no-repeat;
1118+
width: 6px;
1119+
height: 6px;
1120+
border-radius: 50%;
1121+
background-color: var(--blue-color, #4a6cf7);
1122+
opacity: 0.85;
10721123
}
10731124

1074-
html[is_premium='true'] #settings-menu div[data-premium='true'],
1075-
html[is_premium='true'] #power-options div[data-premium='true'] {
1125+
html[tier='premium'] #settings-menu div[data-premium='true'],
1126+
html[tier='premium'] #power-options div[data-premium='true'] {
10761127
opacity: 1;
10771128
}
10781129

src/options/header.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,23 @@ html[dark_mode='false'] #header-light { display: none }
5454
letter-spacing: 0.02em;
5555
}
5656

57+
#header-slot-indicator {
58+
display: flex;
59+
align-items: center;
60+
gap: 4px;
61+
}
62+
#header-slot-indicator .slot-dot {
63+
width: 7px;
64+
height: 7px;
65+
border-radius: 50%;
66+
background-color: var(--label-color);
67+
opacity: 0.25;
68+
}
69+
#header-slot-indicator .slot-dot[filled] {
70+
background-color: var(--blue-color, #4a6cf7);
71+
opacity: 0.85;
72+
}
73+
5774
#search_bar {
5875
margin-left: auto;
5976
margin-right: auto;

src/options/main.html

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,10 @@
190190
<span id="premium_required_header_premium">Premium</span>
191191
</div>
192192
</div>
193-
<p id="premium_required_description">Premium required. Sign in to upgrade.</p>
193+
<p id="premium_required_description">
194+
<span class="premium-note-main">Sign in to enable any 2 premium features.</span>
195+
<span class="premium-note-sub">Upgrade to unlock them all.</span>
196+
</p>
194197
<div id="premium_required_buttons">
195198
<button id="premium-required-cancel-button">Cancel</button>
196199
<button id="premium-required-signin-button">Sign In</button>
@@ -208,6 +211,10 @@
208211
</div>
209212
<span id="upgrade_header_action">Upgrade</span>
210213
</div>
214+
<div id="upgrade_slot_limit_note" hidden>
215+
<span class="premium-note-main">Free tier allows 2 premium features.</span>
216+
<span class="premium-note-sub">Upgrade or deselect one.</span>
217+
</div>
211218
<div id="upgrade_plans">
212219
<div class="upgrade-plan" data-plan="monthly">
213220
<div class="plan-name">Monthly</div>
@@ -292,6 +299,8 @@
292299
<input type=search placeholder='Search' autofocus tabindex="0"></input>
293300
</div>
294301

302+
<div id='header-slot-indicator' hidden title='Free premium features used'></div>
303+
295304
<a title='On/Off' id="power-icon">
296305
<div id='power-pop-up'>Button is disabled because the schedule is active.</div>
297306
<svg id="header-toggle" class='global_enable header-toggle' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">

0 commit comments

Comments
 (0)