Home Reference Source

packages/skygear-core/lib/push.js

/**
 * Copyright 2015 Oursky Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import _ from 'lodash';

import {ErrorCodes, SkygearError} from './error';

/**
 * Push container
 */
export class PushContainer {

  /**
   * @param  {Container} container - the Skygear container
   * @return {PushContainer}
   */
  constructor(container) {
    /**
     * @private
     */
    this.container = container;

    this._deviceID = null;
  }

  /**
   * @private
   *
   * Subsclass should override the implementation and provide the device type
   */
  inferDeviceType() {
    // To be implmented by subclass
    // TODO: probably web / node, handle it later
    throw new Error('Failed to infer type, please supply a value');
  }

  /**
   * You can register your device for receiving push notifications.
   *
   * @param {string} token - the device token
   * @param {string} type - the device type (either 'ios' or 'android')
   * @param {string} topic - the device topic, refer to application bundle
   * identifier on iOS and application package name on Android
   **/
  async registerDevice(token, type, topic) {
    if (!token) {
      throw new Error('Token cannot be empty');
    }
    if (!type) {
      type = this.inferDeviceType();
    }

    let deviceID;
    if (this.deviceID) {
      deviceID = this.deviceID;
    }

    try {
      const body = await this.container.makeRequest('device:register', {
        type: type,
        id: deviceID,
        topic: topic,
        device_token: token //eslint-disable-line camelcase
      });
      return this._setDeviceID(body.result.id);
    } catch (error) {
      // Will set the deviceID to null and try again iff deviceID is not null.
      // The deviceID can be deleted remotely, by apns feedback.
      // If the current deviceID is already null, will regards as server fail.
      let errorCode = null;
      if (error.error) {
        errorCode = error.error.code;
      }
      if (this.deviceID && errorCode === ErrorCodes.ResourceNotFound) {
        await this._setDeviceID(null);
        return this.registerDevice(token, type, topic);
      } else {
        throw error;
      }
    }
  }

  /**
   * Unregisters the current user from the current device.
   * This should be called when the user logouts.
   **/
  async unregisterDevice() {
    if (!this.deviceID) {
      throw new SkygearError('Missing device id', ErrorCodes.InvalidArgument);
    }

    try {
      await this.container.makeRequest('device:unregister', {
        id: this.deviceID
      });
    } catch (error) {
      let errorCode = null;
      if (error.error) {
        errorCode = error.error.code;
      }
      if (errorCode === ErrorCodes.ResourceNotFound) {
        // regard it as success
        return this._setDeviceID(null);
      } else {
        throw error;
      }
    }
  }

  /**
   * Send a push notification to all devices associated with the specified
   * users.
   *
   * @param {string|string[]} users - a list of User IDs
   * @param {Object} notification - push notification payload
   * @param {Object} notification.apns - push notification payload for APNS
   * @param {Object} notification.gcm - push notification payload for GCM
   * @param {?string} topic - the device topic, refer to application bundle
   * identifier on iOS and application package name on Android
   * @return {Object[]} list of users to which notification was sent
   *
   * @see https://developers.google.com/cloud-messaging/concept-options
   * @see https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html
   **/
  async sendToUser(users, notification, topic) {
    if (!_.isArray(users)) {
      users = [users];
    }
    let userIDs = _.map(users, function (user) {
      if (typeof user === 'string') {
        return user;
      }
      return user.id;
    });

    const result = await this.container.makeRequest('push:user', {
      user_ids: userIDs, //eslint-disable-line camelcase
      notification,
      topic
    });
    return result.result;
  }

  /**
   * Send a push notification to specified devices.
   *
   * @param {string|string[]} devices - a list of Device IDs
   * @param {Object} notification - push notification payload
   * @param {Object} notification.apns - push notification payload for APNS
   * @param {Object} notification.gcm - push notification payload for GCM
   * @param {?string} topic - the device topic, refer to application bundle
   * identifier on iOS and application package name on Android
   * @return {Object[]} list of users to which notification was sent
   *
   * @see https://developers.google.com/cloud-messaging/concept-options
   * @see https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html
   **/
  async sendToDevice(devices, notification, topic) {
    if (!_.isArray(devices)) {
      devices = [devices];
    }
    let deviceIDs = _.map(devices, function (device) {
      if (typeof device === 'string') {
        return device;
      }
      return device.id;
    });
    const result = await this.container.makeRequest('push:device', {
      device_ids: deviceIDs, //eslint-disable-line camelcase
      notification,
      topic
    });
    return result.result;
  }

  /**
   * The device ID
   *
   * @return {String}
   */
  get deviceID() {
    return this._deviceID;
  }

  async _getDeviceID() {
    try {
      const deviceID = await this.container.store.getItem('skygear-deviceid');
      this._deviceID = deviceID;
      return deviceID;
    } catch (err) {
      console.warn('Failed to get deviceid', err);
      this._deviceID = null;
      return null;
    }
  }

  async _setDeviceID(value) {
    this._deviceID = value;
    try {
      const store = this.container.store;
      if (value === null) {
        await store.removeItem('skygear-deviceid');
      } else {
        await store.setItem('skygear-deviceid', value);
      }
    } catch (err) {
      console.warn('Failed to persist deviceid', err);
    } finally {
      this.container.pubsub._reconfigurePubsubIfNeeded();
    }
    return value;
  }
}