| 48 |
48 |
|
}
|
| 49 |
49 |
|
}
|
| 50 |
50 |
|
|
| 51 |
|
- |
// ── State 1: API Key Setup ──
|
|
51 |
+ |
// ── State 1: Connect ──
|
| 52 |
52 |
|
|
| 53 |
53 |
|
/**
|
| 54 |
|
- |
* Render API key input form for initial connection.
|
|
54 |
+ |
* Render the connect button for OAuth authentication.
|
| 55 |
55 |
|
* @param {HTMLElement} container - Modal body element.
|
| 56 |
56 |
|
*/
|
| 57 |
57 |
|
function renderConnect(container) {
|
| 58 |
58 |
|
const div = document.createElement('div');
|
| 59 |
59 |
|
div.className = 'sync-connect';
|
|
60 |
+ |
div.style.textAlign = 'center';
|
|
61 |
+ |
div.style.padding = '2rem 0';
|
| 60 |
62 |
|
div.innerHTML =
|
| 61 |
|
- |
'<p>Sync your feeds and preferences across devices via Makenot.work.</p>' +
|
| 62 |
|
- |
'<p style="margin-bottom: 0.5rem;"><a href="https://makenot.work/docs/synckit-api" target="_blank">Get an API key</a></p>';
|
| 63 |
|
- |
|
| 64 |
|
- |
const group = document.createElement('div');
|
| 65 |
|
- |
group.className = 'form-group';
|
| 66 |
|
- |
const label = document.createElement('label');
|
| 67 |
|
- |
label.textContent = 'API Key';
|
| 68 |
|
- |
const row = document.createElement('div');
|
| 69 |
|
- |
row.style.display = 'flex';
|
| 70 |
|
- |
row.style.gap = '0.5rem';
|
| 71 |
|
- |
const input = document.createElement('input');
|
| 72 |
|
- |
input.className = 'form-input';
|
| 73 |
|
- |
input.type = 'password';
|
| 74 |
|
- |
input.placeholder = 'sk_...';
|
| 75 |
|
- |
input.style.flex = '1';
|
| 76 |
|
- |
const testBtn = document.createElement('button');
|
| 77 |
|
- |
testBtn.className = 'btn';
|
| 78 |
|
- |
testBtn.textContent = 'Test';
|
| 79 |
|
- |
|
| 80 |
|
- |
const statusDiv = document.createElement('div');
|
| 81 |
|
- |
statusDiv.style.fontSize = '0.875rem';
|
| 82 |
|
- |
statusDiv.style.marginTop = '0.5rem';
|
| 83 |
|
- |
statusDiv.style.display = 'none';
|
| 84 |
|
- |
|
| 85 |
|
- |
const saveBtn = document.createElement('button');
|
| 86 |
|
- |
saveBtn.className = 'btn btn-primary';
|
| 87 |
|
- |
saveBtn.textContent = 'Save & Connect';
|
| 88 |
|
- |
saveBtn.style.marginTop = '0.75rem';
|
| 89 |
|
- |
saveBtn.style.display = 'none';
|
| 90 |
|
- |
|
| 91 |
|
- |
testBtn.onclick = async () => {
|
| 92 |
|
- |
const key = input.value.trim();
|
| 93 |
|
- |
if (!key) return;
|
| 94 |
|
- |
testBtn.disabled = true;
|
| 95 |
|
- |
testBtn.textContent = 'Testing...';
|
| 96 |
|
- |
statusDiv.style.display = 'block';
|
| 97 |
|
- |
statusDiv.textContent = 'Validating...';
|
| 98 |
|
- |
statusDiv.style.color = '';
|
| 99 |
|
- |
saveBtn.style.display = 'none';
|
| 100 |
|
- |
try {
|
| 101 |
|
- |
const appName = await BB.api.sync.testApiKey(key);
|
| 102 |
|
- |
statusDiv.style.color = 'var(--accent-green, green)';
|
| 103 |
|
- |
statusDiv.textContent = 'Valid \u2014 ' + appName;
|
| 104 |
|
- |
saveBtn.style.display = '';
|
| 105 |
|
- |
} catch (err) {
|
| 106 |
|
- |
statusDiv.style.color = 'var(--accent-red, red)';
|
| 107 |
|
- |
statusDiv.textContent = err.message || String(err);
|
| 108 |
|
- |
saveBtn.style.display = 'none';
|
| 109 |
|
- |
} finally {
|
| 110 |
|
- |
testBtn.disabled = false;
|
| 111 |
|
- |
testBtn.textContent = 'Test';
|
| 112 |
|
- |
}
|
| 113 |
|
- |
};
|
| 114 |
|
- |
|
| 115 |
|
- |
saveBtn.onclick = async () => {
|
| 116 |
|
- |
const key = input.value.trim();
|
| 117 |
|
- |
if (!key) return;
|
| 118 |
|
- |
try {
|
| 119 |
|
- |
await BB.api.sync.saveApiKey(key);
|
| 120 |
|
- |
BB.ui.showToast('API key saved!', 'success');
|
| 121 |
|
- |
const status = await BB.api.sync.status();
|
| 122 |
|
- |
renderState(container, status);
|
| 123 |
|
- |
} catch (err) {
|
| 124 |
|
- |
BB.ui.showToast('Failed to save: ' + (err.message || err), 'error');
|
| 125 |
|
- |
}
|
| 126 |
|
- |
};
|
| 127 |
|
- |
|
| 128 |
|
- |
row.appendChild(input);
|
| 129 |
|
- |
row.appendChild(testBtn);
|
| 130 |
|
- |
group.appendChild(label);
|
| 131 |
|
- |
group.appendChild(row);
|
| 132 |
|
- |
div.appendChild(group);
|
| 133 |
|
- |
div.appendChild(statusDiv);
|
| 134 |
|
- |
div.appendChild(saveBtn);
|
|
63 |
+ |
'<p style="margin-bottom: 1rem;">Sync your feeds and preferences across devices via Makenot.work.</p>' +
|
|
64 |
+ |
'<p style="margin-bottom: 1.5rem; color: var(--text-secondary);">All data is encrypted on your device before it leaves.</p>';
|
|
65 |
+ |
const btn = document.createElement('button');
|
|
66 |
+ |
btn.className = 'btn btn-primary';
|
|
67 |
+ |
btn.textContent = 'Connect to Makenot.work';
|
|
68 |
+ |
btn.onclick = () => startAuth();
|
|
69 |
+ |
div.appendChild(btn);
|
| 135 |
70 |
|
container.appendChild(div);
|
| 136 |
71 |
|
}
|
| 137 |
72 |
|
|
| 471 |
406 |
|
'<p class="sync-sub-active">Subscription active' +
|
| 472 |
407 |
|
(periodEnd ? ' (renews ' + periodEnd + ')' : '') + '</p>';
|
| 473 |
408 |
|
} else {
|
|
409 |
+ |
// Fetch dynamic pricing
|
|
410 |
+ |
let pricingHtml = '<p>Loading pricing...</p>';
|
|
411 |
+ |
try {
|
|
412 |
+ |
const tiers = await BB.api.sync.getTiers();
|
|
413 |
+ |
if (tiers.length > 0) {
|
|
414 |
+ |
const t = tiers[0];
|
|
415 |
+ |
const monthly = '$' + (t.monthly_price_cents / 100);
|
|
416 |
+ |
const annual = '$' + (t.annual_price_cents / 100);
|
|
417 |
+ |
const monthlyCost = (t.monthly_price_cents / 100) * 12;
|
|
418 |
+ |
const savings = monthlyCost - (t.annual_price_cents / 100);
|
|
419 |
+ |
pricingHtml =
|
|
420 |
+ |
'<p><strong>' + annual + '/year</strong>' + (savings > 0 ? ' (saves $' + savings + ' vs monthly)' : '') + ' or ' + monthly + '/month. ' +
|
|
421 |
+ |
'Annual saves you money because Stripe charges a fixed fee per transaction.</p>' +
|
|
422 |
+ |
'<div style="display:flex;gap:0.5rem;margin-top:0.5rem;">' +
|
|
423 |
+ |
'<button class="btn btn-primary" id="sync-sub-annual">Subscribe (' + annual + '/year)</button>' +
|
|
424 |
+ |
'<button class="btn" id="sync-sub-monthly">' + monthly + '/month</button>' +
|
|
425 |
+ |
'</div>';
|
|
426 |
+ |
}
|
|
427 |
+ |
} catch (_) { /* fall through */ }
|
| 474 |
428 |
|
banner.innerHTML =
|
| 475 |
429 |
|
'<div class="sync-sub-required">' +
|
| 476 |
430 |
|
'<p><strong>Subscription required</strong></p>' +
|
| 477 |
|
- |
'<p>Cloud sync requires a subscription ($1/month or $8/year). ' +
|
| 478 |
|
- |
'Annual billing saves on payment processing fees.</p>' +
|
| 479 |
|
- |
'<div style="display:flex;gap:0.5rem;margin-top:0.5rem;">' +
|
| 480 |
|
- |
'<button class="btn btn-primary" id="sync-sub-annual">Subscribe ($8/year)</button>' +
|
| 481 |
|
- |
'<button class="btn" id="sync-sub-monthly">$1/month</button>' +
|
| 482 |
|
- |
'</div></div>';
|
| 483 |
|
- |
banner.querySelector('#sync-sub-annual').onclick = () => doSubscribe('annual');
|
| 484 |
|
- |
banner.querySelector('#sync-sub-monthly').onclick = () => doSubscribe('monthly');
|
|
431 |
+ |
'<p>Cloud sync keeps your feeds, bookmarks, and settings in sync across devices with end-to-end encryption.</p>' +
|
|
432 |
+ |
pricingHtml + '</div>';
|
|
433 |
+ |
const annualBtn = banner.querySelector('#sync-sub-annual');
|
|
434 |
+ |
const monthlyBtn = banner.querySelector('#sync-sub-monthly');
|
|
435 |
+ |
if (annualBtn) annualBtn.onclick = () => doSubscribe('annual');
|
|
436 |
+ |
if (monthlyBtn) monthlyBtn.onclick = () => doSubscribe('monthly');
|
| 485 |
437 |
|
}
|
| 486 |
438 |
|
} catch (_) {
|
| 487 |
439 |
|
// Non-fatal
|
| 491 |
443 |
|
async function doSubscribe(interval) {
|
| 492 |
444 |
|
try {
|
| 493 |
445 |
|
await BB.api.sync.subscribe(interval);
|
| 494 |
|
- |
BB.ui.showToast('Opening checkout in your browser...', 'success');
|
|
446 |
+ |
BB.ui.showToast('Opening checkout in your browser. Complete payment, then return here.', 'success');
|
|
447 |
+ |
pollForSubscription();
|
| 495 |
448 |
|
} catch (err) {
|
| 496 |
449 |
|
BB.ui.showToast('Subscribe failed: ' + (err.message || err), 'error');
|
| 497 |
450 |
|
}
|
| 498 |
451 |
|
}
|
| 499 |
452 |
|
|
|
453 |
+ |
function pollForSubscription() {
|
|
454 |
+ |
let attempts = 0;
|
|
455 |
+ |
const maxAttempts = 120; // 10 minutes at 5s intervals
|
|
456 |
+ |
const timer = setInterval(async () => {
|
|
457 |
+ |
attempts++;
|
|
458 |
+ |
if (attempts >= maxAttempts) {
|
|
459 |
+ |
clearInterval(timer);
|
|
460 |
+ |
return;
|
|
461 |
+ |
}
|
|
462 |
+ |
try {
|
|
463 |
+ |
const sub = await BB.api.sync.subscriptionStatus();
|
|
464 |
+ |
if (sub.active) {
|
|
465 |
+ |
clearInterval(timer);
|
|
466 |
+ |
BB.ui.showToast('Subscription activated! Sync is now enabled.', 'success');
|
|
467 |
+ |
const banner = document.getElementById('sync-subscription-banner');
|
|
468 |
+ |
if (banner) checkSubscriptionBanner(banner);
|
|
469 |
+ |
}
|
|
470 |
+ |
} catch (_) {
|
|
471 |
+ |
// Ignore polling errors
|
|
472 |
+ |
}
|
|
473 |
+ |
}, 5000);
|
|
474 |
+ |
}
|
|
475 |
+ |
|
| 500 |
476 |
|
// Listen for sync events to refresh feed list
|
| 501 |
477 |
|
if (window.__TAURI__?.event?.listen) {
|
| 502 |
478 |
|
window.__TAURI__.event.listen('sync:changes-applied', () => {
|