Source: index.js

"use strict";

import {Platform, NativeModules} from "react-native";
import packageJson from "./package.json";
const {MixpanelReactNative} = NativeModules;
import MixpanelMain from "mixpanel-react-native/javascript/mixpanel-main";

const DevicePlatform = {
  Unknown: "Unknown",
  Android: "android",
  iOS: "ios",
};

const ERROR_MESSAGE = {
  INVALID_OBJECT: " is not a valid json object",
  INVALID_STRING: " is not a valid string",
  REQUIRED_DOUBLE: " is not a valid number",
};

const PARAMS = {
  TOKEN: "token",
  DISTINCT_ID: "distinctId",
  ALIAS: "alias",
  EVENT_NAME: "eventName",
  GROUP_KEY: "groupKey",
  PROPERTIES: "properties",
  PROPERTY_NAME: "propertyName",
  PROP: "prop",
  NAME: "name",
  CHARGE: "charge",
  PROPERTY_VALUE: "property value",
};

const DEFAULT_OPT_OUT = false;

/**
 * The primary class for integrating Mixpanel with your app.
 */
export class Mixpanel {
  constructor(token, trackAutomaticEvents, useNative = true, storage) {
    if (!StringHelper.isValid(token)) {
      StringHelper.raiseError(PARAMS.TOKEN);
    }
    if (trackAutomaticEvents == null) {
      throw new Error(`trackAutomaticEvents is undefined`);
    }
    this.token = token;
    this.trackAutomaticEvents = trackAutomaticEvents;
    if (!useNative) {
      this.mixpanelImpl = new MixpanelMain(
        token,
        trackAutomaticEvents,
        storage
      );
      return;
    }

    if (!MixpanelReactNative) {
      console.warn(
        "MixpanelReactNative is not available; using JavaScript mode. If you prefer not to use the JavaScript mode, please follow the guide in the GitHub repository: https://github.com/mixpanel/mixpanel-react-native."
      );
      this.mixpanelImpl = new MixpanelMain(
        token,
        trackAutomaticEvents,
        storage
      );
    } else {
      this.mixpanelImpl = MixpanelReactNative;
    }
  }

  /**
   * Initializes Mixpanel
   *
   * @param {boolean} optOutTrackingDefault Optional Whether or not Mixpanel can start tracking by default. See optOutTracking()
   * @param {object} superProperties  Optional A Map containing the key value pairs of the super properties to register
   * @param {string} serverURL Optional Set the base URL used for Mixpanel API requests. See setServerURL()
   *
   */
  async init(
    optOutTrackingDefault = DEFAULT_OPT_OUT,
    superProperties = {},
    serverURL = "https://api.mixpanel.com"
  ) {
    await this.mixpanelImpl.initialize(
      this.token,
      this.trackAutomaticEvents,
      optOutTrackingDefault,
      {...Helper.getMetaData(), ...superProperties},
      serverURL
    );
  }

  /**
   * @deprecated since version 1.3.0. To initialize Mixpanel, please use the instance method `init` instead. See the example below:
   *
   * <pre><code>
   * const trackAutomaticEvents = true;
   * const mixpanel = new Mixpanel('your project token', trackAutomaticEvents);
   * mixpanel.init();
   * </code></pre>
   *
   * Initializes Mixpanel and return an instance of Mixpanel the given project token.
   *
   * @param {string} token your project token.
   * @param {boolean} trackAutomaticEvents Whether or not to automatically track common mobile events
   * @param {boolean} Optional Whether or not Mixpanel can start tracking by default. See optOutTracking()
   *
   */
  static async init(
    token,
    trackAutomaticEvents,
    optOutTrackingDefault = DEFAULT_OPT_OUT
  ) {
    await MixpanelReactNative.initialize(
      token,
      trackAutomaticEvents,
      optOutTrackingDefault,
      Helper.getMetaData(),
      "https://api.mixpanel.com"
    );
    return new Mixpanel(token, trackAutomaticEvents);
  }

