All PowerUp iframes failing

Calling t.popup() with an args object successfully loads the iframe HTML, but window.TrelloPowerUp.iframe() throws TypeError: t is not iterable from iframe.ts:72. Removing args allows the iframe client to initialize normally.

I use a call like this:

    const popupOpts = {
      args: { view: "settings" },
      title: "Settings",
      url: "./details.html",
      height: 265,
    };
    return t.popup(popupOpts);

Then in the iframe when it appears, it fails here:

const t = window.TrelloPowerUp.iframe(); // <--- this LINE

We are seeing this error on ALL Power-Ups:

Uncaught TypeError: t is not iterable
Ut iframe.ts:72
iframe index.js:69
js details.js:15

This happens ONLY if you used args in the call of the popup or the modal. We use args extensively and therefore our Power Ups are failing. I see other Power-Ups function, and validated they are not using args.

This appears to have started since early this AM.

Is there anyone else seeing this same issue? @Trello

NOTE: I have submitted a P1 Developer Support Request for this as well: ECOHELP-148080. Confirmed it was a change to iframe.ts in the last 24 hours that has caused this Trello Wide outage.

For those STUCK like me, it appears Trello posted an update that broke the whole world on Friday night and then went home for the weekend. :frowning:

What I did was this created my OWN wrapper around calling iframes. The solution is to use the exact EXISTING popup(), modal() and signUrl() options and convert them to URL parameters and then use my own getArg() on the other size to extract those params.

export default class TrelloFrame {
  /**
   * Signs an embedded iframe URL without passing Trello's broken object-form
   * `args`, which Power-Up client 1.25.1 attempts to spread as an iterable.
   *
   * @param {TrelloObject} t
   * @param {string} url
   * @param {Record<string, unknown>} args
   * @returns {string}
   */
  static signUrl(t, url, args) {
    return t.signUrl(this.#appendArgsToUrl(url, args));
  }
  /**
   * Opens a Trello popup while bypassing Trello's broken `args` handling.
   *
   * @param {TrelloObject} t
   * @param {TrelloPopupIFrameOptions} options
   * @returns {Promise<unknown>}
   */
  static openPopup(t, options) {
    return t.popup(this.#normalizeOptions(options));
  }
  /**
   * Opens a Trello modal while bypassing Trello's broken `args` handling.
   *
   * @param {TrelloObject} t
   * @param {TrelloModalOptions} options
   * @returns {Promise<unknown>}
   */
  static openModal(t, options) {
    return t.modal(this.#normalizeOptions(options));
  }
  /**
   * Reads an argument from the current iframe URL.
   *
   * @param {string} name
   * @param {*} [defaultValue]
   * @returns {*}
   */
  static getArg(name, defaultValue = undefined) {
    const params = new URLSearchParams(window.location.search);
    if (!params.has(name)) {
      return defaultValue;
    }
    const values = params.getAll(name);
    if (values.length > 1) {
      return values.map((value) => this.#deserialize(value));
    }
    return this.#deserialize(values[0], defaultValue);
  }
  /**
   * Converts Trello `args` into query-string values.
   *
   * @param {object} options
   * @returns {object}
   */
  static #normalizeOptions(options) {
    const normalized = {
      ...options,
    };
    if (
      normalized.args &&
      typeof normalized.args === "object" &&
      !Array.isArray(normalized.args)
    ) {
      normalized.url = this.#appendArgsToUrl(
        normalized.url,
        normalized.args
      );
      delete normalized.args;
    }
    return normalized;
  }
  /**
   * Adds argument values to the URL query string without disturbing
   * Trello's signed hash/context.
   *
   * @param {string} rawUrl
   * @param {Record<string, unknown>} args
   * @returns {string}
   */
  static #appendArgsToUrl(rawUrl, args) {
    const [urlWithoutHash, existingHash = ""] =
      String(rawUrl).split("#", 2);
    const url = new URL(
      urlWithoutHash,
      window.location.href
    );
    for (const [key, value] of Object.entries(args)) {
      if (value === undefined || value === null) {
        continue;
      }
      url.searchParams.delete(key);
      if (Array.isArray(value)) {
        for (const item of value) {
          url.searchParams.append(
            key,
            this.#serialize(item)
          );
        }
        continue;
      }
      url.searchParams.set(
        key,
        this.#serialize(value)
      );
    }
    const relativeOrAbsoluteUrl =
      this.#restoreUrlStyle(rawUrl, url);
    return (
      relativeOrAbsoluteUrl +
      (existingHash ? `#${existingHash}` : "")
    );
  }
  /**
   * Serializes an argument for query-string transport.
   *
   * @param {*} value
   * @returns {string}
   */
  static #serialize(value) {
    if (typeof value === "object" && value !== null) {
      return JSON.stringify(value);
    }
    return String(value);
  }
  /**
   * Deserializes an argument from the query string.
   *
   * Objects and arrays are restored from JSON. Primitive strings remain
   * strings unless the supplied default value establishes another type.
   *
   * @param {string} value
   * @param {*} [defaultValue]
   * @returns {*}
   */
  static #deserialize(value, defaultValue = undefined) {
    if (value === null) {
      return defaultValue;
    }
    if (typeof defaultValue === "boolean") {
      if (value === "true") {
        return true;
      }
      if (value === "false") {
        return false;
      }
      return defaultValue;
    }
    if (typeof defaultValue === "number") {
      const parsed = Number(value);
      return Number.isNaN(parsed)
        ? defaultValue
        : parsed;
    }
    const trimmed = value.trim();
    if (
      (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
      (trimmed.startsWith("[") && trimmed.endsWith("]"))
    ) {
      try {
        return JSON.parse(trimmed);
      } catch {
        // Preserve the original string.
      }
    }
    return value;
  }
  /**
   * Keeps relative URLs relative and absolute URLs absolute.
   *
   * @param {string} originalUrl
   * @param {URL} parsedUrl
   * @returns {string}
   */
  static #restoreUrlStyle(originalUrl, parsedUrl) {
    const original = String(originalUrl);
    if (/^[a-z][a-z\d+\-.]*:/i.test(original)) {
      return parsedUrl.toString();
    }
    if (original.startsWith("//")) {
      return `//${parsedUrl.host}${parsedUrl.pathname}${parsedUrl.search}`;
    }
    if (original.startsWith("/")) {
      return `${parsedUrl.pathname}${parsedUrl.search}`;
    }
    const originalPath = original.split("?")[0];
    const filename = originalPath.substring(
      originalPath.lastIndexOf("/") + 1
    );
    const prefix = originalPath.substring(
      0,
      originalPath.length - filename.length
    );
    return `${prefix}${filename}${parsedUrl.search}`;
  }
}

I just replace everywhere I did t.popup() or t.modal()… like this:

      TrelloFrame.openModal(t, {
        title: "Modal Window",
        url: "./details.html",
        args: { page: "settings" },
      });

Just finished porting all 6 of my PowerUps to this and they are working now.