Skip to content

Commit

Permalink
feat(installations): Firebase JS SDK V9 modular API (#7095)
Browse files Browse the repository at this point in the history
  • Loading branch information
exaby73 authored Jun 13, 2023
1 parent db49dfe commit 08cb0c2
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 5 deletions.
32 changes: 31 additions & 1 deletion packages/installations/__tests__/installations.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from '@jest/globals';

import { firebase } from '../lib';
import { firebase, getInstallations, onIdChange } from '../lib';

describe('installations()', function () {
describe('namespace', function () {
Expand All @@ -20,4 +20,34 @@ describe('installations()', function () {
);
});
});

describe('modular', function () {
describe('getInstallations', function () {
it('returns an instance of Installations', async function () {
const installations = getInstallations();
expect(installations).toBeDefined();
expect(installations.app).toBeDefined();
});

it('supports multiple apps', async function () {
const app = firebase.app();
const secondaryApp = firebase.app('secondaryFromNative');

const installations = getInstallations();
const installationsForApp = getInstallations(secondaryApp);

expect(installations.app).toEqual(app);
expect(installationsForApp.app).toEqual(secondaryApp);
});
});

describe('onIdChange', function () {
it('throws an unsupported error', async function () {
const installations = getInstallations();
expect(() => onIdChange(installations, () => {})).toThrow(
'onIdChange() is unsupported by the React Native Firebase SDK.',
);
});
});
});
});
166 changes: 166 additions & 0 deletions packages/installations/e2e/installations.modular.e2e.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* Copyright (c) 2016-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library 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.
*
*/

const jwt = require('jsonwebtoken');

const ID_LENGTH = 22;
const PROJECT_ID = 448618578101; // this is "magic", it's the react-native-firebase-testing project ID

describe('installations() modular', function () {
describe('firebase v8 compatibility', function () {
describe('getId()', function () {
it('returns a valid installation id', async function () {
const id = await firebase.installations().getId();
id.should.be.a.String();
id.length.should.be.equals(ID_LENGTH);
});
});

describe('getToken()', function () {
it('returns a valid auth token with no arguments', async function () {
const id = await firebase.installations().getId();
const token = await firebase.installations().getToken();
token.should.be.a.String();
token.should.not.equal('');
const decodedToken = jwt.decode(token);
decodedToken.fid.should.equal(id); // fid == firebase installations id
decodedToken.projectNumber.should.equal(PROJECT_ID);

// token time is "Unix epoch time", which is in seconds vs javascript milliseconds
if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) {
return Promise.reject(
new Error('Token already expired: ' + JSON.stringify(decodedToken)),
);
}

const token2 = await firebase.installations().getToken(true);
token2.should.be.a.String();
token2.should.not.equal('');
const decodedToken2 = jwt.decode(token2);
decodedToken2.fid.should.equal(id);
decodedToken2.projectNumber.should.equal(PROJECT_ID);

// token time is "Unix epoch time", which is in seconds vs javascript milliseconds
if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) {
return Promise.reject(new Error('Token already expired'));
}
(token === token2).should.be.false();
});
});

describe('delete()', function () {
it('successfully deletes', async function () {
const id = await firebase.installations().getId();
id.should.be.a.String();
id.length.should.be.equals(ID_LENGTH);
await firebase.installations().delete();

// New id should be different
const id2 = await firebase.installations().getId();
id2.should.be.a.String();
id2.length.should.be.equals(ID_LENGTH);
(id === id2).should.be.false();

const token = await firebase.installations().getToken(false);
const decodedToken = jwt.decode(token);
decodedToken.fid.should.equal(id2); // fid == firebase installations id

// token time is "Unix epoch time", which is in seconds vs javascript milliseconds
decodedToken.projectNumber.should.equal(PROJECT_ID);
if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) {
return Promise.reject(new Error('Token already expired'));
}
});
});
});

