diff --git a/lib/constants.js b/lib/constants.js new file mode 100644 index 00000000..8e24dc81 --- /dev/null +++ b/lib/constants.js @@ -0,0 +1,26 @@ +import { reverseKeyValues } from './utils'; + +export const ConnectionResult = { + SUCCESS: 0, + SERVICE_MISSING: 1, + SERVICE_VERSION_UPDATE_REQUIRED: 2, + SERVICE_DISABLED: 3, + SIGN_IN_REQUIRED: 4, + INVALID_ACCOUNT: 5, + RESOLUTION_REQUIRED: 6, + NETWORK_ERROR: 7, + INTERNAL_ERROR: 8, + SERVICE_INVALID: 9, + DEVELOPER_ERROR: 10, + LICENSE_CHECK_FAILED: 11, + CANCELED: 13, + TIMEOUT: 14, + INTERRUPTED: 15, + API_UNAVAILABLE: 16, + SIGN_IN_FAILED: 17, + SERVICE_UPDATING: 18, + SERVICE_MISSING_PERMISSION: 19, + RESTRICTED_PROFILE: 20, +}; + +export const ConnectionResultReverse = reverseKeyValues(ConnectionResult); diff --git a/lib/firebase.js b/lib/firebase.js new file mode 100644 index 00000000..82785c18 --- /dev/null +++ b/lib/firebase.js @@ -0,0 +1,225 @@ +/** + * @providesModule Firebase + * @flow + */ +import { NativeModules, NativeEventEmitter } from 'react-native'; + +import Log from './utils/log'; +import { promisify } from './utils'; +import Singleton from './utils/singleton'; + +// modules +import Auth from './modules/auth'; +import Storage from './modules/storage'; +import Database from './modules/database'; +import Messaging from './modules/messaging'; +import Analytics from './modules/analytics'; + +let log; +const instances = { default: null }; +const FirebaseModule = NativeModules.RNFirebase; +const FirebaseModuleEvt = new NativeEventEmitter(FirebaseModule); + +/** + * @class Firebase + */ +export default class Firebase extends Singleton { + + /** + * + * @param options + */ + constructor(options: Object = {}) { + const instance = super(options); + + instance.options = Object.assign({ errorOnMissingPlayServices: true }, options); + instance._debug = instance.options.debug || false; + + Log.enable(instance._debug); + log = instance._log = new Log('firebase'); + + log.info('Creating new firebase instance'); + + instance._remoteConfig = instance.options.remoteConfig || {}; + delete instance.options.remoteConfig; + + instance.configured = instance.options.configure || false; + + instance.eventHandlers = {}; + + log.info('Calling configure with options', instance.options); + instance.configurePromise = instance.configure(instance.options); + + instance._auth = new Auth(instance, instance.options); + + if (instance.options.errorOnMissingPlayServices && !this.googleApiAvailability.isAvailable) { + throw new Error(`Google Play Services is required to run this application but no valid installation was found (Code ${this.googleApiAvailability.status}).`); + } + } + + _db: ?Object; + _log: ?Object; + _auth: ?Object; + _store: ?Object; + _storage: ?Object; + _presence: ?Object; + _analytics: ?Object; + _constants: ?Object; + _messaging: ?Object; + _remoteConfig: ?Object; + + /** + * Support web version of initApp. + * @param options + * @param name + * @returns {*} + */ + static initializeApp(options: Object = {}, name: string = 'default') { + if (!instances[name]) instances[name] = new Firebase(options); + return instances[name]; + } + + + /** + * + * @param opts + * @returns {Promise.|*|Promise.} + */ + configure(opts: Object = {}) { + if (!this.configurePromise) { + const firebaseOptions = Object.assign({}, this.options, opts); + + this.configurePromise = promisify('configureWithOptions', FirebaseModule)(firebaseOptions) + .then((configuredProperties) => { + log.info('Native configureWithOptions success', configuredProperties); + this.configured = true; + this.firebaseOptions = configuredProperties; + return configuredProperties; + }).catch((err) => { + log.info('Native error occurred while calling configure', err); + }); + } + return this.configurePromise; + } + + onReady(cb: Function) { + // TODO wut o.O + return this.configurePromise = this.configurePromise.then(cb); + } + + /** + * Wrappers + * We add methods from each wrapper to this instance + * when they are needed. Not sure if this is a good + * idea or not (imperative vs. direct manipulation/proxy) + */ + auth() { + return this._auth; + } + + + database() { + if (!this._db) { + this._db = new Database(this); + } + return this._db; + } + + + analytics() { + if (!this._analytics) { + this._analytics = new Analytics(this); + } + return this._analytics; + } + + // storage + storage() { + if (!this._storage) { + this._storage = new Storage(this); + } + return this._storage; + } + + messaging() { + if (!this._messaging) { + this._messaging = new Messaging(this); + } + return this._messaging; + } + + remoteConfig() { + if (!this._remoteConfig) { + this._remoteConfig = new RemoteConfig(this); + } + return this._remoteConfig; + } + + get ServerValue(): Promise<*> { + return promisify('serverValue', FirebaseModule)(); + } + + get apps(): Array { + return Object.keys(instances); + } + + /** + * Returns androids GoogleApiAvailability status and message if available. + * @returns {GoogleApiAvailabilityType|{isAvailable: boolean, status: number}} + */ + get googleApiAvailability(): GoogleApiAvailabilityType { + // if not available then return a fake object for ios - saves doing platform specific logic. + return FirebaseModule.googleApiAvailability || { isAvailable: true, status: 0 }; + } + + /** + * Logger + */ + get log(): Log { + return this._log; + } + + /** + * Redux store + **/ + get store(): ?Object { + return this._store; + } + + get constants(): Object { + if (!this._constants) { + this._constants = Object.assign({}, Storage.constants); + } + return this._constants; + } + + /** + * Set the redux store helper + */ + setStore(store: Object) { + if (store) { + this.log.info('Setting the store for Firebase instance'); + this._store = store; + } + } + + /** + * Global event handlers for the single Firebase instance + */ + on(name: string, cb: Function, nativeModule: Object = FirebaseModuleEvt) { + if (!this.eventHandlers[name]) { + this.eventHandlers[name] = []; + } + + const sub = nativeModule.addListener(name, cb); + this.eventHandlers[name].push(sub); + return sub; + } + + off(name: string) { + if (this.eventHandlers[name]) { + this.eventHandlers[name] + .forEach(subscription => subscription.remove()); + } + } +} diff --git a/lib/flow.js b/lib/flow.js new file mode 100644 index 00000000..406341af --- /dev/null +++ b/lib/flow.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +declare module 'react-native' { + // noinspection ES6ConvertVarToLetConst + declare var exports: any; +} + +declare type AuthResultType = { + authenticated: boolean, + user: Object|null +}; + +declare type CredentialType = { + provider: string, + token: string, + secret: string +}; + +declare type GoogleApiAvailabilityType = { + status: number, + isAvailable: boolean, + isUserResolvableError?: boolean, + error?: string +}; diff --git a/lib/modules/base.js b/lib/modules/base.js new file mode 100644 index 00000000..ab674937 --- /dev/null +++ b/lib/modules/base.js @@ -0,0 +1,79 @@ +/** + * @flow + */ +import { NativeModules, NativeEventEmitter } from 'react-native'; + +import Log from '../utils/log'; +import EventEmitter from './../utils/eventEmitter'; + +const FirebaseModule = NativeModules.RNFirebase; +const FirebaseModuleEvt = new NativeEventEmitter(FirebaseModule); + +const logs = {}; + +type FirebaseOptions = {}; + +// TODO cleanup +export class Base extends EventEmitter { + constructor(firebase: Object, options: FirebaseOptions = {}) { + super(); + this.firebase = firebase; + this.eventHandlers = {}; + this.options = Object.assign({}, firebase.options, options); + } + + // Logger + get log(): Log { + if (!logs[this.namespace]) logs[this.namespace] = new Log(this.namespace, this.firebase._debug); + return logs[this.namespace]; + } + + /** + * app instance + **/ + get app(): Object { + return this.firebase.app; + } + + whenReady(promise: Promise<*>): Promise<*> { + return this.firebase.configurePromise.then(() => promise); + } + + // Event handlers + // proxy to firebase instance + _on(name, cb, nativeModule) { + return new Promise((resolve) => { + // if (!this.eventHandlers[name]) { + // this.eventHandlers[name] = {}; + // } + if (!nativeModule) { + nativeModule = FirebaseModuleEvt; + } + const sub = nativeModule.addListener(name, cb); + this.eventHandlers[name] = sub; + resolve(sub); + }); + } + + _off(name) { + return new Promise((resolve) => { + if (this.eventHandlers[name]) { + const subscription = this.eventHandlers[name]; + subscription.remove(); // Remove subscription + delete this.eventHandlers[name]; + resolve(subscription); + } + }); + } +} + +export class ReferenceBase extends Base { + constructor(firebase: Object, path: string) { + super(firebase); + this.path = path || '/'; + } + + get key(): string|null { + return this.path === '/' ? null : this.path.substring(this.path.lastIndexOf('/') + 1); + } +} diff --git a/lib/utils/eventEmitter.js b/lib/utils/eventEmitter.js new file mode 100644 index 00000000..67c15ba0 --- /dev/null +++ b/lib/utils/eventEmitter.js @@ -0,0 +1,313 @@ +// TODO - this is just a raw copy of eventEmitter3 - until i can implement a lightweight version + +'use strict'; + +var has = Object.prototype.hasOwnProperty + , prefix = '~'; + +/** + * Constructor to create a storage for our `EE` objects. + * An `Events` instance is a plain object whose properties are event names. + * + * @constructor + * @api private + */ +function Events() {} + +// +// We try to not inherit from `Object.prototype`. In some engines creating an +// instance in this way is faster than calling `Object.create(null)` directly. +// If `Object.create(null)` is not supported we prefix the event names with a +// character to make sure that the built-in object properties are not +// overridden or used as an attack vector. +// +if (Object.create) { + Events.prototype = Object.create(null); + + // + // This hack is needed because the `__proto__` property is still inherited in + // some old browsers like Android 4, iPhone 5.1, Opera 11 and Safari 5. + // + if (!new Events().__proto__) prefix = false; +} + +/** + * Representation of a single event listener. + * + * @param {Function} fn The listener function. + * @param {Mixed} context The context to invoke the listener with. + * @param {Boolean} [once=false] Specify if the listener is a one-time listener. + * @constructor + * @api private + */ +function EE(fn, context, once) { + this.fn = fn; + this.context = context; + this.once = once || false; +} + +/** + * Minimal `EventEmitter` interface that is molded against the Node.js + * `EventEmitter` interface. + * + * @constructor + * @api public + */ +function EventEmitter() { + this._events = new Events(); + this._eventsCount = 0; +} + +/** + * Return an array listing the events for which the emitter has registered + * listeners. + * + * @returns {Array} + * @api public + */ +EventEmitter.prototype.eventNames = function eventNames() { + var names = [] + , events + , name; + + if (this._eventsCount === 0) return names; + + for (name in (events = this._events)) { + if (has.call(events, name)) names.push(prefix ? name.slice(1) : name); + } + + if (Object.getOwnPropertySymbols) { + return names.concat(Object.getOwnPropertySymbols(events)); + } + + return names; +}; + +/** + * Return the listeners registered for a given event. + * + * @param {String|Symbol} event The event name. + * @param {Boolean} exists Only check if there are listeners. + * @returns {Array|Boolean} + * @api public + */ +EventEmitter.prototype.listeners = function listeners(event, exists) { + var evt = prefix ? prefix + event : event + , available = this._events[evt]; + + if (exists) return !!available; + if (!available) return []; + if (available.fn) return [available.fn]; + + for (var i = 0, l = available.length, ee = new Array(l); i < l; i++) { + ee[i] = available[i].fn; + } + + return ee; +}; + +/** + * Calls each of the listeners registered for a given event. + * + * @param {String|Symbol} event The event name. + * @returns {Boolean} `true` if the event had listeners, else `false`. + * @api public + */ +EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { + var evt = prefix ? prefix + event : event; + + if (!this._events[evt]) return false; + + var listeners = this._events[evt] + , len = arguments.length + , args + , i; + + if (listeners.fn) { + if (listeners.once) this.removeListener(event, listeners.fn, undefined, true); + + switch (len) { + case 1: return listeners.fn.call(listeners.context), true; + case 2: return listeners.fn.call(listeners.context, a1), true; + case 3: return listeners.fn.call(listeners.context, a1, a2), true; + case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; + case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; + case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; + } + + for (i = 1, args = new Array(len -1); i < len; i++) { + args[i - 1] = arguments[i]; + } + + listeners.fn.apply(listeners.context, args); + } else { + var length = listeners.length + , j; + + for (i = 0; i < length; i++) { + if (listeners[i].once) this.removeListener(event, listeners[i].fn, undefined, true); + + switch (len) { + case 1: listeners[i].fn.call(listeners[i].context); break; + case 2: listeners[i].fn.call(listeners[i].context, a1); break; + case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; + case 4: listeners[i].fn.call(listeners[i].context, a1, a2, a3); break; + default: + if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { + args[j - 1] = arguments[j]; + } + + listeners[i].fn.apply(listeners[i].context, args); + } + } + } + + return true; +}; + +/** + * Add a listener for a given event. + * + * @param {String|Symbol} event The event name. + * @param {Function} fn The listener function. + * @param {Mixed} [context=this] The context to invoke the listener with. + * @returns {EventEmitter} `this`. + * @api public + */ +EventEmitter.prototype.on = function on(event, fn, context) { + var listener = new EE(fn, context || this) + , evt = prefix ? prefix + event : event; + + if (!this._events[evt]) this._events[evt] = listener, this._eventsCount++; + else if (!this._events[evt].fn) this._events[evt].push(listener); + else this._events[evt] = [this._events[evt], listener]; + + return this; +}; + +/** + * Add a one-time listener for a given event. + * + * @param {String|Symbol} event The event name. + * @param {Function} fn The listener function. + * @param {Mixed} [context=this] The context to invoke the listener with. + * @returns {EventEmitter} `this`. + * @api public + */ +EventEmitter.prototype.once = function once(event, fn, context) { + var listener = new EE(fn, context || this, true) + , evt = prefix ? prefix + event : event; + + if (!this._events[evt]) this._events[evt] = listener, this._eventsCount++; + else if (!this._events[evt].fn) this._events[evt].push(listener); + else this._events[evt] = [this._events[evt], listener]; + + return this; +}; + +/** + * Remove the listeners of a given event. + * + * @param {String|Symbol} event The event name. + * @param {Function} fn Only remove the listeners that match this function. + * @param {Mixed} context Only remove the listeners that have this context. + * @param {Boolean} once Only remove one-time listeners. + * @returns {EventEmitter} `this`. + * @api public + */ +EventEmitter.prototype.removeListener = function removeListener(event, fn, context, once) { + var evt = prefix ? prefix + event : event; + + if (!this._events[evt]) return this; + if (!fn) { + if (--this._eventsCount === 0) this._events = new Events(); + else delete this._events[evt]; + return this; + } + + var listeners = this._events[evt]; + + if (listeners.fn) { + if ( + listeners.fn === fn + && (!once || listeners.once) + && (!context || listeners.context === context) + ) { + if (--this._eventsCount === 0) this._events = new Events(); + else delete this._events[evt]; + } + } else { + for (var i = 0, events = [], length = listeners.length; i < length; i++) { + if ( + listeners[i].fn !== fn + || (once && !listeners[i].once) + || (context && listeners[i].context !== context) + ) { + events.push(listeners[i]); + } + } + + // + // Reset the array, or remove it completely if we have no more listeners. + // + if (events.length) this._events[evt] = events.length === 1 ? events[0] : events; + else if (--this._eventsCount === 0) this._events = new Events(); + else delete this._events[evt]; + } + + return this; +}; + +/** + * Remove all listeners, or those of the specified event. + * + * @param {String|Symbol} [event] The event name. + * @returns {EventEmitter} `this`. + * @api public + */ +EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { + var evt; + + if (event) { + evt = prefix ? prefix + event : event; + if (this._events[evt]) { + if (--this._eventsCount === 0) this._events = new Events(); + else delete this._events[evt]; + } + } else { + this._events = new Events(); + this._eventsCount = 0; + } + + return this; +}; + +// +// Alias methods names because people roll like that. +// +EventEmitter.prototype.off = EventEmitter.prototype.removeListener; +EventEmitter.prototype.addListener = EventEmitter.prototype.on; + +// +// This function doesn't apply anymore. +// +EventEmitter.prototype.setMaxListeners = function setMaxListeners() { + return this; +}; + +// +// Expose the prefix. +// +EventEmitter.prefixed = prefix; + +// +// Allow `EventEmitter` to be imported as module namespace. +// +EventEmitter.EventEmitter = EventEmitter; + +// +// Expose the module. +// +if ('undefined' !== typeof module) { + module.exports = EventEmitter; +} diff --git a/lib/utils/log.js b/lib/utils/log.js new file mode 100644 index 00000000..3ddab6d6 --- /dev/null +++ b/lib/utils/log.js @@ -0,0 +1,40 @@ +import { windowOrGlobal } from './'; + +((base) => { + window = base || window; + if (!window.localStorage) window.localStorage = {}; +})(windowOrGlobal); + +export default class Log { + constructor(namespace) { + this._namespace = namespace || 'RNFirebase'; + require('bows').config({ padLength: 20 }); + this.loggers = {}; + } + + get warn() { + return this._createOrGetLogger('warn'); + } + + get info() { + return this._createOrGetLogger('info'); + } + + get error() { + return this._createOrGetLogger('error'); + } + + get debug() { + return this._createOrGetLogger('debug'); + } + + static enable(booleanOrStringDebug) { + window.localStorage.debug = booleanOrStringDebug; + window.localStorage.debugColors = !!window.localStorage.debug; + } + + _createOrGetLogger(level) { + if (!this.loggers[level]) this.loggers[level] = require('bows')(this._namespace, `[${level}]`); + return this.loggers[level]; + } +} diff --git a/lib/utils/singleton.js b/lib/utils/singleton.js new file mode 100644 index 00000000..d84a48da --- /dev/null +++ b/lib/utils/singleton.js @@ -0,0 +1,35 @@ +const Symbol = require('es6-symbol'); + +class Singleton { + constructor() { + const Class = this.constructor; + if (!Class[this.singleton]) { + Class[this.singleton] = this; + } + + return Class[this.singleton]; + } + + static get instance() { + if (!this[this.singleton]) { + this[this.singleton] = new this(); + } + + return this[this.singleton]; + } + + static set instance(instance) { + this[this.singleton] = instance; + return this[this.singleton]; + } + + static get singleton() { + return Symbol(this.namespace); + } + + static reset() { + delete this[this.singleton]; + } +} + +export default Singleton;