BMS Icon

Chrome Extension Setup

Install the BMS Rate Capture extension to capture rates from Acre with one click

v1.6 — Smarter rate capture & reliable auto-parse! The extension now targets rate tables specifically (instead of grabbing the whole page), captures up to 50,000 characters (~150+ deals), and retries data injection so rates parse and save automatically without clicking any buttons.

You MUST re-download ALL 5 files (manifest.json, popup.html, popup.js, background.js, icon.png) and reload the extension in chrome://extensions.
Installation Instructions
  1. 1Download all 4 files below into a single folder (e.g. bms-rate-capture).
  2. 2Open Chrome and go to chrome://extensions
  3. 3Enable "Developer mode" (toggle in the top-right corner)
  4. 4Click "Load unpacked" and select the folder where you saved the files
  5. 5Pin the extension to your toolbar. When on an Acre rate page, click the extension button and hit "Capture Rates".
  6. 6BMS will open in a new tab, parse the rates, and save them automatically — no further action needed.
Note: The app URL is set to https://bespokemortgagesolutions.com. If your app URL changes, re-download the popup.js file from this page.
manifest.json
{
  "manifest_version": 3,
  "name": "BMS Rate Capture",
  "version": "1.6",
  "description": "Capture mortgage rates from Acre and save to BMS automatically",
  "permissions": [
    "activeTab",
    "scripting",
    "storage"
  ],
  "host_permissions": [
    "https://*.acre-platform.com/*",
    "https://acre-platform.com/*",
    "https://*.myac.re/*",
    "https://myac.re/*",
    "https://bespokemortgagesolutions.com/*"
  ],
  "action": {
    "default_popup": "popup.html",
    "default_title": "Capture Rates",
    "default_icon": {
      "16": "icon.png",
      "48": "icon.png",
      "128": "icon.png"
    }
  },
  "icons": {
    "16": "icon.png",
    "48": "icon.png",
    "128": "icon.png"
  },
  "background": {
    "service_worker": "background.js"
  }
}
popup.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body { width: 260px; padding: 16px; font-family: -apple-system, sans-serif; background: #1A2626; margin: 0; }
    .logo { display: flex; align-items: center; gap: 9px; margin-bottom: 14px; }
    .logo-icon { width: 32px; height: 32px; background: #083032; border-radius: 7px; display: flex; align-items: center; justify-content: center; font-weight: bold; color: white; font-size: 11px; }
    h2 { font-size: 16px; margin: 0; color: #ffffff; font-weight: 700; }
    p { font-size: 11px; color: #a8b5b5; margin: 0 0 14px; line-height: 1.4; }
    .btn-group { display: flex; flex-direction: column; gap: 9px; }
    .btn { width: 100%; padding: 13px; border: none; border-radius: 9px; font-size: 15px; font-weight: 700; cursor: pointer; transition: opacity 0.15s; color: white; }
    .btn:hover { opacity: 0.88; }
    .btn-resi { background: #66A998; }
    .btn-btl { background: #D9A948; }
    .btn-ltd { background: #8B5CF6; }
    .btn:disabled { opacity: 0.5; cursor: default; }
    #status { margin-top: 12px; font-size: 11px; color: #a8b5b5; text-align: center; }
    .success { color: #4ade80; }
    .error { color: #f87171; }
  </style>
</head>
<body>
  <div class="logo">
    <div class="logo-icon">BMS</div>
    <h2>Rate Capture</h2>
  </div>
  <p>Click a product type to capture rates from the current Acre page.</p>
  <div class="btn-group">
    <button class="btn btn-resi" data-ptype="Residential">Resi</button>
    <button class="btn btn-btl" data-ptype="Buy to Let">BTL</button>
    <button class="btn btn-ltd" data-ptype="LTD Buy To Let">LTD</button>
  </div>
  <div id="status"></div>
  <script src="popup.js"></script>
</body>
</html>
popup.js
const MAX_CHARS = 50000;

document.querySelectorAll('.btn').forEach(btn => {
  btn.addEventListener('click', async () => {
    const productType = btn.dataset.ptype;
    const status = document.getElementById('status');
    const allBtns = document.querySelectorAll('.btn');
    allBtns.forEach(b => b.disabled = true);
    status.textContent = 'Reading page...';
    status.className = '';

    try {
      const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });

      if (!tab.url.includes('acre') && !tab.url.includes('myac')) {
        status.textContent = 'Not on an Acre page. Open Acre first.';
        status.className = 'error';
        allBtns.forEach(b => b.disabled = false);
        return;
      }

      const results = await chrome.scripting.executeScript({
        target: { tabId: tab.id },
        function: () => {
          // Try to find rate tables specifically — avoids capturing calculations, headers, etc.
          const tables = document.querySelectorAll('table');
          if (tables.length > 0) {
            let content = '';
            tables.forEach(t => {
              t.querySelectorAll('tr').forEach(tr => {
                const cells = Array.from(tr.querySelectorAll('td, th')).map(c => c.textContent.trim());
                if (cells.length > 0) content += cells.join(' | ') + '\n';
              });
              content += '\n';
            });
            if (content.length > 200) return content;
          }
          // Fallback: try main content area
          const main = document.querySelector('main, [role="main"], #main-content, .main-content');
          if (main && main.innerText.length > 200) return main.innerText;
          // Last resort: full page text
          return document.body.innerText;
        }
      });

      let pageContent = results[0].result || '';
      if (pageContent.length < 50) {
        status.textContent = 'Could not read page content.';
        status.className = 'error';
        allBtns.forEach(b => b.disabled = false);
        return;
      }

      // Truncate to first 50,000 characters (~150+ deals) for parsing
      if (pageContent.length > MAX_CHARS) {
        pageContent = pageContent.substring(0, MAX_CHARS);
      }

      const appUrl = 'https://bespokemortgagesolutions.com';
      const captureUrl = appUrl + '/RateCapture#fromExtension=1&autoSave=1';
      const newTab = await chrome.tabs.create({ url: captureUrl });

      // Send data to background service worker for injection
      chrome.runtime.sendMessage({
        type: 'injectRateData',
        tabId: newTab.id,
        data: { content: pageContent, product_type: productType }
      });

      status.textContent = 'Capturing & saving...';
      status.className = 'success';
      setTimeout(() => window.close(), 3000);
    } catch (err) {
      status.textContent = 'Error: ' + err.message;
      status.className = 'error';
      allBtns.forEach(b => b.disabled = false);
    }
  });
});
background.js
// Background service worker — handles data injection after popup closes
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'injectRateData') {
    const tabId = message.tabId;
    const data = message.data;

    function inject(attempt) {
      chrome.scripting.executeScript({
        target: { tabId: tabId },
        function: (d) => {
          sessionStorage.setItem('rateCaptureData', JSON.stringify(d));
          window.dispatchEvent(new CustomEvent('rateCaptureDataReady'));
        },
        args: [data]
      }).then(() => {
        // Retry once after 2s in case the React app wasn't ready yet
        if (attempt === 0) {
          setTimeout(() => inject(1), 2000);
        }
      }).catch(() => {
        if (attempt === 0) {
          setTimeout(() => inject(1), 2000);
        }
      });
    }

    chrome.tabs.get(tabId, (tab) => {
      if (tab && tab.status === 'complete') {
        inject(0);
      } else {
        chrome.tabs.onUpdated.addListener(function listener(tid, info) {
          if (tid === tabId && info.status === 'complete') {
            chrome.tabs.onUpdated.removeListener(listener);
            inject(0);
          }
        });
      }
    });

    sendResponse({ success: true });
  }
  return true;
});
icon.png
icon.png

Save this as icon.png in the same folder as the other files.