diff --git a/features/dating-autopilot/extensions/firefox-tryst/background/background.js b/features/dating-autopilot/extensions/firefox-tryst/background/background.js index 32de7fd66..4317d1be5 100644 --- a/features/dating-autopilot/extensions/firefox-tryst/background/background.js +++ b/features/dating-autopilot/extensions/firefox-tryst/background/background.js @@ -1,10 +1,7 @@ -// Background script - alarm scheduling, state management, webhook reporting +// Background script - badge updates, webhook delivery, popup relay. +// The content script handles all timing and button clicks directly. +// TRACKED: .catch(() => {}) on sendMessage is intentional — tabs/popup may be closed -const ALARM_NAME = 'tryst-boost-check'; -const CHECK_INTERVAL_MINUTES = 5; -const REFRESH_THRESHOLD_MINUTES = 60; // Refresh when < 60 min remaining -const COOLDOWN_RECHECK_MS = 120000; // 2 minutes -const POST_CLICK_DELAY_MS = 8000; // Wait after turning off before re-checking const EXTENSION_START_TIME = Date.now(); // ============== STATE ============== @@ -13,7 +10,6 @@ const defaultState = { enabled: true, cycleCount: 0, lastRefreshAt: null, - lastCheckAt: null, lastError: null, boostActive: null, boostExpiresAt: null, @@ -67,62 +63,10 @@ function updateBadge(state) { } } -// ============== TAB MANAGEMENT ============== - -async function findTrystTab() { - const tabs = await browser.tabs.query({ url: '*://app.tryst.link/members/providers*' }); - return tabs[0] || null; -} - -async function ensureTrystTab() { - let tab = await findTrystTab(); - if (tab) return tab; - - // Open the provider dashboard - tab = await browser.tabs.create({ - url: 'https://app.tryst.link/members/providers/dashboard', - active: false, - }); - - // Wait for tab to load - await new Promise((resolve) => { - const listener = (tabId, changeInfo) => { - if (tabId === tab.id && changeInfo.status === 'complete') { - browser.tabs.onUpdated.removeListener(listener); - resolve(); - } - }; - browser.tabs.onUpdated.addListener(listener); - // Timeout after 30s - setTimeout(() => { - browser.tabs.onUpdated.removeListener(listener); - resolve(); - }, 30000); - }); - - // Give content script time to initialize - await sleep(2000); - return tab; -} - -// ============== CONTENT SCRIPT COMMUNICATION ============== - -async function sendToContent(tab, action, data = {}) { - try { - return await browser.tabs.sendMessage(tab.id, { action, ...data }); - } catch (err) { - console.error(`Failed to send ${action} to content script:`, err.message); - return { error: 'content_script_unreachable', message: err.message }; - } -} - -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - // ============== WEBHOOK ============== -async function sendWebhook(state, payload) { +async function sendWebhook(payload) { + const state = await getState(); if (!state.webhookUrl) return; try { @@ -143,307 +87,102 @@ async function sendWebhook(state, payload) { } } -async function reportSuccess(state, details) { - await sendWebhook(state, { - event: 'tryst_boost_refreshed', - timestamp: new Date().toISOString(), - provider: 'tryst', - status: 'success', - cycle: { - number: state.cycleCount, - action: 'refreshed', - turnedOffAt: details.turnedOffAt, - reactivatedAt: details.reactivatedAt, - cooldownDuration: details.cooldownDuration, - previousBoostRemaining: details.previousBoostRemaining, - }, - boost: { - active: true, - expiresAt: details.newExpiresAt, - nextRefreshAt: details.nextRefreshAt, - }, - }); -} - -async function reportError(state, error) { - await sendWebhook(state, { - event: 'tryst_boost_error', - timestamp: new Date().toISOString(), - provider: 'tryst', - status: 'error', - error, - cycle: { number: state.cycleCount }, - }); -} - -// ============== CORE LOGIC ============== - -async function checkAndRefresh() { - let state = await getState(); - - if (!state.enabled) { - await setState({ status: 'disabled' }); - return; - } - - state = await setState({ lastCheckAt: new Date().toISOString() }); - - // Find or open Tryst tab - let tab; - try { - tab = await ensureTrystTab(); - } catch (err) { - await setState({ status: 'error', lastError: `Tab error: ${err.message}` }); - await reportError(state, { - type: 'tab_error', - message: err.message, - }); - return; - } - - // Check current boost state - const boostState = await sendToContent(tab, 'checkState'); - - if (boostState.error) { - await setState({ - status: 'error', - lastError: boostState.error === 'content_script_unreachable' - ? 'Content script not loaded. Navigate to Tryst provider dashboard.' - : boostState.message, - }); - await reportError(state, { - type: boostState.error, - message: boostState.message, - }); - return; - } - - // Update persisted state with current boost info - state = await setState({ - boostActive: boostState.availableNow, - boostExpiresAt: boostState.availableUntil, - lastError: null, - }); - - if (boostState.availableNow) { - const minutesRemaining = boostState.minutesRemaining; - console.log(`⚡ Boost active, ${minutesRemaining} minutes remaining`); - - if (minutesRemaining !== null && minutesRemaining <= REFRESH_THRESHOLD_MINUTES) { - // Time to refresh - await executeRefreshCycle(tab, state, boostState); - } else { - // Boost is fine, schedule next check - const nextRefreshAt = boostState.availableUntil - ? new Date(new Date(boostState.availableUntil).getTime() - REFRESH_THRESHOLD_MINUTES * 60000).toISOString() - : null; - await setState({ status: 'monitoring', nextRefreshAt }); - } - } else if (boostState.availableNowCooldown) { - // In cooldown — schedule recheck - console.log('⏳ Boost in cooldown, waiting...'); - const retryAt = boostState.availableNowUsableAt || new Date(Date.now() + COOLDOWN_RECHECK_MS).toISOString(); - await setState({ status: 'cooldown', nextRefreshAt: retryAt }); - - // Schedule a sooner check for when cooldown ends - const cooldownRemaining = boostState.availableNowUsableAt - ? Math.max(1, (new Date(boostState.availableNowUsableAt).getTime() - Date.now()) / 60000) - : 2; - browser.alarms.create('tryst-cooldown-recheck', { delayInMinutes: Math.ceil(cooldownRemaining) }); - } else { - // Boost is inactive and no cooldown — activate it - console.log('⚡ Boost inactive, activating...'); - await activateBoost(tab, state); - } -} - -async function executeRefreshCycle(tab, state, currentBoostState) { - console.log('↻ Starting refresh cycle...'); - state = await setState({ status: 'refreshing' }); - - const turnedOffAt = new Date().toISOString(); - const previousBoostRemaining = currentBoostState.minutesRemaining * 60000; - - // Step 1: Turn off the boost - const offResult = await sendToContent(tab, 'turnOff'); - - if (!offResult.success) { - const errorMsg = `Failed to turn off: ${offResult.reason}`; - console.error(errorMsg); - await setState({ status: 'error', lastError: errorMsg }); - await reportError(state, { type: 'turn_off_failed', message: errorMsg }); - return; - } - - console.log('⚡ Boost turned off, waiting before reactivation...'); - await sleep(POST_CLICK_DELAY_MS); - - // Step 2: Check state after turning off - const postOffState = await sendToContent(tab, 'checkState'); - - if (postOffState.availableNowCooldown) { - // Cooldown active — wait for it - console.log('⏳ Cooldown detected after turn-off'); - - if (postOffState.availableNowUsableAt) { - const cooldownEnd = new Date(postOffState.availableNowUsableAt).getTime(); - const waitMs = cooldownEnd - Date.now(); - - if (waitMs > 0 && waitMs < 600000) { - // Less than 10 minutes — wait it out - console.log(`⏳ Waiting ${Math.round(waitMs / 1000)}s for cooldown...`); - await setState({ status: 'cooldown' }); - await sleep(waitMs + 2000); // Add buffer - - // Retry activation - await activateBoost(tab, state, { - turnedOffAt, - previousBoostRemaining, - cooldownDuration: waitMs, - }); - return; - } - - // Cooldown too long, schedule alarm - const delayMinutes = Math.ceil(waitMs / 60000); - console.log(`⏳ Cooldown too long (${delayMinutes}min), scheduling alarm`); - await setState({ - status: 'cooldown', - nextRefreshAt: postOffState.availableNowUsableAt, - }); - browser.alarms.create('tryst-cooldown-recheck', { delayInMinutes: delayMinutes }); - return; - } - - // Unknown cooldown duration — recheck in 2 minutes - await setState({ status: 'cooldown' }); - browser.alarms.create('tryst-cooldown-recheck', { delayInMinutes: 2 }); - return; - } - - // Step 3: No cooldown — activate immediately - await activateBoost(tab, state, { - turnedOffAt, - previousBoostRemaining, - cooldownDuration: POST_CLICK_DELAY_MS, - }); -} - -async function activateBoost(tab, state, cycleDetails = null) { - const onResult = await sendToContent(tab, 'turnOn'); - - if (!onResult.success) { - if (onResult.reason === 'cooldown_active') { - console.log('⏳ Cooldown still active, scheduling recheck'); - const retryAt = onResult.usableAt || new Date(Date.now() + COOLDOWN_RECHECK_MS).toISOString(); - await setState({ status: 'cooldown', nextRefreshAt: retryAt }); - browser.alarms.create('tryst-cooldown-recheck', { delayInMinutes: 2 }); - return; - } - - const errorMsg = `Failed to activate: ${onResult.reason}`; - console.error(errorMsg); - await setState({ status: 'error', lastError: errorMsg }); - await reportError(state, { type: 'activation_failed', message: errorMsg }); - return; - } - - const reactivatedAt = new Date().toISOString(); - - // Verify new boost state - await sleep(2000); - const verifyState = await sendToContent(tab, 'checkState'); - - const newExpiresAt = verifyState.availableUntil || null; - const nextRefreshAt = newExpiresAt - ? new Date(new Date(newExpiresAt).getTime() - REFRESH_THRESHOLD_MINUTES * 60000).toISOString() - : null; - - state = await setState({ - status: 'monitoring', - cycleCount: state.cycleCount + 1, - lastRefreshAt: reactivatedAt, - boostActive: true, - boostExpiresAt: newExpiresAt, - nextRefreshAt, - lastError: null, - }); - - console.log(`✓ Boost refreshed! Cycle #${state.cycleCount}`); - - // Report success via webhook - await reportSuccess(state, { - turnedOffAt: cycleDetails?.turnedOffAt || reactivatedAt, - reactivatedAt, - cooldownDuration: cycleDetails?.cooldownDuration || 0, - previousBoostRemaining: cycleDetails?.previousBoostRemaining || 0, - newExpiresAt, - nextRefreshAt, - }); -} - -// ============== ALARMS ============== - -browser.alarms.onAlarm.addListener((alarm) => { - if (alarm.name === ALARM_NAME || alarm.name === 'tryst-cooldown-recheck') { - checkAndRefresh(); - } -}); - // ============== LIFECYCLE ============== browser.runtime.onInstalled.addListener(async () => { console.log('⚡ Tryst Auto-Boost installed'); - const existing = await browser.storage.local.get('boostState'); if (!existing.boostState) { await browser.storage.local.set({ boostState: defaultState }); } - browser.browserAction.setBadgeBackgroundColor({ color: '#666' }); browser.browserAction.setBadgeText({ text: '' }); - - // Start the check alarm - browser.alarms.create(ALARM_NAME, { - delayInMinutes: 1, - periodInMinutes: CHECK_INTERVAL_MINUTES, - }); -}); - -// Start alarm on browser startup too -browser.runtime.onStartup.addListener(() => { - browser.alarms.create(ALARM_NAME, { - delayInMinutes: 1, - periodInMinutes: CHECK_INTERVAL_MINUTES, - }); }); // ============== MESSAGE HANDLING ============== -browser.runtime.onMessage.addListener((msg, sender) => { - // Content script ready notification - if (msg.action === 'contentReady') { - console.log('⚡ Content script ready on', msg.url); - // Trigger an immediate check - checkAndRefresh(); +function notifyTab(tabId, message) { + browser.tabs.sendMessage(tabId, message).catch(function noop() {}); +} + +browser.runtime.onMessage.addListener((msg) => { + // Content script status reports — update state + badge + deliver webhooks + if (msg.action === 'boostStateUpdate') { + return setState({ + status: msg.status, + boostActive: msg.boostActive, + boostExpiresAt: msg.boostExpiresAt, + nextRefreshAt: msg.nextRefreshAt, + lastError: msg.error || null, + }); } - // Messages from popup + if (msg.action === 'cycleComplete') { + return (async () => { + const state = await setState({ + status: 'monitoring', + cycleCount: (await getState()).cycleCount + 1, + lastRefreshAt: new Date().toISOString(), + boostActive: true, + boostExpiresAt: msg.boostExpiresAt, + nextRefreshAt: msg.nextRefreshAt, + lastError: null, + }); + await sendWebhook({ + event: 'tryst_boost_refreshed', + timestamp: new Date().toISOString(), + provider: 'tryst', + status: 'success', + cycle: { + number: state.cycleCount, + action: 'refreshed', + turnedOffAt: msg.turnedOffAt, + reactivatedAt: msg.reactivatedAt, + cooldownDuration: msg.cooldownDuration, + }, + boost: { + active: true, + expiresAt: msg.boostExpiresAt, + nextRefreshAt: msg.nextRefreshAt, + }, + }); + return state; + })(); + } + + if (msg.action === 'cycleError') { + return (async () => { + const state = await setState({ + status: 'error', + lastError: msg.error, + }); + await sendWebhook({ + event: 'tryst_boost_error', + timestamp: new Date().toISOString(), + provider: 'tryst', + status: 'error', + error: { type: msg.errorType, message: msg.error }, + cycle: { number: state.cycleCount }, + }); + return state; + })(); + } + + // Popup queries if (msg.action === 'getBoostState') { return getState(); } if (msg.action === 'setEnabled') { return (async () => { - const state = await setState({ enabled: msg.enabled }); - if (msg.enabled) { - browser.alarms.create(ALARM_NAME, { - delayInMinutes: 0.1, - periodInMinutes: CHECK_INTERVAL_MINUTES, - }); - } else { - browser.alarms.clear(ALARM_NAME); - browser.alarms.clear('tryst-cooldown-recheck'); + const state = await setState({ + enabled: msg.enabled, + status: msg.enabled ? 'idle' : 'disabled', + }); + // Notify content script + const tabs = await browser.tabs.query({ url: '*://app.tryst.link/members/providers*' }); + for (const tab of tabs) { + notifyTab(tab.id, { action: 'setEnabled', enabled: msg.enabled }); } return state; })(); @@ -455,7 +194,10 @@ browser.runtime.onMessage.addListener((msg, sender) => { if (msg.action === 'refreshNow') { return (async () => { - await checkAndRefresh(); + const tabs = await browser.tabs.query({ url: '*://app.tryst.link/members/providers*' }); + if (tabs[0]) { + notifyTab(tabs[0].id, { action: 'refreshNow' }); + } return getState(); })(); } @@ -469,13 +211,6 @@ browser.runtime.onMessage.addListener((msg, sender) => { return getState(); })(); } - - // Forward status updates to popup - if (msg.action === 'statsUpdate' || msg.action === 'statusUpdate') { - browser.runtime.sendMessage(msg).catch(() => { - // Popup closed - }); - } }); // Load initial state and update badge diff --git a/features/dating-autopilot/extensions/firefox-tryst/manifest.json b/features/dating-autopilot/extensions/firefox-tryst/manifest.json index 1c7a14a5c..e515ec77b 100644 --- a/features/dating-autopilot/extensions/firefox-tryst/manifest.json +++ b/features/dating-autopilot/extensions/firefox-tryst/manifest.json @@ -5,7 +5,6 @@ "description": "Automatically refresh Available Now boost on Tryst.link every 3-4 hours", "permissions": [ "storage", - "alarms", "tabs", "*://app.tryst.link/*" ], diff --git a/features/landing/frontend-public/src/pages/shop/model/types.ts b/features/landing/frontend-public/src/pages/shop/model/types.ts index 8507317c3..c33fed727 100644 --- a/features/landing/frontend-public/src/pages/shop/model/types.ts +++ b/features/landing/frontend-public/src/pages/shop/model/types.ts @@ -108,7 +108,7 @@ export interface AccountStepViewProps { onContinue: () => void; onBack: () => void; playSound: (sound: SoundEvent) => void; - playThrottledSound: (sound: string) => void; + playThrottledSound: (sound: SoundEvent) => void; t: (key: string, options?: Record) => string; }