Skip to content

Latest commit

 

History

History
211 lines (160 loc) · 7.74 KB

File metadata and controls

211 lines (160 loc) · 7.74 KB

AGENTS.md

This file provides guidance to AI coding agents (Codex, etc.) when working with code in this repository.

Project Overview

BonusVarsler is a browser extension and userscript that displays notifications when users visit online stores that offer cashback bonus through various loyalty programs.

Roadmap

  1. Done: Trumf, re:member, DNB support
  2. Next: Add OBOS, SAS EuroBonus support

Architecture

The project uses TypeScript with a modular architecture. Source code is in src/ and bundled to output files.

Source Structure (src/)

src/
├── main.ts                 # Shared initialization logic
├── config/
│   ├── constants.ts        # Storage keys, timeouts, URLs
│   ├── services.ts         # Service definitions and fallbacks
│   └── domain-aliases.ts   # Domain → canonical host mappings
├── core/
│   ├── settings.ts         # User settings management
│   ├── feed.ts             # Feed fetching and caching
│   ├── merchant-matching.ts # Host matching logic
│   └── adblock-detection.ts # Adblock detection
├── platform/
│   ├── extension.ts        # Browser extension entry point
│   └── userscript.ts       # Userscript entry point
├── storage/
│   ├── types.ts            # StorageAdapter interface
│   ├── extension-storage.ts # browser.storage.local adapter
│   └── gm-storage.ts       # GM_getValue/setValue adapter
├── network/
│   ├── types.ts            # FetchAdapter interface
│   ├── extension-fetch.ts  # Extension fetch (via background)
│   └── gm-fetch.ts         # GM_xmlhttpRequest adapter
├── i18n/
│   ├── types.ts            # I18nAdapter interface
│   ├── extension-i18n.ts   # Extension i18n (loads _locales)
│   └── static-i18n.ts      # Static Norwegian strings (userscript)
└── ui/
    ├── views/
    │   ├── notification.ts     # Main notification UI
    │   ├── service-selector.ts # First-run service picker
    │   └── reminder.ts         # Cashback portal reminder
    ├── components/
    │   ├── shadow-host.ts      # Shadow DOM container
    │   ├── draggable.ts        # Corner-snap drag behavior
    │   └── icons.ts            # Base64 icon data
    └── styles/
        ├── index.ts            # Style exports
        ├── notification.css    # Main notification styles
        └── service-selector.css

Output Files (bundled from TypeScript)

  • content.js - Extension content script (bundled from src/platform/extension.ts)
  • BonusVarsler.user.js - Userscript (bundled from src/platform/userscript.ts)
  • Trumf-Bonusvarsler-Lite.user.js - Legacy userscript copy