describe('modular', function () {
describe('getId()', function () {
it('returns a valid installation id', async function () {
const { getInstallations, getId } = installationsModular;
const installations = getInstallations();

const id = await getId(installations);
id.should.be.a.String();
id.length.should.be.equals(ID_LENGTH);
});
});

describe('getToken()', function () {
it('returns a valid auth token with no arguments', async function () {
const { getInstallations, getId, getToken } = installationsModular;
const installations = getInstallations();

const id = await getId(installations);
const token = await getToken(installations);
token.should.be.a.String();
token.should.not.equal('');
const decodedToken = jwt.decode(token);
decodedToken.fid.should.equal(id); // fid == firebase installations id
decodedToken.projectNumber.should.equal(PROJECT_ID);

// token time is "Unix epoch time", which is in seconds vs javascript milliseconds
if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) {
return Promise.reject(
new Error('Token already expired: ' + JSON.stringify(decodedToken)),
);
}

const token2 = await getToken(installations, true);
token2.should.be.a.String();
token2.should.not.equal('');
const decodedToken2 = jwt.decode(token2);
decodedToken2.fid.should.equal(id);
decodedToken2.projectNumber.should.equal(PROJECT_ID);

// token time is "Unix epoch time", which is in seconds vs javascript milliseconds
if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) {
return Promise.reject(new Error('Token already expired'));
}
(token === token2).should.be.false();
});
});

