index.js

Back
const {ElvWalletClient, Utils} = require("@eluvio/elv-client-js/src/walletClient/index");
const EVENTS = require("./Events");

const UUID = () => {
  return "XXXXXXXX".replace(/[X]/g, () => {
    const r = Math.floor(Math.random() * 16);
    return r.toString(16);
  });
};


// https://stackoverflow.com/questions/4068373/center-a-popup-window-on-screen
const Popup = ({url, title, w, h}) => {
  // Fixes dual-screen position
  const dualScreenLeft = window.screenLeft || window.screenX;
  const dualScreenTop = window.screenTop || window.screenY;

  const width = window.innerWidth || document.documentElement.clientWidth || screen.width;
  const height = window.innerHeight || document.documentElement.clientHeight || screen.height;

  const systemZoom = width / window.screen.availWidth;
  const left = (width - w) / 2 / systemZoom + dualScreenLeft;
  const top = (height - h) / 2 / systemZoom + dualScreenTop;
  const newWindow = window.open(url, title,
    `
      scrollbars=yes,
      width=${w / systemZoom},
      height=${h / systemZoom},
      top=${top},
      left=${left}
    `
  );

  if(window.focus) newWindow.focus();

  return newWindow;
};

let __id = 0;
class Id {
  static next(){
    __id++;
    return __id;
  }
}

const iframePermissions = {
  allow: [
    "accelerometer",
    "autoplay",
    "clipboard-read",
    "clipboard-write",
    "encrypted-media *",
    "fullscreen",
    "gyroscope",
    "picture-in-picture",
    "camera",
    "microphone"
  ].join(";"),
  sandbox: [
    "allow-same-origin",
    "allow-downloads",
    "allow-scripts",
    "allow-forms",
    "allow-modals",
    "allow-pointer-lock",
    "allow-orientation-lock",
    "allow-popups",
    "allow-popups-to-escape-sandbox",
    "allow-presentation",
    "allow-downloads-without-user-activation",
    "allow-storage-access-by-user-activation"
  ].join(" ")
};

const LOG_LEVELS = {
  DEBUG: 0,
  WARN: 1,
  ERROR: 2
};

/**
 * This page contains documentation for client setup, navigation and other management.
 <br /><br />
 * <a href="./module-ElvWalletFrameClient_Methods.html">For details on retrieving information from and performing actions in the wallet, see the wallet client methods page.</a>
 * ### Wallet Client Proxy
 *
 * Most methods available in the [Eluvio Wallet Client](https://eluv-io.github.io/elv-client-js/wallet-client/index.html) are also available via proxy in the frame client. Simply access them through `walletFrameClient.walletClient`. Certain methods, such as those that generate signatures, are not available.
 <br /><br />
 * ```javascript
 * await walletFrameClient.walletClient.UserItems({
 *   start: 50,
 *   limit: 10
 * });
 * ```
 */
class ElvWalletFrameClient {
  Throw(error) {
    throw new Error(`Eluvio Media Wallet Client | ${error}`);
  }

  Log({message, level=this.LOG_LEVELS.WARN}) {
    if(level < this.logLevel) { return; }

    if(typeof message === "string") {
      message = `Eluvio Media Wallet Client | ${message}`;
    }

    switch(level) {
      case this.LOG_LEVELS.DEBUG:
        // eslint-disable-next-line no-console
        console.log(message);
        return;
      case this.LOG_LEVELS.WARN:
        // eslint-disable-next-line no-console
        console.warn(message);
        return;
      case this.LOG_LEVELS.ERROR:
        // eslint-disable-next-line no-console
        console.error(message);
        return;
    }
  }

  Destroy() {
    window.removeEventListener("message", this.EventHandler);

    if(this.Close) {
      this.Close();
    }

    if(this.target.close) {
      this.target.close();
    }
  }