  /**
   * Set the base URL used for Mixpanel API requests.
   * Useful if you need to proxy Mixpanel requests. Defaults to https://api.mixpanel.com.
   * To route data to Mixpanel's EU servers, set to https://api-eu.mixpanel.com
   *
   * @param {string} serverURL the base URL used for Mixpanel API requests
   *
   */
  setServerURL(serverURL) {
    this.mixpanelImpl.setServerURL(this.token, serverURL);
  }

  /**
   * This allows enabling or disabling of all Mixpanel logs at run time.
   * All logging is disabled by default. Usually, this is only required if
   * you are running into issues with the SDK that you want to debug
   *
   * @param {boolean} loggingEnabled whether to enable logging
   *
   */
  setLoggingEnabled(loggingEnabled) {
    this.mixpanelImpl.setLoggingEnabled(this.token, loggingEnabled);
  }

  /**
   * This allows enabling or disabling whether or not Mixpanel flushes events
   * when the app enters the background on iOS. This is set to true by default.
   *
   * @param {boolean} flushOnBackground whether to enable logging
   *
   */
  setFlushOnBackground(flushOnBackground) {
    if (Platform.OS === "ios") {
      MixpanelReactNative.setFlushOnBackground(this.token, flushOnBackground);
    } else {
      console.warn(
        "Mixpanel setFlushOnBackground was called and ignored because this method only works on iOS."
      );
    }
  }

  /**
   * This controls whether to automatically send the client IP Address as part of event tracking.
   * With an IP address, geo-location is possible down to neighborhoods within a city,
   * although the Mixpanel Dashboard will just show you city level location specificity.
   *
   * @param {boolean} useIpAddressForGeolocation whether to automatically send the client IP Address.
   * Defaults to true.
   *
   */
  setUseIpAddressForGeolocation(useIpAddressForGeolocation) {
    this.mixpanelImpl.setUseIpAddressForGeolocation(
      this.token,
      useIpAddressForGeolocation
    );
  }

  /**
   * Set the number of events sent in a single network request to the Mixpanel server.
   * By configuring this value, you can optimize network usage and manage the frequency of communication between the client and the server. The maximum size is 50; any value over 50 will default to 50.
   *
   * @param {integer} flushBatchSize whether to automatically send the client IP Address.
   * Defaults to true.
   *
   */
  setFlushBatchSize(flushBatchSize) {
    this.mixpanelImpl.setFlushBatchSize(this.token, flushBatchSize);
  }

  /**
   * Will return true if the user has opted out from tracking.
   *
   * @return {Promise<boolean>} true if user has opted out from tracking. Defaults to false.
   */
  hasOptedOutTracking() {
    return this.mixpanelImpl.hasOptedOutTracking(this.token);
  }

  /**
   * Use this method to opt-in an already opted-out user from tracking. People updates and track
   * calls will be sent to Mixpanel after using this method.
   * This method will internally track an opt-in event to your project.
   *
   */
  optInTracking() {
    this.mixpanelImpl.optInTracking(this.token);
  }

  /**
   * Use this method to opt-out a user from tracking. Events and people updates that haven't been
   * flushed yet will be deleted. Use flush() before calling this method if you want
   * to send all the queues to Mixpanel before.
   *
   * This method will also remove any user-related information from the device.
   */
  optOutTracking() {
    this.mixpanelImpl.optOutTracking(this.token);
  }

  /**
   * Associate all future calls to track() with the user identified by
   * the given distinct id.
   *
   * <p>Calls to track() made before corresponding calls to identify
   * will use an anonymous locally generated distinct id, which means it is best to call identify
   * early to ensure that your Mixpanel funnels and retention analytics can continue to track the
   * user throughout their lifetime. We recommend calling identify when the user authenticates.
   *
   * <p>Once identify is called, the local distinct id persists across restarts of
   * your application.
   *
   * @param {string} distinctId a string uniquely identifying this user. Events sent to
   *     Mixpanel using the same disinct_id will be considered associated with the
   *     same visitor/customer for retention and funnel reporting, so be sure that the given
   *     value is globally unique for each individual user you intend to track.
   * @returns {Promise} A promise that resolves when the identify is successful.
   *     It does not return any value.
   *
   */
  identify(distinctId) {
    return new Promise((resolve, reject) => {
      if (!StringHelper.isValid(distinctId)) {
        StringHelper.raiseError(PARAMS.DISTINCT_ID);
        reject(new Error("Invalid distinctId"));
      }
      this.mixpanelImpl
        .identify(this.token, distinctId)
        .then(() => {
          resolve();
        })
        .catch((err) => {
          reject(err);
        });
    });
  }

