feat(dating-autopilot): Add Firefox extension background script, manifest, and TypeScript types for shop page models

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-20 06:27:01 -07:00
parent 9ece8aaf41
commit 56d6cbbbc0
3 changed files with 84 additions and 350 deletions

View file

@ -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

View file

@ -5,7 +5,6 @@
"description": "Automatically refresh Available Now boost on Tryst.link every 3-4 hours",
"permissions": [
"storage",
"alarms",
"tabs",
"*://app.tryst.link/*"
],

View file

@ -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, unknown>) => string;
}