Relay button disappeared

As of sometime within the last few hours, the Relay button for sales invoices and credit notes has disappeared. We use this to fiscalise all our sales invoices and credit notes as per local requirements. We contracted a local developer to develop this extension for us, and there is even a link on manager.io to their site (I assume it’s only visible when viewed from within Zimbabwe). Is this a mistake, or a deliberate change? If the latter, how can we now achieve similar functionality, hopefully without having to front further development costs?

We are using Cloud edition, currently on version 25.11.10.3109.

It seems that the relay feature has indeed been removed from the manager. The developer has mentioned that relay will be completely replaced with an extension.

2 Likes

Oh dear. I hope that the new functionality is available soon, with clear steps to transition over. It’s going to cause big problems for us if our fiscalisation is offline for an extended period, or if we have to go through the time and expense of getting a new integration developed.

I’ve had a quick look at Extensions, and it looks like we should be able to achieve the same results, but I need to spend a bit more time on it to figure it out.

Is it easy enough to create an extension that replicates the functionality of the Relay button? If someone is able to share a template then I could see if I can get it to work with our fiscalisation integration.

have you figure out? I still working on this integration

No, I haven’t.

Yes, we can still create a replica of the Relay button as before by using the Script Extension (Settings – Obsolete Features – Extensions). However, this feature is already obsolete and may be removed in the future.

Here’s an example of the Script Extension I’m referring to. If your application only relies on the Data payload sent by the relay, no further adjustments to your application are necessary.

/* ========================================================================  
   RELAY SCRIPTEXTENSION  
=========================================================================== */