  /**
   * This constructor should not be used. Please use <a href="#.InitializePopup">InitializeFrame</a> or <a href="#.InitializePopup">InitializePopup</a> instead.
   *
```javascript
import { ElvWalletFrameClient } from "@eluvio/elv-wallet-frame-client";

// Initialize in iframe at target element
const frameClient = await ElvWalletFrameClient.InitializeFrame({
 requestor: "My App",
 walletAppUrl: "https://wallet.contentfabric.io",
 target: document.getElementById("#wallet-target")
});

// Or initialize in a popup
const frameClient = await ElvWalletFrameClient.InitializePopup({
 requestor: "My App",
 walletAppUrl: "https://wallet.contentfabric.io",
});
```
   * @constructor
   */
  constructor({
    appUUID,
    requestor,
    walletAppUrl="https://wallet.contentfabric.io",
    target,
    Close,
    timeout=300
  }) {
    if(!walletAppUrl) {
      this.Throw("walletAppUrl not specified");
    }

    if(!target) {
      this.Throw("target not specified");
    }

    this.appUUID = appUUID;
    this.requestor = requestor;
    this.walletAppUrl = walletAppUrl;
    this.target = target;
    this.Close = Close;
    this.timeout = timeout;
    this.LOG_LEVELS = LOG_LEVELS;
    this.logLevel = this.LOG_LEVELS.WARN;
    this.EVENTS = EVENTS;

    this.eventListeners = {};
    Object.keys(EVENTS).forEach(key => this.eventListeners[key] = []);

    this.EventHandler = this.EventHandler.bind(this);

    window.addEventListener("message", this.EventHandler);

    // Ensure client is destroyed when target window closes
    this.AddEventListener(this.EVENTS.CLOSE, () => this.Destroy());

    // Initialize wallet client proxy
    this.walletClient = {};
    ElvWalletClient.AllowedMethods().forEach(methodName =>
      this.walletClient[methodName] = async (...args) => {
        return await this.SendMessage({
          action: "walletClientProxy",
          params: {
            methodName,
            params: Utils.MakeClonable(args)
          }
        });
      }
    );
  }

  EventHandler(event) {
    const message = event.data;

    if(
      message.type !== "ElvMediaWalletEvent" ||
      message.appUUID !== this.appUUID ||
      !EVENTS[message.event]
    ) {
      return;
    }

    const listeners = message.event === EVENTS.ALL ?
      this.eventListeners[EVENTS.ALL] :
      [...this.eventListeners[message.event], ...this.eventListeners[EVENTS.ALL]];

    listeners.forEach(async Listener => {
      try {
        await Listener(message);
      } catch(error) {
        this.Log({
          message: `${message.event} listener error:`,
          level: this.LOG_LEVELS.WARN
        });
        this.Log({
          message: error,
          level: this.LOG_LEVELS.WARN
        });
      }
    });
  }


  /**
   * Event keys that can be registered in AddEventListener.
   *
   * Available options: LOADED, LOG_IN, LOG_OUT, ROUTE_CHANGE, CLOSE, ALL
   *
   * Also accessible as a property via `walletClient.EVENTS`
   *
   * @methodGroup Events
   */
  Events() {
    return this.EVENTS;
  }

  /**
   * Add an event listener for the specified event
   *
   * Example:
   *
   * `walletClient.AddEventListener(walletClient.EVENTS.LOG_IN, HandleLogin);`
   *
   * @methodGroup Events
   * @param {string} event - An event key from <a href="#Events">Events</a>
   * @param {function} Listener - An event listener
   */
  AddEventListener(event, Listener) {
    if(!EVENTS[event]) { this.Throw(`AddEventListener: Invalid event ${event}`); }

    if(typeof Listener !== "function") { this.Throw("AddEventListener: Listener is not a function"); }

    this.eventListeners[event].push(Listener);
  }

  /**
   * Remove the specified event listener
   *
   * @methodGroup Events
   * @param {string} event - An event key from <a href="#Events">Events</a>
   * @param {function} Listener - The listener to remove
   */
  RemoveEventListener(event, Listener) {
    if(!EVENTS[event]) { this.Throw(`RemoveEventListener: Invalid event ${event}`); }
    if(typeof Listener !== "function") { this.Throw("RemoveEventListener: Listener is not a function"); }

    this.eventListeners[event] = this.eventListeners[event].filter(f => f !== Listener);
  }