  /**
   * @deprecated The alias method creates an alias which Mixpanel will use to remap one id to another.
   * Multiple aliases can point to the same identifier.
   *
   *  `mixpane.alias("New ID", mixpane.distinctId)`
   *  `mixpane.alias("Newer ID", mixpane.distinctId)`
   *
   * <p>This call does not identify the user after. You must still call identify()
   *  if you wish the new alias to be used for Events and People.
   *
   * @param {string} alias A unique identifier that you want to use as an identifier for this user.
   * @param {string} distinctId the current distinct_id that alias will be mapped to.
   */
  alias(alias, distinctId) {
    if (!StringHelper.isValid(alias)) {
      StringHelper.raiseError(PARAMS.ALIAS);
    }
    if (!StringHelper.isValid(distinctId)) {
      StringHelper.raiseError(PARAMS.DISTINCT_ID);
    }
    this.mixpanelImpl.alias(this.token, alias, distinctId);
  }

  /**
   * Track an event.
   *
   * <p>Every call to track eventually results in a data point sent to Mixpanel. These data points
   * are what are measured, counted, and broken down to create your Mixpanel reports. Events
   * have a string name, and an optional set of name/value pairs that describe the properties of
   * that event.
   *
   * @param {string} eventName The name of the event to send
   * @param {object} properties A Map containing the key value pairs of the properties to include in this event.
   *                   Pass null if no extra properties exist.
   */
  track(eventName, properties) {
    if (!StringHelper.isValid(eventName)) {
      StringHelper.raiseError(PARAMS.EVENT_NAME);
    }
    if (!ObjectHelper.isValidOrUndefined(properties)) {
      ObjectHelper.raiseError(PARAMS.PROPERTIES);
    }
    this.mixpanelImpl.track(this.token, eventName, {
      ...Helper.getMetaData(),
      ...properties,
    });
  }

  /**
   * Returns a Mixpanel People object that can be used to set and increment
   * People Analytics properties.
   *
   * @return {People} an instance of People that you can use to update
   *     records in Mixpanel People Analytics
   */
  getPeople() {
    if (this.people) {
      return this.people;
    } else {
      this.people = new People(this.token, this.mixpanelImpl);
      return this.people;
    }
  }

  /**
   * Track an event with specific groups.
   *
   * <p>Every call to track eventually results in a data point sent to Mixpanel. These data points
   * are what are measured, counted, and broken down to create your Mixpanel reports. Events
   * have a string name, and an optional set of name/value pairs that describe the properties of
   * that event. Group key/value pairs are upserted into the property map before tracking.
   *
   * @param {string} eventName The name of the event to send
   * @param {object} properties A Map containing the key value pairs of the properties to include in this event.
   *                   Pass null if no extra properties exist.
   * @param {object} groups A Map containing the group key value pairs for this event.
   *
   */
  trackWithGroups(eventName, properties, groups) {
    if (!StringHelper.isValid(eventName)) {
      StringHelper.raiseError(PARAMS.EVENT_NAME);
    }
    if (!ObjectHelper.isValidOrUndefined(properties)) {
      ObjectHelper.raiseError(PARAMS.PROPERTIES);
    }
    this.mixpanelImpl.trackWithGroups(
      this.token,
      eventName,
      {
        ...Helper.getMetaData(),
        ...properties,
      },
      groups
    );
  }

  /**
   * Set the group this user belongs to.
   *
   * @param {string} groupKey The property name associated with this group type (must already have been set up).
   * @param {object} groupID The group the user belongs to.
   */
  setGroup(groupKey, groupID) {
    if (!StringHelper.isValid(groupKey)) {
      StringHelper.raiseError(PARAMS.GROUP_KEY);
    }
    this.mixpanelImpl.setGroup(this.token, groupKey, groupID);
  }

