diff --git a/android/src/main/java/io/fullstack/firestack/auth/FirestackAuth.java b/android/src/main/java/io/fullstack/firestack/auth/FirestackAuth.java index 89cce37..3f194ba 100644 --- a/android/src/main/java/io/fullstack/firestack/auth/FirestackAuth.java +++ b/android/src/main/java/io/fullstack/firestack/auth/FirestackAuth.java @@ -25,9 +25,12 @@ import com.google.firebase.auth.UserProfileChangeRequest; import com.google.firebase.auth.FacebookAuthProvider; import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.FirebaseUser; import com.google.firebase.auth.GetTokenResult; import com.google.firebase.auth.GoogleAuthProvider; +import com.google.firebase.auth.EmailAuthProvider; + import io.fullstack.firestack.Utils; @@ -50,7 +53,7 @@ public FirestackAuth(ReactApplicationContext reactContext) { mReactContext = reactContext; mAuth = FirebaseAuth.getInstance(); - Log.d(TAG, "New FirestackAuth instance"); + Log.d(TAG, "New FirestackAuth instance"); } @Override @@ -165,6 +168,43 @@ public void signInWithProvider(final String provider, final String authToken, fi Utils.todoNote(TAG, "signInWithProvider", callback); } + @ReactMethod + public void linkPassword(final String email, final String password, final Callback callback) { + FirebaseUser user = mAuth.getCurrentUser(); + + if (user != null) { + AuthCredential credential = EmailAuthProvider.getCredential(email, password); + user + .linkWithCredential(credential) + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + if (task.isSuccessful()) { + Log.d(TAG, "user linked with password credential"); + userCallback(mAuth.getCurrentUser(), callback); + } else { + userErrorCallback(task, callback); + } + } catch (Exception ex) { + userExceptionCallback(ex, callback); + } + } + }); + } else { + callbackNoUser(callback, true); + } + } + + @ReactMethod + public void link(final String provider, final String authToken, final String authSecret, final Callback callback) { + if (provider.equals("password")) { + linkPassword(authToken, authSecret, callback); + } else + // TODO other providers + Utils.todoNote(TAG, "linkWithProvider", callback); + } + @ReactMethod public void signInAnonymously(final Callback callback) { Log.d(TAG, "signInAnonymously:called:"); @@ -435,9 +475,32 @@ public void signOut(final Callback callback) { callback.invoke(null, resp); } + @ReactMethod + public void reloadUser(final Callback callback) { + FirebaseUser user = mAuth.getCurrentUser(); + + if (user == null) { + callbackNoUser(callback, false); + } else { + user.reload() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (task.isSuccessful()) { + Log.d(TAG, "user reloaded"); + userCallback(mAuth.getCurrentUser(), callback); + } else { + userErrorCallback(task, callback); + } + } + }); + } + } + @ReactMethod public void getCurrentUser(final Callback callback) { FirebaseUser user = mAuth.getCurrentUser(); + if (user == null) { callbackNoUser(callback, false); } else { @@ -513,19 +576,15 @@ public void onComplete(@NonNull Task task) { private void userErrorCallback(Task task, final Callback onFail) { WritableMap error = Arguments.createMap(); - error.putInt("errorCode", task.getException().hashCode()); - error.putString("errorMessage", task.getException().getMessage()); - error.putString("allErrorMessage", task.getException().toString()); - + error.putString("code", ((FirebaseAuthException) task.getException()).getErrorCode()); + error.putString("message", task.getException().getMessage()); onFail.invoke(error); } private void userExceptionCallback(Exception ex, final Callback onFail) { WritableMap error = Arguments.createMap(); - error.putInt("errorCode", ex.hashCode()); - error.putString("errorMessage", ex.getMessage()); - error.putString("allErrorMessage", ex.toString()); - + error.putInt("code", ex.hashCode()); + error.putString("message", ex.getMessage()); onFail.invoke(error); } diff --git a/android/src/main/java/io/fullstack/firestack/messaging/FirestackMessaging.java b/android/src/main/java/io/fullstack/firestack/messaging/FirestackMessaging.java index 7ef06d2..4114e76 100644 --- a/android/src/main/java/io/fullstack/firestack/messaging/FirestackMessaging.java +++ b/android/src/main/java/io/fullstack/firestack/messaging/FirestackMessaging.java @@ -165,21 +165,31 @@ public void unsubscribeFromTopic(String topic, final Callback callback) { } } + // String senderId, String messageId, String messageType, @ReactMethod - public void send(String senderId, String messageId, String messageType, ReadableMap params, final Callback callback) { + public void send(ReadableMap params, final Callback callback) { + ReadableMap data = params.getMap("data"); FirebaseMessaging fm = FirebaseMessaging.getInstance(); - RemoteMessage.Builder remoteMessage = new RemoteMessage.Builder(senderId); - remoteMessage.setMessageId(messageId); - remoteMessage.setMessageType(messageType); - ReadableMapKeySetIterator iterator = params.keySetIterator(); + RemoteMessage.Builder remoteMessage = new RemoteMessage.Builder(params.getString("sender")); + + remoteMessage.setMessageId(params.getString("id")); + remoteMessage.setMessageType(params.getString("type")); + + if (params.hasKey("ttl")) { + remoteMessage.setTtl(params.getInt("ttl")); + } + + if (params.hasKey("collapseKey")) { + remoteMessage.setCollapseKey(params.getString("collapseKey")); + } + + ReadableMapKeySetIterator iterator = data.keySetIterator(); while (iterator.hasNextKey()) { String key = iterator.nextKey(); - ReadableType type = params.getType(key); + ReadableType type = data.getType(key); if (type == ReadableType.String) { - remoteMessage.addData(key, params.getString(key)); - Log.d(TAG, "Firebase send: " + key); - Log.d(TAG, "Firebase send: " + params.getString(key)); + remoteMessage.addData(key, data.getString(key)); } } @@ -187,9 +197,10 @@ public void send(String senderId, String messageId, String messageType, Readable fm.send(remoteMessage.build()); WritableMap res = Arguments.createMap(); res.putString("status", "success"); + Log.d(TAG, "send: Message sent"); callback.invoke(null, res); } catch (Exception e) { - Log.e(TAG, "Error sending message", e); + Log.e(TAG, "send: error sending message", e); WritableMap error = Arguments.createMap(); error.putString("code", e.toString()); error.putString("message", e.toString()); diff --git a/docs/api/authentication.md b/docs/api/authentication.md index 05f2ffe..4ae8684 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -227,7 +227,7 @@ Refreshes the current user. ```javascript firestack.auth().currentUser - .getToken() + .reload() .then((user) => {}) .catch(); ``` diff --git a/docs/api/storage b/docs/api/storage deleted file mode 100644 index 8b13789..0000000 --- a/docs/api/storage +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/api/storage.md b/docs/api/storage.md new file mode 100644 index 0000000..11c7bf4 --- /dev/null +++ b/docs/api/storage.md @@ -0,0 +1,91 @@ + +# Storage + +Firestack mimics the [Web Firebase SDK Storage](https://firebase.google.com/docs/storage/web/start), whilst +providing some iOS and Android specific functionality. + +All Storage operations are accessed via `storage()`. + +## Uploading files + +### Simple + +```javascript +firestack.storage() + .ref('/files/1234') + .putFile('/path/to/file/1234') + .then(uploadedFile => { + //success + }) + .catch(err => { + //Error + }); +``` + +### Listen to upload state + +```javascript +const unsubscribe = firestack.storage() + .ref('/files/1234') + .putFile('/path/to/file/1234') + .on('state_changed', snapshot => { + //Current upload state + }, err => { + //Error + unsubscribe(); + }, uploadedFile => { + //Success + unsubscribe(); + }); +``` + +## Downloading files + +### Simple + +```javascript +firestack.storage() + .ref('/files/1234') + .downloadFile('/path/to/save/file') + .then(downloadedFile => { + //success + }) + .catch(err => { + //Error + }); +``` + +### Listen to download state + +```javascript +const unsubscribe = firestack.storage() + .ref('/files/1234') + .downloadFile('/path/to/save/file') + .on('state_changed', snapshot => { + //Current download state + }, err => { + //Error + unsubscribe(); + }, downloadedFile => { + //Success + unsubscribe(); + }); +``` + +## TODO + +There are a few methods which have not yet been implemented for Storage: + +### Reference +- put() +- putString() + +### UploadTask +- cancel() +- pause() +- resume() + +### DownloadTask +- cancel() +- pause() +- resume() diff --git a/lib/modules/auth/Email.js b/lib/modules/auth/Email.js new file mode 100644 index 0000000..92f4d68 --- /dev/null +++ b/lib/modules/auth/Email.js @@ -0,0 +1,9 @@ +export default { + credential(email, password) { + return { + token: email, + secret: password, + provider: 'password', + }; + }, +}; diff --git a/lib/modules/auth.js b/lib/modules/auth/index.js similarity index 77% rename from lib/modules/auth.js rename to lib/modules/auth/index.js index a067952..e5d0591 100644 --- a/lib/modules/auth.js +++ b/lib/modules/auth/index.js @@ -1,9 +1,10 @@ // @flow import { NativeModules, NativeEventEmitter } from 'react-native'; -import User from './user'; -import { Base } from './base'; -import { promisify } from '../utils'; +import User from './../user'; +import { Base } from './../base'; +import EmailAuthProvider from './Email'; +import { promisify } from './../../utils'; const FirestackAuth = NativeModules.FirestackAuth; const FirestackAuthEvt = new NativeEventEmitter(FirestackAuth); @@ -22,6 +23,9 @@ export default class Auth extends Base { this._authResult = null; this.authenticated = false; + // attach auth providers + // TODO add missing providers + this.EmailAuthProvider = EmailAuthProvider; // start listening straight away // generally though the initial event fired will get ignored // but this is ok as we fake it with the getCurrentUser below @@ -75,7 +79,7 @@ export default class Auth extends Base { */ createUserWithEmailAndPassword(email: string, password: string): Promise { this.log.info('Creating user with email and password', email); - return promisify('createUserWithEmail', FirestackAuth)(email, password); + return promisify('createUserWithEmail', FirestackAuth, 'auth/')(email, password); } /** @@ -86,7 +90,7 @@ export default class Auth extends Base { */ signInWithEmailAndPassword(email: string, password: string): Promise { this.log.info('Signing in user with email and password', email); - return promisify('signInWithEmail', FirestackAuth)(email, password); + return promisify('signInWithEmail', FirestackAuth, 'auth/')(email, password); } // TODO move user methods to User class @@ -97,14 +101,14 @@ export default class Auth extends Base { * @return {Promise} A promise resolved upon completion */ updateEmail(email: string): Promise { - return promisify('updateUserEmail', FirestackAuth)(email); + return promisify('updateUserEmail', FirestackAuth, 'auth/')(email); } /** * Send verification email to current user. */ sendEmailVerification(): Promise { - return promisify('sendEmailVerification', FirestackAuth)(); + return promisify('sendEmailVerification', FirestackAuth, 'auth/')(); } /** @@ -113,7 +117,7 @@ export default class Auth extends Base { * @return {Promise} */ updatePassword(password: string): Promise { - return promisify('updateUserPassword', FirestackAuth)(password); + return promisify('updateUserPassword', FirestackAuth, 'auth/')(password); } /** @@ -122,7 +126,15 @@ export default class Auth extends Base { * @return {Promise} */ updateProfile(updates: Object = {}): Promise { - return promisify('updateUserProfile', FirestackAuth)(updates); + return promisify('updateUserProfile', FirestackAuth, 'auth/')(updates); + } + + /** + * + * @param credential + */ + link(credential: CredentialType) { + return promisify('link', FirestackAuth, 'auth/')(credential.provider, credential.token, credential.secret); } /** @@ -139,7 +151,7 @@ export default class Auth extends Base { * @return {Promise} A promise resolved upon completion */ signInWithCredential(credential: CredentialType): Promise { - return promisify('signInWithProvider', FirestackAuth)(credential.provider, credential.token, credential.secret); + return promisify('signInWithProvider', FirestackAuth, 'auth/')(credential.provider, credential.token, credential.secret); } /** @@ -147,7 +159,7 @@ export default class Auth extends Base { * @return {Promise} A promise resolved upon completion */ reauthenticateUser(credential: CredentialType): Promise { - return promisify('reauthenticateWithCredentialForProvider', FirestackAuth)(credential.provider, credential.token, credential.secret); + return promisify('reauthenticateWithCredentialForProvider', FirestackAuth, 'auth/')(credential.provider, credential.token, credential.secret); } /** @@ -155,7 +167,7 @@ export default class Auth extends Base { * @return {Promise} A promise resolved upon completion */ signInAnonymously(): Promise { - return promisify('signInAnonymously', FirestackAuth)(); + return promisify('signInAnonymously', FirestackAuth, 'auth/')(); } /** @@ -163,7 +175,7 @@ export default class Auth extends Base { * @param {string} email The email to send password reset instructions */ sendPasswordResetEmail(email: string): Promise { - return promisify('sendPasswordResetWithEmail', FirestackAuth)(email); + return promisify('sendPasswordResetWithEmail', FirestackAuth, 'auth/')(email); } /** @@ -171,7 +183,15 @@ export default class Auth extends Base { * @return {Promise} */ deleteUser(): Promise { - return promisify('deleteUser', FirestackAuth)(); + return promisify('deleteUser', FirestackAuth, 'auth/')(); + } + + /** + * Delete the current user + * @return {Promise} + */ + reloadUser(): Promise { + return promisify('reloadUser', FirestackAuth, 'auth/')(); } /** @@ -179,7 +199,7 @@ export default class Auth extends Base { * @return {Promise} */ getToken(): Promise { - return promisify('getToken', FirestackAuth)(); + return promisify('getToken', FirestackAuth, 'auth/')(); } @@ -188,7 +208,7 @@ export default class Auth extends Base { * @return {Promise} */ signOut(): Promise { - return promisify('signOut', FirestackAuth)(); + return promisify('signOut', FirestackAuth, 'auth/')(); } /** @@ -196,7 +216,7 @@ export default class Auth extends Base { * @return {Promise} */ getCurrentUser(): Promise { - return promisify('getCurrentUser', FirestackAuth)(); + return promisify('getCurrentUser', FirestackAuth, 'auth/')(); } /** diff --git a/lib/modules/database/index.js b/lib/modules/database/index.js index fe0a21a..f745391 100644 --- a/lib/modules/database/index.js +++ b/lib/modules/database/index.js @@ -192,7 +192,7 @@ export default class Database extends Base { if (this.subscriptions[handle] && this.subscriptions[handle][eventName]) { this.subscriptions[handle][eventName].forEach((cb) => { - cb(new Snapshot(new Reference(this, path.split('/'), modifiersString.split('|')), snapshot), body); + cb(new Snapshot(new Reference(this, path, modifiersString.split('|')), snapshot), body); }); } else { FirestackDatabase.off(path, modifiersString, eventName, () => { diff --git a/lib/modules/messaging.js b/lib/modules/messaging.js index eb5da3b..1a59d70 100644 --- a/lib/modules/messaging.js +++ b/lib/modules/messaging.js @@ -5,6 +5,15 @@ import { promisify } from '../utils'; const FirestackMessaging = NativeModules.FirestackMessaging || NativeModules.FirestackCloudMessaging; const FirestackMessagingEvt = new NativeEventEmitter(FirestackMessaging); +type RemoteMessage = { + id: string, + type: string, + ttl?: number, + sender: string, + collapseKey?: string, + data: Object, +}; + /** * @class Messaging */ @@ -39,20 +48,21 @@ export default class Messaging extends Base { return promisify('getToken', FirestackMessaging)(); } - sendMessage(details: Object = {}, type: string = 'local') { - const methodName = `send${type == 'local' ? 'Local' : 'Remote'}`; - this.log.info('sendMessage', methodName, details); - return promisify(methodName, FirestackMessaging)(details); - } - - scheduleMessage(details: Object = {}, type: string = 'local') { - const methodName = `schedule${type == 'local' ? 'Local' : 'Remote'}`; - return promisify(methodName, FirestackMessaging)(details); - } + // sendMessage(details: Object = {}, type: string = 'local') { + // const methodName = `send${type == 'local' ? 'Local' : 'Remote'}`; + // this.log.info('sendMessage', methodName, details); + // return promisify(methodName, FirestackMessaging)(details); + // } + // + // scheduleMessage(details: Object = {}, type: string = 'local') { + // const methodName = `schedule${type == 'local' ? 'Local' : 'Remote'}`; + // return promisify(methodName, FirestackMessaging)(details); + // } // OLD - send(senderId, messageId, messageType, msg) { - return promisify('send', FirestackMessaging)(senderId, messageId, messageType, msg); + send(remoteMessage: RemoteMessage) { + if (!remoteMessage || !remoteMessage.data) return Promise.reject(new Error('Invalid remote message format provided.')); + return promisify('send', FirestackMessaging)(remoteMessage); } // diff --git a/lib/modules/storage/reference.js b/lib/modules/storage/reference.js index 267153f..6abfe99 100644 --- a/lib/modules/storage/reference.js +++ b/lib/modules/storage/reference.js @@ -66,8 +66,6 @@ export default class StorageRef extends ReferenceBase { } //Additional methods compared to Web API - - //TODO: Listeners /** * Downloads a reference to the device * @param {String} filePath Where to store the file diff --git a/lib/modules/user.js b/lib/modules/user.js index 429bd67..a86f36f 100644 --- a/lib/modules/user.js +++ b/lib/modules/user.js @@ -92,6 +92,10 @@ export default class User { return this._auth.deleteUser(...args); } + reload(...args) { + return this._auth.reloadUser(...args); + } + // TODO valueOrNul token - optional promise getToken(...args) { return this._auth.getToken(...args); diff --git a/lib/utils/index.js b/lib/utils/index.js index 54bd80d..bce8ba7 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -4,14 +4,18 @@ const hasOwnProperty = Object.hasOwnProperty; const DEFAULT_CHUNK_SIZE = 50; // internal promise handler -const _handler = (resolve, reject, err, resp) => { +const _handler = (resolve, reject, errorPrefix, err, resp) => { // resolve / reject after events etc setImmediate(() => { - if (err) return reject(err); + if (err) return reject(errorPrefix ? { code: toWebSDKErrorCode(err.code || err.errCode || err.errorCode || '', errorPrefix), message: err.message } : err); return resolve(resp); }); }; +export function toWebSDKErrorCode(code, prefix) { + return code.toLowerCase().replace('error_', prefix).replace(/_/g, '-'); +} + /** * Deep get a value from an object. * @website https://github.com/Salakar/deeps @@ -131,13 +135,14 @@ export function noop(): void { * Wraps a native module method to support promises. * @param fn * @param NativeModule + * @param errorPrefix */ -export function promisify(fn: Function, NativeModule: Object): Function { +export function promisify(fn: Function, NativeModule: Object, errorPrefix): Function { return (...args) => { return new Promise((resolve, reject) => { const _fn = typeof fn === 'function' ? fn : NativeModule[fn]; if (!_fn || typeof _fn !== 'function') return reject(new Error('Missing function for promisify.')); - return _fn.apply(NativeModule, [...args, _handler.bind(_handler, resolve, reject)]); + return _fn.apply(NativeModule, [...args, _handler.bind(_handler, resolve, reject, errorPrefix)]); }); }; }