  /**
   * Request the wallet app navigate to the specified page.
   *
   * When specifying a marketplace, you must provide either:
   <pre>
   - tenantSlug and marketplaceSlug - Slugs for the tenant and marketplace
   - marketplaceHash - Version hash of a marketplace
   - marketplaceId - Object ID of a marketplace
   </pre>
   * Currently supported pages:
   <pre>
   - 'login' - The login page
   - 'wallet' - The user's global wallet
   - 'items' - List of items in the user's wallet
   - 'item' - A specific item in the user's wallet
     -- Required param: `contractAddress` or `contractId`
     -- Required param: `tokenId`
   - 'profile' - The user's profile
   - 'marketplaces'
   - 'marketplace':
     -- Required param: marketplace parameters
   - 'marketplaceItem`
     -- Required params: `sku`, marketplace parameters
   - 'marketplaceWallet' - The user's collection for the specified marketplace
     -- Required params: marketplace parameters
   - `drop`
     -- Required params: `tenantSlug`, `eventSlug`, `dropId`, marketplace parameters
   - `listings`
   - `marketplaceListings`
     -- Required params: marketplace parameters
   </pre>

   * @methodGroup Navigation
   * @namedParams
   * @param {string=} page - A named app path
   * @param {Object=} params - URL parameters for the specified path, e.g. { tokenId: <token-id> } for an 'item' page.
   * @param {string=} path - An absolute app path
   * @param {boolean=} loginRequired - If login was specified, this parameter will control whether the login prompt is dismissible
   * @param {Array<string>=} marketplaceFilters - A list of filters to limit items shown in the marketplace store page
   *
   * @returns {string} - Returns the actual route to which the app has navigated
   */
  async Navigate({page, path, loginRequired, params, marketplaceFilters=[]}) {
    return this.SendMessage({
      action: "navigate",
      params: {
        page,
        path,
        params,
        loginRequired,
        marketplaceFilters
      }
    });
  }

  /**
   * Retrieve the current location path of the wallet app
   *
   * @methodGroup Navigation
   * @returns {string} - The current path of the wallet app
   */
  async CurrentPath() {
    return this.SendMessage({
      action: "currentPath"
    });
  }


  /**
   * Request the navigation header and footer to be shown or hidden in the wallet
   *
   * @methodGroup Navigation
   * @namedParams
   * @param {boolean=} enabled=true - True to show navigation, false to hide it
   */
  async ToggleNavigation(enabled=true) {
    return this.SendMessage({
      action: "toggleNavigation",
      params: {
        enabled
      },
      noResponse: true
    });
  }

  /**
   * Set whether the wallet should be displayed in dark mode
   *
   * @methodGroup Navigation
   * @namedParams
   * @param {boolean=} enabled=true - True to enable dark mode, false to disable
   */
  async ToggleDarkMode(enabled=true) {
    return this.SendMessage({
      action: "toggleDarkMode",
      params: {
        enabled
      },
      noResponse: true
    });
  }

  /**
   * Request the wallet enter/exit 'side panel' mode, where certain elements are hidden
   *
   * @methodGroup Navigation
   * @namedParams
   * @param {boolean=} enabled=true - Whether side panel mode should be enabled
   */
  async ToggleSidePanelMode(enabled=true) {
    return this.SendMessage({
      action: "toggleSidePanelMode",
      params: {
        enabled
      },
      noResponse: true
    });
  }

  // Alias
  async SignIn() {
    return await this.LogIn(arguments[0] || {});
  }

