Skip to content

Commit 08cb0c2

Browse files
authored
feat(installations): Firebase JS SDK V9 modular API (#7095)
1 parent db49dfe commit 08cb0c2

File tree

9 files changed

+320
-5
lines changed

9 files changed

+320
-5
lines changed

packages/installations/__tests__/installations.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from '@jest/globals';
22

3-
import { firebase } from '../lib';
3+
import { firebase, getInstallations, onIdChange } from '../lib';
44

55
describe('installations()', function () {
66
describe('namespace', function () {
@@ -20,4 +20,34 @@ describe('installations()', function () {
2020
);
2121
});
2222
});
23+
24+
describe('modular', function () {
25+
describe('getInstallations', function () {
26+
it('returns an instance of Installations', async function () {
27+
const installations = getInstallations();
28+
expect(installations).toBeDefined();
29+
expect(installations.app).toBeDefined();
30+
});
31+
32+
it('supports multiple apps', async function () {
33+
const app = firebase.app();
34+
const secondaryApp = firebase.app('secondaryFromNative');
35+
36+
const installations = getInstallations();
37+
const installationsForApp = getInstallations(secondaryApp);
38+
39+
expect(installations.app).toEqual(app);
40+
expect(installationsForApp.app).toEqual(secondaryApp);
41+
});
42+
});
43+
44+
describe('onIdChange', function () {
45+
it('throws an unsupported error', async function () {
46+
const installations = getInstallations();
47+
expect(() => onIdChange(installations, () => {})).toThrow(
48+
'onIdChange() is unsupported by the React Native Firebase SDK.',
49+
);
50+
});
51+
});
52+
});
2353
});
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright (c) 2016-present Invertase Limited & Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this library except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
const jwt = require('jsonwebtoken');
19+
20+
const ID_LENGTH = 22;
21+
const PROJECT_ID = 448618578101; // this is "magic", it's the react-native-firebase-testing project ID
22+
23+
describe('installations() modular', function () {
24+
describe('firebase v8 compatibility', function () {
25+
describe('getId()', function () {
26+
it('returns a valid installation id', async function () {
27+
const id = await firebase.installations().getId();
28+
id.should.be.a.String();
29+
id.length.should.be.equals(ID_LENGTH);
30+
});
31+
});
32+
33+
describe('getToken()', function () {
34+
it('returns a valid auth token with no arguments', async function () {
35+
const id = await firebase.installations().getId();
36+
const token = await firebase.installations().getToken();
37+
token.should.be.a.String();
38+
token.should.not.equal('');
39+
const decodedToken = jwt.decode(token);
40+
decodedToken.fid.should.equal(id); // fid == firebase installations id
41+
decodedToken.projectNumber.should.equal(PROJECT_ID);
42+
43+
// token time is "Unix epoch time", which is in seconds vs javascript milliseconds
44+
if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) {
45+
return Promise.reject(
46+
new Error('Token already expired: ' + JSON.stringify(decodedToken)),
47+
);
48+
}
49+
50+
const token2 = await firebase.installations().getToken(true);
51+
token2.should.be.a.String();
52+
token2.should.not.equal('');
53+
const decodedToken2 = jwt.decode(token2);
54+
decodedToken2.fid.should.equal(id);
55+
decodedToken2.projectNumber.should.equal(PROJECT_ID);
56+
57+
// token time is "Unix epoch time", which is in seconds vs javascript milliseconds
58+
if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) {
59+
return Promise.reject(new Error('Token already expired'));
60+
}
61+
(token === token2).should.be.false();
62+
});
63+
});
64+
65+
describe('delete()', function () {
66+
it('successfully deletes', async function () {
67+
const id = await firebase.installations().getId();
68+
id.should.be.a.String();
69+
id.length.should.be.equals(ID_LENGTH);
70+
await firebase.installations().delete();
71+
72+
// New id should be different
73+
const id2 = await firebase.installations().getId();
74+
id2.should.be.a.String();
75+
id2.length.should.be.equals(ID_LENGTH);
76+
(id === id2).should.be.false();
77+
78+
const token = await firebase.installations().getToken(false);
79+
const decodedToken = jwt.decode(token);
80+
decodedToken.fid.should.equal(id2); // fid == firebase installations id
81+
82+
// token time is "Unix epoch time", which is in seconds vs javascript milliseconds
83+
decodedToken.projectNumber.should.equal(PROJECT_ID);
84+
if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) {
85+
return Promise.reject(new Error('Token already expired'));
86+
}
87+
});
88+
});
89+
});
90+
91+
describe('modular', function () {
92+
describe('getId()', function () {
93+
it('returns a valid installation id', async function () {
94+
const { getInstallations, getId } = installationsModular;
95+
const installations = getInstallations();
96+
97+
const id = await getId(installations);
98+
id.should.be.a.String();
99+
id.length.should.be.equals(ID_LENGTH);
100+
});
101+
});
102+
103+
describe('getToken()', function () {
104+
it('returns a valid auth token with no arguments', async function () {
105+
const { getInstallations, getId, getToken } = installationsModular;
106+
const installations = getInstallations();
107+
108+
const id = await getId(installations);
109+
const token = await getToken(installations);
110+
token.should.be.a.String();
111+
token.should.not.equal('');
112+
const decodedToken = jwt.decode(token);
113+
decodedToken.fid.should.equal(id); // fid == firebase installations id
114+
decodedToken.projectNumber.should.equal(PROJECT_ID);
115+
116+
// token time is "Unix epoch time", which is in seconds vs javascript milliseconds
117+
if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) {
118+
return Promise.reject(
119+
new Error('Token already expired: ' + JSON.stringify(decodedToken)),
120+
);
121+
}
122+
123+
const token2 = await getToken(installations, true);
124+
token2.should.be.a.String();
125+
token2.should.not.equal('');
126+
const decodedToken2 = jwt.decode(token2);
127+
decodedToken2.fid.should.equal(id);
128+
decodedToken2.projectNumber.should.equal(PROJECT_ID);
129+
130+
// token time is "Unix epoch time", which is in seconds vs javascript milliseconds
131+
if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) {
132+
return Promise.reject(new Error('Token already expired'));
133+
}
134+
(token === token2).should.be.false();
135+
});
136+
});
137+
138+
describe('deleteInstallations()', function () {
139+
it('successfully deletes', async function () {
140+
const { getInstallations, getId, getToken, deleteInstallations } = installationsModular;
141+
const installations = getInstallations();
142+
143+
const id = await getId(installations);
144+
id.should.be.a.String();
145+
id.length.should.be.equals(ID_LENGTH);
146+
await deleteInstallations(installations);
147+
148+
// New id should be different
149+
const id2 = await getId(installations);
150+
id2.should.be.a.String();
151+
id2.length.should.be.equals(ID_LENGTH);
152+
(id === id2).should.be.false();
153+
154+
const token = await getToken(installations, false);
155+
const decodedToken = jwt.decode(token);
156+
decodedToken.fid.should.equal(id2); // fid == firebase installations id
157+
158+
// token time is "Unix epoch time", which is in seconds vs javascript milliseconds
159+
decodedToken.projectNumber.should.equal(PROJECT_ID);
160+
if (decodedToken.exp < Math.round(new Date().getTime() / 1000)) {
161+
return Promise.reject(new Error('Token already expired'));
162+
}
163+
});
164+
});
165+
});
166+
});

