@mooncompany/uplink-chat 0.5.0
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.
Potentially problematic release.
This version of @mooncompany/uplink-chat might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/bin/uplink.js +279 -0
- package/middleware/error-handler.js +69 -0
- package/package.json +93 -0
- package/public/css/agents.36b98c0f.css +1469 -0
- package/public/css/agents.css +1469 -0
- package/public/css/app.a6a7f8f5.css +2731 -0
- package/public/css/app.css +2731 -0
- package/public/css/artifacts.css +444 -0
- package/public/css/commands.css +55 -0
- package/public/css/connection.css +131 -0
- package/public/css/dashboard.css +233 -0
- package/public/css/developer.css +328 -0
- package/public/css/files.css +123 -0
- package/public/css/markdown.css +156 -0
- package/public/css/message-actions.css +278 -0
- package/public/css/mobile.css +614 -0
- package/public/css/panels-unified.css +483 -0
- package/public/css/premium.css +415 -0
- package/public/css/realtime.css +189 -0
- package/public/css/satellites.css +401 -0
- package/public/css/shortcuts.css +185 -0
- package/public/css/split-view.4def0262.css +673 -0
- package/public/css/split-view.css +673 -0
- package/public/css/theme-generator.css +391 -0
- package/public/css/themes.css +387 -0
- package/public/css/timestamps.css +54 -0
- package/public/css/variables.css +78 -0
- package/public/dist/bundle.b55050c4.js +15757 -0
- package/public/favicon.svg +24 -0
- package/public/img/agents/ada.png +0 -0
- package/public/img/agents/clarice.png +0 -0
- package/public/img/agents/dennis-nedry.png +0 -0
- package/public/img/agents/elliot-alderson.png +0 -0
- package/public/img/agents/main.png +0 -0
- package/public/img/agents/scotty.png +0 -0
- package/public/img/agents/top-flight-security.png +0 -0
- package/public/index.html +1083 -0
- package/public/js/agents-data.js +234 -0
- package/public/js/agents-ui.js +72 -0
- package/public/js/agents.js +1525 -0
- package/public/js/app.js +79 -0
- package/public/js/appearance-settings.js +111 -0
- package/public/js/artifacts.js +432 -0
- package/public/js/audio-queue.js +168 -0
- package/public/js/bootstrap.js +54 -0
- package/public/js/chat.js +1211 -0
- package/public/js/commands.js +581 -0
- package/public/js/connection-api.js +121 -0
- package/public/js/connection.js +1231 -0
- package/public/js/context-tracker.js +271 -0
- package/public/js/core.js +172 -0
- package/public/js/dashboard.js +452 -0
- package/public/js/developer.js +432 -0
- package/public/js/encryption.js +124 -0
- package/public/js/errors.js +122 -0
- package/public/js/event-bus.js +77 -0
- package/public/js/fetch-utils.js +171 -0
- package/public/js/file-handler.js +229 -0
- package/public/js/files.js +352 -0
- package/public/js/gateway-chat.js +538 -0
- package/public/js/logger.js +112 -0
- package/public/js/markdown.js +190 -0
- package/public/js/message-actions.js +431 -0
- package/public/js/message-renderer.js +288 -0
- package/public/js/missed-messages.js +235 -0
- package/public/js/mobile-debug.js +95 -0
- package/public/js/notifications.js +367 -0
- package/public/js/offline-queue.js +178 -0
- package/public/js/onboarding.js +543 -0
- package/public/js/panels.js +156 -0
- package/public/js/premium.js +412 -0
- package/public/js/realtime-voice.js +844 -0
- package/public/js/satellite-sync.js +256 -0
- package/public/js/satellite-ui.js +175 -0
- package/public/js/satellites.js +1516 -0
- package/public/js/settings.js +1087 -0
- package/public/js/shortcuts.js +381 -0
- package/public/js/split-chat.js +1234 -0
- package/public/js/split-resize.js +211 -0
- package/public/js/splitview.js +340 -0
- package/public/js/storage.js +408 -0
- package/public/js/streaming-handler.js +324 -0
- package/public/js/stt-settings.js +316 -0
- package/public/js/theme-generator.js +661 -0
- package/public/js/themes.js +164 -0
- package/public/js/timestamps.js +198 -0
- package/public/js/tts-settings.js +575 -0
- package/public/js/ui.js +267 -0
- package/public/js/update-notifier.js +143 -0
- package/public/js/utils/constants.js +165 -0
- package/public/js/utils/sanitize.js +93 -0
- package/public/js/utils/sse-parser.js +195 -0
- package/public/js/voice.js +883 -0
- package/public/manifest.json +58 -0
- package/public/moon_texture.jpg +0 -0
- package/public/sw.js +221 -0
- package/public/three.min.js +6 -0
- package/server/channel.js +529 -0
- package/server/chat.js +270 -0
- package/server/config-store.js +362 -0
- package/server/config.js +159 -0
- package/server/context.js +131 -0
- package/server/gateway-commands.js +211 -0
- package/server/gateway-proxy.js +318 -0
- package/server/index.js +22 -0
- package/server/logger.js +89 -0
- package/server/middleware/auth.js +188 -0
- package/server/middleware.js +218 -0
- package/server/openclaw-discover.js +308 -0
- package/server/premium/index.js +156 -0
- package/server/premium/license.js +140 -0
- package/server/realtime/bridge.js +837 -0
- package/server/realtime/index.js +349 -0
- package/server/realtime/tts-stream.js +446 -0
- package/server/routes/agents.js +564 -0
- package/server/routes/artifacts.js +174 -0
- package/server/routes/chat.js +311 -0
- package/server/routes/config-settings.js +345 -0
- package/server/routes/config.js +603 -0
- package/server/routes/files.js +307 -0
- package/server/routes/index.js +18 -0
- package/server/routes/media.js +451 -0
- package/server/routes/missed-messages.js +107 -0
- package/server/routes/premium.js +75 -0
- package/server/routes/push.js +156 -0
- package/server/routes/satellite.js +406 -0
- package/server/routes/status.js +251 -0
- package/server/routes/stt.js +35 -0
- package/server/routes/voice.js +260 -0
- package/server/routes/webhooks.js +203 -0
- package/server/routes.js +206 -0
- package/server/runtime-config.js +336 -0
- package/server/share.js +305 -0
- package/server/stt/faster-whisper.js +72 -0
- package/server/stt/groq.js +51 -0
- package/server/stt/index.js +196 -0
- package/server/stt/openai.js +49 -0
- package/server/sync.js +244 -0
- package/server/tailscale-https.js +175 -0
- package/server/tts.js +646 -0
- package/server/update-checker.js +172 -0
- package/server/utils/filename.js +129 -0
- package/server/utils.js +147 -0
- package/server/watchdog.js +318 -0
- package/server/websocket/broadcast.js +359 -0
- package/server/websocket/connections.js +339 -0
- package/server/websocket/index.js +215 -0
- package/server/websocket/routing.js +277 -0
- package/server/websocket/sync.js +102 -0
- package/server.js +404 -0
- package/utils/detect-tool-usage.js +93 -0
- package/utils/errors.js +158 -0
- package/utils/html-escape.js +84 -0
- package/utils/id-sanitize.js +94 -0
- package/utils/response.js +130 -0
- package/utils/with-retry.js +105 -0
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// PREMIUM MODULE
|
|
3
|
+
// Client-side premium status & UI gating
|
|
4
|
+
// ============================================
|
|
5
|
+
|
|
6
|
+
let premiumStatus = { active: false, features: {}, themes: ['midnight'], comingSoon: false };
|
|
7
|
+
|
|
8
|
+
// ── Fetch premium status from server ──
|
|
9
|
+
|
|
10
|
+
async function fetchStatus() {
|
|
11
|
+
try {
|
|
12
|
+
const res = await fetch('/api/premium/status');
|
|
13
|
+
if (res.ok) {
|
|
14
|
+
premiumStatus = await res.json();
|
|
15
|
+
}
|
|
16
|
+
} catch (e) {
|
|
17
|
+
console.warn('Premium: Failed to fetch status', e);
|
|
18
|
+
}
|
|
19
|
+
return premiumStatus;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ── UI Updates ──
|
|
23
|
+
|
|
24
|
+
function updateUI() {
|
|
25
|
+
const isActive = premiumStatus.active;
|
|
26
|
+
|
|
27
|
+
// Premium section in settings
|
|
28
|
+
const freeState = document.getElementById('premiumFreeState');
|
|
29
|
+
const activeState = document.getElementById('premiumActiveState');
|
|
30
|
+
const activeBadge = document.getElementById('premiumActiveBadge');
|
|
31
|
+
|
|
32
|
+
if (freeState) freeState.classList.toggle('setting-hidden', isActive);
|
|
33
|
+
if (activeState) activeState.classList.toggle('setting-hidden', !isActive);
|
|
34
|
+
if (activeBadge) activeBadge.classList.toggle('setting-hidden', !isActive);
|
|
35
|
+
|
|
36
|
+
// Voice lock icon
|
|
37
|
+
const voiceLock = document.getElementById('voicePremiumLock');
|
|
38
|
+
if (voiceLock) voiceLock.classList.toggle('setting-hidden', isActive);
|
|
39
|
+
|
|
40
|
+
// Voice section overlay
|
|
41
|
+
const voiceSection = document.querySelector('[data-section="voice"] .settings-section-body');
|
|
42
|
+
if (voiceSection) {
|
|
43
|
+
if (!isActive) {
|
|
44
|
+
voiceSection.classList.add('premium-locked');
|
|
45
|
+
// Add overlay if not already present
|
|
46
|
+
if (!voiceSection.querySelector('.premium-overlay')) {
|
|
47
|
+
const overlay = document.createElement('div');
|
|
48
|
+
overlay.className = 'premium-overlay';
|
|
49
|
+
if (premiumStatus.comingSoon) {
|
|
50
|
+
overlay.innerHTML = '<div class="premium-overlay-content">' +
|
|
51
|
+
'<span class="premium-overlay-icon">🔒</span>' +
|
|
52
|
+
'<span class="premium-overlay-text">Premium · Coming Soon</span>' +
|
|
53
|
+
'</div>';
|
|
54
|
+
} else {
|
|
55
|
+
overlay.innerHTML = '<div class="premium-overlay-content">' +
|
|
56
|
+
'<span class="premium-overlay-icon">🔒</span>' +
|
|
57
|
+
'<span class="premium-overlay-text">Requires Uplink Premium</span>' +
|
|
58
|
+
'<button class="setting-btn setting-btn-primary premium-overlay-btn" data-premium-show-modal="Voice chat">Upgrade to Premium</button>' +
|
|
59
|
+
'</div>';
|
|
60
|
+
overlay.querySelector('[data-premium-show-modal]').addEventListener('click', function() {
|
|
61
|
+
showUpgradeModal('Voice chat');
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
voiceSection.appendChild(overlay);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
voiceSection.classList.remove('premium-locked');
|
|
68
|
+
const overlay = voiceSection.querySelector('.premium-overlay');
|
|
69
|
+
if (overlay) overlay.remove();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Theme select - restrict options for free users
|
|
74
|
+
const themeSelect = document.getElementById('themeSelect');
|
|
75
|
+
if (themeSelect) {
|
|
76
|
+
const allowedThemes = premiumStatus.themes || ['midnight'];
|
|
77
|
+
Array.from(themeSelect.options).forEach(function(opt) {
|
|
78
|
+
if (!allowedThemes.includes(opt.value)) {
|
|
79
|
+
opt.disabled = true;
|
|
80
|
+
opt.textContent = opt.textContent.replace(' 🔒', '') + ' 🔒';
|
|
81
|
+
} else {
|
|
82
|
+
opt.disabled = false;
|
|
83
|
+
opt.textContent = opt.textContent.replace(' 🔒', '');
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// If current theme is not allowed, revert to midnight
|
|
88
|
+
if (!isActive && !allowedThemes.includes(themeSelect.value)) {
|
|
89
|
+
themeSelect.value = 'midnight';
|
|
90
|
+
if (window.UplinkThemes) {
|
|
91
|
+
window.UplinkThemes.apply('midnight');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Agents tab gate is handled in satellites.js switchTab()
|
|
97
|
+
|
|
98
|
+
// Dispatch event for other modules
|
|
99
|
+
window.dispatchEvent(new CustomEvent('uplink:premiumChange', {
|
|
100
|
+
detail: premiumStatus
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Activation ──
|
|
105
|
+
|
|
106
|
+
async function activate(key) {
|
|
107
|
+
try {
|
|
108
|
+
const res = await fetch('/api/premium/activate', {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
body: JSON.stringify({ key: key })
|
|
112
|
+
});
|
|
113
|
+
const data = await res.json();
|
|
114
|
+
|
|
115
|
+
if (data.success) {
|
|
116
|
+
premiumStatus = {
|
|
117
|
+
active: data.active,
|
|
118
|
+
features: data.features,
|
|
119
|
+
themes: data.themes,
|
|
120
|
+
activatedAt: data.activatedAt
|
|
121
|
+
};
|
|
122
|
+
updateUI();
|
|
123
|
+
return { success: true };
|
|
124
|
+
}
|
|
125
|
+
return { success: false, error: data.message || 'Invalid key' };
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return { success: false, error: 'Connection error' };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function deactivate() {
|
|
132
|
+
try {
|
|
133
|
+
const res = await fetch('/api/premium/deactivate', { method: 'POST' });
|
|
134
|
+
const data = await res.json();
|
|
135
|
+
if (data.success) {
|
|
136
|
+
premiumStatus = {
|
|
137
|
+
active: false,
|
|
138
|
+
features: data.features,
|
|
139
|
+
themes: data.themes
|
|
140
|
+
};
|
|
141
|
+
updateUI();
|
|
142
|
+
}
|
|
143
|
+
return data;
|
|
144
|
+
} catch (e) {
|
|
145
|
+
return { success: false, error: 'Connection error' };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Event Handlers ──
|
|
150
|
+
|
|
151
|
+
function setupEvents() {
|
|
152
|
+
// "Enter Key" button toggles key input
|
|
153
|
+
const enterKeyBtn = document.getElementById('premiumEnterKeyBtn');
|
|
154
|
+
const keyEntry = document.getElementById('premiumKeyEntry');
|
|
155
|
+
if (enterKeyBtn && keyEntry) {
|
|
156
|
+
enterKeyBtn.addEventListener('click', function() {
|
|
157
|
+
keyEntry.classList.toggle('setting-hidden');
|
|
158
|
+
const input = document.getElementById('premiumKeyInput');
|
|
159
|
+
if (input && !keyEntry.classList.contains('setting-hidden')) {
|
|
160
|
+
input.focus();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Activate button
|
|
166
|
+
const activateBtn = document.getElementById('premiumActivateBtn');
|
|
167
|
+
if (activateBtn) {
|
|
168
|
+
activateBtn.addEventListener('click', async function() {
|
|
169
|
+
var input = document.getElementById('premiumKeyInput');
|
|
170
|
+
var status = document.getElementById('premiumKeyStatus');
|
|
171
|
+
var key = input ? input.value.trim() : '';
|
|
172
|
+
|
|
173
|
+
if (!key) {
|
|
174
|
+
if (status) status.textContent = 'Please enter a license key';
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
activateBtn.disabled = true;
|
|
179
|
+
activateBtn.textContent = 'Activating...';
|
|
180
|
+
|
|
181
|
+
var result = await activate(key);
|
|
182
|
+
|
|
183
|
+
if (result.success) {
|
|
184
|
+
if (status) {
|
|
185
|
+
status.textContent = '✓ Premium activated!';
|
|
186
|
+
status.classList.add('premium-success');
|
|
187
|
+
}
|
|
188
|
+
// Clear input
|
|
189
|
+
if (input) input.value = '';
|
|
190
|
+
} else {
|
|
191
|
+
if (status) {
|
|
192
|
+
status.textContent = result.error || 'Invalid license key';
|
|
193
|
+
status.classList.add('premium-error');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
activateBtn.disabled = false;
|
|
198
|
+
activateBtn.textContent = 'Activate';
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Enter key in input triggers activate
|
|
203
|
+
var keyInput = document.getElementById('premiumKeyInput');
|
|
204
|
+
if (keyInput) {
|
|
205
|
+
keyInput.addEventListener('keydown', function(e) {
|
|
206
|
+
if (e.key === 'Enter') {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
var btn = document.getElementById('premiumActivateBtn');
|
|
209
|
+
if (btn) btn.click();
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Deactivate button
|
|
215
|
+
var deactivateBtn = document.getElementById('premiumDeactivateBtn');
|
|
216
|
+
if (deactivateBtn) {
|
|
217
|
+
deactivateBtn.addEventListener('click', async function() {
|
|
218
|
+
if (!confirm('Deactivate Premium? You can re-enter your key anytime.')) return;
|
|
219
|
+
await deactivate();
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Upgrade buttons (anywhere in the UI) - open modal
|
|
224
|
+
document.addEventListener('click', function(e) {
|
|
225
|
+
if (e.target.matches('[data-premium-upgrade]')) {
|
|
226
|
+
e.preventDefault();
|
|
227
|
+
showUpgradeModal();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Note: agents tab gate is handled in satellites.js switchTab()
|
|
232
|
+
// The old agentsBtn intercept was removed - there is no standalone agents button
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Feature descriptions for upgrade modal ──
|
|
236
|
+
|
|
237
|
+
var featureInfo = {
|
|
238
|
+
'Voice chat': {
|
|
239
|
+
icon: '🎙️',
|
|
240
|
+
title: 'Voice Chat',
|
|
241
|
+
desc: 'Talk to your AI with push-to-talk or hands-free mode. Responses are spoken back with natural TTS.',
|
|
242
|
+
},
|
|
243
|
+
'Agent management': {
|
|
244
|
+
icon: '🤖',
|
|
245
|
+
title: 'Agent Management',
|
|
246
|
+
desc: 'Create, configure, and switch between multiple AI agents - each with their own personality and settings.',
|
|
247
|
+
},
|
|
248
|
+
'Premium themes': {
|
|
249
|
+
icon: '🎨',
|
|
250
|
+
title: 'All Themes',
|
|
251
|
+
desc: 'Unlock every built-in theme plus the custom theme generator. Make Uplink look exactly how you want.',
|
|
252
|
+
},
|
|
253
|
+
'Early access': {
|
|
254
|
+
icon: '🚀',
|
|
255
|
+
title: 'Early Access',
|
|
256
|
+
desc: 'Get new features first - including upcoming split view, custom themes, and more.',
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
var defaultFeatureInfo = {
|
|
261
|
+
icon: '✨',
|
|
262
|
+
title: 'Premium Feature',
|
|
263
|
+
desc: 'This feature is part of Uplink Premium.',
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
var allFeatures = [
|
|
267
|
+
{ check: '✓', text: 'Voice chat with TTS', highlight: false },
|
|
268
|
+
{ check: '✓', text: 'All themes + custom generator', highlight: false },
|
|
269
|
+
{ check: '✓', text: 'Agent management', highlight: false },
|
|
270
|
+
{ check: '✓', text: 'Early access to new features', highlight: false },
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
// ── Upgrade modal ──
|
|
274
|
+
|
|
275
|
+
function showUpgradeModal(featureName) {
|
|
276
|
+
// Remove existing modal if any
|
|
277
|
+
var existing = document.querySelector('.premium-modal');
|
|
278
|
+
if (existing) existing.remove();
|
|
279
|
+
|
|
280
|
+
var info = featureInfo[featureName] || defaultFeatureInfo;
|
|
281
|
+
|
|
282
|
+
// Build feature list - highlight the relevant one
|
|
283
|
+
var featureListHTML = allFeatures.map(function(f) {
|
|
284
|
+
var isHighlight = featureName && f.text.toLowerCase().indexOf(featureName.toLowerCase().replace(/^premium\s*/i, '')) !== -1;
|
|
285
|
+
return '<li>' +
|
|
286
|
+
'<span class="feature-check">' + f.check + '</span>' +
|
|
287
|
+
'<span' + (isHighlight ? ' class="feature-highlight"' : '') + '>' + f.text + '</span>' +
|
|
288
|
+
'</li>';
|
|
289
|
+
}).join('');
|
|
290
|
+
|
|
291
|
+
var modal = document.createElement('div');
|
|
292
|
+
modal.className = 'premium-modal entering';
|
|
293
|
+
modal.innerHTML =
|
|
294
|
+
'<div class="premium-modal-card">' +
|
|
295
|
+
'<div class="premium-modal-header">' +
|
|
296
|
+
'<button class="premium-modal-close" aria-label="Close"><svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="1" y1="1" x2="13" y2="13"/><line x1="13" y1="1" x2="1" y2="13"/></svg></button>' +
|
|
297
|
+
'<span class="premium-modal-icon">' + info.icon + '</span>' +
|
|
298
|
+
'<h3 class="premium-modal-title">' + info.title + '</h3>' +
|
|
299
|
+
'<p class="premium-modal-desc">' + info.desc + '</p>' +
|
|
300
|
+
'</div>' +
|
|
301
|
+
'<div class="premium-modal-body">' +
|
|
302
|
+
'<ul class="premium-modal-features">' + featureListHTML + '</ul>' +
|
|
303
|
+
'</div>' +
|
|
304
|
+
'<div class="premium-modal-footer">' +
|
|
305
|
+
(premiumStatus.comingSoon
|
|
306
|
+
? '<div class="premium-modal-price">Premium is coming soon</div>' +
|
|
307
|
+
'<div class="premium-modal-coming-soon">We\'re putting the finishing touches on Premium features. Stay tuned!</div>'
|
|
308
|
+
: '<div class="premium-modal-price">One-time purchase — yours forever</div>' +
|
|
309
|
+
'<a class="premium-modal-buy" href="https://store.moonco.pro" target="_blank" rel="noopener">Get Uplink Premium</a>' +
|
|
310
|
+
'<button class="premium-modal-key" data-premium-has-key>I already have a key</button>'
|
|
311
|
+
) +
|
|
312
|
+
'</div>' +
|
|
313
|
+
'</div>';
|
|
314
|
+
|
|
315
|
+
document.body.appendChild(modal);
|
|
316
|
+
|
|
317
|
+
// Animate in
|
|
318
|
+
requestAnimationFrame(() => {
|
|
319
|
+
modal.classList.remove('entering');
|
|
320
|
+
modal.classList.add('visible');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// Close handlers
|
|
324
|
+
var closeModal = function() {
|
|
325
|
+
modal.classList.remove('visible');
|
|
326
|
+
setTimeout(function() { modal.remove(); }, 250);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Close on backdrop click
|
|
330
|
+
modal.addEventListener('click', function(e) {
|
|
331
|
+
if (e.target === modal) closeModal();
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Close button
|
|
335
|
+
modal.querySelector('.premium-modal-close').addEventListener('click', function(e) {
|
|
336
|
+
e.stopPropagation();
|
|
337
|
+
closeModal();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Escape key
|
|
341
|
+
var escHandler = function(e) {
|
|
342
|
+
if (e.key === 'Escape') {
|
|
343
|
+
closeModal();
|
|
344
|
+
document.removeEventListener('keydown', escHandler);
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
document.addEventListener('keydown', escHandler);
|
|
348
|
+
|
|
349
|
+
// "I have a key" → open settings to premium section
|
|
350
|
+
var hasKeyBtn = modal.querySelector('[data-premium-has-key]');
|
|
351
|
+
if (hasKeyBtn) hasKeyBtn.addEventListener('click', function() {
|
|
352
|
+
closeModal();
|
|
353
|
+
// Open settings panel
|
|
354
|
+
var settingsPanel = document.getElementById('settingsPanel');
|
|
355
|
+
if (settingsPanel && !settingsPanel.classList.contains('visible')) {
|
|
356
|
+
if (window.UplinkPanels) {
|
|
357
|
+
window.UplinkPanels.open('settings');
|
|
358
|
+
} else {
|
|
359
|
+
settingsPanel.classList.add('visible');
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
// Expand premium section
|
|
363
|
+
setTimeout(function() {
|
|
364
|
+
var premiumHeader = document.querySelector('[data-section="premium"] .settings-section-header');
|
|
365
|
+
var premiumBody = document.getElementById('section-premium');
|
|
366
|
+
if (premiumHeader && premiumBody && premiumBody.classList.contains('collapsed')) {
|
|
367
|
+
premiumHeader.click();
|
|
368
|
+
}
|
|
369
|
+
// Show key entry
|
|
370
|
+
var keyEntry = document.getElementById('premiumKeyEntry');
|
|
371
|
+
if (keyEntry) {
|
|
372
|
+
keyEntry.classList.remove('setting-hidden');
|
|
373
|
+
var input = document.getElementById('premiumKeyInput');
|
|
374
|
+
if (input) input.focus();
|
|
375
|
+
}
|
|
376
|
+
}, 300);
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Toast (kept as fallback for edge cases) ──
|
|
381
|
+
|
|
382
|
+
function showUpgradeToast(featureName) {
|
|
383
|
+
// Use modal instead of toast for primary gates
|
|
384
|
+
showUpgradeModal(featureName);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ── Init ──
|
|
388
|
+
|
|
389
|
+
async function init() {
|
|
390
|
+
await fetchStatus();
|
|
391
|
+
updateUI();
|
|
392
|
+
setupEvents();
|
|
393
|
+
console.log('Premium: Initialized (' + (premiumStatus.active ? 'active' : 'free') + ')');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Expose API
|
|
397
|
+
export const UplinkPremium = {
|
|
398
|
+
isActive: function() { return premiumStatus.active; },
|
|
399
|
+
hasFeature: function(f) { return premiumStatus.active || !(premiumStatus.features && premiumStatus.features[f] === false); },
|
|
400
|
+
getStatus: function() { return premiumStatus; },
|
|
401
|
+
refresh: async function() { await fetchStatus(); updateUI(); },
|
|
402
|
+
showUpgradeToast: showUpgradeToast,
|
|
403
|
+
showUpgradeModal: showUpgradeModal
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
import { UplinkCore } from './core.js';
|
|
407
|
+
|
|
408
|
+
// Backward compat: assign to window
|
|
409
|
+
window.UplinkPremium = UplinkPremium;
|
|
410
|
+
|
|
411
|
+
// Register and init
|
|
412
|
+
UplinkCore.registerModule('premium', init);
|