  /**
   * Sign the user in to the wallet app. Authorization can be provided in three ways:
   <ul>
    <li>- Wallet app token retrieved from elv-wallet-app-client (Preferred)</li>
    <li>- ID token from an OAuth flow</li>
    <li>- Eluvio authorization token previously retrieved from exchanging an ID token</li>
   <br/>
   *
   * NOTE: This is only to be used if authorization is performed outside of the wallet app. To direct the
   * wallet application to the login page, use the <a href="#Navigate">Navigate</a> method
   *
   * @methodGroup Authorization
   * @namedParams
   * @param {string=} clientAuthToken - An app token retrieved via elv-wallet-app-client. If this is provided, no other parameters are necessary.
   * @param {string=} email - The email address of the user
   * @param {string=} address - The address of the user
   * @param {string=} tenantId - A tenant Id to associate with the login
   * @param {string=} idToken - An OAuth ID token to authenticate with
   * @param {string=} authToken - An Eluvio authorization token
   * @param {string=} fabricToken - An Eluvio authorization token signed by the user
   * @param {string=} walletName - If signing in from an external wallet such as metamask, the name of the wallet
   * @param {number=} expiresAt - A unix epoch timestamp indicating when the specified authorization expires
   */
  async LogIn({clientAuthToken, email, idToken, authToken, fabricToken, address, walletName, tenantId, expiresAt}) {
    return this.SendMessage({
      action: "login",
      params: {
        clientAuthToken,
        idToken,
        authToken,
        fabricToken,
        address,
        tenantId,
        walletName,
        expiresAt,
        user: {
          name,
          email
        }
      }
    });
  }

  // Alias
  async SignOut() {
    return await this.LogOut(arguments[0] || {});
  }

  /**
   * Sign the current user out
   *
   * @methodGroup Authorization
   */
  async LogOut() {
    this.SendMessage({
      action: "logout",
      params: {}
    });

    await Promise.race([
      new Promise(resolve => {
        this.AddEventListener(EVENTS.LOADED , () => resolve());
      }),
      new Promise(resolve => setTimeout(resolve, 5000))
    ]);
  }

  /**
   * Reload the wallet application
   *
   * @methodGroup Navigation
   */
  async Reload() {
    return this.SendMessage({
      action: "reload",
      params: {}
    });
  }

  /**
   * Initialize the media wallet in a new window.
   *
   * Calling client.Destroy() will close the popup.
   *
   * @methodGroup Constructor
   * @namedParams
   * @param {string} requestor - The name of your application. This field is used in permission prompts, e.g.
   <br />
   <br />
   `<requestor> is requesting to perform <action>`
   * @param {string=} walletAppUrl=https://wallet.contentfabric.io - The URL of the Eluvio Media Wallet app
   * @param {string=} tenantSlug - Specify the URL slug of your tenant. Required if specifying marketplaceSlug
   * @param {string=} marketplaceSlug - Specify the URL slug of your marketplace
   * @param {string=} marketplaceId - Specify the ID of your marketplace. Not necessary if marketplaceSlug is specified
   * @param {string=} previewMarketplaceId - Specify the ID of a marketplace to show a preview for.
   * @param {boolean=} requireLogin=false - If specified, users will be required to log in before accessing any page in the app
   * @param {boolean=} loginOnly=false - If specified, only the login flow will be shown. Be sure to register an event listener for the `LOG_IN` event. `client.Destroy()` can be used to close the popup after login. Note that once this mode is activated, it cannot be deactivated - you must re-initialize the popup/frame.
   * @param {boolean=} captureLogin=false - If specified, the parent frame will be responsible for handling login requests. When the user attempts to log in, the LOG_IN_REQUESTED event will be fired.
   * @param {boolean=} darkMode=false - Specify whether the app should be in dark mode
   *
   * @returns {Promise<ElvWalletFrameClient>} - The ElvWalletFrameClient initialized to communicate with the media wallet app in the new window.
   */
  static async InitializePopup({
    requestor,
    walletAppUrl="https://wallet.contentfabric.io",
    tenantSlug,
    marketplaceSlug,
    marketplaceId,
    marketplaceHash,
    previewMarketplaceId,
    requireLogin=false,
    loginOnly=false,
    captureLogin=false,
    darkMode=false
  }) {
    const appUUID = UUID();

    walletAppUrl = new URL(walletAppUrl);

    walletAppUrl.searchParams.set("appUUID", appUUID);

    walletAppUrl.searchParams.set("origin", window.location.origin);

    if(marketplaceSlug) {
      walletAppUrl.searchParams.set("mid", `${tenantSlug}/${marketplaceSlug}`);
    } else if(marketplaceId || marketplaceHash) {
      walletAppUrl.searchParams.set("mid", marketplaceHash || marketplaceId);
    }

    if(walletAppUrl.searchParams.has("mid") && !walletAppUrl.hash) {
      walletAppUrl.hash = `#/marketplaces/redirect/${tenantSlug}/${marketplaceSlug}/store`;
    }

    if(requireLogin){
      walletAppUrl.searchParams.set("rl", "");
    }

    if(loginOnly) {
      walletAppUrl.searchParams.set("lo", "");
    }

    if(captureLogin) {
      walletAppUrl.searchParams.set("cl", "");
    }

    if(darkMode) {
      walletAppUrl.searchParams.set("dk", "");
    }

    if(previewMarketplaceId) {
      walletAppUrl.searchParams.set("preview", previewMarketplaceId);
    }

    const target = Popup({url: walletAppUrl.toString(), title: "Eluvio Media Wallet", w: 400, h: 700});

    const client = new ElvWalletFrameClient({
      appUUID,
      requestor,
      walletAppUrl: walletAppUrl.toString(),
      target,
      Close: () => target.close()
    });

    // Ensure app is initialized
    await client.AwaitMessage("init");

    // Watch for popup to be closed
    let popupInterval = setInterval(() => {
      if(target.closed) {
        clearInterval(popupInterval);
        client.EventHandler({data: { type: "ElvMediaWalletEvent", event: EVENTS.CLOSE }});
      }
    }, 1000);

    return client;
  }