  /**
   * Returns a MixpanelGroup object that can be used to set and increment
   * Group Analytics properties.
   *
   * @param {string} groupKey String identifying the type of group (must be already in use as a group key)
   * @param {object} groupID Object identifying the specific group
   * @return an instance of MixpanelGroup that you can use to update
   *     records in Mixpanel Group Analytics
   */
  getGroup(groupKey, groupID) {
    if (this.group) {
      return this.group;
    } else {
      this.group = new MixpanelGroup(
        this.token,
        groupKey,
        groupID,
        this.mixpanelImpl
      );
      return this.group;
    }
  }

  /**
   * Add a group to this user's membership for a particular group key
   *
   * @param {string} groupKey The property name associated with this group type (must already have been set up).
   * @param {object} groupID The new group the user belongs to.
   */
  addGroup(groupKey, groupID) {
    if (!StringHelper.isValid(groupKey)) {
      StringHelper.raiseError(PARAMS.GROUP_KEY);
    }
    this.mixpanelImpl.addGroup(this.token, groupKey, groupID);
  }

  /**
   * Remove a group from this user's membership for a particular group key
   *
   * @param {string} groupKey The property name associated with this group type (must already have been set up).
   * @param {object} groupID The group value to remove.
   */
  removeGroup(groupKey, groupID) {
    if (!StringHelper.isValid(groupKey)) {
      StringHelper.raiseError(PARAMS.GROUP_KEY);
    }
    this.mixpanelImpl.removeGroup(this.token, groupKey, groupID);
  }

  /**
   * Permanently deletes this group's record from Group Analytics.
   *
   * @param {string} groupKey String identifying the type of group (must be already in use as a group key)
   * @param {object} groupID Object identifying the specific group
   * <p>Calling deleteGroup deletes an entire record completely. Any future calls
   * to Group Analytics using the same group value will create and store new values.
   */
  deleteGroup(groupKey, groupID) {
    if (!StringHelper.isValid(groupKey)) {
      StringHelper.raiseError(PARAMS.GROUP_KEY);
    }
    this.mixpanelImpl.deleteGroup(this.token, groupKey, groupID);
  }

  /**
   * Register properties that will be sent with every subsequent call to track().
   *
   * <p>SuperProperties are a collection of properties that will be sent with every event to Mixpanel,
   * and persist beyond the lifetime of your application.
   *
   * <p>Setting a superProperty with registerSuperProperties will store a new superProperty,
   * possibly overwriting any existing superProperty with the same name (to set a
   * superProperty only if it is currently unset, use registerSuperPropertiesOnce())
   *
   * <p>SuperProperties will persist even if your application is taken completely out of memory.
   * to remove a superProperty, call unregisterSuperProperty() or clearSuperProperties()
   *
   * @param {object} properties A Map containing super properties to register
   */
  registerSuperProperties(properties) {
    if (!ObjectHelper.isValidOrUndefined(properties)) {
      ObjectHelper.raiseError(PARAMS.PROPERTIES);
    }
    this.mixpanelImpl.registerSuperProperties(this.token, properties || {});
  }

  /**
   * Register super properties for events, only if no other super property with the
   * same names has already been registered.
   *
   * <p>Calling registerSuperPropertiesOnce will never overwrite existing properties.
   *
   * @param {object} properties A Map containing the super properties to register.
   */
  registerSuperPropertiesOnce(properties) {
    if (!ObjectHelper.isValidOrUndefined(properties)) {
      ObjectHelper.raiseError(PARAMS.PROPERTIES);
    }
    this.mixpanelImpl.registerSuperPropertiesOnce(this.token, properties || {});
  }

  /**
   * Remove a single superProperty, so that it will not be sent with future calls to track().
   *
   * <p>If there is a superProperty registered with the given name, it will be permanently
   * removed from the existing superProperties.
   * To clear all superProperties, use clearSuperProperties()
   *
   * @param {string} propertyName name of the property to unregister
   */
  unregisterSuperProperty(propertyName) {
    if (!StringHelper.isValid(propertyName)) {
      StringHelper.raiseError(PARAMS.PROPERTY_NAME);
    }
    this.mixpanelImpl.unregisterSuperProperty(this.token, propertyName);
  }