packages/installations/lib/index.d.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export namespace FirebaseInstallationsTypes {
8989
* stable, URL-safe base64 string identifier that uniquely identifies the app instance.
9090
* NOTE: If the application already has an existing FirebaseInstanceID then the InstanceID identifier will be used.
9191
*
92-
* @return Firebase Installation ID, this is a url-safe base64 string of a 128-bit integer.
92+
* @return Firebase Installation ID, this is an url-safe base64 string of a 128-bit integer.
9393
*/
9494
getId(): Promise<string>;
9595

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

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

142+
export * from './modular';
143+
142144
export default defaultExport;
143145

144146
/**
@@ -147,12 +149,14 @@ export default defaultExport;
147149
declare module '@react-native-firebase/app' {
148150
namespace ReactNativeFirebase {
149151
import FirebaseModuleWithStaticsAndApp = ReactNativeFirebase.FirebaseModuleWithStaticsAndApp;
152+
150153
interface Module {
151154
installations: FirebaseModuleWithStaticsAndApp<
152155
FirebaseInstallationsTypes.Module,
153156
FirebaseInstallationsTypes.Statics
154157
>;
155158
}
159+
156160
interface FirebaseApp {
157161
installations(): FirebaseInstallationsTypes.Module;
158162
}

packages/installations/lib/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,5 @@ export default createModuleNamespace({
7777
// installations().X(...);
7878
// firebase.installations().X(...);
7979
export const firebase = getFirebaseRoot();
80+
81+
export * from './modular';
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { ReactNativeFirebase } from '@react-native-firebase/app';
2+
import { FirebaseInstallationsTypes } from '../index';
3+
4+
/**
5+
* Returns an instance of Installations associated with the given FirebaseApp instance.
6+
*/
7+
export declare function getInstallations(
8+
app?: ReactNativeFirebase.FirebaseApp,
9+
): FirebaseInstallationsTypes.Module;
10+
11+
/**
12+
* Deletes the Firebase Installation and all associated data.
13+
*/
14+
export declare function deleteInstallations(
15+
installations?: FirebaseInstallationsTypes.Module,
16+
): Promise<void>;
17+
18+
/**
19+
* Creates a Firebase Installation if there isn't one for the app and returns the Installation ID.
20+
*/
21+
export declare function getId(installations: FirebaseInstallationsTypes.Module): Promise<string>;
22+
23+
/**
24+
* Returns a Firebase Installations auth token, identifying the current Firebase Installation.
25+
*/
26+
export declare function getToken(
27+
installations: FirebaseInstallationsTypes.Module,
28+
forceRefresh?: boolean,
29+
): Promise<string>;
30+
31+
declare type IdChangeCallbackFn = (installationId: string) => void;
32+
declare type IdChangeUnsubscribeFn = () => void;
33+
34+
/**
35+
* Throw an error since react-native-firebase does not support this method.
36+
*
37+
* Sets a new callback that will get called when Installation ID changes. Returns an unsubscribe function that will remove the callback when called.
38+
*/
39+
export declare function onIdChange(
40+
installations: FirebaseInstallationsTypes.Module,
41+
callback: IdChangeCallbackFn,
42+
): IdChangeUnsubscribeFn;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright (c) 2016-present Invertase Limited & Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this library except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
import { firebase } from '..';
19+
20+
/**
21+
* @param {import("@react-native-firebase/app").ReactNativeFirebase.FirebaseApp} app
22+
* @returns {import("..").FirebaseInstallationsTypes.Module}
23+
*/
24+
export function getInstallations(app) {
25+
if (app) {
26+
return firebase.app(app.name).installations();
27+
}
28+
return firebase.app().installations();
29+
}
30+
31+
/**
32+
* @param {import("..").FirebaseInstallationsTypes.Module} installations
33+
* @returns {Promise<void>}
34+
*/
35+
export function deleteInstallations(installations) {
36+
return firebase.app(installations.app.name).installations().delete();
37+
}
38+
39+
/**
40+
* @param {import("..").FirebaseInstallationsTypes.Module} installations
41+
* @returns {Promise<string>}
42+
*/
43+
export function getId(installations) {
44+
return firebase.app(installations.app.name).installations().getId();
45+
}
46+
47+
/**
48+
* @param {import("..").FirebaseInstallationsTypes.Module} installations
49+
* @param {boolean | undefined} forceRefresh
50+
* @returns {Promise<string>}
51+
*/
52+
export function getToken(installations, forceRefresh) {
53+
return firebase.app(installations.app.name).installations().getToken(forceRefresh);
54+
}
55+
56+
/**
57+
* @param {import("..").FirebaseInstallationsTypes.Module} installations
58+
* @param {(string) => void} callback
59+
* @returns {() => void}
60+
*/
61+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
62+
export function onIdChange(installations, callback) {
63+
throw new Error('onIdChange() is unsupported by the React Native Firebase SDK.');
64+
}

tests/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import jet from 'jet/platform/react-native';
4343
import React from 'react';
4444
import { AppRegistry, Button, NativeModules, Text, View } from 'react-native';
4545
import DeviceInfo from 'react-native-device-info';
46+
import * as installationsModular from '@react-native-firebase/installations';
4647

4748
jet.exposeContextProperty('NativeModules', NativeModules);
4849
jet.exposeContextProperty('NativeEventEmitter', NativeEventEmitter);
@@ -56,6 +57,7 @@ jet.exposeContextProperty('perfModular', perfModular);
5657
jet.exposeContextProperty('appCheckModular', appCheckModular);
5758
jet.exposeContextProperty('messagingModular', messagingModular);
5859
jet.exposeContextProperty('storageModular', storageModular);
60+
jet.exposeContextProperty('installationsModular', installationsModular);
5961

6062
firebase.database().useEmulator('localhost', 9000);
6163
firebase.auth().useEmulator('http://localhost:9099');

tests/e2e/.mocharc.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ module.exports = {
88
retries: 4,
99
bail: true,
1010
exit: true,
11-
recursive: true,
1211
require: 'node_modules/jet/platform/node',
1312
spec: [
1413
'../packages/app/e2e/**/*.e2e.js',

0 commit comments

Comments
 (0)