Other Key Files

  • background.js - Extension service worker for feed fetching (handles CORS)
  • options.html/js/css - Settings page (standalone, not bundled)
  • manifest.json - Extension manifest (Manifest V3)
  • _locales/*/messages.json - i18n translations (6 languages)
  • data/services.json - Canonical service definitions
  • data/sitelist.json - Merchant feed (generated by scraper)
  • data/remember-domains.json - Manual domain mappings for re:member

Build Scripts

  • build.js - Main build script (downloads feeds, checks CSP, bundles TS, creates packages)
  • scripts/bundle.ts - Bun bundler for TypeScript → JS
  • scripts/scrape-feeds.ts - Scrapes merchant data from all services

Platform Adapter Pattern

The codebase uses dependency injection for platform-specific APIs:

interface PlatformAdapters {
  storage: StorageAdapter;      // Persistent storage
  sessionStorage: SessionStorageAdapter; // Session-only storage
  fetcher: FetchAdapter;        // Network requests
  i18n: I18nAdapter;            // Translations
}

Each platform (extension/userscript) provides its own implementations in src/platform/*.ts.

Key Features

  • First-run service selector: New users pick which services to enable
  • Adblock detection: URL fetch + DOM banner checks (skips on CSP-restricted sites)
  • Settings pane: Theme, start minimized, default position, hidden sites
  • Draggable: Drag notification to any corner, position saved per-site
  • i18n: 6 languages (no, en, sv, da, fr, es)
  • Per-site hiding: Permanently hide notifications for specific sites
  • Minimized mode: Collapses to header with cashback badge
  • Reminder notification: Shows on cashback portal pages

Key Implementation Details

  • Shadow DOM isolates styles from host page
  • CSS custom properties (--bg, --text, --accent, etc.) enable theming
  • Theme applied via class on shadow host: tbvl-light, tbvl-dark, tbvl-system
  • Feed cached in browser storage for 48 hours
  • CSP-restricted sites skip URL-based adblock checks

Storage Keys (v1)

Defined in src/config/constants.ts:

  • BonusVarsler_FeedData_v1 / _FeedTime_v1 / _HostIndex_v1 - Cached feed
  • BonusVarsler_HiddenSites - Array of hidden hostnames
  • BonusVarsler_Theme - "light", "dark", or "system"
  • BonusVarsler_StartMinimized - Boolean
  • BonusVarsler_Position - Default position
  • BonusVarsler_SitePositions - Per-site position overrides
  • BonusVarsler_Language - Language code
  • BonusVarsler_EnabledServices - Array of enabled service IDs
  • BonusVarsler_SetupComplete - Boolean (first-run flow)
  • BonusVarsler_SetupShowCount - Number (first-run flow)

Development Workflow

LogBuy API Credentials

scripts/scrape-feeds.ts requires these environment variables for Visma LogBuy scraping:

  • LOGBUY_USERNAME
  • LOGBUY_PASSWORD
  • LOGBUY_ACCESSKEY

Use values from an authorized LogBuy API/extension account. Do not commit them. In GitHub Actions, configure repository secrets with these exact names; .github/workflows/update-feed.yml validates them and passes them to the scraper step.

Building

node build.js

This will:

  1. Run scripts/scrape-feeds.ts to fetch merchant data
  2. Check CSP on merchant sites (cached for 24h)
  3. Update CSP_RESTRICTED_SITES in source files
  4. Bundle TypeScript via scripts/bundle.ts
  5. Create dist/bonusvarsler-X.X.X.xpi and dist/bonusvarsler-X.X.X-chrome.zip

Scraper Cache

The scraper caches results for 5 hours in .scraper-cache.json. Delete the cache file to force a fresh scrape.

Adding a New Service

Service Types

  • Tracking-based (like Trumf, re:member): User clicks through to service site
  • Code-based (like DNB): User gets a rebate code to enter at checkout

Files to Modify

  1. data/services.json - Add service definition:

    "serviceid": {
      "id": "serviceid",
      "name": "Service Name",
      "clickthroughUrl": "https://...",
      "reminderDomain": "service.no",
      "color": "#HEXCOLOR",
      "defaultEnabled": false,
      "type": "code"
    }
  2. src/config/services.ts - Add to SERVICES_FALLBACK and SERVICE_ORDER

  3. options.js - Add to SERVICES_FALLBACK (~line 7)

  4. _locales/*/messages.json - Add service-specific i18n strings (all 6 locales)

  5. scripts/scrape-feeds.ts - Add scraping function for the new service

Creating Service Icon (Hue-Shifted)

# Adjust hue value (0-200) to match service color
convert icon-64.png -modulate 100,100,HUE icon-64-serviceid.png

Then add base64 data to src/ui/components/icons.ts.

Code-Based Service Specifics

For code-based services (type: "code"):

  • Button shows the rebate code instead of "Get X bonus"
  • First click copies code, second click opens link
  • Skip adblock detection (codes work regardless)
  • Add service-specific checklist instructions

Language

The userscript UI is in Norwegian. The extension supports multiple languages via _locales/. Use Norwegian for the primary language.