describe('deleteInstallations()', function () {
it('successfully deletes', async function () {
const { getInstallations, getId, getToken, deleteInstallations } = installationsModular;
const installations = getInstallations();

const id = await getId(installations);
id.should.be.a.String();
id.length.should.be.equals(ID_LENGTH);
await deleteInstallations(installations);

// New id should be different
const id2 = await getId(installations);
id2.should.be.a.String();
id2.length.should.be.equals(ID_LENGTH);
(id === id2).should.be.false();

const token = await getToken(installations, false);
const decodedToken = jwt.decode(token);
decodedToken.fid.should.equal(id2); // fid == firebase installations id

// token time is "Unix epoch time", which is in seconds vs javascript milliseconds
decodedToken.projectNumber.should.equal(PROJECT_ID);
if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) {
return Promise.reject(new Error('Token already expired'));
}
});
});
});
});
10 changes: 7 additions & 3 deletions packages/installations/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export namespace FirebaseInstallationsTypes {
* stable, URL-safe base64 string identifier that uniquely identifies the app instance.
* NOTE: If the application already has an existing FirebaseInstanceID then the InstanceID identifier will be used.
*
* @return Firebase Installation ID, this is a url-safe base64 string of a 128-bit integer.
* @return Firebase Installation ID, this is an url-safe base64 string of a 128-bit integer.
*/
getId(): Promise<string>;

Expand All @@ -109,15 +109,15 @@ export namespace FirebaseInstallationsTypes {
* Deletes the Firebase Installation and all associated data from the Firebase backend.
* This call may cause Firebase Cloud Messaging, Firebase Remote Config, Firebase Predictions,
* or Firebase In-App Messaging to not function properly. Fetching a new installations ID should
* reset all of the dependent services to a stable state again. A network connection is required
* reset all the dependent services to a stable state again. A network connection is required
* for the method to succeed. If it fails, the existing installation data remains untouched.
*/
delete(): Promise<void>;

/**
* TODO implement id change listener for android.
*
* Sets a new callback that will get called when Installlation ID changes.
* Sets a new callback that will get called when Installation ID changes.
* Returns an unsubscribe function that will remove the callback when called.
* Only the Android SDK supports sending ID change events.
*
Expand All @@ -139,6 +139,8 @@ export const firebase: ReactNativeFirebase.Module & {
): ReactNativeFirebase.FirebaseApp & { installations(): FirebaseInstallationsTypes.Module };
};

export * from './modular';

export default defaultExport;

/**
Expand All @@ -147,12 +149,14 @@ export default defaultExport;
declare module '@react-native-firebase/app' {
namespace ReactNativeFirebase {
import FirebaseModuleWithStaticsAndApp = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp;

interface Module {
installations: FirebaseModuleWithStaticsAndApp<
FirebaseInstallationsTypes.Module,
FirebaseInstallationsTypes.Statics
>;
}

interface FirebaseApp {
installations(): FirebaseInstallationsTypes.Module;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/installations/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,5 @@ export default createModuleNamespace({
// installations().X(...);
// firebase.installations().X(...);
export const firebase = getFirebaseRoot();

export * from './modular';
42 changes: 42 additions & 0 deletions packages/installations/lib/modular/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ReactNativeFirebase } from '@react-native-firebase/app';
import { FirebaseInstallationsTypes } from '../index';

/**
* Returns an instance of Installations associated with the given FirebaseApp instance.
*/
export declare function getInstallations(
app?: ReactNativeFirebase.FirebaseApp,
): FirebaseInstallationsTypes.Module;

/**
* Deletes the Firebase Installation and all associated data.
*/
export declare function deleteInstallations(
installations?: FirebaseInstallationsTypes.Module,
): Promise<void>;

/**
* Creates a Firebase Installation if there isn't one for the app and returns the Installation ID.
*/
export declare function getId(installations: FirebaseInstallationsTypes.Module): Promise<string>;

/**
* Returns a Firebase Installations auth token, identifying the current Firebase Installation.
*/
export declare function getToken(
installations: FirebaseInstallationsTypes.Module,
forceRefresh?: boolean,
): Promise<string>;

declare type IdChangeCallbackFn = (installationId: string) => void;
declare type IdChangeUnsubscribeFn = () => void;

/**
* Throw an error since react-native-firebase does not support this method.
*
* Sets a new callback that will get called when Installation ID changes. Returns an unsubscribe function that will remove the callback when called.
*/
export declare function onIdChange(
installations: FirebaseInstallationsTypes.Module,
callback: IdChangeCallbackFn,
): IdChangeUnsubscribeFn;
64 changes: 64 additions & 0 deletions packages/installations/lib/modular/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (c) 2016-present Invertase Limited & Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this library 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 { firebase } from '..';

/**
* @param {import("@react-native-firebase/app").ReactNativeFirebase.FirebaseApp} app
* @returns {import("..").FirebaseInstallationsTypes.Module}
*/
export function getInstallations(app) {
if (app) {
return firebase.app(app.name).installations();
}
return firebase.app().installations();
}

/**
* @param {import("..").FirebaseInstallationsTypes.Module} installations
* @returns {Promise<void>}
*/
export function deleteInstallations(installations) {
return firebase.app(installations.app.name).installations().delete();
}

/**
* @param {import("..").FirebaseInstallationsTypes.Module} installations
* @returns {Promise<string>}
*/
export function getId(installations) {
return firebase.app(installations.app.name).installations().getId();
}

/**
* @param {import("..").FirebaseInstallationsTypes.Module} installations
* @param {boolean | undefined} forceRefresh
* @returns {Promise<string>}
*/
export function getToken(installations, forceRefresh) {
return firebase.app(installations.app.name).installations().getToken(forceRefresh);
}

/**
* @param {import("..").FirebaseInstallationsTypes.Module} installations
* @param {(string) => void} callback
* @returns {() => void}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function onIdChange(installations, callback) {
throw new Error('onIdChange() is unsupported by the React Native Firebase SDK.');
}
2 changes: 2 additions & 0 deletions tests/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import jet from 'jet/platform/react-native';
import React from 'react';
import { AppRegistry, Button, NativeModules, Text, View } from 'react-native';
import DeviceInfo from 'react-native-device-info';
import * as installationsModular from '@react-native-firebase/installations';

jet.exposeContextProperty('NativeModules', NativeModules);
jet.exposeContextProperty('NativeEventEmitter', NativeEventEmitter);
Expand All @@ -56,6 +57,7 @@ jet.exposeContextProperty('perfModular', perfModular);
jet.exposeContextProperty('appCheckModular', appCheckModular);
jet.exposeContextProperty('messagingModular', messagingModular);
jet.exposeContextProperty('storageModular', storageModular);
jet.exposeContextProperty('installationsModular', installationsModular);

firebase.database().useEmulator('localhost', 9000);
firebase.auth().useEmulator('http://localhost:9099');
Expand Down
1 change: 0 additions & 1 deletion tests/e2e/.mocharc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ module.exports = {
retries: 4,
bail: true,
exit: true,
recursive: true,
require: 'node_modules/jet/platform/node',
spec: [
'../packages/app/e2e/**/*.e2e.js',
Expand Down
Loading

0 comments on commit 08cb0c2

Please sign in to comment.