  /**
   * Returns a json object of the user's current super properties
   *
   *<p>SuperProperties are a collection of properties that will be sent with every event to Mixpanel,
   * and persist beyond the lifetime of your application.
   *
   * @return {Promise<object>} Super properties for this Mixpanel instance.
   */
  getSuperProperties() {
    return this.mixpanelImpl.getSuperProperties(this.token);
  }

  /**
   * Erase all currently registered superProperties.
   *
   * <p>Future tracking calls to Mixpanel will not contain the specific
   * superProperties registered before the clearSuperProperties method was called.
   *
   * <p>To remove a single superProperty, use unregisterSuperProperty()
   */
  clearSuperProperties() {
    this.mixpanelImpl.clearSuperProperties(this.token);
  }

  /**
   * Begin timing of an event. Calling timeEvent("Thing") will not send an event, but
   * when you eventually call track("Thing"), your tracked event will be sent with a "$duration"
   * property, representing the number of seconds between your calls.
   *
   * @param {string} eventName the name of the event to track with timing.
   */
  timeEvent(eventName) {
    if (!StringHelper.isValid(eventName)) {
      StringHelper.raiseError(PARAMS.EVENT_NAME);
    }
    this.mixpanelImpl.timeEvent(this.token, eventName);
  }

  /**
   * Retrieves the time elapsed for the named event since timeEvent() was called.
   *
   * @param {string} eventName the name of the event to be tracked that was previously called with timeEvent()
   *
   * @return {Promise<number>} Time elapsed since timeEvent(String) was called for the given eventName.
   */
  eventElapsedTime(eventName) {
    if (!StringHelper.isValid(eventName)) {
      StringHelper.raiseError(PARAMS.EVENT_NAME);
    }
    return this.mixpanelImpl.eventElapsedTime(this.token, eventName);
  }

  /**
      Clear super properties and generates a new random distinctId for this instance.
      Useful for clearing data when a user logs out.
     */
  reset() {
    this.mixpanelImpl.reset(this.token);
  }

  /**
   * Returns the current distinct id of the user.
   * This is either the id automatically generated by the library or the id that has been passed by a call to identify().
   *
   * example of usage:
   * <pre>
   * <code>
   * const distinctId = await mixpanel.getDistinctId();
   * </code>
   * </pre>
   *
   * @return {Promise<string>} A Promise to the distinct id associated with Mixpanel event and People Analytics
   *
   */
  getDistinctId() {
    return this.mixpanelImpl.getDistinctId(this.token);
  }

  /**
   * Returns the current device id of the device.
   * This id automatically generated by the library and regenerated when logout or reset is called.
   *
   * example of usage:
   * <pre>
   * <code>
   * const deviceId = await mixpanel.getDeviceId();
   * </code>
   * </pre>
   *
   * @return {Promise<string>} A Promise to the device id
   *
   */
  getDeviceId() {
    return this.mixpanelImpl.getDeviceId(this.token);
  }

  /**
   * Push all queued Mixpanel events and People Analytics changes to Mixpanel servers.
   *
   * <p>Events and People messages are pushed gradually throughout
   * the lifetime of your application. This means that to ensure that all messages
   * are sent to Mixpanel when your application is shut down, you will
   * need to call flush() to let the Mixpanel library know it should
   * send all remaining messages to the server.
   */
  flush() {
    this.mixpanelImpl.flush(this.token);
  }
}

/**
 * Core class for using Mixpanel People Analytics features.
 *
 * <p>The People object is used to update properties in a user's People Analytics record,
 * and to manage the receipt of push notifications sent via Mixpanel Engage.
 * For this reason, it's important to call identify(String) on the People
 * object before you work with it. Once you call identify, the user identity will
 * persist across stops and starts of your application, until you make another
 * call to identify using a different id.
 *
 */
