# subscription/2.2.0/assets/js/admin-components/save.js

Subscriptions for WooCommerce with Stripe Recurring Payments, version 2.2.0. 164 lines.

- Page: https://pluginprobe.com/plugins/subscription/2.2.0/code/assets/js/admin-components/save.js
- Raw: https://pluginprobe.com/plugins/subscription/2.2.0/raw/assets/js/admin-components/save.js
- Modified: 2026-09-14T12:17:12+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/subscription/2.2.0/code/assets/js/admin-components/save.js#L10-L20`.

```javascript
/**
 * WPSubsSave — one way to write to the REST API and one way to say what
 * happened.
 *
 * Every admin screen that writes had grown its own copy of the same three
 * things: a `fetch` wrapper, a button-busy lock, and some way of reporting the
 * result. The wrappers had already drifted (one handled 204, the others did
 * not) and the reporting had not settled at all — a banner here, a blocking
 * `window.alert` there. This is that trio, once.
 *
 * Usage:
 *   var save = WPSubsSave.bind({ restUrl: cfg.restUrl, nonce: cfg.nonce, i18n: i18n });
 *
 *   save.api("PUT", "/terms/12", { status: "active" });   // → Promise
 *
 *   save.run(btn, function () {                            // busy + toast
 *     return save.api("PUT", "/relations/3", body);
 *   }, { success: i18n.saved });
 *
 * `window.confirm` is deliberately left alone: a toast reports, it does not
 * ask, and a destructive action still needs a real answer.
 */
(function () {
  "use strict";

  /**
   * Say something happened. Falls back to nothing rather than throwing when
   * the toast component is not on the page.
   *
   * @param {string} message Text.
   * @param {string} [type]  "success" (default) or "error".
   */
  function notify(message, type) {
    if (window.WPSubsToast && message) {
      window.WPSubsToast.show(message, type);
    }
  }

  /**
   * Mark a button busy while its request is in flight, and lock the controls
   * beside it so the same write cannot be fired twice or abandoned midway.
   * The `is-loading` class draws the spinner (admin-components/buttons.css).
   *
   * @param {HTMLElement} btn     Button.
   * @param {boolean}     loading Loading state.
   */
  function busy(btn, loading) {
    if (!btn) {
      return;
    }
    btn.disabled = loading;
    btn.classList.toggle("is-loading", loading);

    // The row the button sits in: a modal footer, or an inline edit form.
    var row = btn.closest(".wpsubs-modal__footer") || btn.parentNode;
    if (row && row.querySelectorAll) {
      row.querySelectorAll("button, input, select, textarea").forEach(function (el) {
        if (el !== btn) {
          el.disabled = loading;
        }
      });
    }

    // Inside a modal, the dismiss affordances go with it. Escape is left
    // working on purpose, as the way out of a request that never returns.
    var modal = btn.closest(".wpsubs-modal");
    if (modal) {
      var close = modal.querySelector(".wpsubs-modal__close");
      if (close) {
        close.disabled = loading;
      }
      var backdrop = modal.querySelector(".wpsubs-modal__backdrop");
      if (backdrop) {
        backdrop.style.pointerEvents = loading ? "none" : "";
      }
    }
  }

  /**
   * Bind the helpers to one screen's REST base, nonce and strings.
   *
   * @param {Object} cfg { restUrl, nonce, i18n }.
   * @return {Object} { api, run, busy, notify }.
   */
  function bind(cfg) {
    cfg = cfg || {};
    var i18n = cfg.i18n || {};

    /**
     * Call a REST endpoint under this screen's base.
     *
     * Rejects with the server's own message when it sends one, so a caller
     * can report the real reason rather than a generic failure.
     *
     * @param {string} method HTTP verb.
     * @param {string} path   Path under the base, e.g. "/groups".
     * @param {Object} [body] JSON body for writes.
     * @return {Promise<Object>} Parsed JSON ({} for an empty body).
     */
    function api(method, path, body) {
      return fetch(cfg.restUrl + path, {
        method: method,
        credentials: "same-origin",
        headers: {
          "Content-Type": "application/json",
          "X-WP-Nonce": cfg.nonce || "",
        },
        body: body ? JSON.stringify(body) : undefined,
      }).then(function (res) {
        // 204 and an empty body are both success with nothing to parse;
        // res.json() would throw on either.
        return res.text().then(function (text) {
          var data = {};
          if (text) {
            try {
              data = JSON.parse(text);
            } catch (e) {
              data = {};
            }
          }
          if (!res.ok) {
            throw new Error((data && data.message) || i18n.genericError || "");
          }
          return data;
        });
      });
    }

    /**
     * The whole write: lock the button, do the work, report the outcome,
     * unlock either way.
     *
     * @param {HTMLElement} btn  Button that triggered it (may be null).
     * @param {Function}    work Returns a Promise for the write.
     * @param {Object}     [opts] { success, error } toast messages; pass
     *                            success: false to stay silent.
     * @return {Promise} Resolves after the work and any follow-up.
     */
    function run(btn, work, opts) {
      opts = opts || {};
      busy(btn, true);

      return Promise.resolve()
        .then(work)
        .then(function (result) {
          busy(btn, false);
          if (false !== opts.success) {
            notify(opts.success || i18n.saved);
          }
          return result;
        })
        .catch(function (err) {
          busy(btn, false);
          notify(opts.error || (err && err.message) || i18n.genericError, "error");
          throw err;
        });
    }

    return { api: api, run: run, busy: busy, notify: notify };
  }

  window.WPSubsSave = { bind: bind, busy: busy, notify: notify };
})();

```
