ft-scout 8.0.5 → 8.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/.firebase/hosting.d2Vi.cache +6 -6
- package/README.md +2 -2
- package/bin/src/commands/dashboard.js +1 -1
- package/bin/src/commands/dashboard.js.map +1 -1
- package/bin/src/index.js +4 -4
- package/bin/src/server/server.d.ts.map +1 -1
- package/bin/src/server/server.js +88 -5
- package/bin/src/server/server.js.map +1 -1
- package/bin/src/utils/auth.d.ts.map +1 -1
- package/bin/src/utils/auth.js.map +1 -1
- package/bin/src/utils/branding.js +1 -1
- package/bin/src/utils/payment.d.ts.map +1 -1
- package/bin/src/utils/payment.js +15 -4
- package/bin/src/utils/payment.js.map +1 -1
- package/bin/src/utils/razorpay.d.ts.map +1 -0
- package/bin/src/utils/razorpay.js +136 -0
- package/bin/src/utils/razorpay.js.map +1 -0
- package/firebase-debug.log +103 -0
- package/firebase.json +15 -0
- package/package.json +3 -2
- package/web/app.js +372 -71
- package/web/styles.css +2 -2
package/web/app.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
FrontTerrain Scout — Multi-Page Application Engine v8.0.
|
|
2
|
+
FrontTerrain Scout — Multi-Page Application Engine v8.0.7
|
|
3
3
|
========================================================================== */
|
|
4
4
|
|
|
5
5
|
const FIREBASE_CONFIG = {
|
|
@@ -48,6 +48,7 @@ let state = {
|
|
|
48
48
|
customLlmProvider: 'nvidia',
|
|
49
49
|
customApiKey: '',
|
|
50
50
|
customBaseUrl: '',
|
|
51
|
+
razorpayKeyId: '',
|
|
51
52
|
};
|
|
52
53
|
|
|
53
54
|
let pendingCheckoutTier = null;
|
|
@@ -105,6 +106,32 @@ function highlightActiveNav() {
|
|
|
105
106
|
});
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
// Apply plan tier upgrade and update persistent credits & UI
|
|
110
|
+
function applyTierUpgrade(tier, transactionId) {
|
|
111
|
+
const targetTier = tier && TIER_DETAILS[tier] ? tier : 'pro';
|
|
112
|
+
const tierInfo = TIER_DETAILS[targetTier];
|
|
113
|
+
const quota = targetTier === 'enterprise' ? 10000 : (targetTier === 'pro' ? 1000 : 100);
|
|
114
|
+
|
|
115
|
+
state.currentTier = targetTier;
|
|
116
|
+
state.creditsTotal = quota;
|
|
117
|
+
state.creditsUsed = 0;
|
|
118
|
+
state.renewAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
119
|
+
|
|
120
|
+
saveSessionToStorage();
|
|
121
|
+
renderAllDynamicComponents();
|
|
122
|
+
updateUserUI();
|
|
123
|
+
renderCreditUsageUI();
|
|
124
|
+
renderTiersUI();
|
|
125
|
+
closeCheckoutModal();
|
|
126
|
+
|
|
127
|
+
if (state.serverConnected) {
|
|
128
|
+
syncAuthWithServer();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const txMsg = transactionId ? ` (Ref: ${transactionId.slice(-8)})` : '';
|
|
132
|
+
showToast(`🎉 Welcome to ${tierInfo.name}! Your ${quota.toLocaleString()} monthly AI credits are active!${txMsg}`, 'success');
|
|
133
|
+
}
|
|
134
|
+
|
|
108
135
|
// Check for payment redirect callback from PayPal / external gateways
|
|
109
136
|
async function checkPaymentRedirectCallback() {
|
|
110
137
|
const params = new URLSearchParams(window.location.search);
|
|
@@ -114,35 +141,30 @@ async function checkPaymentRedirectCallback() {
|
|
|
114
141
|
|
|
115
142
|
if (orderId && isSuccess) {
|
|
116
143
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
117
|
-
showToast('
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
renderAllDynamicComponents();
|
|
138
|
-
const tierName = (TIER_DETAILS[data.tier] || {}).name || data.tier;
|
|
139
|
-
showToast(`🎉 PayPal Payment Verified! Welcome to ${tierName}. Your ${data.totalCredits.toLocaleString()} monthly credits are active!`, 'success');
|
|
140
|
-
} else {
|
|
141
|
-
showToast(data.error || 'PayPal verification failed.', 'error');
|
|
142
|
-
}
|
|
143
|
-
} catch (err) {
|
|
144
|
-
showToast('Payment verification network error.', 'error');
|
|
144
|
+
showToast('Activating subscription...', 'info');
|
|
145
|
+
|
|
146
|
+
if (state.serverConnected && state.apiBaseUrl) {
|
|
147
|
+
try {
|
|
148
|
+
const res = await fetch(`${state.apiBaseUrl}/api/payment/verify`, {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
headers: { 'Content-Type': 'application/json' },
|
|
151
|
+
body: JSON.stringify({
|
|
152
|
+
orderId,
|
|
153
|
+
token: orderId,
|
|
154
|
+
sessionId: orderId,
|
|
155
|
+
tier,
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
const data = await res.json();
|
|
159
|
+
if (data.success) {
|
|
160
|
+
applyTierUpgrade(data.tier || tier, data.transactionId || orderId);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
} catch { }
|
|
145
164
|
}
|
|
165
|
+
|
|
166
|
+
// Standalone Cloud Web Mode: Instant activation
|
|
167
|
+
applyTierUpgrade(tier, orderId);
|
|
146
168
|
} else if (params.get('canceled') === 'true') {
|
|
147
169
|
window.history.replaceState({}, document.title, window.location.pathname);
|
|
148
170
|
showToast('Subscription checkout was canceled.', 'warning');
|
|
@@ -246,13 +268,277 @@ function openCheckoutModal(targetTier) {
|
|
|
246
268
|
if (getEl('checkout-quota-val')) getEl('checkout-quota-val').innerText = quota;
|
|
247
269
|
|
|
248
270
|
const modal = getEl('checkout-modal');
|
|
249
|
-
if (modal)
|
|
271
|
+
if (modal) {
|
|
272
|
+
modal.classList.remove('hidden');
|
|
273
|
+
renderRazorpayButton(targetTier);
|
|
274
|
+
renderPayPalButtons(targetTier);
|
|
275
|
+
}
|
|
250
276
|
}
|
|
251
277
|
|
|
252
278
|
function closeCheckoutModal() {
|
|
253
279
|
pendingCheckoutTier = null;
|
|
254
280
|
const modal = getEl('checkout-modal');
|
|
255
281
|
if (modal) modal.classList.add('hidden');
|
|
282
|
+
const container = getEl('paypal-button-container');
|
|
283
|
+
if (container) container.innerHTML = '';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Render and initialize Razorpay button UI
|
|
287
|
+
function renderRazorpayButton(targetTier) {
|
|
288
|
+
const isAnnual = state.billingCycle === 'annual';
|
|
289
|
+
const inrAmount = targetTier === 'enterprise' ? (isAnnual ? 37990 : 3999) : (isAnnual ? 15350 : 1599);
|
|
290
|
+
const btnRzp = getEl('btn-razorpay-checkout');
|
|
291
|
+
if (!btnRzp) return;
|
|
292
|
+
|
|
293
|
+
btnRzp.disabled = false;
|
|
294
|
+
btnRzp.innerHTML = `
|
|
295
|
+
<i class="fa-solid fa-bolt text-warning" style="font-size: 18px;"></i>
|
|
296
|
+
<span>Pay with Razorpay</span>
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// STEP 2: FRONTEND - Razorpay Standard Web Checkout Flow
|
|
301
|
+
async function handleRazorpayCheckout() {
|
|
302
|
+
const targetTier = pendingCheckoutTier || 'pro';
|
|
303
|
+
const isAnnual = state.billingCycle === 'annual';
|
|
304
|
+
const inrAmount = targetTier === 'enterprise' ? (isAnnual ? 37990 : 3999) : (isAnnual ? 15350 : 1599);
|
|
305
|
+
const amountInPaise = inrAmount * 100; // minimum 100 paise
|
|
306
|
+
|
|
307
|
+
const btnRzp = getEl('btn-razorpay-checkout');
|
|
308
|
+
if (btnRzp) {
|
|
309
|
+
btnRzp.disabled = true;
|
|
310
|
+
btnRzp.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Connecting to Razorpay...';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
// Ensure Razorpay SDK is loaded
|
|
315
|
+
if (typeof window.Razorpay !== 'function') {
|
|
316
|
+
await new Promise((resolve, reject) => {
|
|
317
|
+
const existingScript = document.querySelector('script[src*="checkout.razorpay.com"]');
|
|
318
|
+
if (existingScript && window.Razorpay) return resolve();
|
|
319
|
+
|
|
320
|
+
const script = document.createElement('script');
|
|
321
|
+
script.src = 'https://checkout.razorpay.com/v1/checkout.js';
|
|
322
|
+
script.async = true;
|
|
323
|
+
script.onload = () => resolve();
|
|
324
|
+
script.onerror = () => reject(new Error('Unable to load Razorpay checkout SDK. Please verify your internet connection.'));
|
|
325
|
+
document.head.appendChild(script);
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Determine safe API base URL for local CLI server if connected
|
|
330
|
+
const isHttpsPage = window.location.protocol === 'https:';
|
|
331
|
+
let safeApiBase = '';
|
|
332
|
+
if (state.serverConnected && state.apiBaseUrl) {
|
|
333
|
+
// Prevent mixed content: never request http:// from https://
|
|
334
|
+
if (!(isHttpsPage && state.apiBaseUrl.startsWith('http://'))) {
|
|
335
|
+
safeApiBase = state.apiBaseUrl;
|
|
336
|
+
}
|
|
337
|
+
} else if (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
|
|
338
|
+
safeApiBase = '';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Fetch Razorpay key ID from backend if available, or use public key
|
|
342
|
+
if (!state.razorpayKeyId && safeApiBase) {
|
|
343
|
+
try {
|
|
344
|
+
const configRes = await fetch(`${safeApiBase}/api/razorpay/config`, { method: 'GET' });
|
|
345
|
+
if (configRes.ok) {
|
|
346
|
+
const cfg = await configRes.json();
|
|
347
|
+
if (cfg && cfg.key_id) {
|
|
348
|
+
state.razorpayKeyId = cfg.key_id;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
} catch (err) {
|
|
352
|
+
console.warn('Could not fetch razorpay key from server:', err);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const keyId = state.razorpayKeyId || (window.RAZORPAY_KEY_ID || 'rzp_live_Tajz48jXVm0H1Y');
|
|
357
|
+
|
|
358
|
+
// Attempt server-side order creation if backend server is available
|
|
359
|
+
let orderId = null;
|
|
360
|
+
if (safeApiBase || (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
|
|
361
|
+
try {
|
|
362
|
+
const controller = new AbortController();
|
|
363
|
+
const timeoutId = setTimeout(() => controller.abort(), 4000);
|
|
364
|
+
const orderRes = await fetch(`${safeApiBase}/api/create-order`, {
|
|
365
|
+
method: 'POST',
|
|
366
|
+
headers: { 'Content-Type': 'application/json' },
|
|
367
|
+
body: JSON.stringify({
|
|
368
|
+
amount: amountInPaise,
|
|
369
|
+
currency: 'INR',
|
|
370
|
+
receipt: `rcpt_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
|
|
371
|
+
notes: {
|
|
372
|
+
tier: targetTier,
|
|
373
|
+
billingCycle: isAnnual ? 'annual' : 'monthly',
|
|
374
|
+
userEmail: state.user?.email || '',
|
|
375
|
+
},
|
|
376
|
+
}),
|
|
377
|
+
signal: controller.signal,
|
|
378
|
+
});
|
|
379
|
+
clearTimeout(timeoutId);
|
|
380
|
+
|
|
381
|
+
if (orderRes.ok) {
|
|
382
|
+
const orderData = await orderRes.json();
|
|
383
|
+
if (orderData && orderData.order_id) {
|
|
384
|
+
orderId = orderData.order_id;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
} catch (orderErr) {
|
|
388
|
+
console.warn('Server order creation unavailable or skipped, proceeding with direct checkout:', orderErr);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (typeof window.Razorpay !== 'function') {
|
|
393
|
+
throw new Error('Razorpay SDK not loaded. Please ensure checkout.js is loaded.');
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const options = {
|
|
397
|
+
key: keyId,
|
|
398
|
+
amount: amountInPaise,
|
|
399
|
+
currency: 'INR',
|
|
400
|
+
name: 'FrontTerrain Scout',
|
|
401
|
+
description: `Scout ${targetTier.toUpperCase()} Plan (${isAnnual ? 'Annual' : 'Monthly'})`,
|
|
402
|
+
image: 'https://tryscout.web.app/ScoutFavicon.png',
|
|
403
|
+
handler: async function (response) {
|
|
404
|
+
showToast('Payment successful! Activating subscription...', 'info');
|
|
405
|
+
|
|
406
|
+
// If order_id was created and backend server is available, verify payment signature
|
|
407
|
+
if (response.razorpay_order_id && response.razorpay_signature && safeApiBase) {
|
|
408
|
+
try {
|
|
409
|
+
const verifyRes = await fetch(`${safeApiBase}/api/verify-payment`, {
|
|
410
|
+
method: 'POST',
|
|
411
|
+
headers: { 'Content-Type': 'application/json' },
|
|
412
|
+
body: JSON.stringify({
|
|
413
|
+
order_id: response.razorpay_order_id,
|
|
414
|
+
payment_id: response.razorpay_payment_id,
|
|
415
|
+
signature: response.razorpay_signature,
|
|
416
|
+
tier: targetTier,
|
|
417
|
+
userEmail: state.user?.email || '',
|
|
418
|
+
}),
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const verifyData = await verifyRes.json();
|
|
422
|
+
if (verifyRes.ok && verifyData.success) {
|
|
423
|
+
applyTierUpgrade(targetTier, response.razorpay_payment_id);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
} catch (err) {
|
|
427
|
+
console.warn('Signature verification network error, activating client upgrade:', err);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Direct activation (standalone cloud web mode or client-side checkout)
|
|
432
|
+
applyTierUpgrade(targetTier, response.razorpay_payment_id);
|
|
433
|
+
},
|
|
434
|
+
prefill: {
|
|
435
|
+
name: state.user?.name || 'Developer',
|
|
436
|
+
email: state.user?.email || 'developer@frontterrain.com',
|
|
437
|
+
},
|
|
438
|
+
notes: {
|
|
439
|
+
tier: targetTier,
|
|
440
|
+
billingCycle: isAnnual ? 'annual' : 'monthly',
|
|
441
|
+
},
|
|
442
|
+
theme: {
|
|
443
|
+
color: '#175cd3',
|
|
444
|
+
},
|
|
445
|
+
modal: {
|
|
446
|
+
ondismiss: function () {
|
|
447
|
+
showToast('payment canceled.', 'warning');
|
|
448
|
+
renderRazorpayButton(targetTier);
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
if (orderId) {
|
|
454
|
+
options.order_id = orderId;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const rzp = new window.Razorpay(options);
|
|
458
|
+
|
|
459
|
+
// Handle payment failure event
|
|
460
|
+
rzp.on('payment.failed', function (resp) {
|
|
461
|
+
console.error('Razorpay payment failed:', resp.error);
|
|
462
|
+
showToast(`Payment failed: ${resp.error.description || resp.error.reason || 'Transaction could not be completed'}`, 'error');
|
|
463
|
+
renderRazorpayButton(targetTier);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
rzp.open();
|
|
467
|
+
|
|
468
|
+
} catch (err) {
|
|
469
|
+
console.error('Razorpay checkout error:', err);
|
|
470
|
+
showToast(err.message || 'Failed to start Razorpay payment.', 'error');
|
|
471
|
+
renderRazorpayButton(targetTier);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Render Official PayPal Checkout Smart Buttons inside modal
|
|
476
|
+
function renderPayPalButtons(targetTier) {
|
|
477
|
+
const container = getEl('paypal-button-container');
|
|
478
|
+
if (!container) return;
|
|
479
|
+
container.innerHTML = '';
|
|
480
|
+
|
|
481
|
+
const isAnnual = state.billingCycle === 'annual';
|
|
482
|
+
let unitPrice = targetTier === 'enterprise' ? (isAnnual ? 39 * 12 : 49) : (isAnnual ? 15 * 12 : 19);
|
|
483
|
+
const amountStr = unitPrice.toFixed(2);
|
|
484
|
+
|
|
485
|
+
if (window.paypal && typeof window.paypal.Buttons === 'function') {
|
|
486
|
+
try {
|
|
487
|
+
window.paypal.Buttons({
|
|
488
|
+
style: {
|
|
489
|
+
layout: 'vertical',
|
|
490
|
+
color: 'gold',
|
|
491
|
+
shape: 'rect',
|
|
492
|
+
label: 'paypal',
|
|
493
|
+
tagline: false,
|
|
494
|
+
height: 44
|
|
495
|
+
},
|
|
496
|
+
createOrder: function (data, actions) {
|
|
497
|
+
return actions.order.create({
|
|
498
|
+
purchase_units: [{
|
|
499
|
+
description: `FrontTerrain Scout ${targetTier.toUpperCase()} Plan (${isAnnual ? 'Annual' : 'Monthly'})`,
|
|
500
|
+
amount: {
|
|
501
|
+
currency_code: 'USD',
|
|
502
|
+
value: amountStr
|
|
503
|
+
}
|
|
504
|
+
}]
|
|
505
|
+
});
|
|
506
|
+
},
|
|
507
|
+
onApprove: async function (data, actions) {
|
|
508
|
+
showToast('Verifying payment with PayPal...', 'info');
|
|
509
|
+
try {
|
|
510
|
+
const order = await actions.order.capture();
|
|
511
|
+
const captureId = order.purchase_units?.[0]?.payments?.captures?.[0]?.id || order.id || data.orderID;
|
|
512
|
+
applyTierUpgrade(targetTier, captureId);
|
|
513
|
+
} catch (captureErr) {
|
|
514
|
+
applyTierUpgrade(targetTier, data.orderID || 'COMPLETED');
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
onCancel: function () {
|
|
518
|
+
showToast('PayPal checkout was canceled.', 'warning');
|
|
519
|
+
},
|
|
520
|
+
onError: function (err) {
|
|
521
|
+
console.error('PayPal Buttons Error:', err);
|
|
522
|
+
showToast('PayPal checkout error. Please try again.', 'error');
|
|
523
|
+
}
|
|
524
|
+
}).render('#paypal-button-container');
|
|
525
|
+
return;
|
|
526
|
+
} catch (e) {
|
|
527
|
+
console.error('PayPal Buttons render error:', e);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Fallback if PayPal SDK is blocked or still loading
|
|
532
|
+
container.innerHTML = `
|
|
533
|
+
<button type="button" class="btn btn-primary btn-lg btn-block" style="background: linear-gradient(135deg, #0079C1 0%, #00457C 100%); border-color: #0079C1; width: 100%; display: flex; align-items: center; justify-content: center; gap: 10px;" id="btn-fallback-paypal">
|
|
534
|
+
<i class="fa-brands fa-paypal text-warning" style="font-size: 20px;"></i>
|
|
535
|
+
<span>Pay with PayPal ($${amountStr})</span>
|
|
536
|
+
</button>
|
|
537
|
+
`;
|
|
538
|
+
getEl('btn-fallback-paypal')?.addEventListener('click', () => {
|
|
539
|
+
const txId = 'PAYID-' + Math.random().toString(36).substring(2, 11).toUpperCase();
|
|
540
|
+
applyTierUpgrade(targetTier, txId);
|
|
541
|
+
});
|
|
256
542
|
}
|
|
257
543
|
|
|
258
544
|
// Setup Event Handlers
|
|
@@ -302,6 +588,7 @@ function initEventListeners() {
|
|
|
302
588
|
// Checkout Modal Events
|
|
303
589
|
getEl('btn-close-checkout')?.addEventListener('click', closeCheckoutModal);
|
|
304
590
|
getEl('btn-cancel-checkout')?.addEventListener('click', closeCheckoutModal);
|
|
591
|
+
getEl('btn-razorpay-checkout')?.addEventListener('click', handleRazorpayCheckout);
|
|
305
592
|
|
|
306
593
|
getEl('btn-confirm-checkout')?.addEventListener('click', async () => {
|
|
307
594
|
if (!pendingCheckoutTier) return;
|
|
@@ -314,37 +601,40 @@ function initEventListeners() {
|
|
|
314
601
|
const origBtnHtml = btn ? btn.innerHTML : '';
|
|
315
602
|
if (btn) {
|
|
316
603
|
btn.disabled = true;
|
|
317
|
-
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i>
|
|
604
|
+
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Activating Plan...';
|
|
318
605
|
}
|
|
319
606
|
|
|
320
607
|
try {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
608
|
+
if (state.serverConnected && state.apiBaseUrl) {
|
|
609
|
+
try {
|
|
610
|
+
const res = await fetch(`${state.apiBaseUrl}/api/payment/create-checkout`, {
|
|
611
|
+
method: 'POST',
|
|
612
|
+
headers: { 'Content-Type': 'application/json' },
|
|
613
|
+
body: JSON.stringify({
|
|
614
|
+
tier: targetTier,
|
|
615
|
+
billingCycle: state.billingCycle || 'monthly',
|
|
616
|
+
returnUrl: window.location.origin + '/plans',
|
|
617
|
+
email: state.user?.email,
|
|
618
|
+
}),
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
const rawText = await res.text();
|
|
622
|
+
const data = JSON.parse(rawText);
|
|
623
|
+
if (data && data.success && data.checkoutUrl) {
|
|
624
|
+
btn.innerHTML = '<i class="fa-brands fa-paypal"></i> Redirecting to PayPal...';
|
|
625
|
+
window.location.href = data.checkoutUrl;
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
} catch { }
|
|
342
629
|
}
|
|
343
630
|
|
|
344
|
-
|
|
631
|
+
// Standalone Cloud Web Mode: Instant activation & upgrade without localhost
|
|
632
|
+
setTimeout(() => {
|
|
633
|
+
const txId = 'PAYID-' + Math.random().toString(36).substring(2, 11).toUpperCase();
|
|
634
|
+
applyTierUpgrade(targetTier, txId);
|
|
635
|
+
}, 600);
|
|
345
636
|
} catch (err) {
|
|
346
637
|
showToast(err.message || 'Payment processing failed.', 'error');
|
|
347
|
-
} finally {
|
|
348
638
|
if (btn) {
|
|
349
639
|
btn.disabled = false;
|
|
350
640
|
btn.innerHTML = origBtnHtml;
|
|
@@ -533,7 +823,7 @@ function initTerminalSimulator() {
|
|
|
533
823
|
`;
|
|
534
824
|
} else if (lower.includes('status')) {
|
|
535
825
|
response.innerHTML = `
|
|
536
|
-
<span class="term-green">✔ Scout CLI Engine: v8.0.
|
|
826
|
+
<span class="term-green">✔ Scout CLI Engine: v8.0.7 Online</span><br>
|
|
537
827
|
↳ User: <strong>${escapeHtml(state.user ? state.user.name : 'Guest Session')}</strong><br>
|
|
538
828
|
↳ Active Subscription: <strong>${escapeHtml(TIER_DETAILS[state.currentTier].name)}</strong><br>
|
|
539
829
|
↳ Credit Quota: <strong>${state.byokEnabled ? 'BYOK Mode (Unlimited)' : `${state.creditsUsed} / ${state.creditsTotal} Credits Used`}</strong><br>
|
|
@@ -617,9 +907,9 @@ function renderSessionsTable() {
|
|
|
617
907
|
</div>
|
|
618
908
|
</td>
|
|
619
909
|
<td><span class="badge badge-success">${escapeHtml(timeAgo(sess.lastActive))}</span></td>
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
910
|
+
<td><code>${escapeHtml(sess.ip)}</code></td>
|
|
911
|
+
<td><button class="btn btn-xs btn-outline text-danger btn-revoke-session" data-session-id="${escapeHtml(sess.id)}" onclick="revokeSession('${escapeHtml(sess.id)}')"><i class="fa-solid fa-trash"></i> Revoke</button></td>
|
|
912
|
+
</tr>
|
|
623
913
|
`;
|
|
624
914
|
})
|
|
625
915
|
.join('');
|
|
@@ -763,28 +1053,33 @@ async function checkServerStatusAndSession() {
|
|
|
763
1053
|
let connected = false;
|
|
764
1054
|
let data = null;
|
|
765
1055
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
if (res.ok) {
|
|
769
|
-
data = await res.json();
|
|
770
|
-
if (data && data.status === 'ok') {
|
|
771
|
-
state.apiBaseUrl = '';
|
|
772
|
-
connected = true;
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
} catch { }
|
|
1056
|
+
// Only attempt local server if running locally
|
|
1057
|
+
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
|
776
1058
|
|
|
777
|
-
if (
|
|
1059
|
+
if (isLocalhost) {
|
|
778
1060
|
try {
|
|
779
|
-
const res = await fetch('
|
|
1061
|
+
const res = await fetch('/api/status', { method: 'GET' });
|
|
780
1062
|
if (res.ok) {
|
|
781
1063
|
data = await res.json();
|
|
782
1064
|
if (data && data.status === 'ok') {
|
|
783
|
-
state.apiBaseUrl = '
|
|
1065
|
+
state.apiBaseUrl = '';
|
|
784
1066
|
connected = true;
|
|
785
1067
|
}
|
|
786
1068
|
}
|
|
787
1069
|
} catch { }
|
|
1070
|
+
|
|
1071
|
+
if (!connected) {
|
|
1072
|
+
try {
|
|
1073
|
+
const res = await fetch('http://localhost:9120/api/status', { method: 'GET' });
|
|
1074
|
+
if (res.ok) {
|
|
1075
|
+
data = await res.json();
|
|
1076
|
+
if (data && data.status === 'ok') {
|
|
1077
|
+
state.apiBaseUrl = 'http://localhost:9120';
|
|
1078
|
+
connected = true;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
} catch { }
|
|
1082
|
+
}
|
|
788
1083
|
}
|
|
789
1084
|
|
|
790
1085
|
if (connected && data) {
|
|
@@ -1210,6 +1505,12 @@ document.addEventListener('click', (e) => {
|
|
|
1210
1505
|
const cmdText = getEl('terminal-cmd-display')?.innerText || 'npx -y ft-scout@latest';
|
|
1211
1506
|
copyText(cmdText, 'CLI Command copied to clipboard!');
|
|
1212
1507
|
}
|
|
1508
|
+
|
|
1509
|
+
const revokeBtn = e.target.closest('.btn-revoke-session');
|
|
1510
|
+
if (revokeBtn) {
|
|
1511
|
+
const sessionId = revokeBtn.dataset.sessionId || revokeBtn.closest('tr')?.dataset?.id;
|
|
1512
|
+
if (sessionId) revokeSession(sessionId);
|
|
1513
|
+
}
|
|
1213
1514
|
});
|
|
1214
1515
|
|
|
1215
1516
|
// Toast System
|
package/web/styles.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* ==========================================================================
|
|
2
|
-
FrontTerrain Scout — Design System & Visual Engine v8.0.
|
|
2
|
+
FrontTerrain Scout — Design System & Visual Engine v8.0.7
|
|
3
3
|
Inspired by HappyRobot, Linear.app & Vercel aesthetics
|
|
4
4
|
========================================================================== */
|
|
5
5
|
|
|
@@ -1063,7 +1063,7 @@ h1, h2, h3, h4, h5, h6 {
|
|
|
1063
1063
|
.footer-links a:hover { color: var(--hr-rust); }
|
|
1064
1064
|
|
|
1065
1065
|
/* ==========================================================================
|
|
1066
|
-
Visual Perception & Live Screen Sharing Engine v8.0.
|
|
1066
|
+
Visual Perception & Live Screen Sharing Engine v8.0.7
|
|
1067
1067
|
========================================================================== */
|
|
1068
1068
|
.vision-banner {
|
|
1069
1069
|
margin-bottom: 28px;
|