linkfeed-pro 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -0
- package/.output/chrome-mv3/_locales/de/messages.json +214 -0
- package/.output/chrome-mv3/_locales/en/messages.json +214 -0
- package/.output/chrome-mv3/_locales/es/messages.json +214 -0
- package/.output/chrome-mv3/_locales/fr/messages.json +214 -0
- package/.output/chrome-mv3/_locales/hi/messages.json +214 -0
- package/.output/chrome-mv3/_locales/id/messages.json +214 -0
- package/.output/chrome-mv3/_locales/it/messages.json +214 -0
- package/.output/chrome-mv3/_locales/nl/messages.json +214 -0
- package/.output/chrome-mv3/_locales/pl/messages.json +214 -0
- package/.output/chrome-mv3/_locales/pt_BR/messages.json +214 -0
- package/.output/chrome-mv3/_locales/pt_PT/messages.json +214 -0
- package/.output/chrome-mv3/_locales/tr/messages.json +214 -0
- package/.output/chrome-mv3/assets/popup-Z_g1HFs5.css +1 -0
- package/.output/chrome-mv3/background.js +42 -0
- package/.output/chrome-mv3/chunks/popup-IxiPwS1E.js +42 -0
- package/.output/chrome-mv3/content-scripts/content.js +179 -0
- package/.output/chrome-mv3/icon-128.png +0 -0
- package/.output/chrome-mv3/icon-16.png +0 -0
- package/.output/chrome-mv3/icon-48.png +0 -0
- package/.output/chrome-mv3/icon.svg +9 -0
- package/.output/chrome-mv3/manifest.json +1 -0
- package/.output/chrome-mv3/popup.html +247 -0
- package/.wxt/eslint-auto-imports.mjs +56 -0
- package/.wxt/tsconfig.json +28 -0
- package/.wxt/types/globals.d.ts +15 -0
- package/.wxt/types/i18n.d.ts +593 -0
- package/.wxt/types/imports-module.d.ts +20 -0
- package/.wxt/types/imports.d.ts +50 -0
- package/.wxt/types/paths.d.ts +32 -0
- package/.wxt/wxt.d.ts +7 -0
- package/entrypoints/background.ts +112 -0
- package/entrypoints/content.ts +656 -0
- package/entrypoints/popup/main.ts +452 -0
- package/entrypoints/popup/modules/auth-modal.ts +219 -0
- package/entrypoints/popup/modules/settings.ts +78 -0
- package/entrypoints/popup/modules/ui-state.ts +95 -0
- package/entrypoints/popup/style.css +844 -0
- package/entrypoints/popup.html +261 -0
- package/lib/constants.ts +9 -0
- package/lib/device-meta.ts +173 -0
- package/lib/i18n.ts +201 -0
- package/lib/license.ts +470 -0
- package/lib/selectors.ts +24 -0
- package/lib/storage.ts +95 -0
- package/lib/telemetry.ts +94 -0
- package/package.json +30 -0
- package/public/_locales/de/messages.json +214 -0
- package/public/_locales/en/messages.json +214 -0
- package/public/_locales/es/messages.json +214 -0
- package/public/_locales/fr/messages.json +214 -0
- package/public/_locales/hi/messages.json +214 -0
- package/public/_locales/id/messages.json +214 -0
- package/public/_locales/it/messages.json +214 -0
- package/public/_locales/nl/messages.json +214 -0
- package/public/_locales/pl/messages.json +214 -0
- package/public/_locales/pt_BR/messages.json +214 -0
- package/public/_locales/pt_PT/messages.json +214 -0
- package/public/_locales/tr/messages.json +214 -0
- package/public/icon-128.png +0 -0
- package/public/icon-16.png +0 -0
- package/public/icon-48.png +0 -0
- package/public/icon.svg +9 -0
- package/tsconfig.json +3 -0
- package/wxt.config.ts +50 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { browser } from '#imports';
|
|
2
|
+
import { getCachedLicense, getDeviceId, getLicenseState, getUserEmail } from '../../lib/license';
|
|
3
|
+
import type { LicenseState } from '@linkfeed/shared';
|
|
4
|
+
import { updateUIForLicenseState, getLicenseUIState } from './modules/ui-state';
|
|
5
|
+
import { APP_URLS } from '../../lib/constants';
|
|
6
|
+
import { loadSettings, handleSettingChange, updateSliderDisplay } from './modules/settings';
|
|
7
|
+
import type { UiLocalePreference } from '../../lib/storage';
|
|
8
|
+
import {
|
|
9
|
+
initLocalization,
|
|
10
|
+
setLocalePreference,
|
|
11
|
+
getLocalePreference,
|
|
12
|
+
getLocaleOptions,
|
|
13
|
+
t,
|
|
14
|
+
} from '../../lib/i18n';
|
|
15
|
+
import {
|
|
16
|
+
showModal,
|
|
17
|
+
cancelFlow,
|
|
18
|
+
sendActivationLink,
|
|
19
|
+
attemptActivation,
|
|
20
|
+
showEmailStep,
|
|
21
|
+
restorePendingState,
|
|
22
|
+
setModalLicenseState,
|
|
23
|
+
showExpiredModal
|
|
24
|
+
} from './modules/auth-modal';
|
|
25
|
+
|
|
26
|
+
let currentLicenseState: LicenseState | null = null;
|
|
27
|
+
let currentUserEmail: string | null = null;
|
|
28
|
+
let activeView: 'hides' | 'display' | 'preferences' = 'hides';
|
|
29
|
+
let primaryView: 'hides' | 'display' = 'hides';
|
|
30
|
+
|
|
31
|
+
// ============================
|
|
32
|
+
// INITIALIZATION
|
|
33
|
+
// ============================
|
|
34
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
35
|
+
await initLocalization();
|
|
36
|
+
setupI18n();
|
|
37
|
+
await loadSettings();
|
|
38
|
+
await loadLicenseState();
|
|
39
|
+
await restorePendingState(loadLicenseState);
|
|
40
|
+
await syncLocaleSelector();
|
|
41
|
+
setupEventListeners();
|
|
42
|
+
renderActiveView();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function setupI18n() {
|
|
46
|
+
document.title = t('extensionName');
|
|
47
|
+
|
|
48
|
+
// Header controls
|
|
49
|
+
const powerLabel = document.getElementById('global-toggle-label');
|
|
50
|
+
if (powerLabel) {
|
|
51
|
+
powerLabel.setAttribute('title', t('powerToggleTitle'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const settingsBtn = document.getElementById('open-settings-btn');
|
|
55
|
+
if (settingsBtn) {
|
|
56
|
+
settingsBtn.setAttribute('title', t('settingsButtonTitle'));
|
|
57
|
+
settingsBtn.setAttribute('aria-label', t('settingsButtonTitle'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const hideBtn = document.getElementById('open-hide-btn');
|
|
61
|
+
if (hideBtn) {
|
|
62
|
+
hideBtn.setAttribute('title', t('hideButtonTitle'));
|
|
63
|
+
hideBtn.setAttribute('aria-label', t('hideButtonTitle'));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const displayBtn = document.getElementById('open-display-btn');
|
|
67
|
+
if (displayBtn) {
|
|
68
|
+
displayBtn.setAttribute('title', t('displayButtonTitle'));
|
|
69
|
+
displayBtn.setAttribute('aria-label', t('displayButtonTitle'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Labels
|
|
73
|
+
const labels: Record<string, string> = {
|
|
74
|
+
hideSidebars: 'removeSidebars',
|
|
75
|
+
hidePromoted: 'hidePromoted',
|
|
76
|
+
hideStartPost: 'hideStartPost',
|
|
77
|
+
hideMessenger: 'hideMessenger',
|
|
78
|
+
hideNavBar: 'hideNavBar',
|
|
79
|
+
autoExpandPosts: 'autoExpandPosts',
|
|
80
|
+
hideRemovedFeedCards: 'hideRemovedFeedCards',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
Object.entries(labels).forEach(([id, key]) => {
|
|
84
|
+
const el = document.getElementById(id)?.parentElement?.querySelector('.setting-label');
|
|
85
|
+
if (el) el.textContent = t(key);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Sections
|
|
89
|
+
const fontSizeLabel = document.querySelector('#fontSize')?.parentElement?.querySelector('.section-label');
|
|
90
|
+
if (fontSizeLabel) fontSizeLabel.textContent = t('textSize');
|
|
91
|
+
|
|
92
|
+
const feedWidthLabel = document.querySelector('#feedWidth')?.parentElement?.querySelector('.section-label');
|
|
93
|
+
if (feedWidthLabel) feedWidthLabel.textContent = t('feedWidth');
|
|
94
|
+
|
|
95
|
+
const feedSpacingLabel = document.querySelector('#feedSpacing')?.parentElement?.querySelector('.section-label');
|
|
96
|
+
if (feedSpacingLabel) feedSpacingLabel.textContent = t('postSpacing');
|
|
97
|
+
|
|
98
|
+
const settingsTitle = document.getElementById('settings-title');
|
|
99
|
+
if (settingsTitle) settingsTitle.textContent = t('settingsPageTitle');
|
|
100
|
+
|
|
101
|
+
const settingsLanguageLabel = document.getElementById('settings-language-label');
|
|
102
|
+
if (settingsLanguageLabel) settingsLanguageLabel.textContent = t('settingsLanguageLabel');
|
|
103
|
+
|
|
104
|
+
const settingsLanguageHelp = document.getElementById('settings-language-help');
|
|
105
|
+
if (settingsLanguageHelp) settingsLanguageHelp.textContent = t('settingsLanguageHelp');
|
|
106
|
+
|
|
107
|
+
const settingsSubscriptionTitle = document.getElementById('settings-subscription-title');
|
|
108
|
+
if (settingsSubscriptionTitle) settingsSubscriptionTitle.textContent = t('settingsSubscriptionTitle');
|
|
109
|
+
|
|
110
|
+
const settingsSubscriptionStatusLabel = document.getElementById('settings-subscription-status-label');
|
|
111
|
+
if (settingsSubscriptionStatusLabel) settingsSubscriptionStatusLabel.textContent = t('settingsSubscriptionStatusLabel');
|
|
112
|
+
|
|
113
|
+
const settingsAccountBtn = document.getElementById('settings-account-btn');
|
|
114
|
+
if (settingsAccountBtn) settingsAccountBtn.textContent = t('settingsAccountButton');
|
|
115
|
+
|
|
116
|
+
const settingsUpgradeBtn = document.getElementById('settings-upgrade-btn');
|
|
117
|
+
if (settingsUpgradeBtn) settingsUpgradeBtn.textContent = t('settingsUpgradeButton');
|
|
118
|
+
|
|
119
|
+
const settingsVersionLabel = document.getElementById('settings-version-label');
|
|
120
|
+
if (settingsVersionLabel) settingsVersionLabel.textContent = t('settingsVersionLabel');
|
|
121
|
+
|
|
122
|
+
// Modal
|
|
123
|
+
const setStepText = (id: string, selector: string, key: string) => {
|
|
124
|
+
const el = document.querySelector(`#${id} ${selector}`);
|
|
125
|
+
if (el) el.textContent = t(key);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
setStepText('modal-email-step', 'h2', 'unlockProTitle');
|
|
129
|
+
setStepText('modal-email-step', '.modal-desc', 'unlockProDesc');
|
|
130
|
+
|
|
131
|
+
const emailInput = document.getElementById('modal-email') as HTMLInputElement;
|
|
132
|
+
if (emailInput) emailInput.placeholder = t('emailPlaceholder');
|
|
133
|
+
|
|
134
|
+
const sendBtn = document.getElementById('modal-send-link-btn');
|
|
135
|
+
if (sendBtn) sendBtn.textContent = t('sendLink');
|
|
136
|
+
|
|
137
|
+
setStepText('modal-check-email-step', 'h2', 'checkEmailTitle');
|
|
138
|
+
const verifyBtn = document.getElementById('modal-verify-btn');
|
|
139
|
+
if (verifyBtn) verifyBtn.textContent = t('verifyBtn');
|
|
140
|
+
|
|
141
|
+
const backBtn = document.getElementById('modal-back-btn');
|
|
142
|
+
if (backBtn) backBtn.textContent = t('differentEmail');
|
|
143
|
+
|
|
144
|
+
setStepText('modal-expired-step', 'h2', 'expiredTitle');
|
|
145
|
+
setStepText('modal-expired-step', '.modal-desc', 'expiredDesc');
|
|
146
|
+
|
|
147
|
+
const upgradeBtn = document.getElementById('modal-upgrade-btn');
|
|
148
|
+
if (upgradeBtn) upgradeBtn.textContent = t('upgradeBtn');
|
|
149
|
+
|
|
150
|
+
renderExtensionVersion();
|
|
151
|
+
renderSubscriptionCard();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function renderExtensionVersion() {
|
|
155
|
+
const versionValue = document.getElementById('settings-version-value');
|
|
156
|
+
if (!versionValue) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const version = browser.runtime.getManifest().version;
|
|
161
|
+
versionValue.textContent = `v${version}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getTrialHoursRemaining(expiresAt?: string): number {
|
|
165
|
+
if (!expiresAt) {
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const diffMs = new Date(expiresAt).getTime() - Date.now();
|
|
170
|
+
if (diffMs <= 0) {
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return Math.max(1, Math.ceil(diffMs / (1000 * 60 * 60)));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function renderSubscriptionCard(extraMessage?: string) {
|
|
178
|
+
const statusEl = document.getElementById('settings-subscription-status');
|
|
179
|
+
const detailEl = document.getElementById('settings-subscription-detail');
|
|
180
|
+
const upgradeBtn = document.getElementById('settings-upgrade-btn');
|
|
181
|
+
|
|
182
|
+
if (!statusEl || !detailEl || !upgradeBtn) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const uiState = currentLicenseState ? getLicenseUIState(currentLicenseState) : 'free';
|
|
187
|
+
|
|
188
|
+
let statusLabel = t('settingsSubscriptionStatusFree');
|
|
189
|
+
let detail = t('settingsSubscriptionFreeDetail');
|
|
190
|
+
let statusClass = 'subscription-status-free';
|
|
191
|
+
let showUpgrade = false;
|
|
192
|
+
|
|
193
|
+
if (uiState === 'pro') {
|
|
194
|
+
statusLabel = t('settingsSubscriptionStatusLifetime');
|
|
195
|
+
statusClass = 'subscription-status-active';
|
|
196
|
+
} else if (uiState === 'trial') {
|
|
197
|
+
statusLabel = t('settingsSubscriptionStatusTrial');
|
|
198
|
+
statusClass = 'subscription-status-trial';
|
|
199
|
+
const hours = getTrialHoursRemaining(currentLicenseState?.expiresAt);
|
|
200
|
+
detail = t('settingsSubscriptionTrialDetail', String(hours));
|
|
201
|
+
showUpgrade = Boolean(currentUserEmail);
|
|
202
|
+
} else if (uiState === 'expired') {
|
|
203
|
+
statusLabel = t('statusExpired');
|
|
204
|
+
statusClass = 'subscription-status-expired';
|
|
205
|
+
showUpgrade = Boolean(currentUserEmail);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (currentUserEmail) {
|
|
209
|
+
detail = currentUserEmail;
|
|
210
|
+
}
|
|
211
|
+
if (extraMessage) {
|
|
212
|
+
detail = detail ? `${detail} • ${extraMessage}` : extraMessage;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
statusEl.textContent = statusLabel;
|
|
216
|
+
statusEl.className = `subscription-status-badge ${statusClass}`;
|
|
217
|
+
detailEl.textContent = detail;
|
|
218
|
+
upgradeBtn.classList.toggle('hidden', !showUpgrade);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function syncLocaleSelector() {
|
|
222
|
+
const select = document.getElementById('locale-select') as HTMLSelectElement | null;
|
|
223
|
+
if (!select) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
select.innerHTML = '';
|
|
228
|
+
|
|
229
|
+
const autoOption = document.createElement('option');
|
|
230
|
+
autoOption.value = 'auto';
|
|
231
|
+
autoOption.textContent = `🌐 ${t('languageAutoOption')}`;
|
|
232
|
+
select.appendChild(autoOption);
|
|
233
|
+
|
|
234
|
+
getLocaleOptions().forEach((localeOption) => {
|
|
235
|
+
const option = document.createElement('option');
|
|
236
|
+
option.value = localeOption.value;
|
|
237
|
+
option.textContent = `${localeOption.flag} ${localeOption.label}`;
|
|
238
|
+
select.appendChild(option);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const preference = await getLocalePreference();
|
|
242
|
+
select.value = preference;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function loadLicenseState() {
|
|
246
|
+
try {
|
|
247
|
+
currentUserEmail = await getUserEmail();
|
|
248
|
+
|
|
249
|
+
// First show cached state
|
|
250
|
+
const cached = await getCachedLicense();
|
|
251
|
+
if (cached) {
|
|
252
|
+
currentLicenseState = cached;
|
|
253
|
+
setModalLicenseState(cached);
|
|
254
|
+
updateUIForLicenseState(cached);
|
|
255
|
+
renderSubscriptionCard();
|
|
256
|
+
} else {
|
|
257
|
+
const revokedState: LicenseState = {
|
|
258
|
+
status: 'revoked',
|
|
259
|
+
features: { wideFeed: false, fontSize: false, autoExpandPosts: false }
|
|
260
|
+
};
|
|
261
|
+
currentLicenseState = revokedState;
|
|
262
|
+
updateUIForLicenseState(revokedState);
|
|
263
|
+
setModalLicenseState(revokedState);
|
|
264
|
+
renderSubscriptionCard();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Then fetch fresh state
|
|
268
|
+
const fresh = await getLicenseState({ force: true });
|
|
269
|
+
currentLicenseState = fresh;
|
|
270
|
+
setModalLicenseState(fresh);
|
|
271
|
+
updateUIForLicenseState(fresh);
|
|
272
|
+
currentUserEmail = await getUserEmail();
|
|
273
|
+
renderSubscriptionCard();
|
|
274
|
+
} catch (error) {
|
|
275
|
+
console.error('Failed to load license state:', error);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function renderActiveView() {
|
|
280
|
+
const hideView = document.getElementById('hide-view');
|
|
281
|
+
const displayView = document.getElementById('display-view');
|
|
282
|
+
const settingsView = document.getElementById('settings-view');
|
|
283
|
+
const hideBtn = document.getElementById('open-hide-btn');
|
|
284
|
+
const displayBtn = document.getElementById('open-display-btn');
|
|
285
|
+
const settingsBtn = document.getElementById('open-settings-btn');
|
|
286
|
+
|
|
287
|
+
hideView?.classList.toggle('hidden', activeView !== 'hides');
|
|
288
|
+
displayView?.classList.toggle('hidden', activeView !== 'display');
|
|
289
|
+
settingsView?.classList.toggle('hidden', activeView !== 'preferences');
|
|
290
|
+
hideBtn?.classList.toggle('active', activeView === 'hides');
|
|
291
|
+
displayBtn?.classList.toggle('active', activeView === 'display');
|
|
292
|
+
settingsBtn?.classList.toggle('active', activeView === 'preferences');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ============================
|
|
296
|
+
// EVENT LISTENERS
|
|
297
|
+
// ============================
|
|
298
|
+
function setupEventListeners() {
|
|
299
|
+
// Header hide view toggle
|
|
300
|
+
document.getElementById('open-hide-btn')?.addEventListener('click', () => {
|
|
301
|
+
primaryView = 'hides';
|
|
302
|
+
activeView = 'hides';
|
|
303
|
+
renderActiveView();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Header display view toggle
|
|
307
|
+
document.getElementById('open-display-btn')?.addEventListener('click', () => {
|
|
308
|
+
primaryView = 'display';
|
|
309
|
+
activeView = 'display';
|
|
310
|
+
renderActiveView();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Header settings view toggle
|
|
314
|
+
document.getElementById('open-settings-btn')?.addEventListener('click', () => {
|
|
315
|
+
activeView = activeView === 'preferences' ? primaryView : 'preferences';
|
|
316
|
+
renderActiveView();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Forced locale selector
|
|
320
|
+
document.getElementById('locale-select')?.addEventListener('change', async (event) => {
|
|
321
|
+
const target = event.target as HTMLSelectElement;
|
|
322
|
+
const value = target.value as UiLocalePreference;
|
|
323
|
+
|
|
324
|
+
await setLocalePreference(value);
|
|
325
|
+
setupI18n();
|
|
326
|
+
await syncLocaleSelector();
|
|
327
|
+
|
|
328
|
+
if (currentLicenseState) {
|
|
329
|
+
updateUIForLicenseState(currentLicenseState);
|
|
330
|
+
setModalLicenseState(currentLicenseState);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
renderSubscriptionCard();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
document.getElementById('settings-account-btn')?.addEventListener('click', () => {
|
|
337
|
+
const authRedirectUrl = `${APP_URLS.AUTH}?redirect=${encodeURIComponent('/account')}`;
|
|
338
|
+
window.open(authRedirectUrl, '_blank');
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
document.getElementById('settings-upgrade-btn')?.addEventListener('click', async () => {
|
|
342
|
+
const email = currentUserEmail ?? await getUserEmail();
|
|
343
|
+
const url = email
|
|
344
|
+
? `${APP_URLS.PRICING}?email=${encodeURIComponent(email)}`
|
|
345
|
+
: APP_URLS.PRICING;
|
|
346
|
+
window.open(url, '_blank');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// Global toggle
|
|
350
|
+
document.getElementById('globalEnabled')?.addEventListener('change', async () => {
|
|
351
|
+
await handleSettingChange();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Free features (checkboxes)
|
|
355
|
+
['hideSidebars', 'hidePromoted', 'hideStartPost', 'hideMessenger', 'hideNavBar'].forEach((id) => {
|
|
356
|
+
document.getElementById(id)?.addEventListener('change', handleSettingChange);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Badge click
|
|
360
|
+
document.getElementById('status-badge')?.addEventListener('click', async () => {
|
|
361
|
+
const uiState = currentLicenseState ? getLicenseUIState(currentLicenseState) : 'free';
|
|
362
|
+
if (uiState === 'expired') {
|
|
363
|
+
showExpiredModal();
|
|
364
|
+
} else {
|
|
365
|
+
let url = uiState === 'pro' ? APP_URLS.ACCOUNT : APP_URLS.HOME;
|
|
366
|
+
if (uiState === 'pro') {
|
|
367
|
+
const deviceId = await getDeviceId();
|
|
368
|
+
if (deviceId) {
|
|
369
|
+
url = `${APP_URLS.ACCOUNT}?deviceId=${encodeURIComponent(deviceId)}`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
window.open(url, '_blank');
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Intercept clicks on premium features when expired or locked
|
|
377
|
+
document.body.addEventListener('click', (e) => {
|
|
378
|
+
const uiState = currentLicenseState ? getLicenseUIState(currentLicenseState) : 'free';
|
|
379
|
+
const target = e.target as HTMLElement;
|
|
380
|
+
const feature = target.closest('.premium-feature');
|
|
381
|
+
|
|
382
|
+
if (feature) {
|
|
383
|
+
if (uiState === 'expired') {
|
|
384
|
+
e.preventDefault();
|
|
385
|
+
e.stopPropagation();
|
|
386
|
+
showExpiredModal();
|
|
387
|
+
} else if (uiState === 'free') {
|
|
388
|
+
e.preventDefault();
|
|
389
|
+
e.stopPropagation();
|
|
390
|
+
showModal();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}, true);
|
|
394
|
+
|
|
395
|
+
// Auto-expand checkbox change (only when not locked)
|
|
396
|
+
const autoExpandCheckbox = document.getElementById('autoExpandPosts') as HTMLInputElement;
|
|
397
|
+
document.getElementById('autoExpandCard')?.addEventListener('click', (e) => {
|
|
398
|
+
const uiState = currentLicenseState ? getLicenseUIState(currentLicenseState) : 'free';
|
|
399
|
+
if (uiState !== 'free' && e.target !== autoExpandCheckbox) {
|
|
400
|
+
autoExpandCheckbox.checked = !autoExpandCheckbox.checked;
|
|
401
|
+
handleSettingChange();
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
autoExpandCheckbox?.addEventListener('change', () => {
|
|
406
|
+
const uiState = currentLicenseState ? getLicenseUIState(currentLicenseState) : 'free';
|
|
407
|
+
if (uiState !== 'free') handleSettingChange();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Hide removed cards checkbox (only when not locked)
|
|
411
|
+
const hideRemovedCardsCheckbox = document.getElementById('hideRemovedFeedCards') as HTMLInputElement;
|
|
412
|
+
document.getElementById('hideRemovedFeedCardsCard')?.addEventListener('click', (e) => {
|
|
413
|
+
const uiState = currentLicenseState ? getLicenseUIState(currentLicenseState) : 'free';
|
|
414
|
+
if (uiState !== 'free' && e.target !== hideRemovedCardsCheckbox) {
|
|
415
|
+
hideRemovedCardsCheckbox.checked = !hideRemovedCardsCheckbox.checked;
|
|
416
|
+
handleSettingChange();
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
hideRemovedCardsCheckbox?.addEventListener('change', () => {
|
|
421
|
+
const uiState = currentLicenseState ? getLicenseUIState(currentLicenseState) : 'free';
|
|
422
|
+
if (uiState !== 'free') handleSettingChange();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Premium sliders
|
|
426
|
+
['fontSize', 'feedWidth', 'feedSpacing'].forEach(id => {
|
|
427
|
+
const slider = document.getElementById(id) as HTMLInputElement;
|
|
428
|
+
slider?.addEventListener('input', () => {
|
|
429
|
+
const uiState = currentLicenseState ? getLicenseUIState(currentLicenseState) : 'free';
|
|
430
|
+
if (uiState !== 'free') {
|
|
431
|
+
updateSliderDisplay(id, Number(slider.value));
|
|
432
|
+
handleSettingChange();
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Modal controls
|
|
438
|
+
document.getElementById('modal-close-btn')?.addEventListener('click', cancelFlow);
|
|
439
|
+
document.getElementById('modal-send-link-btn')?.addEventListener('click', () => sendActivationLink(loadLicenseState));
|
|
440
|
+
document.getElementById('modal-verify-btn')?.addEventListener('click', () => attemptActivation(loadLicenseState));
|
|
441
|
+
document.getElementById('modal-back-btn')?.addEventListener('click', showEmailStep);
|
|
442
|
+
|
|
443
|
+
// Enter key handling in modal
|
|
444
|
+
document.getElementById('modal-email')?.addEventListener('keypress', (e) => {
|
|
445
|
+
if ((e as KeyboardEvent).key === 'Enter') sendActivationLink(loadLicenseState);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Close modal on backdrop click
|
|
449
|
+
document.getElementById('trial-modal')?.addEventListener('click', (e) => {
|
|
450
|
+
if (e.target === e.currentTarget) cancelFlow();
|
|
451
|
+
});
|
|
452
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { requestLicenseLink, activateLicenseLink } from '../../../lib/license';
|
|
2
|
+
import { pendingTrialStorage, userEmailStorage } from '../../../lib/storage';
|
|
3
|
+
import { APP_URLS } from '../../../lib/constants';
|
|
4
|
+
import { browser } from '#imports';
|
|
5
|
+
import { t } from '../../../lib/i18n';
|
|
6
|
+
import { getLicenseUIState } from './ui-state';
|
|
7
|
+
import type { LicenseState } from '@linkfeed/shared';
|
|
8
|
+
|
|
9
|
+
let pendingEmail: string | null = null;
|
|
10
|
+
let pendingActivationToken: string | null = null;
|
|
11
|
+
let currentLicenseState: LicenseState | null = null;
|
|
12
|
+
let activationInterval: number | null = null;
|
|
13
|
+
|
|
14
|
+
export function setModalLicenseState(state: LicenseState | null) {
|
|
15
|
+
currentLicenseState = state;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function startActivationPolling(onSuccess: () => Promise<void>) {
|
|
19
|
+
stopActivationPolling();
|
|
20
|
+
activationInterval = window.setInterval(() => {
|
|
21
|
+
attemptActivation(onSuccess);
|
|
22
|
+
}, 4000);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function stopActivationPolling() {
|
|
26
|
+
if (activationInterval) {
|
|
27
|
+
clearInterval(activationInterval);
|
|
28
|
+
activationInterval = null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function showModal() {
|
|
33
|
+
const modal = document.getElementById('trial-modal');
|
|
34
|
+
const emailStep = document.getElementById('modal-email-step');
|
|
35
|
+
const checkEmailStep = document.getElementById('modal-check-email-step');
|
|
36
|
+
const expiredStep = document.getElementById('modal-expired-step');
|
|
37
|
+
|
|
38
|
+
modal?.classList.remove('hidden');
|
|
39
|
+
expiredStep?.classList.add('hidden');
|
|
40
|
+
|
|
41
|
+
if (!pendingEmail) {
|
|
42
|
+
emailStep?.classList.remove('hidden');
|
|
43
|
+
checkEmailStep?.classList.add('hidden');
|
|
44
|
+
|
|
45
|
+
(document.getElementById('modal-email') as HTMLInputElement).value = '';
|
|
46
|
+
document.getElementById('modal-email-error')?.classList.add('hidden');
|
|
47
|
+
document.getElementById('modal-check-email-error')?.classList.add('hidden');
|
|
48
|
+
|
|
49
|
+
setTimeout(() => (document.getElementById('modal-email') as HTMLInputElement)?.focus(), 100);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function hideModal() {
|
|
54
|
+
document.getElementById('trial-modal')?.classList.add('hidden');
|
|
55
|
+
stopActivationPolling();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function clearPendingState() {
|
|
59
|
+
pendingEmail = null;
|
|
60
|
+
pendingActivationToken = null;
|
|
61
|
+
await pendingTrialStorage.removeValue();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function cancelFlow() {
|
|
65
|
+
hideModal();
|
|
66
|
+
await clearPendingState();
|
|
67
|
+
document.getElementById('modal-email-step')?.classList.remove('hidden');
|
|
68
|
+
document.getElementById('modal-check-email-step')?.classList.add('hidden');
|
|
69
|
+
document.getElementById('modal-email-error')?.classList.add('hidden');
|
|
70
|
+
document.getElementById('modal-check-email-error')?.classList.add('hidden');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function showCheckEmailStep() {
|
|
74
|
+
document.getElementById('modal-email-step')?.classList.add('hidden');
|
|
75
|
+
document.getElementById('modal-check-email-step')?.classList.remove('hidden');
|
|
76
|
+
|
|
77
|
+
const display = document.getElementById('modal-email-display');
|
|
78
|
+
if (display && pendingEmail) {
|
|
79
|
+
display.textContent = pendingEmail;
|
|
80
|
+
const checkEmailDesc = document.querySelector('#modal-check-email-step .modal-desc');
|
|
81
|
+
if (checkEmailDesc) {
|
|
82
|
+
checkEmailDesc.textContent = t('checkEmailDesc', pendingEmail);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function showEmailStep() {
|
|
88
|
+
await clearPendingState();
|
|
89
|
+
document.getElementById('modal-email-step')?.classList.remove('hidden');
|
|
90
|
+
document.getElementById('modal-check-email-step')?.classList.add('hidden');
|
|
91
|
+
document.getElementById('modal-email-error')?.classList.add('hidden');
|
|
92
|
+
document.getElementById('modal-check-email-error')?.classList.add('hidden');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function showExpiredModal() {
|
|
96
|
+
const modal = document.getElementById('trial-modal');
|
|
97
|
+
const emailStep = document.getElementById('modal-email-step');
|
|
98
|
+
const checkEmailStep = document.getElementById('modal-check-email-step');
|
|
99
|
+
const expiredStep = document.getElementById('modal-expired-step');
|
|
100
|
+
|
|
101
|
+
modal?.classList.remove('hidden');
|
|
102
|
+
emailStep?.classList.add('hidden');
|
|
103
|
+
checkEmailStep?.classList.add('hidden');
|
|
104
|
+
expiredStep?.classList.remove('hidden');
|
|
105
|
+
|
|
106
|
+
stopActivationPolling();
|
|
107
|
+
|
|
108
|
+
const upgradeBtn = document.getElementById('modal-upgrade-btn');
|
|
109
|
+
if (upgradeBtn) {
|
|
110
|
+
upgradeBtn.onclick = () => {
|
|
111
|
+
userEmailStorage.getValue().then((email) => {
|
|
112
|
+
const emailParam = email ? `?email=${encodeURIComponent(email)}` : '';
|
|
113
|
+
browser.tabs.create({ url: `${APP_URLS.UPGRADE}${emailParam}` });
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const closeBtn = document.getElementById('modal-close-btn');
|
|
119
|
+
if (closeBtn) {
|
|
120
|
+
closeBtn.onclick = hideModal;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function sendActivationLink(onSuccess: () => Promise<void>) {
|
|
125
|
+
const emailInput = document.getElementById('modal-email') as HTMLInputElement;
|
|
126
|
+
const errorEl = document.getElementById('modal-email-error');
|
|
127
|
+
const btn = document.getElementById('modal-send-link-btn') as HTMLButtonElement;
|
|
128
|
+
|
|
129
|
+
const email = emailInput?.value.trim();
|
|
130
|
+
if (!email || !email.includes('@')) {
|
|
131
|
+
if (errorEl) {
|
|
132
|
+
errorEl.textContent = t('errorInvalidEmail');
|
|
133
|
+
errorEl.classList.remove('hidden');
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
btn.disabled = true;
|
|
139
|
+
btn.textContent = t('sending');
|
|
140
|
+
errorEl?.classList.add('hidden');
|
|
141
|
+
|
|
142
|
+
const result = await requestLicenseLink(email);
|
|
143
|
+
|
|
144
|
+
if (!result.success || !result.activationToken) {
|
|
145
|
+
if (errorEl) {
|
|
146
|
+
errorEl.textContent = result.message;
|
|
147
|
+
errorEl.classList.remove('hidden');
|
|
148
|
+
}
|
|
149
|
+
btn.disabled = false;
|
|
150
|
+
btn.textContent = t('sendLink');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
pendingEmail = email;
|
|
155
|
+
pendingActivationToken = result.activationToken;
|
|
156
|
+
await pendingTrialStorage.setValue({ email, activationToken: result.activationToken, timestamp: Date.now() });
|
|
157
|
+
showCheckEmailStep();
|
|
158
|
+
startActivationPolling(onSuccess);
|
|
159
|
+
|
|
160
|
+
btn.disabled = false;
|
|
161
|
+
btn.textContent = t('sendLink');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function attemptActivation(onSuccess: () => Promise<void>) {
|
|
165
|
+
if (!pendingActivationToken) return;
|
|
166
|
+
|
|
167
|
+
const errorEl = document.getElementById('modal-check-email-error');
|
|
168
|
+
const btn = document.getElementById('modal-verify-btn') as HTMLButtonElement;
|
|
169
|
+
|
|
170
|
+
btn.disabled = true;
|
|
171
|
+
btn.textContent = t('verifying');
|
|
172
|
+
errorEl?.classList.add('hidden');
|
|
173
|
+
|
|
174
|
+
const result = await activateLicenseLink(pendingActivationToken);
|
|
175
|
+
|
|
176
|
+
if (!result.success) {
|
|
177
|
+
if (result.status === 'email_unverified') {
|
|
178
|
+
btn.disabled = false;
|
|
179
|
+
btn.textContent = t('verifyBtn');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (result.status === 'trial_used') {
|
|
183
|
+
showExpiredModal();
|
|
184
|
+
} else if (errorEl) {
|
|
185
|
+
errorEl.textContent = result.message;
|
|
186
|
+
errorEl.classList.remove('hidden');
|
|
187
|
+
}
|
|
188
|
+
btn.disabled = false;
|
|
189
|
+
btn.textContent = t('verifyBtn');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
await clearPendingState();
|
|
194
|
+
hideModal();
|
|
195
|
+
await onSuccess();
|
|
196
|
+
stopActivationPolling();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function restorePendingState(onSuccess: () => Promise<void>) {
|
|
200
|
+
try {
|
|
201
|
+
const pendingTrial = await pendingTrialStorage.getValue();
|
|
202
|
+
if (pendingTrial && pendingTrial.email && pendingTrial.activationToken && pendingTrial.timestamp) {
|
|
203
|
+
const isRecent = Date.now() - pendingTrial.timestamp < 60 * 60 * 1000;
|
|
204
|
+
const uiState = currentLicenseState ? getLicenseUIState(currentLicenseState) : 'free';
|
|
205
|
+
|
|
206
|
+
if (isRecent && uiState === 'free') {
|
|
207
|
+
pendingEmail = pendingTrial.email;
|
|
208
|
+
pendingActivationToken = pendingTrial.activationToken;
|
|
209
|
+
showModal();
|
|
210
|
+
showCheckEmailStep();
|
|
211
|
+
startActivationPolling(onSuccess);
|
|
212
|
+
} else if (!isRecent) {
|
|
213
|
+
await pendingTrialStorage.removeValue();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
console.error('Failed to restore pending state:', error);
|
|
218
|
+
}
|
|
219
|
+
}
|