(function RelayExtension() {
  'use strict';
  
  const RELAY_ENDPOINT = "https://localhost:7097/relay";

  /* Extract API Key (unchanged) */
  function extractApiKeyFromDOM() {
    try {
      for (const s of document.querySelectorAll('script')) {
        const txt = s.textContent || '';
        const m = txt.match(/API_KEY\s*=\s*['"]([^'"]+)['"]/);
        if (m) return m[1];
      }
    } catch (e) {}
    return null;
  }
  function getToken() {
    return window.API_KEY || extractApiKeyFromDOM() || null;
  }

  /* Allowed pages */
  const allowedHandlers = [
    '/sales-invoice-view',
    '/sales-invoices',
    '/credit-note-view',
    '/credit-notes',
    '/delivery-note-view',
    '/delivery-notes'
  ];

  let currentHandler = null;
  (function detectPage() {
    const path = window.location.pathname || '';
    currentHandler = allowedHandlers.find(h => path.includes(h)) || null;
    if (!currentHandler) {
      console.log('[RelayExt] Not invoice page — extension inactive');
      return;
    }
    console.log('[RelayExt] Running on page:', currentHandler);
  })();
  if (!currentHandler) return;

  /* Request context */
  let latestContextData = null;
  window.parent.postMessage({ type: 'page-request' }, '*');
  window.parent.postMessage({ type: 'context-request' }, '*');

  window.addEventListener('message', (ev) => {
    if (ev.source !== window.parent) return;
    const d = ev.data;
    if (!d) return;
    if (d.type === 'context-response') {
      latestContextData = d.body;
      console.log('[RelayExt] Context:', latestContextData);
    }
  });

  /* Insert Relay button next to PDF button */
  function injectRelayButton() {
    if (document.getElementById('relay-ext-btn')) return;

    const pdfBtn = document.querySelector(
      ".card-header .flex button.btn.group[onclick^='getPdf']"
    );

    if (!pdfBtn) {
      console.warn("[RelayExt] PDF button not found.");
      return;
    }

    const relayBtn = document.createElement('button');
    relayBtn.id = 'relay-ext-btn';
    relayBtn.textContent = "🚀 Relay";
    relayBtn.className = "btn";
    relayBtn.style.marginLeft = "6px";
    relayBtn.onclick = () => relayFlow();

    pdfBtn.insertAdjacentElement("afterend", relayBtn);
    console.log("[RelayExt] Relay button added after PDF.");
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", injectRelayButton);
  } else {
    injectRelayButton();
  }

  /* Fetch helpers */
  async function fetchJSON(url) {
    console.log('[RelayExt] Fetch:', url);
    const token = getToken();
    const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
    const res = await fetch(url, { headers, credentials: 'include' });
    if (!res.ok) throw new Error("HTTP " + res.status);
    return await res.json();
  }
  async function safeFetch(url) {
    try { return await fetchJSON(url); }
    catch (e) { console.warn('[RelayExt] safeFetch failed:', url, e.message); return null; }
  }

  /* Invoice endpoint */
  function resolveInvoiceEndpoint(ctx) {
    const key = ctx.key || ctx.Key;
    const type = (ctx.type || '').toLowerCase();
    if (type === 'salesinvoice') return `/api3/sales-invoice-form/${key}`;
    if (type === 'creditnote') return `/api3/credit-note-form/${key}`;
    return null;
  }

  /* Main Relay Flow */
  async function relayFlow() {

    const invoiceKey = latestContextData?.key || latestContextData?.Key;
    if (!invoiceKey) return alert("No invoice key.");

    console.log("[RelayExt] Running relay for:", invoiceKey);

    let dictCurrency = {};
    let dictBusiness = {};
    const dictInvoice = {};
    const dictCustomer = {};
    const dictInventory = {};
    const dictTaxCode = {};
    const dictForeignCurrency = {};

    const endpoint = resolveInvoiceEndpoint(latestContextData);
    const invoice = await fetchJSON(endpoint);
    dictInvoice[invoiceKey] = invoice;

    const base = await safeFetch('/api3/base-currency-form');
    if (base) dictCurrency = base;

    const biz = await safeFetch('/api3/business-details-form');
    if (biz) dictBusiness = biz;

    let customerObj = null;
    if (invoice.customer) {
      customerObj = await safeFetch(`/api3/customer-form/${invoice.customer}`);
      if (customerObj) dictCustomer[invoice.customer] = customerObj;
    }

    const curId = customerObj?.currency || customerObj?.Currency;
    if (curId) {
      const fc = await safeFetch(`/api3/foreign-currency-form/${curId}`);
      if (fc) dictForeignCurrency[curId] = fc;
    }

    if (Array.isArray(invoice.lines)) {
      for (const line of invoice.lines) {

        if (line.item) {
          for (const k of [
            '/api3/inventory-item-form/',
            '/api3/non-inventory-item-form/',
            '/api3/inventory-kit-form/'
          ]) {
            const it = await safeFetch(k + line.item);
            if (it) { dictInventory[line.item] = it; break; }
          }
        }

        if (line.taxCode) {
          const tc = await safeFetch(`/api3/tax-code-form/${line.taxCode}`);
          if (tc) dictTaxCode[line.taxCode] = tc;
        }
      }
    }
    
    function toPascalCaseKeys(obj) {
        if (Array.isArray(obj)) {
            return obj.map(toPascalCaseKeys);
        }
    
        if (obj !== null && typeof obj === "object") {
            const newObj = {};
    
            for (const key of Object.keys(obj)) {
    
                // GUID key → do NOT pascal-case
                const isGuid = /^[0-9a-fA-F-]{36}$/.test(key);
    
                const newKey = isGuid
                    ? key                     // leave GUID as-is
                    : key.charAt(0).toUpperCase() + key.slice(1);
    
                newObj[newKey] = toPascalCaseKeys(obj[key]);
            }
    
            return newObj;
        }
    
        return obj;
    }

    const finalPayload = {
      BaseCurrency: dictCurrency,
      BusinessDetails: dictBusiness,
      SalesInvoice: dictInvoice,
      Customer: dictCustomer,
      InventoryItem: dictInventory,
      TaxCode: dictTaxCode,
      ForeignCurrency: dictForeignCurrency
    };

    console.log("[RelayExt] FINAL PAYLOAD:", finalPayload);

    /* Build fields */
    const Referrer = window.location.href;
    const Api = window.location.origin + "/api2";
    const Token = getToken() || "";

    let View = "";
    const iframe = document.querySelector("#nonBatchView iframe");
    if (iframe) View = iframe.getAttribute("srcdoc") || "";

    /* POST via form — SAME TAB */
    const form = document.createElement("form");
    form.method = "POST";
    form.action = RELAY_ENDPOINT;
    form.target = "_self";

    function add(name, value) {
      const i = document.createElement("input");
      i.type = "hidden";
      i.name = name;
      i.value = value;
      form.appendChild(i);
    }

    add("Referrer", Referrer);
    add("Api", Api);
    add("Key", invoiceKey);
    add("Token", Token);
    add("View", View);
    
    const finalPayloadPascal = toPascalCaseKeys(finalPayload);
    add("Data", JSON.stringify(finalPayloadPascal));


    document.body.appendChild(form);
    form.submit();
    form.remove();

    console.log("[RelayExt] Relay sent via form SAME TAB.");
  }

})();

1 Like