  /**
   * Initialize the media wallet in a new iframe. The target can be an existing iframe or an element in which to create the iframe,
   * and the target can be passed in either as an element directly, or by element ID.
   *
   * @methodGroup Constructor
   *
   * @namedParams
   * @param {string} requestor - The name of your application. This field is used in permission prompts, e.g.
   <br />
   <br />
   `<requestor> is requesting to perform <action>`
   * @param {string=} walletAppUrl=https://wallet.contentfabric.io - The URL of the Eluvio Media Wallet app
   * @param {Object | string} target - An HTML element or the ID of an element
   * @param {string=} tenantSlug - Specify the URL slug of your tenant. Required if specifying marketplace slug
   * @param {string=} marketplaceSlug - Specify the URL slug of your marketplace
   * @param {string=} marketplaceId - Specify the ID of your marketplace. Not necessary if marketplaceSlug is specified
   * @param {string=} previewMarketplaceId - Specify the ID of a marketplace to show a preview for.
   * @param {boolean=} requireLogin=false - If specified, users will be required to log in before accessing any page in the app
   * @param {boolean=} loginOnly=false - If specified, only the login flow will be shown. Be sure to register an event listener for the `LOG_IN` event. Note that once this mode is activated, it cannot be deactivated - you must re-initialize the popup/frame.
   * @param {boolean=} captureLogin - If specified, the parent frame will be responsible for handling login requests. When the user attempts to log in, the LOG_IN_REQUESTED event will be fired.
   * @param {boolean=} darkMode=false - Specify whether the app should be in dark mode
   *
   * @returns {Promise<ElvWalletFrameClient>} - The ElvWalletFrameClient initialized to communicate with the media wallet app in the new iframe.
   */
  static async InitializeFrame({
    requestor,
    walletAppUrl="https://wallet.contentfabric.io",
    target,
    tenantSlug,
    marketplaceSlug,
    marketplaceId,
    marketplaceHash,
    previewMarketplaceId,
    requireLogin=false,
    loginOnly=false,
    captureLogin=false,
    darkMode=false
  }) {
    const appUUID = UUID();

    if(typeof target === "string") {
      const targetElement = document.getElementById(target);

      if(!targetElement) {
        throw Error(`Eluvio Media Wallet Client: Unable to find element with target ID ${target}`);
      }

      target = targetElement;
    }

    if((target.tagName || target.nodeName).toLowerCase() !== "iframe") {
      let parent = target;
      parent.innerHTML = "";

      target = document.createElement("iframe");
      parent.appendChild(target);
    }

    target.classList.add("-elv-media-wallet-frame");
    target.sandbox = iframePermissions.sandbox;
    target.setAttribute("allowFullScreen", "");
    target.allow = iframePermissions.allow;
    target.title = "Eluvio Media Wallet";

    walletAppUrl = new URL(walletAppUrl);

    walletAppUrl.searchParams.set("appUUID", appUUID);

    walletAppUrl.searchParams.set("origin", window.location.origin);

    if(marketplaceSlug) {
      walletAppUrl.searchParams.set("mid", `${tenantSlug}/${marketplaceSlug}`);
    } else if(marketplaceId || marketplaceHash) {
      walletAppUrl.searchParams.set("mid", marketplaceHash || marketplaceId);
    }

    if(walletAppUrl.searchParams.has("mid") && !walletAppUrl.hash) {
      walletAppUrl.hash = `#/marketplaces/redirect/${tenantSlug}/${marketplaceSlug}/store`;
    }

    if(requireLogin){
      walletAppUrl.searchParams.set("rl", "");
    }

    if(loginOnly) {
      walletAppUrl.searchParams.set("lo", "");
    }

    if(captureLogin) {
      walletAppUrl.searchParams.set("cl", "");
    }

    if(darkMode) {
      walletAppUrl.searchParams.set("dk", "");
    }

    if(previewMarketplaceId) {
      walletAppUrl.searchParams.set("preview", previewMarketplaceId);
    }

    const client = new ElvWalletFrameClient({
      appUUID,
      requestor,
      walletAppUrl: walletAppUrl.toString(),
      target: target.contentWindow,
      Close: () => target && target.parentNode && target.parentNode.removeChild(target)
    });

    // Ensure app is initialized
    target.src = walletAppUrl.toString();
    await client.AwaitMessage("init");

    return client;
  }

  async SendMessage({action, params, noResponse=false}) {
    const requestId = `action-${Id.next()}`;

    this.target.postMessage({
      type: "ElvMediaWalletClientRequest",
      requestor: this.requestor,
      requestId,
      action,
      params
    }, this.walletAppUrl);

    if(noResponse) { return; }

    return (await this.AwaitMessage(requestId));
  }

  async AwaitMessage(requestId) {
    const timeout = this.timeout;
    return await new Promise((resolve, reject) => {
      let methodListener;

      // Initialize or reset timeout
      let timeoutId;
      const touchTimeout = () => {
        if(timeoutId) {
          clearTimeout(timeoutId);
        }

        if(timeout > 0) {
          timeoutId = setTimeout(() => {
            if(typeof window !== "undefined") {
              window.removeEventListener("message", methodListener);
            }

            reject(`Request ${requestId} timed out`);
          }, timeout * 1000);
        }
      };

      methodListener = async (event) => {
        try {
          const message = event.data;

          if(message.type !== "ElvMediaWalletResponse" || message.requestId !== requestId) {
            return;
          }

          clearTimeout(timeoutId);

          window.removeEventListener("message", methodListener);

          if(message.error) {
            reject(message.error);
          } else {
            resolve(message.response);
          }
        } catch(error){
          clearTimeout(timeoutId);

          window.removeEventListener("message", methodListener);

          reject(error);
        }
      };

      // Start the timeout
      touchTimeout();

      window.addEventListener("message", methodListener);
    });
  }
}

/**
 * `client.EVENTS` contains event keys for the AddEventListener and RemoveEventListener methods
 *
 * - `client.EVENTS.LOADED` - Wallet app has finished loading and authentication is settled. If a user is currently logged in, a `LOG_IN` event will have preceded this event.
 * - `client.EVENTS.LOG_IN` - User has logged in. Event data contains user address.
 * - `client.EVENTS.LOG_OUT` - User has logged out. Event data contains user address.
 * - `client.EVENTS.CLOSE` - Target window or frame has been closed or has otherwise unloaded the wallet app.
 * - `client.EVENTS.ROUTE_CHANGE` - The wallet app's current route has changed. Event data contains the current route of the app.
 * - `client.EVENTS.RESIZE` - This event will specify the full height and width of the wallet application as currently rendered
 * - `client.EVENTS.ALL` - Any of the above events has occurred.
 */
ElvWalletFrameClient.EVENTS = EVENTS;
ElvWalletFrameClient.LOG_LEVELS = LOG_LEVELS;

Object.assign(ElvWalletFrameClient.prototype, require("./ClientMethods"));

exports.ElvWalletFrameClient = ElvWalletFrameClient;