export class People {
  constructor(token, mixpanelImpl) {
    if (!StringHelper.isValid(token)) {
      StringHelper.raiseError(PARAMS.TOKEN);
    }
    this.token = token;
    this.mixpanelImpl = mixpanelImpl;
  }

  /**
   * Sets a single property with the given name and value for this user.
   * The given name and value will be assigned to the user in Mixpanel People Analytics,
   * possibly overwriting an existing property with the same name.
   *
   * @param {string} prop The name of the Mixpanel property. This must be a String, for example "Zip Code"
   * @param {object} to The value of the Mixpanel property. For "Zip Code", this value might be the String "90210"
   */
  set(prop, to) {
    let properties = {};
    if (ObjectHelper.isValid(prop)) {
      properties = JSON.parse(JSON.stringify(prop || {}));
    } else {
      if (!StringHelper.isValid(prop)) {
        StringHelper.raiseError(PARAMS.PROP);
      }
      properties[prop] = to;
    }
    this.mixpanelImpl.set(this.token, properties);
  }

  /**
   * Works just like set(), except it will not overwrite existing property values. This is useful for properties like "First login date".
   *
   * @param {string} prop The name of the Mixpanel property. This must be a String, for example "Zip Code"
   * @param {object} to The value of the Mixpanel property. For "Zip Code", this value might be the String "90210"
   */
  setOnce(prop, to) {
    let properties = {};
    if (ObjectHelper.isValid(prop)) {
      prop = prop || {};
      properties = JSON.parse(JSON.stringify(prop));
    } else {
      if (!StringHelper.isValid(prop)) {
        StringHelper.raiseError(PARAMS.PROP);
      }
      properties[prop] = to;
    }
    this.mixpanelImpl.setOnce(this.token, properties);
  }

  /**
   * Add the given amount to an existing property on the identified user. If the user does not already
   * have the associated property, the amount will be added to zero. To reduce a property,
   * provide a negative number for the value.
   *
   * @param {string} prop the People Analytics property that should have its value changed
   * @param {number} by the amount to be added to the current value of the named property
   *
   */
  increment(prop, by) {
    var add = {};
    if (ObjectHelper.isValid(prop)) {
      Object.keys(prop).forEach(function (key) {
        var val = prop[key];
        if (isNaN(parseFloat(val))) {
          throw new Error(
            `${PARAMS.PROPERTY_VALUE}${ERROR_MESSAGE.REQUIRED_DOUBLE}`
          );
        }
        add[key] = val;
      });
    } else {
      by = by || 1;
      if (isNaN(parseFloat(by))) {
        throw new Error(
          `${PARAMS.PROPERTY_VALUE}${ERROR_MESSAGE.REQUIRED_DOUBLE}`
        );
      }

      if (!StringHelper.isValid(prop)) {
        StringHelper.raiseError(PARAMS.NAME);
      }

      add[prop] = by;
    }
    this.mixpanelImpl.increment(this.token, add);
  }

  /**
   * Appends a value to a list-valued property. If the property does not currently exist,
   * it will be created as a list of one element. If the property does exist and doesn't
   * currently have a list value, the append will be ignored.
   * @param {string} name the People Analytics property that should have it's value appended to
   * @param {object} value the new value that will appear at the end of the property's list
   */
  append(name, value) {
    let appendProp = {};
    if (!StringHelper.isValid(name)) {
      StringHelper.raiseError(PARAMS.NAME);
    } else {
      appendProp[name] = value;
    }

    if (DevicePlatform.iOS === Helper.getDevicePlatform()) {
      this.mixpanelImpl.append(this.token, appendProp);
    } else {
      this.mixpanelImpl.append(this.token, name, value);
    }
  }

  /**
   * Adds values to a list-valued property only if they are not already present in the list.
   * If the property does not currently exist, it will be created with the given list as it's value.
   * If the property exists and is not list-valued, the union will be ignored.
   *
   * @param {string} name name of the list-valued property to set or modify
   * @param {array} value an array of values to add to the property value if not already present
   */
  union(name, value) {
    if (!StringHelper.isValid(name)) {
      StringHelper.raiseError(PARAMS.NAME);
    }

