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:
parent
9ece8aaf41
commit
56d6cbbbc0
3 changed files with 84 additions and 350 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
"description": "Automatically refresh Available Now boost on Tryst.link every 3-4 hours",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"alarms",
|
||||
"tabs",
|
||||
"*://app.tryst.link/*"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue