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.
Files changed (65) hide show
  1. package/.claude/settings.local.json +9 -0
  2. package/.output/chrome-mv3/_locales/de/messages.json +214 -0
  3. package/.output/chrome-mv3/_locales/en/messages.json +214 -0
  4. package/.output/chrome-mv3/_locales/es/messages.json +214 -0
  5. package/.output/chrome-mv3/_locales/fr/messages.json +214 -0
  6. package/.output/chrome-mv3/_locales/hi/messages.json +214 -0
  7. package/.output/chrome-mv3/_locales/id/messages.json +214 -0
  8. package/.output/chrome-mv3/_locales/it/messages.json +214 -0
  9. package/.output/chrome-mv3/_locales/nl/messages.json +214 -0
  10. package/.output/chrome-mv3/_locales/pl/messages.json +214 -0
  11. package/.output/chrome-mv3/_locales/pt_BR/messages.json +214 -0
  12. package/.output/chrome-mv3/_locales/pt_PT/messages.json +214 -0
  13. package/.output/chrome-mv3/_locales/tr/messages.json +214 -0
  14. package/.output/chrome-mv3/assets/popup-Z_g1HFs5.css +1 -0
  15. package/.output/chrome-mv3/background.js +42 -0
  16. package/.output/chrome-mv3/chunks/popup-IxiPwS1E.js +42 -0
  17. package/.output/chrome-mv3/content-scripts/content.js +179 -0
  18. package/.output/chrome-mv3/icon-128.png +0 -0
  19. package/.output/chrome-mv3/icon-16.png +0 -0
  20. package/.output/chrome-mv3/icon-48.png +0 -0
  21. package/.output/chrome-mv3/icon.svg +9 -0
  22. package/.output/chrome-mv3/manifest.json +1 -0
  23. package/.output/chrome-mv3/popup.html +247 -0
  24. package/.wxt/eslint-auto-imports.mjs +56 -0
  25. package/.wxt/tsconfig.json +28 -0
  26. package/.wxt/types/globals.d.ts +15 -0
  27. package/.wxt/types/i18n.d.ts +593 -0
  28. package/.wxt/types/imports-module.d.ts +20 -0
  29. package/.wxt/types/imports.d.ts +50 -0
  30. package/.wxt/types/paths.d.ts +32 -0
  31. package/.wxt/wxt.d.ts +7 -0
  32. package/entrypoints/background.ts +112 -0
  33. package/entrypoints/content.ts +656 -0
  34. package/entrypoints/popup/main.ts +452 -0
  35. package/entrypoints/popup/modules/auth-modal.ts +219 -0
  36. package/entrypoints/popup/modules/settings.ts +78 -0
  37. package/entrypoints/popup/modules/ui-state.ts +95 -0
  38. package/entrypoints/popup/style.css +844 -0
  39. package/entrypoints/popup.html +261 -0
  40. package/lib/constants.ts +9 -0
  41. package/lib/device-meta.ts +173 -0
  42. package/lib/i18n.ts +201 -0
  43. package/lib/license.ts +470 -0
  44. package/lib/selectors.ts +24 -0
  45. package/lib/storage.ts +95 -0
  46. package/lib/telemetry.ts +94 -0
  47. package/package.json +30 -0
  48. package/public/_locales/de/messages.json +214 -0
  49. package/public/_locales/en/messages.json +214 -0
  50. package/public/_locales/es/messages.json +214 -0
  51. package/public/_locales/fr/messages.json +214 -0
  52. package/public/_locales/hi/messages.json +214 -0
  53. package/public/_locales/id/messages.json +214 -0
  54. package/public/_locales/it/messages.json +214 -0
  55. package/public/_locales/nl/messages.json +214 -0
  56. package/public/_locales/pl/messages.json +214 -0
  57. package/public/_locales/pt_BR/messages.json +214 -0
  58. package/public/_locales/pt_PT/messages.json +214 -0
  59. package/public/_locales/tr/messages.json +214 -0
  60. package/public/icon-128.png +0 -0
  61. package/public/icon-16.png +0 -0
  62. package/public/icon-48.png +0 -0
  63. package/public/icon.svg +9 -0
  64. package/tsconfig.json +3 -0
  65. 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
+ }