    value = Array.isArray(value) ? value : [value];

    if (DevicePlatform.iOS === Helper.getDevicePlatform()) {
      this.mixpanelImpl.union(this.token, {[name]: value});
      this.mixpanelImpl.union(this.token, {[name]: value});
    } else {
      this.mixpanelImpl.union(this.token, name, value);
    }
  }

  /**
   * Remove value from a list-valued property only if they are already present in the list.
   * If the property does not currently exist, the remove will be ignored.
   * If the property exists and is not list-valued, the remove will be ignored.
   * @param {string} name the People Analytics property that should have it's value removed from
   * @param {object} value the value that will be removed from the property's list
   */
  remove(name, value) {
    let removeProp = {};
    if (!StringHelper.isValid(name)) {
      StringHelper.raiseError(PARAMS.NAME);
    } else {
      removeProp[name] = value;
    }

    if (DevicePlatform.iOS === Helper.getDevicePlatform()) {
      this.mixpanelImpl.remove(this.token, removeProp);
    } else {
      this.mixpanelImpl.remove(this.token, name, value);
    }
  }

  /**
   * permanently removes the property with the given name from the user's profile
   * @param {string} name name of a property to unset
   */
  unset(name) {
    if (!StringHelper.isValid(name)) {
      StringHelper.raiseError(PARAMS.PROPERTY_NAME);
    }
    this.mixpanelImpl.unset(this.token, name);
  }

  /**
   * Track a revenue transaction for the identified people profile.
   *
   * @param {number} charge the amount of money exchanged. Positive amounts represent purchases or income from the customer, negative amounts represent refunds or payments to the customer.
   * @param {object} properties an optional collection of properties to associate with this transaction.
   */
  trackCharge(charge, properties) {
    if (isNaN(parseFloat(charge))) {
      throw new Error(`${PARAMS.CHARGE}${ERROR_MESSAGE.REQUIRED_DOUBLE}`);
    }

    if (!ObjectHelper.isValidOrUndefined(properties)) {
      ObjectHelper.raiseError(PARAMS.PROPERTIES);
    }
    this.mixpanelImpl.trackCharge(this.token, charge, properties || {});
  }

  /**
   * Permanently clear the whole transaction history for the identified people profile.
   */
  clearCharges() {
    this.mixpanelImpl.clearCharges(this.token);
  }

  /**
   * Permanently deletes the identified user's record from People Analytics.
   *
   * <p>Calling deleteUser deletes an entire record completely. Any future calls
   * to People Analytics using the same distinct id will create and store new values.
   */
  deleteUser() {
    this.mixpanelImpl.deleteUser(this.token);
  }
}

/**
 * Core class for using Mixpanel Group Analytics features.
 *
 * <p>The MixpanelGroup object is used to update properties in a group's Group Analytics record.
 */
export class MixpanelGroup {
  constructor(token, groupKey, groupID, mixpanelImpl) {
    if (!StringHelper.isValid(token)) {
      StringHelper.raiseError(PARAMS.TOKEN);
    }
    this.token = token;
    this.groupKey = groupKey;
    this.groupID = groupID;
    this.mixpanelImpl = mixpanelImpl;
  }

  /**
   * Sets a single property with the given name and value for this group.
   * The given name and value will be assigned to the user in Mixpanel Group Analytics,
   * possibly overwriting an existing property with the same name.
   *
   * @param {string} prop The name of the Mixpanel property. This must be a String, for example "Zip Code"
   * @param {string} to The value to set on the given property name. For "Zip Code", this value might be the String "90210"
   */
  set(prop, to) {
    let properties = {};
    if (ObjectHelper.isValid(prop)) {
      properties = JSON.parse(JSON.stringify(prop || {}));
    } else {
      if (!StringHelper.isValid(prop)) {
        StringHelper.raiseError(PARAMS.PROP);
      }
      properties[prop] = to;
    }
    this.mixpanelImpl.groupSetProperties(
      this.token,
      this.groupKey,
      this.groupID,
      properties
    );
  }

  /**
   * Works just like groupSet() except it will not overwrite existing property values. This is useful for properties like "First login date".
   *
   * @param {string} prop The name of the Mixpanel property. This must be a String, for example "Zip Code"
   * @param {string} to The value to set on the given property name. For "Zip Code", this value might be the String "90210"
   */
  setOnce(prop, to) {
    let properties = {};
    if (ObjectHelper.isValid(prop)) {
      properties = JSON.parse(JSON.stringify(prop || {}));
    } else {
      if (!StringHelper.isValid(prop)) {
        StringHelper.raiseError(PARAMS.PROP);
      }
      properties[prop] = to;
    }
    this.mixpanelImpl.groupSetPropertyOnce(
      this.token,
      this.groupKey,
      this.groupID,
      properties
    );
  }

  /**
   * Permanently removes the property with the given name from the group's profile
   *
   * @param {string} prop name of a property to unset
   */
  unset(prop) {
    if (!StringHelper.isValid(prop)) {
      StringHelper.raiseError(PARAMS.PROPERTY_NAME);
    }
    this.mixpanelImpl.groupUnsetProperty(
      this.token,
      this.groupKey,
      this.groupID,
      prop
    );
  }

  /**
   * Remove value from a list-valued property only if it is already present in the list.
   * If the property does not currently exist, the remove will be ignored.
   * If the property exists and is not list-valued, the remove will be ignored.
   *
   * @param {string} name the Group Analytics list-valued property that should have a value removed
   * @param {any} value the value that will be removed from the list
   */
  remove(name, value) {
    if (!StringHelper.isValid(name)) {
      StringHelper.raiseError(PARAMS.PROPERTY_NAME);
    }

    this.mixpanelImpl.groupRemovePropertyValue(
      this.token,
      this.groupKey,
      this.groupID,
      name,
      value
    );
  }

  /**
   * Adds values to a list-valued property only if they are not already present in the list.
   * If the property does not currently exist, it will be created with the given list as its value.
   * If the property exists and is not list-valued, the union will be ignored.
   *
   * @param {string} name name of the list-valued property to set or modify
   * @param {array} value an array of values to add to the property value if not already present
   */
  union(name, value) {
    if (!StringHelper.isValid(name)) {
      StringHelper.raiseError(PARAMS.PROPERTY_NAME);
    }
    value = Array.isArray(value) ? value : [value];
    this.mixpanelImpl.groupUnionProperty(
      this.token,
      this.groupKey,
      this.groupID,
      name,
      value
    );
  }
}

class Helper {
  /**
      Get the library data from package.json file.
     */
  static getMetaData() {
    let metadata = JSON.parse(JSON.stringify(packageJson.metadata));
    metadata["$lib_version"] = packageJson.version;
    return metadata;
  }

  /**
      Get current device platform.
     */
  static getDevicePlatform() {
    switch (Platform.OS) {
      case "android":
        return DevicePlatform.Android;
      case "ios":
        return DevicePlatform.iOS;
      default:
        return DevicePlatform.Unknown;
    }
  }
}

class StringHelper {
  /**
      Check whether the parameter is not a blank string.
     */
  static isValid(str) {
    return typeof str === "string" && !/^\s*$/.test(str);
  }

  /**
      Check whether the parameter is undefined or not a blank string.
     */
  static isValidOrUndefined(str) {
    return str === undefined || StringHelper.isValid(str);
  }

  /**
      Raise a string validation error.
     */
  static raiseError(paramName) {
    throw new Error(`${paramName}${ERROR_MESSAGE.INVALID_STRING}`);
  }
}

class ObjectHelper {
  /**
      Check whether the parameter is an object.
     */
  static isValid(obj) {
    return typeof obj === "object";
  }

  /**
      Check whether the parameter is undefined or an object.
     */
  static isValidOrUndefined(obj) {
    return obj === undefined || ObjectHelper.isValid(obj);
  }

  /**
      Raise an object validation error.
     */
  static raiseError(paramName) {
    throw new Error(`${paramName}${ERROR_MESSAGE.INVALID_OBJECT}`);
  }
}