From 8b42af1bd033b54768cb03a1c43c0e3b16ee809c Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Thu, 13 Aug 2020 11:40:20 -0700 Subject: [PATCH 01/22] copied orginal FIS sdk into packages-expp and made unit tests passed --- packages-exp/installations-exp/.eslintrc.js | 26 + packages-exp/installations-exp/karma.conf.js | 35 ++ packages-exp/installations-exp/package.json | 58 +++ .../installations-exp/rollup.config.js | 70 +++ .../installations-exp/src/api/common.test.ts | 74 +++ .../installations-exp/src/api/common.ts | 111 ++++ .../api/create-installation-request.test.ts | 165 ++++++ .../src/api/create-installation-request.ts | 67 +++ .../api/delete-installation-request.test.ts | 123 +++++ .../src/api/delete-installation-request.ts | 50 ++ .../api/generate-auth-token-request.test.ts | 150 ++++++ .../src/api/generate-auth-token-request.ts | 79 +++ .../src/functions/delete-installation.test.ts | 129 +++++ .../src/functions/delete-installation.ts | 50 ++ .../src/functions/get-id.test.ts | 89 ++++ .../installations-exp/src/functions/get-id.ts | 38 ++ .../src/functions/get-token.test.ts | 465 +++++++++++++++++ .../src/functions/get-token.ts | 44 ++ .../installations-exp/src/functions/index.ts | 21 + .../src/functions/on-id-change.test.ts | 52 ++ .../src/functions/on-id-change.ts | 37 ++ .../helpers/buffer-to-base64-url-safe.test.ts | 36 ++ .../src/helpers/buffer-to-base64-url-safe.ts | 21 + .../src/helpers/extract-app-config.test.ts | 60 +++ .../src/helpers/extract-app-config.ts | 57 +++ .../src/helpers/fid-changed.test.ts | 103 ++++ .../src/helpers/fid-changed.ts | 110 ++++ .../src/helpers/generate-fid.test.ts | 128 +++++ .../src/helpers/generate-fid.ts | 55 ++ .../helpers/get-installation-entry.test.ts | 477 ++++++++++++++++++ .../src/helpers/get-installation-entry.ts | 227 +++++++++ .../src/helpers/idb-manager.test.ts | 194 +++++++ .../src/helpers/idb-manager.ts | 123 +++++ .../src/helpers/refresh-auth-token.test.ts | 208 ++++++++ .../src/helpers/refresh-auth-token.ts | 209 ++++++++ packages-exp/installations-exp/src/index.ts | 85 ++++ .../src/interfaces/api-response.ts | 34 ++ .../src/interfaces/app-config.ts | 23 + .../src/interfaces/firebase-dependencies.ts | 24 + .../src/interfaces/installation-entry.ts | 110 ++++ .../src/testing/compare-headers.test.ts | 44 ++ .../src/testing/compare-headers.ts | 41 ++ .../src/testing/fake-generators.ts | 68 +++ .../installations-exp/src/testing/setup.ts | 30 ++ .../installations-exp/src/util/constants.ts | 31 ++ .../installations-exp/src/util/errors.ts | 72 +++ .../installations-exp/src/util/get-key.ts | 23 + .../installations-exp/src/util/sleep.test.ts | 37 ++ .../installations-exp/src/util/sleep.ts | 23 + packages-exp/installations-exp/tsconfig.json | 14 + .../installations-types-exp/index.d.ts | 54 ++ .../installations-types-exp/package.json | 29 ++ .../installations-types-exp/tsconfig.json | 7 + 53 files changed, 4690 insertions(+) create mode 100644 packages-exp/installations-exp/.eslintrc.js create mode 100644 packages-exp/installations-exp/karma.conf.js create mode 100644 packages-exp/installations-exp/package.json create mode 100644 packages-exp/installations-exp/rollup.config.js create mode 100644 packages-exp/installations-exp/src/api/common.test.ts create mode 100644 packages-exp/installations-exp/src/api/common.ts create mode 100644 packages-exp/installations-exp/src/api/create-installation-request.test.ts create mode 100644 packages-exp/installations-exp/src/api/create-installation-request.ts create mode 100644 packages-exp/installations-exp/src/api/delete-installation-request.test.ts create mode 100644 packages-exp/installations-exp/src/api/delete-installation-request.ts create mode 100644 packages-exp/installations-exp/src/api/generate-auth-token-request.test.ts create mode 100644 packages-exp/installations-exp/src/api/generate-auth-token-request.ts create mode 100644 packages-exp/installations-exp/src/functions/delete-installation.test.ts create mode 100644 packages-exp/installations-exp/src/functions/delete-installation.ts create mode 100644 packages-exp/installations-exp/src/functions/get-id.test.ts create mode 100644 packages-exp/installations-exp/src/functions/get-id.ts create mode 100644 packages-exp/installations-exp/src/functions/get-token.test.ts create mode 100644 packages-exp/installations-exp/src/functions/get-token.ts create mode 100644 packages-exp/installations-exp/src/functions/index.ts create mode 100644 packages-exp/installations-exp/src/functions/on-id-change.test.ts create mode 100644 packages-exp/installations-exp/src/functions/on-id-change.ts create mode 100644 packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.test.ts create mode 100644 packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.ts create mode 100644 packages-exp/installations-exp/src/helpers/extract-app-config.test.ts create mode 100644 packages-exp/installations-exp/src/helpers/extract-app-config.ts create mode 100644 packages-exp/installations-exp/src/helpers/fid-changed.test.ts create mode 100644 packages-exp/installations-exp/src/helpers/fid-changed.ts create mode 100644 packages-exp/installations-exp/src/helpers/generate-fid.test.ts create mode 100644 packages-exp/installations-exp/src/helpers/generate-fid.ts create mode 100644 packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts create mode 100644 packages-exp/installations-exp/src/helpers/get-installation-entry.ts create mode 100644 packages-exp/installations-exp/src/helpers/idb-manager.test.ts create mode 100644 packages-exp/installations-exp/src/helpers/idb-manager.ts create mode 100644 packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts create mode 100644 packages-exp/installations-exp/src/helpers/refresh-auth-token.ts create mode 100644 packages-exp/installations-exp/src/index.ts create mode 100644 packages-exp/installations-exp/src/interfaces/api-response.ts create mode 100644 packages-exp/installations-exp/src/interfaces/app-config.ts create mode 100644 packages-exp/installations-exp/src/interfaces/firebase-dependencies.ts create mode 100644 packages-exp/installations-exp/src/interfaces/installation-entry.ts create mode 100644 packages-exp/installations-exp/src/testing/compare-headers.test.ts create mode 100644 packages-exp/installations-exp/src/testing/compare-headers.ts create mode 100644 packages-exp/installations-exp/src/testing/fake-generators.ts create mode 100644 packages-exp/installations-exp/src/testing/setup.ts create mode 100644 packages-exp/installations-exp/src/util/constants.ts create mode 100644 packages-exp/installations-exp/src/util/errors.ts create mode 100644 packages-exp/installations-exp/src/util/get-key.ts create mode 100644 packages-exp/installations-exp/src/util/sleep.test.ts create mode 100644 packages-exp/installations-exp/src/util/sleep.ts create mode 100644 packages-exp/installations-exp/tsconfig.json create mode 100644 packages-exp/installations-types-exp/index.d.ts create mode 100644 packages-exp/installations-types-exp/package.json create mode 100644 packages-exp/installations-types-exp/tsconfig.json diff --git a/packages-exp/installations-exp/.eslintrc.js b/packages-exp/installations-exp/.eslintrc.js new file mode 100644 index 00000000000..ca80aa0f69a --- /dev/null +++ b/packages-exp/installations-exp/.eslintrc.js @@ -0,0 +1,26 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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. + */ + +module.exports = { + extends: '../../config/.eslintrc.js', + parserOptions: { + project: 'tsconfig.json', + // to make vscode-eslint work with monorepo + // https://github.com/typescript-eslint/typescript-eslint/issues/251#issuecomment-463943250 + tsconfigRootDir: __dirname + } +}; diff --git a/packages-exp/installations-exp/karma.conf.js b/packages-exp/installations-exp/karma.conf.js new file mode 100644 index 00000000000..1699a0681ec --- /dev/null +++ b/packages-exp/installations-exp/karma.conf.js @@ -0,0 +1,35 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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. + */ + +const karmaBase = require('../../config/karma.base'); + +const files = [`src/**/*.test.ts`]; + +module.exports = function (config) { + const karmaConfig = { + ...karmaBase, + // files to load into karma + files, + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ['mocha'] + }; + + config.set(karmaConfig); +}; + +module.exports.files = files; diff --git a/packages-exp/installations-exp/package.json b/packages-exp/installations-exp/package.json new file mode 100644 index 00000000000..235397dfcb2 --- /dev/null +++ b/packages-exp/installations-exp/package.json @@ -0,0 +1,58 @@ +{ + "name": "@firebase/installations-exp", + "version": "0.1.0", + "private": true, + "author": "Firebase (https://firebase.google.com/)", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "esm2017": "dist/index.esm2017.js", + "types": "dist/src/index.d.ts", + "license": "Apache-2.0", + "files": [ + "dist" + ], + "scripts": { + "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", + "build": "rollup -c", + "build:deps": "lerna run --scope @firebase/'{app,installations-exp}' --include-dependencies build", + "dev": "rollup -c -w", + "test": "yarn type-check && yarn test:karma && yarn lint", + "test:ci": "node ../../scripts/run_tests_in_ci.js", + "test:karma": "karma start --single-run", + "test:debug": "karma start --browsers=Chrome --auto-watch", + "type-check": "tsc -p . --noEmit", + "serve": "yarn serve:build && yarn serve:host", + "serve:build": "rollup -c test-app/rollup.config.js", + "serve:host": "http-server -c-1 test-app", + "prepare": "yarn build" + }, + "repository": { + "directory": "packages-exp/installations-exp", + "type": "git", + "url": "https://github.com/firebase/firebase-js-sdk.git" + }, + "bugs": { + "url": "https://github.com/firebase/firebase-js-sdk/issues" + }, + "devDependencies": { + "rollup": "2.23.0", + "rollup-plugin-commonjs": "10.1.0", + "rollup-plugin-json": "4.0.0", + "rollup-plugin-node-resolve": "5.2.0", + "rollup-plugin-typescript2": "0.27.1", + "rollup-plugin-uglify": "6.0.4", + "typescript": "3.9.7" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + }, + "dependencies": { + "@firebase/installations-types-exp": "0.1.0", + "@firebase/util": "0.3.0", + "@firebase/component": "0.1.17", + "idb": "3.0.2", + "tslib": "^1.11.1" + } +} diff --git a/packages-exp/installations-exp/rollup.config.js b/packages-exp/installations-exp/rollup.config.js new file mode 100644 index 00000000000..a10d038fd95 --- /dev/null +++ b/packages-exp/installations-exp/rollup.config.js @@ -0,0 +1,70 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 json from 'rollup-plugin-json'; +import typescriptPlugin from 'rollup-plugin-typescript2'; +import pkg from './package.json'; +import typescript from 'typescript'; + +const deps = Object.keys({ ...pkg.peerDependencies, ...pkg.dependencies }); + +/** + * ES5 Builds + */ +const es5BuildPlugins = [typescriptPlugin({ typescript }), json()]; + +const es5Builds = [ + { + input: 'src/index.ts', + output: [ + { file: pkg.main, format: 'cjs', sourcemap: true }, + { file: pkg.module, format: 'es', sourcemap: true } + ], + plugins: es5BuildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + } +]; + +/** + * ES2017 Builds + */ +const es2017BuildPlugins = [ + typescriptPlugin({ + typescript, + tsconfigOverride: { + compilerOptions: { + target: 'es2017' + } + } + }), + json({ preferConst: true }) +]; + +const es2017Builds = [ + { + input: 'src/index.ts', + output: { + file: pkg.esm2017, + format: 'es', + sourcemap: true + }, + plugins: es2017BuildPlugins, + external: id => deps.some(dep => id === dep || id.startsWith(`${dep}/`)) + } +]; + +export default [...es5Builds, ...es2017Builds]; diff --git a/packages-exp/installations-exp/src/api/common.test.ts b/packages-exp/installations-exp/src/api/common.test.ts new file mode 100644 index 00000000000..95829751ba7 --- /dev/null +++ b/packages-exp/installations-exp/src/api/common.test.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { expect } from 'chai'; +import { SinonStub, stub } from 'sinon'; +import '../testing/setup'; +import { retryIfServerError } from './common'; + +describe('common', () => { + describe('retryIfServerError', () => { + let fetchStub: SinonStub<[], Promise>; + + beforeEach(() => { + fetchStub = stub(); + }); + + it('retries once if the server returns a 5xx error', async () => { + const expectedResponse = new Response(); + fetchStub.onCall(0).resolves(new Response(null, { status: 500 })); + fetchStub.onCall(1).resolves(expectedResponse); + + await expect(retryIfServerError(fetchStub)).to.eventually.equal( + expectedResponse + ); + expect(fetchStub).to.be.calledTwice; + }); + + it('does not retry again if the server returns a 5xx error twice', async () => { + const expectedResponse = new Response(null, { status: 500 }); + fetchStub.onCall(0).resolves(new Response(null, { status: 500 })); + fetchStub.onCall(1).resolves(expectedResponse); + fetchStub.onCall(2).resolves(new Response()); + + await expect(retryIfServerError(fetchStub)).to.eventually.equal( + expectedResponse + ); + expect(fetchStub).to.be.calledTwice; + }); + + it('does not retry if the error is not 5xx', async () => { + const expectedResponse = new Response(null, { status: 404 }); + fetchStub.resolves(expectedResponse); + + await expect(retryIfServerError(fetchStub)).to.eventually.equal( + expectedResponse + ); + expect(fetchStub).to.be.calledOnce; + }); + + it('does not retry if response is ok', async () => { + const expectedResponse = new Response(); + fetchStub.resolves(expectedResponse); + + await expect(retryIfServerError(fetchStub)).to.eventually.equal( + expectedResponse + ); + expect(fetchStub).to.be.calledOnce; + }); + }); +}); diff --git a/packages-exp/installations-exp/src/api/common.ts b/packages-exp/installations-exp/src/api/common.ts new file mode 100644 index 00000000000..3283f764723 --- /dev/null +++ b/packages-exp/installations-exp/src/api/common.ts @@ -0,0 +1,111 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { FirebaseError } from '@firebase/util'; +import { GenerateAuthTokenResponse } from '../interfaces/api-response'; +import { AppConfig } from '../interfaces/app-config'; +import { + CompletedAuthToken, + RegisteredInstallationEntry, + RequestStatus +} from '../interfaces/installation-entry'; +import { + INSTALLATIONS_API_URL, + INTERNAL_AUTH_VERSION +} from '../util/constants'; +import { ERROR_FACTORY, ErrorCode } from '../util/errors'; + +export function getInstallationsEndpoint({ projectId }: AppConfig): string { + return `${INSTALLATIONS_API_URL}/projects/${projectId}/installations`; +} + +export function extractAuthTokenInfoFromResponse( + response: GenerateAuthTokenResponse +): CompletedAuthToken { + return { + token: response.token, + requestStatus: RequestStatus.COMPLETED, + expiresIn: getExpiresInFromResponseExpiresIn(response.expiresIn), + creationTime: Date.now() + }; +} + +export async function getErrorFromResponse( + requestName: string, + response: Response +): Promise { + const responseJson: ErrorResponse = await response.json(); + const errorData = responseJson.error; + return ERROR_FACTORY.create(ErrorCode.REQUEST_FAILED, { + requestName, + serverCode: errorData.code, + serverMessage: errorData.message, + serverStatus: errorData.status + }); +} + +export function getHeaders({ apiKey }: AppConfig): Headers { + return new Headers({ + 'Content-Type': 'application/json', + Accept: 'application/json', + 'x-goog-api-key': apiKey + }); +} + +export function getHeadersWithAuth( + appConfig: AppConfig, + { refreshToken }: RegisteredInstallationEntry +): Headers { + const headers = getHeaders(appConfig); + headers.append('Authorization', getAuthorizationHeader(refreshToken)); + return headers; +} + +export interface ErrorResponse { + error: { + code: number; + message: string; + status: string; + }; +} + +/** + * Calls the passed in fetch wrapper and returns the response. + * If the returned response has a status of 5xx, re-runs the function once and + * returns the response. + */ +export async function retryIfServerError( + fn: () => Promise +): Promise { + const result = await fn(); + + if (result.status >= 500 && result.status < 600) { + // Internal Server Error. Retry request. + return fn(); + } + + return result; +} + +function getExpiresInFromResponseExpiresIn(responseExpiresIn: string): number { + // This works because the server will never respond with fractions of a second. + return Number(responseExpiresIn.replace('s', '000')); +} + +function getAuthorizationHeader(refreshToken: string): string { + return `${INTERNAL_AUTH_VERSION} ${refreshToken}`; +} diff --git a/packages-exp/installations-exp/src/api/create-installation-request.test.ts b/packages-exp/installations-exp/src/api/create-installation-request.test.ts new file mode 100644 index 00000000000..05c0c8dfbbf --- /dev/null +++ b/packages-exp/installations-exp/src/api/create-installation-request.test.ts @@ -0,0 +1,165 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { FirebaseError } from '@firebase/util'; +import { expect } from 'chai'; +import { SinonStub, stub } from 'sinon'; +import { CreateInstallationResponse } from '../interfaces/api-response'; +import { AppConfig } from '../interfaces/app-config'; +import { + InProgressInstallationEntry, + RequestStatus +} from '../interfaces/installation-entry'; +import { compareHeaders } from '../testing/compare-headers'; +import { getFakeAppConfig } from '../testing/fake-generators'; +import '../testing/setup'; +import { + INSTALLATIONS_API_URL, + INTERNAL_AUTH_VERSION, + PACKAGE_VERSION +} from '../util/constants'; +import { ErrorResponse } from './common'; +import { createInstallationRequest } from './create-installation-request'; + +const FID = 'defenders-of-the-faith'; + +describe('createInstallationRequest', () => { + let appConfig: AppConfig; + let fetchSpy: SinonStub<[RequestInfo, RequestInit?], Promise>; + let inProgressInstallationEntry: InProgressInstallationEntry; + let response: CreateInstallationResponse; + + beforeEach(() => { + appConfig = getFakeAppConfig(); + + inProgressInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.IN_PROGRESS, + registrationTime: Date.now() + }; + + response = { + refreshToken: 'refreshToken', + authToken: { + token: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', + expiresIn: '604800s' + }, + fid: FID + }; + fetchSpy = stub(self, 'fetch'); + }); + + describe('successful request', () => { + beforeEach(() => { + fetchSpy.resolves(new Response(JSON.stringify(response))); + }); + + it('registers a pending InstallationEntry', async () => { + const registeredInstallationEntry = await createInstallationRequest( + appConfig, + inProgressInstallationEntry + ); + expect(registeredInstallationEntry.registrationStatus).to.equal( + RequestStatus.COMPLETED + ); + }); + + it('calls the createInstallation server API with correct parameters', async () => { + const expectedHeaders = new Headers({ + 'Content-Type': 'application/json', + Accept: 'application/json', + 'x-goog-api-key': 'apiKey' + }); + const expectedBody = { + fid: FID, + authVersion: INTERNAL_AUTH_VERSION, + appId: appConfig.appId, + sdkVersion: PACKAGE_VERSION + }; + const expectedRequest: RequestInit = { + method: 'POST', + headers: expectedHeaders, + body: JSON.stringify(expectedBody) + }; + const expectedEndpoint = `${INSTALLATIONS_API_URL}/projects/projectId/installations`; + + await createInstallationRequest(appConfig, inProgressInstallationEntry); + expect(fetchSpy).to.be.calledOnceWith(expectedEndpoint, expectedRequest); + const actualHeaders = fetchSpy.lastCall.lastArg.headers; + compareHeaders(expectedHeaders, actualHeaders); + }); + }); + + it('returns the FID from the request if the response does not contain one', async () => { + response = { + refreshToken: 'refreshToken', + authToken: { + token: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', + expiresIn: '604800s' + } + }; + fetchSpy.resolves(new Response(JSON.stringify(response))); + + const registeredInstallationEntry = await createInstallationRequest( + appConfig, + inProgressInstallationEntry + ); + expect(registeredInstallationEntry.fid).to.equal(FID); + }); + + describe('failed request', () => { + it('throws a FirebaseError with the error information from the server', async () => { + const errorResponse: ErrorResponse = { + error: { + code: 409, + message: 'Requested entity already exists', + status: 'ALREADY_EXISTS' + } + }; + + fetchSpy.resolves( + new Response(JSON.stringify(errorResponse), { status: 409 }) + ); + + await expect( + createInstallationRequest(appConfig, inProgressInstallationEntry) + ).to.be.rejectedWith(FirebaseError); + }); + + it('retries once if the server returns a 5xx error', async () => { + const errorResponse: ErrorResponse = { + error: { + code: 500, + message: 'Internal server error', + status: 'SERVER_ERROR' + } + }; + + fetchSpy + .onCall(0) + .resolves(new Response(JSON.stringify(errorResponse), { status: 500 })); + fetchSpy.onCall(1).resolves(new Response(JSON.stringify(response))); + + await expect( + createInstallationRequest(appConfig, inProgressInstallationEntry) + ).to.be.fulfilled; + expect(fetchSpy).to.be.calledTwice; + }); + }); +}); diff --git a/packages-exp/installations-exp/src/api/create-installation-request.ts b/packages-exp/installations-exp/src/api/create-installation-request.ts new file mode 100644 index 00000000000..07e0c7bb35c --- /dev/null +++ b/packages-exp/installations-exp/src/api/create-installation-request.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { CreateInstallationResponse } from '../interfaces/api-response'; +import { AppConfig } from '../interfaces/app-config'; +import { + InProgressInstallationEntry, + RegisteredInstallationEntry, + RequestStatus +} from '../interfaces/installation-entry'; +import { INTERNAL_AUTH_VERSION, PACKAGE_VERSION } from '../util/constants'; +import { + extractAuthTokenInfoFromResponse, + getErrorFromResponse, + getHeaders, + getInstallationsEndpoint, + retryIfServerError +} from './common'; + +export async function createInstallationRequest( + appConfig: AppConfig, + { fid }: InProgressInstallationEntry +): Promise { + const endpoint = getInstallationsEndpoint(appConfig); + + const headers = getHeaders(appConfig); + const body = { + fid, + authVersion: INTERNAL_AUTH_VERSION, + appId: appConfig.appId, + sdkVersion: PACKAGE_VERSION + }; + + const request: RequestInit = { + method: 'POST', + headers, + body: JSON.stringify(body) + }; + + const response = await retryIfServerError(() => fetch(endpoint, request)); + if (response.ok) { + const responseValue: CreateInstallationResponse = await response.json(); + const registeredInstallationEntry: RegisteredInstallationEntry = { + fid: responseValue.fid || fid, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: responseValue.refreshToken, + authToken: extractAuthTokenInfoFromResponse(responseValue.authToken) + }; + return registeredInstallationEntry; + } else { + throw await getErrorFromResponse('Create Installation', response); + } +} diff --git a/packages-exp/installations-exp/src/api/delete-installation-request.test.ts b/packages-exp/installations-exp/src/api/delete-installation-request.test.ts new file mode 100644 index 00000000000..278b4965882 --- /dev/null +++ b/packages-exp/installations-exp/src/api/delete-installation-request.test.ts @@ -0,0 +1,123 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { FirebaseError } from '@firebase/util'; +import { expect } from 'chai'; +import { SinonStub, stub } from 'sinon'; +import { AppConfig } from '../interfaces/app-config'; +import { + RegisteredInstallationEntry, + RequestStatus +} from '../interfaces/installation-entry'; +import { compareHeaders } from '../testing/compare-headers'; +import { getFakeAppConfig } from '../testing/fake-generators'; +import '../testing/setup'; +import { + INSTALLATIONS_API_URL, + INTERNAL_AUTH_VERSION +} from '../util/constants'; +import { ErrorResponse } from './common'; +import { deleteInstallationRequest } from './delete-installation-request'; + +const FID = 'foreclosure-of-a-dream'; + +describe('deleteInstallationRequest', () => { + let appConfig: AppConfig; + let fetchSpy: SinonStub<[RequestInfo, RequestInit?], Promise>; + let registeredInstallationEntry: RegisteredInstallationEntry; + + beforeEach(() => { + appConfig = getFakeAppConfig(); + + registeredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + requestStatus: RequestStatus.NOT_STARTED + } + }; + + fetchSpy = stub(self, 'fetch'); + }); + + describe('successful request', () => { + beforeEach(() => { + fetchSpy.resolves(new Response()); + }); + + it('calls the deleteInstallation server API with correct parameters', async () => { + const expectedHeaders = new Headers({ + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `${INTERNAL_AUTH_VERSION} refreshToken`, + 'x-goog-api-key': 'apiKey' + }); + const expectedRequest: RequestInit = { + method: 'DELETE', + headers: expectedHeaders + }; + const expectedEndpoint = `${INSTALLATIONS_API_URL}/projects/projectId/installations/${FID}`; + + await deleteInstallationRequest(appConfig, registeredInstallationEntry); + + expect(fetchSpy).to.be.calledOnceWith(expectedEndpoint, expectedRequest); + const actualHeaders = fetchSpy.lastCall.lastArg.headers; + compareHeaders(expectedHeaders, actualHeaders); + }); + }); + + describe('failed request', () => { + it('throws a FirebaseError with the error information from the server', async () => { + const errorResponse: ErrorResponse = { + error: { + code: 409, + message: 'Requested entity already exists', + status: 'ALREADY_EXISTS' + } + }; + + fetchSpy.resolves( + new Response(JSON.stringify(errorResponse), { status: 409 }) + ); + + await expect( + deleteInstallationRequest(appConfig, registeredInstallationEntry) + ).to.be.rejectedWith(FirebaseError); + }); + + it('retries once if the server returns a 5xx error', async () => { + const errorResponse: ErrorResponse = { + error: { + code: 500, + message: 'Internal server error', + status: 'SERVER_ERROR' + } + }; + + fetchSpy + .onCall(0) + .resolves(new Response(JSON.stringify(errorResponse), { status: 500 })); + fetchSpy.onCall(1).resolves(new Response()); + + await expect( + deleteInstallationRequest(appConfig, registeredInstallationEntry) + ).to.be.fulfilled; + expect(fetchSpy).to.be.calledTwice; + }); + }); +}); diff --git a/packages-exp/installations-exp/src/api/delete-installation-request.ts b/packages-exp/installations-exp/src/api/delete-installation-request.ts new file mode 100644 index 00000000000..5fba2314c5a --- /dev/null +++ b/packages-exp/installations-exp/src/api/delete-installation-request.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { AppConfig } from '../interfaces/app-config'; +import { RegisteredInstallationEntry } from '../interfaces/installation-entry'; +import { + getErrorFromResponse, + getHeadersWithAuth, + getInstallationsEndpoint, + retryIfServerError +} from './common'; + +export async function deleteInstallationRequest( + appConfig: AppConfig, + installationEntry: RegisteredInstallationEntry +): Promise { + const endpoint = getDeleteEndpoint(appConfig, installationEntry); + + const headers = getHeadersWithAuth(appConfig, installationEntry); + const request: RequestInit = { + method: 'DELETE', + headers + }; + + const response = await retryIfServerError(() => fetch(endpoint, request)); + if (!response.ok) { + throw await getErrorFromResponse('Delete Installation', response); + } +} + +function getDeleteEndpoint( + appConfig: AppConfig, + { fid }: RegisteredInstallationEntry +): string { + return `${getInstallationsEndpoint(appConfig)}/${fid}`; +} diff --git a/packages-exp/installations-exp/src/api/generate-auth-token-request.test.ts b/packages-exp/installations-exp/src/api/generate-auth-token-request.test.ts new file mode 100644 index 00000000000..a7f74115681 --- /dev/null +++ b/packages-exp/installations-exp/src/api/generate-auth-token-request.test.ts @@ -0,0 +1,150 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { FirebaseError } from '@firebase/util'; +import { expect } from 'chai'; +import { SinonStub, stub } from 'sinon'; +import { GenerateAuthTokenResponse } from '../interfaces/api-response'; +import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + CompletedAuthToken, + RegisteredInstallationEntry, + RequestStatus +} from '../interfaces/installation-entry'; +import { compareHeaders } from '../testing/compare-headers'; +import { getFakeDependencies } from '../testing/fake-generators'; +import '../testing/setup'; +import { + INSTALLATIONS_API_URL, + INTERNAL_AUTH_VERSION, + PACKAGE_VERSION +} from '../util/constants'; +import { ErrorResponse } from './common'; +import { generateAuthTokenRequest } from './generate-auth-token-request'; + +const FID = 'evil-has-no-boundaries'; + +describe('generateAuthTokenRequest', () => { + let dependencies: FirebaseDependencies; + let fetchSpy: SinonStub<[RequestInfo, RequestInit?], Promise>; + let registeredInstallationEntry: RegisteredInstallationEntry; + let response: GenerateAuthTokenResponse; + + beforeEach(() => { + dependencies = getFakeDependencies(); + + registeredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + requestStatus: RequestStatus.NOT_STARTED + } + }; + + response = { + token: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCeKKF2QT4fwpMeJf36POk6yJV_adQssw5c', + expiresIn: '604800s' + }; + + fetchSpy = stub(self, 'fetch'); + }); + + describe('successful request', () => { + beforeEach(() => { + fetchSpy.resolves(new Response(JSON.stringify(response))); + }); + + it('fetches a new Authentication Token', async () => { + const completedAuthToken: CompletedAuthToken = await generateAuthTokenRequest( + dependencies, + registeredInstallationEntry + ); + expect(completedAuthToken.requestStatus).to.equal( + RequestStatus.COMPLETED + ); + }); + + it('calls the generateAuthToken server API with correct parameters', async () => { + const expectedHeaders = new Headers({ + 'Content-Type': 'application/json', + Accept: 'application/json', + Authorization: `${INTERNAL_AUTH_VERSION} refreshToken`, + 'x-goog-api-key': 'apiKey', + 'x-firebase-client': 'a/1.2.3 b/2.3.4' + }); + const expectedBody = { + installation: { + sdkVersion: PACKAGE_VERSION + } + }; + const expectedRequest: RequestInit = { + method: 'POST', + headers: expectedHeaders, + body: JSON.stringify(expectedBody) + }; + const expectedEndpoint = `${INSTALLATIONS_API_URL}/projects/projectId/installations/${FID}/authTokens:generate`; + + await generateAuthTokenRequest(dependencies, registeredInstallationEntry); + + expect(fetchSpy).to.be.calledOnceWith(expectedEndpoint, expectedRequest); + const actualHeaders = fetchSpy.lastCall.lastArg.headers; + compareHeaders(expectedHeaders, actualHeaders); + }); + }); + + describe('failed request', () => { + it('throws a FirebaseError with the error information from the server', async () => { + const errorResponse: ErrorResponse = { + error: { + code: 409, + message: 'Requested entity already exists', + status: 'ALREADY_EXISTS' + } + }; + + fetchSpy.resolves( + new Response(JSON.stringify(errorResponse), { status: 409 }) + ); + + await expect( + generateAuthTokenRequest(dependencies, registeredInstallationEntry) + ).to.be.rejectedWith(FirebaseError); + }); + + it('retries once if the server returns a 5xx error', async () => { + const errorResponse: ErrorResponse = { + error: { + code: 500, + message: 'Internal server error', + status: 'SERVER_ERROR' + } + }; + + fetchSpy + .onCall(0) + .resolves(new Response(JSON.stringify(errorResponse), { status: 500 })); + fetchSpy.onCall(1).resolves(new Response(JSON.stringify(response))); + + await expect( + generateAuthTokenRequest(dependencies, registeredInstallationEntry) + ).to.be.fulfilled; + expect(fetchSpy).to.be.calledTwice; + }); + }); +}); diff --git a/packages-exp/installations-exp/src/api/generate-auth-token-request.ts b/packages-exp/installations-exp/src/api/generate-auth-token-request.ts new file mode 100644 index 00000000000..f32650179f6 --- /dev/null +++ b/packages-exp/installations-exp/src/api/generate-auth-token-request.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { GenerateAuthTokenResponse } from '../interfaces/api-response'; +import { AppConfig } from '../interfaces/app-config'; +import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + CompletedAuthToken, + RegisteredInstallationEntry +} from '../interfaces/installation-entry'; +import { PACKAGE_VERSION } from '../util/constants'; +import { + extractAuthTokenInfoFromResponse, + getErrorFromResponse, + getHeadersWithAuth, + getInstallationsEndpoint, + retryIfServerError +} from './common'; + +export async function generateAuthTokenRequest( + { appConfig, platformLoggerProvider }: FirebaseDependencies, + installationEntry: RegisteredInstallationEntry +): Promise { + const endpoint = getGenerateAuthTokenEndpoint(appConfig, installationEntry); + + const headers = getHeadersWithAuth(appConfig, installationEntry); + + // If platform logger exists, add the platform info string to the header. + const platformLogger = platformLoggerProvider.getImmediate({ + optional: true + }); + if (platformLogger) { + headers.append('x-firebase-client', platformLogger.getPlatformInfoString()); + } + + const body = { + installation: { + sdkVersion: PACKAGE_VERSION + } + }; + + const request: RequestInit = { + method: 'POST', + headers, + body: JSON.stringify(body) + }; + + const response = await retryIfServerError(() => fetch(endpoint, request)); + if (response.ok) { + const responseValue: GenerateAuthTokenResponse = await response.json(); + const completedAuthToken: CompletedAuthToken = extractAuthTokenInfoFromResponse( + responseValue + ); + return completedAuthToken; + } else { + throw await getErrorFromResponse('Generate Auth Token', response); + } +} + +function getGenerateAuthTokenEndpoint( + appConfig: AppConfig, + { fid }: RegisteredInstallationEntry +): string { + return `${getInstallationsEndpoint(appConfig)}/${fid}/authTokens:generate`; +} diff --git a/packages-exp/installations-exp/src/functions/delete-installation.test.ts b/packages-exp/installations-exp/src/functions/delete-installation.test.ts new file mode 100644 index 00000000000..376a35e53bd --- /dev/null +++ b/packages-exp/installations-exp/src/functions/delete-installation.test.ts @@ -0,0 +1,129 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { expect } from 'chai'; +import { SinonStub, stub } from 'sinon'; +import * as deleteInstallationRequestModule from '../api/delete-installation-request'; +import { get, set } from '../helpers/idb-manager'; +import { AppConfig } from '../interfaces/app-config'; +import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + InProgressInstallationEntry, + RegisteredInstallationEntry, + RequestStatus, + UnregisteredInstallationEntry +} from '../interfaces/installation-entry'; +import { getFakeDependencies } from '../testing/fake-generators'; +import '../testing/setup'; +import { ErrorCode } from '../util/errors'; +import { sleep } from '../util/sleep'; +import { deleteInstallation } from './delete-installation'; + +const FID = 'children-of-the-damned'; + +describe('deleteInstallation', () => { + let dependencies: FirebaseDependencies; + let deleteInstallationRequestSpy: SinonStub< + [AppConfig, RegisteredInstallationEntry], + Promise + >; + + beforeEach(() => { + dependencies = getFakeDependencies(); + + deleteInstallationRequestSpy = stub( + deleteInstallationRequestModule, + 'deleteInstallationRequest' + ).callsFake( + () => sleep(100) // Request would take some time + ); + }); + + it('resolves without calling server API if there is no installation', async () => { + await expect(deleteInstallation(dependencies)).to.be.fulfilled; + expect(deleteInstallationRequestSpy).not.to.have.been.called; + }); + + it('deletes and resolves without calling server API if the installation is unregistered', async () => { + const entry: UnregisteredInstallationEntry = { + registrationStatus: RequestStatus.NOT_STARTED, + fid: FID + }; + await set(dependencies.appConfig, entry); + + await expect(deleteInstallation(dependencies)).to.be.fulfilled; + expect(deleteInstallationRequestSpy).not.to.have.been.called; + await expect(get(dependencies.appConfig)).to.eventually.be.undefined; + }); + + it('rejects without calling server API if the installation is pending', async () => { + const entry: InProgressInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.IN_PROGRESS, + registrationTime: Date.now() - 3 * 1000 + }; + await set(dependencies.appConfig, entry); + + await expect(deleteInstallation(dependencies)).to.be.rejectedWith( + ErrorCode.DELETE_PENDING_REGISTRATION + ); + expect(deleteInstallationRequestSpy).not.to.have.been.called; + }); + + it('rejects without calling server API if the installation is registered and app is offline', async () => { + const entry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + token: 'authToken', + expiresIn: 123456, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() + } + }; + await set(dependencies.appConfig, entry); + stub(navigator, 'onLine').value(false); + + await expect(deleteInstallation(dependencies)).to.be.rejectedWith( + ErrorCode.APP_OFFLINE + ); + expect(deleteInstallationRequestSpy).not.to.have.been.called; + }); + + it('deletes and resolves after calling server API if the installation is registered', async () => { + const entry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + token: 'authToken', + expiresIn: 123456, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() + } + }; + await set(dependencies.appConfig, entry); + + await expect(deleteInstallation(dependencies)).to.be.fulfilled; + expect(deleteInstallationRequestSpy).to.have.been.calledOnceWith( + dependencies.appConfig, + entry + ); + await expect(get(dependencies.appConfig)).to.eventually.be.undefined; + }); +}); diff --git a/packages-exp/installations-exp/src/functions/delete-installation.ts b/packages-exp/installations-exp/src/functions/delete-installation.ts new file mode 100644 index 00000000000..53f912569cc --- /dev/null +++ b/packages-exp/installations-exp/src/functions/delete-installation.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { deleteInstallationRequest } from '../api/delete-installation-request'; +import { remove, update } from '../helpers/idb-manager'; +import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { RequestStatus } from '../interfaces/installation-entry'; +import { ERROR_FACTORY, ErrorCode } from '../util/errors'; + +export async function deleteInstallation( + dependencies: FirebaseDependencies +): Promise { + const { appConfig } = dependencies; + + const entry = await update(appConfig, oldEntry => { + if (oldEntry && oldEntry.registrationStatus === RequestStatus.NOT_STARTED) { + // Delete the unregistered entry without sending a deleteInstallation request. + return undefined; + } + return oldEntry; + }); + + if (entry) { + if (entry.registrationStatus === RequestStatus.IN_PROGRESS) { + // Can't delete while trying to register. + throw ERROR_FACTORY.create(ErrorCode.DELETE_PENDING_REGISTRATION); + } else if (entry.registrationStatus === RequestStatus.COMPLETED) { + if (!navigator.onLine) { + throw ERROR_FACTORY.create(ErrorCode.APP_OFFLINE); + } else { + await deleteInstallationRequest(appConfig, entry); + await remove(appConfig); + } + } + } +} diff --git a/packages-exp/installations-exp/src/functions/get-id.test.ts b/packages-exp/installations-exp/src/functions/get-id.test.ts new file mode 100644 index 00000000000..a1c03700b44 --- /dev/null +++ b/packages-exp/installations-exp/src/functions/get-id.test.ts @@ -0,0 +1,89 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { expect } from 'chai'; +import { SinonStub, stub } from 'sinon'; +import * as getInstallationEntryModule from '../helpers/get-installation-entry'; +import * as refreshAuthTokenModule from '../helpers/refresh-auth-token'; +import { AppConfig } from '../interfaces/app-config'; +import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + RegisteredInstallationEntry, + RequestStatus +} from '../interfaces/installation-entry'; +import { getFakeDependencies } from '../testing/fake-generators'; +import '../testing/setup'; +import { getId } from './get-id'; + +const FID = 'disciples-of-the-watch'; + +describe('getId', () => { + let dependencies: FirebaseDependencies; + let getInstallationEntrySpy: SinonStub< + [AppConfig], + Promise + >; + + beforeEach(() => { + dependencies = getFakeDependencies(); + + getInstallationEntrySpy = stub( + getInstallationEntryModule, + 'getInstallationEntry' + ); + }); + + it('returns the FID in InstallationEntry returned by getInstallationEntry', async () => { + getInstallationEntrySpy.resolves({ + installationEntry: { + fid: FID, + registrationStatus: RequestStatus.NOT_STARTED + }, + registrationPromise: Promise.resolve({} as RegisteredInstallationEntry) + }); + + const fid = await getId(dependencies); + expect(fid).to.equal(FID); + expect(getInstallationEntrySpy).to.be.calledOnce; + }); + + it('calls refreshAuthToken if the installation is registered', async () => { + getInstallationEntrySpy.resolves({ + installationEntry: { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + requestStatus: RequestStatus.NOT_STARTED + } + } + }); + + const refreshAuthTokenSpy = stub( + refreshAuthTokenModule, + 'refreshAuthToken' + ).resolves({ + token: 'authToken', + expiresIn: 123456, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() + }); + + await getId(dependencies); + expect(refreshAuthTokenSpy).to.be.calledOnce; + }); +}); diff --git a/packages-exp/installations-exp/src/functions/get-id.ts b/packages-exp/installations-exp/src/functions/get-id.ts new file mode 100644 index 00000000000..28a692af10f --- /dev/null +++ b/packages-exp/installations-exp/src/functions/get-id.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { getInstallationEntry } from '../helpers/get-installation-entry'; +import { refreshAuthToken } from '../helpers/refresh-auth-token'; +import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; + +export async function getId( + dependencies: FirebaseDependencies +): Promise { + const { installationEntry, registrationPromise } = await getInstallationEntry( + dependencies.appConfig + ); + + if (registrationPromise) { + registrationPromise.catch(console.error); + } else { + // If the installation is already registered, update the authentication + // token if needed. + refreshAuthToken(dependencies).catch(console.error); + } + + return installationEntry.fid; +} diff --git a/packages-exp/installations-exp/src/functions/get-token.test.ts b/packages-exp/installations-exp/src/functions/get-token.test.ts new file mode 100644 index 00000000000..be633924e05 --- /dev/null +++ b/packages-exp/installations-exp/src/functions/get-token.test.ts @@ -0,0 +1,465 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { expect } from 'chai'; +import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; +import * as createInstallationRequestModule from '../api/create-installation-request'; +import * as generateAuthTokenRequestModule from '../api/generate-auth-token-request'; +import { get, set } from '../helpers/idb-manager'; +import { AppConfig } from '../interfaces/app-config'; +import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + CompletedAuthToken, + InProgressInstallationEntry, + RegisteredInstallationEntry, + RequestStatus, + UnregisteredInstallationEntry +} from '../interfaces/installation-entry'; +import { getFakeDependencies } from '../testing/fake-generators'; +import '../testing/setup'; +import { TOKEN_EXPIRATION_BUFFER } from '../util/constants'; +import { ERROR_FACTORY, ErrorCode } from '../util/errors'; +import { sleep } from '../util/sleep'; +import { getToken } from './get-token'; + +const FID = 'dont-talk-to-strangers'; +const AUTH_TOKEN = 'authToken'; +const NEW_AUTH_TOKEN = 'newAuthToken'; +const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000; + +/** + * A map of different states of the database and a function that creates the + * said state. + */ +const setupInstallationEntryMap: Map< + string, + (appConfig: AppConfig) => Promise +> = new Map([ + [ + 'existing and valid auth token', + async (appConfig: AppConfig) => { + const entry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + token: AUTH_TOKEN, + expiresIn: ONE_WEEK_MS, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() + } + }; + await set(appConfig, entry); + } + ], + [ + 'expired auth token', + async (appConfig: AppConfig) => { + const entry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + token: AUTH_TOKEN, + expiresIn: ONE_WEEK_MS, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() - 2 * ONE_WEEK_MS + } + }; + await set(appConfig, entry); + } + ], + [ + 'pending auth token', + async (appConfig: AppConfig) => { + const entry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + requestStatus: RequestStatus.IN_PROGRESS, + requestTime: Date.now() - 3 * 1000 + } + }; + + await set(appConfig, entry); + + // Finish pending request after 500 ms + // eslint-disable-next-line @typescript-eslint/no-floating-promises + sleep(500).then(async () => { + const updatedEntry: RegisteredInstallationEntry = { + ...entry, + authToken: { + token: NEW_AUTH_TOKEN, + expiresIn: ONE_WEEK_MS, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() + } + }; + await set(appConfig, updatedEntry); + }); + } + ], + [ + 'no auth token', + async (appConfig: AppConfig) => { + const entry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + requestStatus: RequestStatus.NOT_STARTED + } + }; + await set(appConfig, entry); + } + ], + [ + 'pending fid registration', + async (appConfig: AppConfig) => { + const entry: InProgressInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.IN_PROGRESS, + registrationTime: Date.now() - 3 * 1000 + }; + + await set(appConfig, entry); + + // Finish pending request after 500 ms + // eslint-disable-next-line @typescript-eslint/no-floating-promises + sleep(500).then(async () => { + const updatedEntry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + token: AUTH_TOKEN, + expiresIn: ONE_WEEK_MS, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() + } + }; + await set(appConfig, updatedEntry); + }); + } + ], + [ + 'unregistered fid', + async (appConfig: AppConfig) => { + const entry: UnregisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.NOT_STARTED + }; + + await set(appConfig, entry); + } + ] +]); + +describe('getToken', () => { + let dependencies: FirebaseDependencies; + let createInstallationRequestSpy: SinonStub< + [AppConfig, InProgressInstallationEntry], + Promise + >; + let generateAuthTokenRequestSpy: SinonStub< + [FirebaseDependencies, RegisteredInstallationEntry], + Promise + >; + + beforeEach(() => { + dependencies = getFakeDependencies(); + + createInstallationRequestSpy = stub( + createInstallationRequestModule, + 'createInstallationRequest' + ).callsFake(async (_, installationEntry) => { + await sleep(100); // Request would take some time + const result: RegisteredInstallationEntry = { + fid: installationEntry.fid, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + token: NEW_AUTH_TOKEN, + expiresIn: ONE_WEEK_MS, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() + } + }; + return result; + }); + generateAuthTokenRequestSpy = stub( + generateAuthTokenRequestModule, + 'generateAuthTokenRequest' + ).callsFake(async () => { + await sleep(100); // Request would take some time + const result: CompletedAuthToken = { + token: NEW_AUTH_TOKEN, + expiresIn: ONE_WEEK_MS, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() + }; + return result; + }); + }); + + describe('basic functionality', () => { + for (const [title, setup] of setupInstallationEntryMap.entries()) { + describe(`when ${title} in the DB`, () => { + beforeEach(() => setup(dependencies.appConfig)); + + it('resolves with an auth token', async () => { + const token = await getToken(dependencies); + expect(token).to.be.oneOf([AUTH_TOKEN, NEW_AUTH_TOKEN]); + }); + + it('saves the token in the DB', async () => { + const token = await getToken(dependencies); + const installationEntry = (await get( + dependencies.appConfig + )) as RegisteredInstallationEntry; + expect(installationEntry).not.to.be.undefined; + expect(installationEntry.registrationStatus).to.equal( + RequestStatus.COMPLETED + ); + expect(installationEntry.authToken.requestStatus).to.equal( + RequestStatus.COMPLETED + ); + expect( + (installationEntry.authToken as CompletedAuthToken).token + ).to.equal(token); + }); + + it('returns the same token on subsequent calls', async () => { + const token1 = await getToken(dependencies); + const token2 = await getToken(dependencies); + expect(token1).to.equal(token2); + }); + }); + } + }); + + describe('when there is no FID in the DB', () => { + it('gets the token by registering a new FID', async () => { + await getToken(dependencies); + expect(createInstallationRequestSpy).to.be.called; + expect(generateAuthTokenRequestSpy).not.to.be.called; + }); + + it('does not register a new FID on subsequent calls', async () => { + await getToken(dependencies); + await getToken(dependencies); + expect(createInstallationRequestSpy).to.be.calledOnce; + }); + + it('throws if the app is offline', async () => { + stub(navigator, 'onLine').value(false); + + await expect(getToken(dependencies)).to.be.rejected; + }); + }); + + describe('when there is a FID in the DB, but no auth token', () => { + let installationEntry: RegisteredInstallationEntry; + + beforeEach(async () => { + installationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + requestStatus: RequestStatus.NOT_STARTED + } + }; + await set(dependencies.appConfig, installationEntry); + }); + + it('gets the token by calling generateAuthToken', async () => { + await getToken(dependencies); + expect(generateAuthTokenRequestSpy).to.be.called; + expect(createInstallationRequestSpy).not.to.be.called; + }); + + it('does not call generateAuthToken twice on subsequent calls', async () => { + await getToken(dependencies); + await getToken(dependencies); + expect(generateAuthTokenRequestSpy).to.be.calledOnce; + }); + + it('does not call generateAuthToken twice on simultaneous calls', async () => { + await Promise.all([getToken(dependencies), getToken(dependencies)]); + expect(generateAuthTokenRequestSpy).to.be.calledOnce; + }); + + it('throws if the app is offline', async () => { + stub(navigator, 'onLine').value(false); + + await expect(getToken(dependencies)).to.be.rejected; + }); + + describe('and the server returns an error', () => { + it('removes the FID from the DB if the server returns a 401 response', async () => { + generateAuthTokenRequestSpy.callsFake(async () => { + throw ERROR_FACTORY.create(ErrorCode.REQUEST_FAILED, { + requestName: 'Generate Auth Token', + serverCode: 401, + serverStatus: 'UNAUTHENTICATED', + serverMessage: 'Invalid Authentication.' + }); + }); + + await expect(getToken(dependencies)).to.be.rejected; + await expect(get(dependencies.appConfig)).to.eventually.be.undefined; + }); + + it('removes the FID from the DB if the server returns a 404 response', async () => { + generateAuthTokenRequestSpy.callsFake(async () => { + throw ERROR_FACTORY.create(ErrorCode.REQUEST_FAILED, { + requestName: 'Generate Auth Token', + serverCode: 404, + serverStatus: 'NOT_FOUND', + serverMessage: 'FID not found.' + }); + }); + + await expect(getToken(dependencies)).to.be.rejected; + await expect(get(dependencies.appConfig)).to.eventually.be.undefined; + }); + + it('does not remove the FID from the DB if the server returns any other response', async () => { + generateAuthTokenRequestSpy.callsFake(async () => { + throw ERROR_FACTORY.create(ErrorCode.REQUEST_FAILED, { + requestName: 'Generate Auth Token', + serverCode: 500, + serverStatus: 'INTERNAL', + serverMessage: 'Internal server error.' + }); + }); + + await expect(getToken(dependencies)).to.be.rejected; + await expect(get(dependencies.appConfig)).to.eventually.deep.equal( + installationEntry + ); + }); + }); + }); + + describe('when there is a registered auth token in the DB', () => { + beforeEach(async () => { + const installationEntry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + token: AUTH_TOKEN, + expiresIn: ONE_WEEK_MS, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() + } + }; + await set(dependencies.appConfig, installationEntry); + }); + + it('does not call any server APIs', async () => { + await getToken(dependencies); + expect(createInstallationRequestSpy).not.to.be.called; + expect(generateAuthTokenRequestSpy).not.to.be.called; + }); + + it('refreshes the token if forceRefresh is true', async () => { + const token = await getToken(dependencies, true); + expect(token).to.equal(NEW_AUTH_TOKEN); + expect(generateAuthTokenRequestSpy).to.be.called; + }); + + it('works even if the app is offline', async () => { + stub(navigator, 'onLine').value(false); + + const token = await getToken(dependencies); + expect(token).to.equal(AUTH_TOKEN); + }); + + it('throws if the app is offline and forceRefresh is true', async () => { + stub(navigator, 'onLine').value(false); + + await expect(getToken(dependencies, true)).to.be.rejected; + }); + }); + + describe('when there is an auth token that is about to expire in the DB', () => { + let clock: SinonFakeTimers; + + beforeEach(async () => { + clock = useFakeTimers({ shouldAdvanceTime: true }); + const installationEntry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + token: AUTH_TOKEN, + expiresIn: ONE_WEEK_MS, + requestStatus: RequestStatus.COMPLETED, + creationTime: + // Expires in ten minutes + Date.now() - ONE_WEEK_MS + TOKEN_EXPIRATION_BUFFER + 10 * 60 * 1000 + } + }; + await set(dependencies.appConfig, installationEntry); + }); + + it('returns a different token after expiration', async () => { + const token1 = await getToken(dependencies); + expect(token1).to.equal(AUTH_TOKEN); + + // Wait 30 minutes. + clock.tick('30:00'); + + const token2 = await getToken(dependencies); + await expect(token2).to.equal(NEW_AUTH_TOKEN); + await expect(token2).not.to.equal(token1); + }); + }); + + describe('when there is an expired auth token in the DB', () => { + beforeEach(async () => { + const installationEntry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + token: AUTH_TOKEN, + expiresIn: ONE_WEEK_MS, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() - 2 * ONE_WEEK_MS + } + }; + await set(dependencies.appConfig, installationEntry); + }); + + it('returns a different token', async () => { + const token = await getToken(dependencies); + expect(token).to.equal(NEW_AUTH_TOKEN); + expect(generateAuthTokenRequestSpy).to.be.called; + }); + + it('throws if the app is offline', async () => { + stub(navigator, 'onLine').value(false); + + await expect(getToken(dependencies)).to.be.rejected; + }); + }); +}); diff --git a/packages-exp/installations-exp/src/functions/get-token.ts b/packages-exp/installations-exp/src/functions/get-token.ts new file mode 100644 index 00000000000..a747626f476 --- /dev/null +++ b/packages-exp/installations-exp/src/functions/get-token.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { getInstallationEntry } from '../helpers/get-installation-entry'; +import { refreshAuthToken } from '../helpers/refresh-auth-token'; +import { AppConfig } from '../interfaces/app-config'; +import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; + +export async function getToken( + dependencies: FirebaseDependencies, + forceRefresh = false +): Promise { + await completeInstallationRegistration(dependencies.appConfig); + + // At this point we either have a Registered Installation in the DB, or we've + // already thrown an error. + const authToken = await refreshAuthToken(dependencies, forceRefresh); + return authToken.token; +} + +async function completeInstallationRegistration( + appConfig: AppConfig +): Promise { + const { registrationPromise } = await getInstallationEntry(appConfig); + + if (registrationPromise) { + // A createInstallation request is in progress. Wait until it finishes. + await registrationPromise; + } +} diff --git a/packages-exp/installations-exp/src/functions/index.ts b/packages-exp/installations-exp/src/functions/index.ts new file mode 100644 index 00000000000..7952e267a45 --- /dev/null +++ b/packages-exp/installations-exp/src/functions/index.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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. + */ + +export * from './get-id'; +export * from './get-token'; +export * from './delete-installation'; +export * from './on-id-change'; diff --git a/packages-exp/installations-exp/src/functions/on-id-change.test.ts b/packages-exp/installations-exp/src/functions/on-id-change.test.ts new file mode 100644 index 00000000000..89175b07eb3 --- /dev/null +++ b/packages-exp/installations-exp/src/functions/on-id-change.test.ts @@ -0,0 +1,52 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { expect } from 'chai'; +import { stub } from 'sinon'; +import '../testing/setup'; +import { onIdChange } from './on-id-change'; +import * as FidChangedModule from '../helpers/fid-changed'; +import { getFakeDependencies } from '../testing/fake-generators'; +import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; + +describe('onIdChange', () => { + let dependencies: FirebaseDependencies; + + beforeEach(() => { + dependencies = getFakeDependencies(); + stub(FidChangedModule); + }); + + it('calls addCallback with the given callback and app key when called', () => { + const callback = stub(); + onIdChange(dependencies, callback); + expect(FidChangedModule.addCallback).to.have.been.calledOnceWith( + dependencies.appConfig, + callback + ); + }); + + it('calls removeCallback with the given callback and app key when unsubscribe is called', () => { + const callback = stub(); + const unsubscribe = onIdChange(dependencies, callback); + unsubscribe(); + expect(FidChangedModule.removeCallback).to.have.been.calledOnceWith( + dependencies.appConfig, + callback + ); + }); +}); diff --git a/packages-exp/installations-exp/src/functions/on-id-change.ts b/packages-exp/installations-exp/src/functions/on-id-change.ts new file mode 100644 index 00000000000..c417b005e43 --- /dev/null +++ b/packages-exp/installations-exp/src/functions/on-id-change.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { addCallback, removeCallback } from '../helpers/fid-changed'; +import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; + +export type IdChangeCallbackFn = (installationId: string) => void; +export type IdChangeUnsubscribeFn = () => void; + +/** + * Sets a new callback that will get called when Installation ID changes. + * Returns an unsubscribe function that will remove the callback when called. + */ +export function onIdChange( + { appConfig }: FirebaseDependencies, + callback: IdChangeCallbackFn +): IdChangeUnsubscribeFn { + addCallback(appConfig, callback); + + return () => { + removeCallback(appConfig, callback); + }; +} diff --git a/packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.test.ts b/packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.test.ts new file mode 100644 index 00000000000..18e56f436f2 --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.test.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { expect } from 'chai'; +import '../testing/setup'; +import { bufferToBase64UrlSafe } from './buffer-to-base64-url-safe'; + +const str = 'hello world'; +const TYPED_ARRAY_REPRESENTATION = new Uint8Array(str.length); +for (let i = 0; i < str.length; i++) { + TYPED_ARRAY_REPRESENTATION[i] = str.charCodeAt(i); +} + +const BASE_64_REPRESENTATION = btoa(str); + +describe('bufferToBase64', () => { + it('returns a base64 representation of a Uint8Array', () => { + expect(bufferToBase64UrlSafe(TYPED_ARRAY_REPRESENTATION)).to.equal( + BASE_64_REPRESENTATION + ); + }); +}); diff --git a/packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.ts b/packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.ts new file mode 100644 index 00000000000..c336ce63528 --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/buffer-to-base64-url-safe.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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. + */ + +export function bufferToBase64UrlSafe(array: Uint8Array): string { + const b64 = btoa(String.fromCharCode(...array)); + return b64.replace(/\+/g, '-').replace(/\//g, '_'); +} diff --git a/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts b/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts new file mode 100644 index 00000000000..110a6d1ab10 --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts @@ -0,0 +1,60 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { FirebaseError } from '@firebase/util'; +import { expect } from 'chai'; +import { AppConfig } from '../interfaces/app-config'; +import { getFakeApp } from '../testing/fake-generators'; +import '../testing/setup'; +import { extractAppConfig } from './extract-app-config'; + +describe('extractAppConfig', () => { + it('returns AppConfig if the argument is a FirebaseApp object that includes an appId', () => { + const firebaseApp = getFakeApp(); + const expected: AppConfig = { + appName: 'appName', + apiKey: 'apiKey', + projectId: 'projectId', + appId: '1:777777777777:web:d93b5ca1475efe57' + }; + expect(extractAppConfig(firebaseApp)).to.deep.equal(expected); + }); + + it('throws if a necessary value is missing', () => { + expect(() => extractAppConfig(undefined as any)).to.throw(FirebaseError); + + let firebaseApp = getFakeApp(); + delete firebaseApp.name; + expect(() => extractAppConfig(firebaseApp)).to.throw(FirebaseError); + + firebaseApp = getFakeApp(); + delete firebaseApp.options; + expect(() => extractAppConfig(firebaseApp)).to.throw(FirebaseError); + + firebaseApp = getFakeApp(); + delete firebaseApp.options.projectId; + expect(() => extractAppConfig(firebaseApp)).to.throw(FirebaseError); + + firebaseApp = getFakeApp(); + delete firebaseApp.options.apiKey; + expect(() => extractAppConfig(firebaseApp)).to.throw(FirebaseError); + + firebaseApp = getFakeApp(); + delete firebaseApp.options.appId; + expect(() => extractAppConfig(firebaseApp)).to.throw(FirebaseError); + }); +}); diff --git a/packages-exp/installations-exp/src/helpers/extract-app-config.ts b/packages-exp/installations-exp/src/helpers/extract-app-config.ts new file mode 100644 index 00000000000..51f1bad2e71 --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/extract-app-config.ts @@ -0,0 +1,57 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { FirebaseApp, FirebaseOptions } from '@firebase/app-types'; +import { FirebaseError } from '@firebase/util'; +import { AppConfig } from '../interfaces/app-config'; +import { ERROR_FACTORY, ErrorCode } from '../util/errors'; + +export function extractAppConfig(app: FirebaseApp): AppConfig { + if (!app || !app.options) { + throw getMissingValueError('App Configuration'); + } + + if (!app.name) { + throw getMissingValueError('App Name'); + } + + // Required app config keys + const configKeys: Array = [ + 'projectId', + 'apiKey', + 'appId' + ]; + + for (const keyName of configKeys) { + if (!app.options[keyName]) { + throw getMissingValueError(keyName); + } + } + + return { + appName: app.name, + projectId: app.options.projectId!, + apiKey: app.options.apiKey!, + appId: app.options.appId! + }; +} + +function getMissingValueError(valueName: string): FirebaseError { + return ERROR_FACTORY.create(ErrorCode.MISSING_APP_CONFIG_VALUES, { + valueName + }); +} diff --git a/packages-exp/installations-exp/src/helpers/fid-changed.test.ts b/packages-exp/installations-exp/src/helpers/fid-changed.test.ts new file mode 100644 index 00000000000..99a9e5a462d --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/fid-changed.test.ts @@ -0,0 +1,103 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { expect } from 'chai'; +import { stub } from 'sinon'; +import '../testing/setup'; +import { AppConfig } from '../interfaces/app-config'; +import { + fidChanged, + addCallback, + removeCallback +} from '../helpers/fid-changed'; +import { getFakeAppConfig } from '../testing/fake-generators'; + +const FID = 'evil-lies-in-every-man'; + +describe('onIdChange', () => { + describe('with single app', () => { + let appConfig: AppConfig; + + beforeEach(() => { + appConfig = getFakeAppConfig(); + }); + + it('calls the provided callback when FID changes', () => { + const stubFn = stub(); + addCallback(appConfig, stubFn); + + fidChanged(appConfig, FID); + + expect(stubFn).to.have.been.calledOnceWith(FID); + }); + + it('calls multiple callbacks', () => { + const stubA = stub(); + addCallback(appConfig, stubA); + const stubB = stub(); + addCallback(appConfig, stubB); + + fidChanged(appConfig, FID); + + expect(stubA).to.have.been.calledOnceWith(FID); + expect(stubB).to.have.been.calledOnceWith(FID); + }); + + it('does not call removed callbacks', () => { + const stubFn = stub(); + addCallback(appConfig, stubFn); + + removeCallback(appConfig, stubFn); + fidChanged(appConfig, FID); + + expect(stubFn).not.to.have.been.called; + }); + + it('does not throw when removeCallback is called multiple times', () => { + const stubFn = stub(); + addCallback(appConfig, stubFn); + + removeCallback(appConfig, stubFn); + removeCallback(appConfig, stubFn); + fidChanged(appConfig, FID); + + expect(stubFn).not.to.have.been.called; + }); + }); + + describe('with multiple apps', () => { + let appConfigA: AppConfig; + let appConfigB: AppConfig; + + beforeEach(() => { + appConfigA = getFakeAppConfig(); + appConfigB = getFakeAppConfig({ appName: 'differentAppName' }); + }); + + it('calls the correct callback when FID changes', () => { + const stubA = stub(); + addCallback(appConfigA, stubA); + const stubB = stub(); + addCallback(appConfigB, stubB); + + fidChanged(appConfigA, FID); + + expect(stubA).to.have.been.calledOnceWith(FID); + expect(stubB).not.to.have.been.called; + }); + }); +}); diff --git a/packages-exp/installations-exp/src/helpers/fid-changed.ts b/packages-exp/installations-exp/src/helpers/fid-changed.ts new file mode 100644 index 00000000000..044950085a6 --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/fid-changed.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { getKey } from '../util/get-key'; +import { AppConfig } from '../interfaces/app-config'; +import { IdChangeCallbackFn } from '../functions'; + +const fidChangeCallbacks: Map> = new Map(); + +/** + * Calls the onIdChange callbacks with the new FID value, and broadcasts the + * change to other tabs. + */ +export function fidChanged(appConfig: AppConfig, fid: string): void { + const key = getKey(appConfig); + + callFidChangeCallbacks(key, fid); + broadcastFidChange(key, fid); +} + +export function addCallback( + appConfig: AppConfig, + callback: IdChangeCallbackFn +): void { + // Open the broadcast channel if it's not already open, + // to be able to listen to change events from other tabs. + getBroadcastChannel(); + + const key = getKey(appConfig); + + let callbackSet = fidChangeCallbacks.get(key); + if (!callbackSet) { + callbackSet = new Set(); + fidChangeCallbacks.set(key, callbackSet); + } + callbackSet.add(callback); +} + +export function removeCallback( + appConfig: AppConfig, + callback: IdChangeCallbackFn +): void { + const key = getKey(appConfig); + + const callbackSet = fidChangeCallbacks.get(key); + + if (!callbackSet) { + return; + } + + callbackSet.delete(callback); + if (callbackSet.size === 0) { + fidChangeCallbacks.delete(key); + } + + // Close broadcast channel if there are no more callbacks. + closeBroadcastChannel(); +} + +function callFidChangeCallbacks(key: string, fid: string): void { + const callbacks = fidChangeCallbacks.get(key); + if (!callbacks) { + return; + } + + for (const callback of callbacks) { + callback(fid); + } +} + +function broadcastFidChange(key: string, fid: string): void { + const channel = getBroadcastChannel(); + if (channel) { + channel.postMessage({ key, fid }); + } + closeBroadcastChannel(); +} + +let broadcastChannel: BroadcastChannel | null = null; +/** Opens and returns a BroadcastChannel if it is supported by the browser. */ +function getBroadcastChannel(): BroadcastChannel | null { + if (!broadcastChannel && 'BroadcastChannel' in self) { + broadcastChannel = new BroadcastChannel('[Firebase] FID Change'); + broadcastChannel.onmessage = e => { + callFidChangeCallbacks(e.data.key, e.data.fid); + }; + } + return broadcastChannel; +} + +function closeBroadcastChannel(): void { + if (fidChangeCallbacks.size === 0 && broadcastChannel) { + broadcastChannel.close(); + broadcastChannel = null; + } +} diff --git a/packages-exp/installations-exp/src/helpers/generate-fid.test.ts b/packages-exp/installations-exp/src/helpers/generate-fid.test.ts new file mode 100644 index 00000000000..5d5e3414d10 --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/generate-fid.test.ts @@ -0,0 +1,128 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { expect } from 'chai'; +import { stub } from 'sinon'; +import '../testing/setup'; +import { generateFid, VALID_FID_PATTERN } from './generate-fid'; + +/** A few random values to generate a FID from. */ +// prettier-ignore +const MOCK_RANDOM_VALUES = [ + [14, 107, 44, 183, 190, 84, 253, 45, 219, 233, 43, 190, 240, 152, 195, 222, 237], + [184, 251, 91, 157, 125, 225, 209, 15, 116, 66, 46, 113, 194, 126, 16, 13, 226], + [197, 123, 13, 142, 239, 129, 252, 139, 156, 36, 219, 192, 153, 52, 182, 231, 177], + [69, 154, 197, 91, 156, 196, 125, 111, 3, 67, 212, 132, 169, 11, 14, 254, 125], + [193, 102, 58, 19, 244, 69, 36, 135, 170, 106, 98, 216, 246, 209, 24, 155, 149], + [252, 59, 222, 160, 82, 160, 82, 186, 14, 172, 196, 114, 146, 191, 196, 194, 146], + [64, 147, 153, 236, 225, 142, 235, 109, 184, 249, 174, 127, 33, 238, 227, 172, 111], + [129, 137, 136, 120, 248, 206, 253, 78, 159, 201, 216, 15, 246, 80, 118, 185, 211], + [117, 150, 2, 180, 116, 230, 45, 188, 183, 43, 152, 100, 50, 255, 101, 175, 190], + [156, 129, 30, 101, 58, 137, 217, 249, 12, 227, 235, 80, 248, 81, 191, 2, 5], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], +]; + +/** The FIDs that should be generated based on MOCK_RANDOM_VALUES. */ +const EXPECTED_FIDS = [ + 'fmsst75U_S3b6Su-8JjD3u', + 'ePtbnX3h0Q90Qi5xwn4QDe', + 'dXsNju-B_IucJNvAmTS257', + 'dZrFW5zEfW8DQ9SEqQsO_n', + 'cWY6E_RFJIeqamLY9tEYm5', + 'fDveoFKgUroOrMRykr_Ewp', + 'cJOZ7OGO6224-a5_Ie7jrG', + 'cYmIePjO_U6fydgP9lB2ud', + 'dZYCtHTmLby3K5hkMv9lr7', + 'fIEeZTqJ2fkM4-tQ-FG_Ag', + 'cAAAAAAAAAAAAAAAAAAAAA', + 'f_____________________' +]; + +describe('generateFid', () => { + it('deterministically generates FIDs based on crypto.getRandomValues', () => { + let randomValueIndex = 0; + stub(crypto, 'getRandomValues').callsFake(array => { + if (!(array instanceof Uint8Array)) { + throw new Error('what'); + } + const values = MOCK_RANDOM_VALUES[randomValueIndex++]; + for (let i = 0; i < array.length; i++) { + array[i] = values[i]; + } + return array; + }); + + for (const expectedFid of EXPECTED_FIDS) { + expect(generateFid()).to.deep.equal(expectedFid); + } + }); + + it('generates valid FIDs', () => { + for (let i = 0; i < 1000; i++) { + const fid = generateFid(); + expect(VALID_FID_PATTERN.test(fid)).to.equal( + true, + `${fid} is not a valid FID` + ); + } + }); + + it('generates FIDs where each character is equally likely to appear in each location', () => { + const numTries = 200000; + + const charOccurrencesMapList: Array> = new Array(22); + for (let i = 0; i < charOccurrencesMapList.length; i++) { + charOccurrencesMapList[i] = new Map(); + } + + for (let i = 0; i < numTries; i++) { + const fid = generateFid(); + + Array.from(fid).forEach((char, location) => { + const map = charOccurrencesMapList[location]; + map.set(char, (map.get(char) || 0) + 1); + }); + } + + for (let i = 0; i < charOccurrencesMapList.length; i++) { + const map = charOccurrencesMapList[i]; + if (i === 0) { + // In the first location only 4 characters (c, d, e, f) are valid. + expect(map.size).to.equal(4); + } else { + // In locations other than the first, all 64 characters are valid. + expect(map.size).to.equal(64); + } + + Array.from(map.entries()).forEach(([_, occurrence]) => { + const expectedOccurrence = numTries / map.size; + + // 10% margin of error + expect(occurrence).to.be.above(expectedOccurrence * 0.9); + expect(occurrence).to.be.below(expectedOccurrence * 1.1); + }); + } + }).timeout(30000); + + it('returns an empty string if FID generation fails', () => { + stub(crypto, 'getRandomValues').throws(); + + const fid = generateFid(); + expect(fid).to.equal(''); + }); +}); diff --git a/packages-exp/installations-exp/src/helpers/generate-fid.ts b/packages-exp/installations-exp/src/helpers/generate-fid.ts new file mode 100644 index 00000000000..5d87df04628 --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/generate-fid.ts @@ -0,0 +1,55 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { bufferToBase64UrlSafe } from './buffer-to-base64-url-safe'; + +export const VALID_FID_PATTERN = /^[cdef][\w-]{21}$/; +export const INVALID_FID = ''; + +/** + * Generates a new FID using random values from Web Crypto API. + * Returns an empty string if FID generation fails for any reason. + */ +export function generateFid(): string { + try { + // A valid FID has exactly 22 base64 characters, which is 132 bits, or 16.5 + // bytes. our implementation generates a 17 byte array instead. + const fidByteArray = new Uint8Array(17); + const crypto = + self.crypto || ((self as unknown) as { msCrypto: Crypto }).msCrypto; + crypto.getRandomValues(fidByteArray); + + // Replace the first 4 random bits with the constant FID header of 0b0111. + fidByteArray[0] = 0b01110000 + (fidByteArray[0] % 0b00010000); + + const fid = encode(fidByteArray); + + return VALID_FID_PATTERN.test(fid) ? fid : INVALID_FID; + } catch { + // FID generation errored + return INVALID_FID; + } +} + +/** Converts a FID Uint8Array to a base64 string representation. */ +function encode(fidByteArray: Uint8Array): string { + const b64String = bufferToBase64UrlSafe(fidByteArray); + + // Remove the 23rd character that was added because of the extra 4 bits at the + // end of our 17 byte array, and the '=' padding. + return b64String.substr(0, 22); +} diff --git a/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts b/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts new file mode 100644 index 00000000000..e1b830bd0c6 --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts @@ -0,0 +1,477 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { AssertionError, expect } from 'chai'; +import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; +import * as createInstallationRequestModule from '../api/create-installation-request'; +import { AppConfig } from '../interfaces/app-config'; +import { + InProgressInstallationEntry, + RegisteredInstallationEntry, + RequestStatus, + UnregisteredInstallationEntry +} from '../interfaces/installation-entry'; +import { getFakeAppConfig } from '../testing/fake-generators'; +import '../testing/setup'; +import { ERROR_FACTORY, ErrorCode } from '../util/errors'; +import { sleep } from '../util/sleep'; +import * as generateFidModule from './generate-fid'; +import { getInstallationEntry } from './get-installation-entry'; +import { get, set } from './idb-manager'; + +const FID = 'cry-of-the-black-birds'; + +describe('getInstallationEntry', () => { + let clock: SinonFakeTimers; + let appConfig: AppConfig; + let createInstallationRequestSpy: SinonStub< + [AppConfig, InProgressInstallationEntry], + Promise + >; + + beforeEach(() => { + clock = useFakeTimers({ now: 1_000_000 }); + appConfig = getFakeAppConfig(); + createInstallationRequestSpy = stub( + createInstallationRequestModule, + 'createInstallationRequest' + ).callsFake( + async (_, installationEntry): Promise => { + await sleep(500); // Request would take some time + const registeredInstallationEntry: RegisteredInstallationEntry = { + // Returns new FID if client FID is invalid. + fid: installationEntry.fid || FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now(), + token: 'token', + expiresIn: 1_000_000_000 + } + }; + return registeredInstallationEntry; + } + ); + }); + + afterEach(() => { + // Clean up all pending requests. + clock.runAll(); + }); + + it('saves the InstallationEntry in the database before returning it', async () => { + const oldDbEntry = await get(appConfig); + expect(oldDbEntry).to.be.undefined; + + const { installationEntry } = await getInstallationEntry(appConfig); + + const newDbEntry = await get(appConfig); + expect(newDbEntry).to.deep.equal(installationEntry); + }); + + it('saves the InstallationEntry in the database if app is offline', async () => { + stub(navigator, 'onLine').value(false); + + const oldDbEntry = await get(appConfig); + expect(oldDbEntry).to.be.undefined; + + const { installationEntry } = await getInstallationEntry(appConfig); + + const newDbEntry = await get(appConfig); + expect(newDbEntry).to.deep.equal(installationEntry); + }); + + it('saves the InstallationEntry in the database when registration completes', async () => { + const { + installationEntry, + registrationPromise + } = await getInstallationEntry(appConfig); + expect(installationEntry.registrationStatus).to.equal( + RequestStatus.IN_PROGRESS + ); + expect(registrationPromise).to.be.an.instanceOf(Promise); + + const oldDbEntry = await get(appConfig); + expect(oldDbEntry).to.deep.equal(installationEntry); + + clock.next(); // Finish registration request. + await expect(registrationPromise).to.be.fulfilled; + + const newDbEntry = await get(appConfig); + expect(newDbEntry!.registrationStatus).to.equal(RequestStatus.COMPLETED); + }); + + it('saves the InstallationEntry in the database when registration fails', async () => { + createInstallationRequestSpy.callsFake(async () => { + await sleep(500); // Request would take some time + throw ERROR_FACTORY.create(ErrorCode.REQUEST_FAILED, { + requestName: 'Create Installation', + serverCode: 500, + serverStatus: 'INTERNAL', + serverMessage: 'Internal server error.' + }); + }); + + const { + installationEntry, + registrationPromise + } = await getInstallationEntry(appConfig); + expect(installationEntry.registrationStatus).to.equal( + RequestStatus.IN_PROGRESS + ); + expect(registrationPromise).to.be.an.instanceOf(Promise); + + const oldDbEntry = await get(appConfig); + expect(oldDbEntry).to.deep.equal(installationEntry); + + clock.next(); // Finish registration request. + await expect(registrationPromise).to.be.rejected; + + const newDbEntry = await get(appConfig); + expect(newDbEntry!.registrationStatus).to.equal(RequestStatus.NOT_STARTED); + }); + + it('removes the InstallationEntry from the database when registration fails with 409', async () => { + createInstallationRequestSpy.callsFake(async () => { + await sleep(500); // Request would take some time + throw ERROR_FACTORY.create(ErrorCode.REQUEST_FAILED, { + requestName: 'Create Installation', + serverCode: 409, + serverStatus: 'INVALID_ARGUMENT', + serverMessage: 'FID can not be used.' + }); + }); + + const { + installationEntry, + registrationPromise + } = await getInstallationEntry(appConfig); + expect(installationEntry.registrationStatus).to.equal( + RequestStatus.IN_PROGRESS + ); + + const oldDbEntry = await get(appConfig); + expect(oldDbEntry).to.deep.equal(installationEntry); + + clock.next(); // Finish registration request. + await expect(registrationPromise).to.be.rejected; + + const newDbEntry = await get(appConfig); + expect(newDbEntry).to.be.undefined; + }); + + it('returns the same FID on subsequent calls', async () => { + const { installationEntry: entry1 } = await getInstallationEntry(appConfig); + const { installationEntry: entry2 } = await getInstallationEntry(appConfig); + expect(entry1.fid).to.equal(entry2.fid); + }); + + describe('when there is no InstallationEntry in database', () => { + let generateInstallationEntrySpy: SinonStub<[], string>; + + beforeEach(() => { + generateInstallationEntrySpy = stub( + generateFidModule, + 'generateFid' + ).returns(FID); + }); + + it('returns a new pending InstallationEntry and triggers createInstallation', async () => { + const { + installationEntry, + registrationPromise + } = await getInstallationEntry(appConfig); + + if (installationEntry.registrationStatus !== RequestStatus.IN_PROGRESS) { + throw new AssertionError('InstallationEntry is not IN_PROGRESS.'); + } + + expect(registrationPromise).to.be.an.instanceOf(Promise); + expect(installationEntry).to.deep.equal({ + fid: FID, + registrationStatus: RequestStatus.IN_PROGRESS, + + // https://github.com/chaijs/chai/issues/644 + registrationTime: installationEntry.registrationTime + }); + expect(generateInstallationEntrySpy).to.be.called; + expect(createInstallationRequestSpy).to.be.called; + }); + + it('returns a new unregistered InstallationEntry if app is offline', async () => { + stub(navigator, 'onLine').value(false); + + const { installationEntry } = await getInstallationEntry(appConfig); + + expect(installationEntry).to.deep.equal({ + fid: FID, + registrationStatus: RequestStatus.NOT_STARTED + }); + expect(generateInstallationEntrySpy).to.be.called; + expect(createInstallationRequestSpy).not.to.be.called; + }); + + it('does not trigger createInstallation REST call on subsequent calls', async () => { + await getInstallationEntry(appConfig); + await getInstallationEntry(appConfig); + + expect(createInstallationRequestSpy).to.be.calledOnce; + }); + + it('returns a registrationPromise on subsequent calls before initial promise resolves', async () => { + const { registrationPromise: promise1 } = await getInstallationEntry( + appConfig + ); + const { registrationPromise: promise2 } = await getInstallationEntry( + appConfig + ); + + expect(createInstallationRequestSpy).to.be.calledOnce; + expect(promise1).to.be.an.instanceOf(Promise); + expect(promise2).to.be.an.instanceOf(Promise); + }); + + it('does not return a registrationPromise on subsequent calls after initial promise resolves', async () => { + const { registrationPromise: promise1 } = await getInstallationEntry( + appConfig + ); + expect(promise1).to.be.an.instanceOf(Promise); + + clock.next(); // Finish registration request. + await expect(promise1).to.be.fulfilled; + + const { registrationPromise: promise2 } = await getInstallationEntry( + appConfig + ); + expect(promise2).to.be.undefined; + + expect(createInstallationRequestSpy).to.be.calledOnce; + }); + + it('waits for the FID from the server if FID generation fails', async () => { + clock.restore(); + clock = useFakeTimers({ + now: 1_000_000, + shouldAdvanceTime: true /* Needed to allow the createInstallation request to complete. */ + }); + + // FID generation fails. + generateInstallationEntrySpy.returns(generateFidModule.INVALID_FID); + + const getInstallationEntryPromise = getInstallationEntry(appConfig); + + const { + installationEntry, + registrationPromise + } = await getInstallationEntryPromise; + + expect(installationEntry.fid).to.equal(FID); + expect(registrationPromise).to.be.undefined; + }); + }); + + describe('when there is an unregistered InstallationEntry in the database', () => { + beforeEach(async () => { + const unregisteredInstallationEntry: UnregisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.NOT_STARTED + }; + await set(appConfig, unregisteredInstallationEntry); + }); + + it('returns a pending InstallationEntry and triggers createInstallation', async () => { + const { + installationEntry, + registrationPromise + } = await getInstallationEntry(appConfig); + + if (installationEntry.registrationStatus !== RequestStatus.IN_PROGRESS) { + throw new AssertionError('InstallationEntry is not IN_PROGRESS.'); + } + + expect(registrationPromise).to.be.an.instanceOf(Promise); + expect(installationEntry).to.deep.equal({ + fid: FID, + registrationStatus: RequestStatus.IN_PROGRESS, + // https://github.com/chaijs/chai/issues/644 + registrationTime: installationEntry.registrationTime + }); + expect(createInstallationRequestSpy).to.be.calledOnce; + }); + + it('returns the same InstallationEntry if the app is offline', async () => { + stub(navigator, 'onLine').value(false); + + const { installationEntry } = await getInstallationEntry(appConfig); + + expect(installationEntry).to.deep.equal({ + fid: FID, + registrationStatus: RequestStatus.NOT_STARTED + }); + expect(createInstallationRequestSpy).not.to.be.called; + }); + }); + + describe('when there is a pending InstallationEntry in the database', () => { + beforeEach(async () => { + const inProgressInstallationEntry: InProgressInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.IN_PROGRESS, + registrationTime: 1_000_000 + }; + await set(appConfig, inProgressInstallationEntry); + }); + + it("returns the same InstallationEntry if the request hasn't timed out", async () => { + clock.now = 1_001_000; // One second after the request was initiated. + + const { installationEntry } = await getInstallationEntry(appConfig); + + expect(installationEntry).to.deep.equal({ + fid: FID, + registrationStatus: RequestStatus.IN_PROGRESS, + registrationTime: 1_000_000 + }); + expect(createInstallationRequestSpy).not.to.be.called; + }); + + it('updates the InstallationEntry and triggers createInstallation if the request fails', async () => { + clock.restore(); + clock = useFakeTimers({ + now: 1_001_000 /* One second after the request was initiated. */, + shouldAdvanceTime: true /* Needed to allow the createInstallation request to complete. */ + }); + + const installationEntryPromise = getInstallationEntry(appConfig); + + // The pending request fails after a while. + clock.tick(3000); + await set(appConfig, { + fid: FID, + registrationStatus: RequestStatus.NOT_STARTED + }); + + const { registrationPromise } = await installationEntryPromise; + + // Let the new getInstallationEntry process start. + await sleep(250); + + const tokenDetails = (await get( + appConfig + )) as InProgressInstallationEntry; + expect(tokenDetails.registrationTime).to.be.at.least( + /* When the first pending request failed. */ 1_004_000 + ); + expect(tokenDetails).to.deep.equal({ + fid: FID, + registrationStatus: RequestStatus.IN_PROGRESS, + // Ignore registrationTime as we already checked it. + registrationTime: tokenDetails.registrationTime + }); + + expect(registrationPromise).to.be.an.instanceOf(Promise); + await registrationPromise; + expect(createInstallationRequestSpy).to.be.calledOnce; + }); + + it('updates the InstallationEntry if the request fails and the app is offline', async () => { + stub(navigator, 'onLine').value(false); + + clock.restore(); + clock = useFakeTimers({ + now: 1_001_000 /* One second after the request was initiated. */, + shouldAdvanceTime: true /* Needed to allow the createInstallation request to complete. */ + }); + + const installationEntryPromise = getInstallationEntry(appConfig); + + // The pending request fails after a while. + clock.tick(3000); + await set(appConfig, { + fid: FID, + registrationStatus: RequestStatus.NOT_STARTED + }); + + const { registrationPromise } = await installationEntryPromise; + + // Let the new getInstallationEntry process start. + await sleep(250); + + expect(await get(appConfig)).to.deep.equal({ + fid: FID, + registrationStatus: RequestStatus.NOT_STARTED + }); + + expect(registrationPromise).to.be.an.instanceOf(Promise); + await expect(registrationPromise).to.be.rejectedWith( + 'Application offline' + ); + expect(createInstallationRequestSpy).not.to.be.called; + }); + + it('returns a new pending InstallationEntry and triggers createInstallation if the request had already timed out', async () => { + clock.now = 1_015_000; // Fifteen seconds after the request was initiated. + + const { installationEntry } = await getInstallationEntry(appConfig); + + expect(installationEntry).to.deep.equal({ + fid: FID, + registrationStatus: RequestStatus.IN_PROGRESS, + registrationTime: 1_015_000 + }); + expect(createInstallationRequestSpy).to.be.calledOnce; + }); + + it('returns a new unregistered InstallationEntry if the request had already timed out and the app is offline', async () => { + stub(navigator, 'onLine').value(false); + clock.now = 1_015_000; // Fifteen seconds after the request was initiated. + + const { installationEntry } = await getInstallationEntry(appConfig); + + expect(installationEntry).to.deep.equal({ + fid: FID, + registrationStatus: RequestStatus.NOT_STARTED + }); + expect(createInstallationRequestSpy).not.to.be.called; + }); + }); + + describe('when there is a registered InstallationEntry in the database', () => { + beforeEach(async () => { + const registeredInstallationEntry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { requestStatus: RequestStatus.NOT_STARTED } + }; + await set(appConfig, registeredInstallationEntry); + }); + + it('returns the InstallationEntry from the database', async () => { + const { installationEntry } = await getInstallationEntry(appConfig); + + expect(installationEntry).to.deep.equal({ + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { requestStatus: RequestStatus.NOT_STARTED } + }); + expect(createInstallationRequestSpy).not.to.be.called; + }); + }); +}); diff --git a/packages-exp/installations-exp/src/helpers/get-installation-entry.ts b/packages-exp/installations-exp/src/helpers/get-installation-entry.ts new file mode 100644 index 00000000000..0edcf8e8b94 --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/get-installation-entry.ts @@ -0,0 +1,227 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { createInstallationRequest } from '../api/create-installation-request'; +import { AppConfig } from '../interfaces/app-config'; +import { + InProgressInstallationEntry, + InstallationEntry, + RegisteredInstallationEntry, + RequestStatus +} from '../interfaces/installation-entry'; +import { PENDING_TIMEOUT_MS } from '../util/constants'; +import { ERROR_FACTORY, ErrorCode, isServerError } from '../util/errors'; +import { sleep } from '../util/sleep'; +import { generateFid, INVALID_FID } from './generate-fid'; +import { remove, set, update } from './idb-manager'; + +export interface InstallationEntryWithRegistrationPromise { + installationEntry: InstallationEntry; + /** Exist iff the installationEntry is not registered. */ + registrationPromise?: Promise; +} + +/** + * Updates and returns the InstallationEntry from the database. + * Also triggers a registration request if it is necessary and possible. + */ +export async function getInstallationEntry( + appConfig: AppConfig +): Promise { + let registrationPromise: Promise | undefined; + + const installationEntry = await update(appConfig, oldEntry => { + const installationEntry = updateOrCreateInstallationEntry(oldEntry); + const entryWithPromise = triggerRegistrationIfNecessary( + appConfig, + installationEntry + ); + registrationPromise = entryWithPromise.registrationPromise; + return entryWithPromise.installationEntry; + }); + + if (installationEntry.fid === INVALID_FID) { + // FID generation failed. Waiting for the FID from the server. + return { installationEntry: await registrationPromise! }; + } + + return { + installationEntry, + registrationPromise + }; +} + +/** + * Creates a new Installation Entry if one does not exist. + * Also clears timed out pending requests. + */ +function updateOrCreateInstallationEntry( + oldEntry: InstallationEntry | undefined +): InstallationEntry { + const entry: InstallationEntry = oldEntry || { + fid: generateFid(), + registrationStatus: RequestStatus.NOT_STARTED + }; + + return clearTimedOutRequest(entry); +} + +/** + * If the Firebase Installation is not registered yet, this will trigger the + * registration and return an InProgressInstallationEntry. + * + * If registrationPromise does not exist, the installationEntry is guaranteed + * to be registered. + */ +function triggerRegistrationIfNecessary( + appConfig: AppConfig, + installationEntry: InstallationEntry +): InstallationEntryWithRegistrationPromise { + if (installationEntry.registrationStatus === RequestStatus.NOT_STARTED) { + if (!navigator.onLine) { + // Registration required but app is offline. + const registrationPromiseWithError = Promise.reject( + ERROR_FACTORY.create(ErrorCode.APP_OFFLINE) + ); + return { + installationEntry, + registrationPromise: registrationPromiseWithError + }; + } + + // Try registering. Change status to IN_PROGRESS. + const inProgressEntry: InProgressInstallationEntry = { + fid: installationEntry.fid, + registrationStatus: RequestStatus.IN_PROGRESS, + registrationTime: Date.now() + }; + const registrationPromise = registerInstallation( + appConfig, + inProgressEntry + ); + return { installationEntry: inProgressEntry, registrationPromise }; + } else if ( + installationEntry.registrationStatus === RequestStatus.IN_PROGRESS + ) { + return { + installationEntry, + registrationPromise: waitUntilFidRegistration(appConfig) + }; + } else { + return { installationEntry }; + } +} + +/** This will be executed only once for each new Firebase Installation. */ +async function registerInstallation( + appConfig: AppConfig, + installationEntry: InProgressInstallationEntry +): Promise { + try { + const registeredInstallationEntry = await createInstallationRequest( + appConfig, + installationEntry + ); + return set(appConfig, registeredInstallationEntry); + } catch (e) { + if (isServerError(e) && e.serverCode === 409) { + // Server returned a "FID can not be used" error. + // Generate a new ID next time. + await remove(appConfig); + } else { + // Registration failed. Set FID as not registered. + await set(appConfig, { + fid: installationEntry.fid, + registrationStatus: RequestStatus.NOT_STARTED + }); + } + throw e; + } +} + +/** Call if FID registration is pending in another request. */ +async function waitUntilFidRegistration( + appConfig: AppConfig +): Promise { + // Unfortunately, there is no way of reliably observing when a value in + // IndexedDB changes (yet, see https://github.com/WICG/indexed-db-observers), + // so we need to poll. + + let entry: InstallationEntry = await updateInstallationRequest(appConfig); + while (entry.registrationStatus === RequestStatus.IN_PROGRESS) { + // createInstallation request still in progress. + await sleep(100); + + entry = await updateInstallationRequest(appConfig); + } + + if (entry.registrationStatus === RequestStatus.NOT_STARTED) { + // The request timed out or failed in a different call. Try again. + const { + installationEntry, + registrationPromise + } = await getInstallationEntry(appConfig); + + if (registrationPromise) { + return registrationPromise; + } else { + // if there is no registrationPromise, entry is registered. + return installationEntry as RegisteredInstallationEntry; + } + } + + return entry; +} + +/** + * Called only if there is a CreateInstallation request in progress. + * + * Updates the InstallationEntry in the DB based on the status of the + * CreateInstallation request. + * + * Returns the updated InstallationEntry. + */ +function updateInstallationRequest( + appConfig: AppConfig +): Promise { + return update(appConfig, oldEntry => { + if (!oldEntry) { + throw ERROR_FACTORY.create(ErrorCode.INSTALLATION_NOT_FOUND); + } + return clearTimedOutRequest(oldEntry); + }); +} + +function clearTimedOutRequest(entry: InstallationEntry): InstallationEntry { + if (hasInstallationRequestTimedOut(entry)) { + return { + fid: entry.fid, + registrationStatus: RequestStatus.NOT_STARTED + }; + } + + return entry; +} + +function hasInstallationRequestTimedOut( + installationEntry: InstallationEntry +): boolean { + return ( + installationEntry.registrationStatus === RequestStatus.IN_PROGRESS && + installationEntry.registrationTime + PENDING_TIMEOUT_MS < Date.now() + ); +} diff --git a/packages-exp/installations-exp/src/helpers/idb-manager.test.ts b/packages-exp/installations-exp/src/helpers/idb-manager.test.ts new file mode 100644 index 00000000000..3dbaa9f5091 --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/idb-manager.test.ts @@ -0,0 +1,194 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { expect } from 'chai'; +import { stub } from 'sinon'; +import { AppConfig } from '../interfaces/app-config'; +import { + InstallationEntry, + RequestStatus +} from '../interfaces/installation-entry'; +import { getFakeAppConfig } from '../testing/fake-generators'; +import '../testing/setup'; +import { clear, get, remove, set, update } from './idb-manager'; +import * as fidChangedModule from './fid-changed'; + +const VALUE_A: InstallationEntry = { + fid: 'VALUE_A', + registrationStatus: RequestStatus.NOT_STARTED +}; +const VALUE_B: InstallationEntry = { + fid: 'VALUE_B', + registrationStatus: RequestStatus.NOT_STARTED +}; + +describe('idb manager', () => { + let appConfig: AppConfig; + + beforeEach(() => { + appConfig = { ...getFakeAppConfig(), appName: 'appName1' }; + }); + + describe('get / set', () => { + it('sets a value and then gets the same value back', async () => { + await set(appConfig, VALUE_A); + const value = await get(appConfig); + expect(value).to.deep.equal(VALUE_A); + }); + + it('gets undefined for a key that does not exist', async () => { + const value = await get(appConfig); + expect(value).to.be.undefined; + }); + + it('sets and gets multiple values with different keys', async () => { + const appConfig2: AppConfig = { + ...getFakeAppConfig(), + appName: 'appName2' + }; + + await set(appConfig, VALUE_A); + await set(appConfig2, VALUE_B); + expect(await get(appConfig)).to.deep.equal(VALUE_A); + expect(await get(appConfig2)).to.deep.equal(VALUE_B); + }); + + it('overwrites a value', async () => { + await set(appConfig, VALUE_A); + await set(appConfig, VALUE_B); + expect(await get(appConfig)).to.deep.equal(VALUE_B); + }); + + it('calls fidChanged when a new FID is generated', async () => { + const fidChangedStub = stub(fidChangedModule, 'fidChanged'); + await set(appConfig, VALUE_A); + + expect(fidChangedStub).to.have.been.calledOnceWith( + appConfig, + VALUE_A.fid + ); + }); + + it('calls fidChanged when the FID changes', async () => { + await set(appConfig, VALUE_A); + + const fidChangedStub = stub(fidChangedModule, 'fidChanged'); + await set(appConfig, VALUE_B); + + expect(fidChangedStub).to.have.been.calledOnceWith( + appConfig, + VALUE_B.fid + ); + }); + + it('does not call fidChanged when the FID is the same', async () => { + await set(appConfig, VALUE_A); + + const fidChangedStub = stub(fidChangedModule, 'fidChanged'); + await set(appConfig, /* Same value */ VALUE_A); + + expect(fidChangedStub).not.to.have.been.called; + }); + }); + + describe('remove', () => { + it('deletes a key', async () => { + await set(appConfig, VALUE_A); + await remove(appConfig); + expect(await get(appConfig)).to.be.undefined; + }); + + it('does not throw if key does not exist', async () => { + await remove(appConfig); + expect(await get(appConfig)).to.be.undefined; + }); + }); + + describe('clear', () => { + it('deletes all keys', async () => { + const appConfig2: AppConfig = { + ...getFakeAppConfig(), + appName: 'appName2' + }; + + await set(appConfig, VALUE_A); + await set(appConfig2, VALUE_B); + await clear(); + expect(await get(appConfig)).to.be.undefined; + expect(await get(appConfig2)).to.be.undefined; + }); + }); + + describe('update', () => { + it('gets and sets a value atomically, returns the new value', async () => { + let isGetCalled = false; + + await set(appConfig, VALUE_A); + + const resultPromise = update(appConfig, oldValue => { + // get is already called for the same key, but it will only complete + // after update transaction finishes, at which point it will return the + // new value. + expect(isGetCalled).to.be.true; + + expect(oldValue).to.deep.equal(VALUE_A); + return VALUE_B; + }); + + // Called immediately after update, but before update completed. + const getPromise = get(appConfig); + isGetCalled = true; + + // Update returns the new value + expect(await resultPromise).to.deep.equal(VALUE_B); + + // If update weren't atomic, this would return the old value. + expect(await getPromise).to.deep.equal(VALUE_B); + }); + + it('calls fidChanged when a new FID is generated', async () => { + const fidChangedStub = stub(fidChangedModule, 'fidChanged'); + await update(appConfig, () => VALUE_A); + + expect(fidChangedStub).to.have.been.calledOnceWith( + appConfig, + VALUE_A.fid + ); + }); + + it('calls fidChanged when the FID changes', async () => { + await set(appConfig, VALUE_A); + + const fidChangedStub = stub(fidChangedModule, 'fidChanged'); + await update(appConfig, () => VALUE_B); + + expect(fidChangedStub).to.have.been.calledOnceWith( + appConfig, + VALUE_B.fid + ); + }); + + it('does not call fidChanged when the FID is the same', async () => { + await set(appConfig, VALUE_A); + + const fidChangedStub = stub(fidChangedModule, 'fidChanged'); + await update(appConfig, () => /* Same value */ VALUE_A); + + expect(fidChangedStub).not.to.have.been.called; + }); + }); +}); diff --git a/packages-exp/installations-exp/src/helpers/idb-manager.ts b/packages-exp/installations-exp/src/helpers/idb-manager.ts new file mode 100644 index 00000000000..8bc464fd115 --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/idb-manager.ts @@ -0,0 +1,123 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { DB, openDb } from 'idb'; +import { AppConfig } from '../interfaces/app-config'; +import { InstallationEntry } from '../interfaces/installation-entry'; +import { getKey } from '../util/get-key'; +import { fidChanged } from './fid-changed'; + +const DATABASE_NAME = 'firebase-installations-database'; +const DATABASE_VERSION = 1; +const OBJECT_STORE_NAME = 'firebase-installations-store'; + +let dbPromise: Promise | null = null; +function getDbPromise(): Promise { + if (!dbPromise) { + dbPromise = openDb(DATABASE_NAME, DATABASE_VERSION, upgradeDB => { + // We don't use 'break' in this switch statement, the fall-through + // behavior is what we want, because if there are multiple versions between + // the old version and the current version, we want ALL the migrations + // that correspond to those versions to run, not only the last one. + // eslint-disable-next-line default-case + switch (upgradeDB.oldVersion) { + case 0: + upgradeDB.createObjectStore(OBJECT_STORE_NAME); + } + }); + } + return dbPromise; +} + +/** Gets record(s) from the objectStore that match the given key. */ +export async function get( + appConfig: AppConfig +): Promise { + const key = getKey(appConfig); + const db = await getDbPromise(); + return db + .transaction(OBJECT_STORE_NAME) + .objectStore(OBJECT_STORE_NAME) + .get(key); +} + +/** Assigns or overwrites the record for the given key with the given value. */ +export async function set( + appConfig: AppConfig, + value: ValueType +): Promise { + const key = getKey(appConfig); + const db = await getDbPromise(); + const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); + const objectStore = tx.objectStore(OBJECT_STORE_NAME); + const oldValue = await objectStore.get(key); + await objectStore.put(value, key); + await tx.complete; + + if (!oldValue || oldValue.fid !== value.fid) { + fidChanged(appConfig, value.fid); + } + + return value; +} + +/** Removes record(s) from the objectStore that match the given key. */ +export async function remove(appConfig: AppConfig): Promise { + const key = getKey(appConfig); + const db = await getDbPromise(); + const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); + await tx.objectStore(OBJECT_STORE_NAME).delete(key); + await tx.complete; +} + +/** + * Atomically updates a record with the result of updateFn, which gets + * called with the current value. If newValue is undefined, the record is + * deleted instead. + * @return Updated value + */ +export async function update( + appConfig: AppConfig, + updateFn: (previousValue: InstallationEntry | undefined) => ValueType +): Promise { + const key = getKey(appConfig); + const db = await getDbPromise(); + const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); + const store = tx.objectStore(OBJECT_STORE_NAME); + const oldValue: InstallationEntry | undefined = await store.get(key); + const newValue = updateFn(oldValue); + + if (newValue === undefined) { + await store.delete(key); + } else { + await store.put(newValue, key); + } + await tx.complete; + + if (newValue && (!oldValue || oldValue.fid !== newValue.fid)) { + fidChanged(appConfig, newValue.fid); + } + + return newValue; +} + +export async function clear(): Promise { + const db = await getDbPromise(); + const tx = db.transaction(OBJECT_STORE_NAME, 'readwrite'); + await tx.objectStore(OBJECT_STORE_NAME).clear(); + await tx.complete; +} diff --git a/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts b/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts new file mode 100644 index 00000000000..c3a66782292 --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts @@ -0,0 +1,208 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { expect } from 'chai'; +import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; +import * as generateAuthTokenRequestModule from '../api/generate-auth-token-request'; +import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + CompletedAuthToken, + RegisteredInstallationEntry, + RequestStatus, + UnregisteredInstallationEntry +} from '../interfaces/installation-entry'; +import { getFakeDependencies } from '../testing/fake-generators'; +import '../testing/setup'; +import { TOKEN_EXPIRATION_BUFFER } from '../util/constants'; +import { sleep } from '../util/sleep'; +import { get, set } from './idb-manager'; +import { refreshAuthToken } from './refresh-auth-token'; + +const FID = 'carry-the-blessed-home'; +const AUTH_TOKEN = 'authTokenFromServer'; +const DB_AUTH_TOKEN = 'authTokenFromDB'; +const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000; + +describe('refreshAuthToken', () => { + let dependencies: FirebaseDependencies; + let generateAuthTokenRequestSpy: SinonStub< + [FirebaseDependencies, RegisteredInstallationEntry], + Promise + >; + + beforeEach(() => { + dependencies = getFakeDependencies(); + + generateAuthTokenRequestSpy = stub( + generateAuthTokenRequestModule, + 'generateAuthTokenRequest' + ).callsFake(async () => { + await sleep(100); // Request would take some time + const result: CompletedAuthToken = { + token: AUTH_TOKEN, + expiresIn: ONE_WEEK_MS, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() + }; + return result; + }); + }); + + it('throws when there is no installation in the DB', async () => { + await expect(refreshAuthToken(dependencies)).to.be.rejected; + }); + + it('throws when there is an unregistered installation in the db', async () => { + const installationEntry: UnregisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.NOT_STARTED + }; + await set(dependencies.appConfig, installationEntry); + + await expect(refreshAuthToken(dependencies)).to.be.rejected; + }); + + describe('when there is a valid auth token in the DB', () => { + beforeEach(async () => { + const installationEntry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + token: AUTH_TOKEN, + expiresIn: ONE_WEEK_MS, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() + } + }; + await set(dependencies.appConfig, installationEntry); + }); + + it('returns the token from the DB', async () => { + const { token } = await refreshAuthToken(dependencies); + expect(token).to.equal(AUTH_TOKEN); + }); + + it('does not call any server APIs', async () => { + await refreshAuthToken(dependencies); + expect(generateAuthTokenRequestSpy).not.to.be.called; + }); + + it('works even if the app is offline', async () => { + stub(navigator, 'onLine').value(false); + + const { token } = await refreshAuthToken(dependencies); + expect(token).to.equal(AUTH_TOKEN); + }); + }); + + describe('when there is an auth token that is about to expire in the DB', () => { + let clock: SinonFakeTimers; + + beforeEach(async () => { + clock = useFakeTimers({ shouldAdvanceTime: true }); + + const installationEntry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + token: DB_AUTH_TOKEN, + expiresIn: ONE_WEEK_MS, + requestStatus: RequestStatus.COMPLETED, + creationTime: + // Expires in ten minutes + Date.now() - ONE_WEEK_MS + TOKEN_EXPIRATION_BUFFER + 10 * 60 * 1000 + } + }; + await set(dependencies.appConfig, installationEntry); + }); + + it('returns a different token after expiration', async () => { + const token1 = await refreshAuthToken(dependencies); + expect(token1.token).to.equal(DB_AUTH_TOKEN); + + // Wait 30 minutes. + clock.tick('30:00'); + + const token2 = await refreshAuthToken(dependencies); + await expect(token2.token).to.equal(AUTH_TOKEN); + await expect(token2.token).not.to.equal(DB_AUTH_TOKEN); + expect(generateAuthTokenRequestSpy).to.be.calledOnce; + }); + }); + + describe('when there is an expired auth token in the DB', () => { + beforeEach(async () => { + const installationEntry: RegisteredInstallationEntry = { + fid: FID, + registrationStatus: RequestStatus.COMPLETED, + refreshToken: 'refreshToken', + authToken: { + token: DB_AUTH_TOKEN, + expiresIn: ONE_WEEK_MS, + requestStatus: RequestStatus.COMPLETED, + creationTime: Date.now() - 2 * ONE_WEEK_MS + } + }; + await set(dependencies.appConfig, installationEntry); + }); + + it('does not call generateAuthToken twice on subsequent calls', async () => { + await refreshAuthToken(dependencies); + await refreshAuthToken(dependencies); + expect(generateAuthTokenRequestSpy).to.be.calledOnce; + }); + + it('does not call generateAuthToken twice on simultaneous calls', async () => { + await Promise.all([ + refreshAuthToken(dependencies), + refreshAuthToken(dependencies) + ]); + expect(generateAuthTokenRequestSpy).to.be.calledOnce; + }); + + it('returns a new token', async () => { + const { token } = await refreshAuthToken(dependencies); + await expect(token).to.equal(AUTH_TOKEN); + await expect(token).not.to.equal(DB_AUTH_TOKEN); + expect(generateAuthTokenRequestSpy).to.be.calledOnce; + }); + + it('throws if the app is offline', async () => { + stub(navigator, 'onLine').value(false); + + await expect(refreshAuthToken(dependencies)).to.be.rejected; + }); + + it('saves the new token in the DB', async () => { + const { token } = await refreshAuthToken(dependencies); + + const installationEntry = (await get( + dependencies.appConfig + )) as RegisteredInstallationEntry; + expect(installationEntry).not.to.be.undefined; + expect(installationEntry.registrationStatus).to.equal( + RequestStatus.COMPLETED + ); + + const authToken = installationEntry.authToken as CompletedAuthToken; + expect(authToken.requestStatus).to.equal(RequestStatus.COMPLETED); + expect(authToken.token).to.equal(token); + }); + }); +}); diff --git a/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts b/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts new file mode 100644 index 00000000000..fa980597342 --- /dev/null +++ b/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts @@ -0,0 +1,209 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { generateAuthTokenRequest } from '../api/generate-auth-token-request'; +import { AppConfig } from '../interfaces/app-config'; +import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + AuthToken, + CompletedAuthToken, + InProgressAuthToken, + InstallationEntry, + RegisteredInstallationEntry, + RequestStatus +} from '../interfaces/installation-entry'; +import { PENDING_TIMEOUT_MS, TOKEN_EXPIRATION_BUFFER } from '../util/constants'; +import { ERROR_FACTORY, ErrorCode, isServerError } from '../util/errors'; +import { sleep } from '../util/sleep'; +import { remove, set, update } from './idb-manager'; + +/** + * Returns a valid authentication token for the installation. Generates a new + * token if one doesn't exist, is expired or about to expire. + * + * Should only be called if the Firebase Installation is registered. + */ +export async function refreshAuthToken( + dependencies: FirebaseDependencies, + forceRefresh = false +): Promise { + let tokenPromise: Promise | undefined; + const entry = await update(dependencies.appConfig, oldEntry => { + if (!isEntryRegistered(oldEntry)) { + throw ERROR_FACTORY.create(ErrorCode.NOT_REGISTERED); + } + + const oldAuthToken = oldEntry.authToken; + if (!forceRefresh && isAuthTokenValid(oldAuthToken)) { + // There is a valid token in the DB. + return oldEntry; + } else if (oldAuthToken.requestStatus === RequestStatus.IN_PROGRESS) { + // There already is a token request in progress. + tokenPromise = waitUntilAuthTokenRequest(dependencies, forceRefresh); + return oldEntry; + } else { + // No token or token expired. + if (!navigator.onLine) { + throw ERROR_FACTORY.create(ErrorCode.APP_OFFLINE); + } + + const inProgressEntry = makeAuthTokenRequestInProgressEntry(oldEntry); + tokenPromise = fetchAuthTokenFromServer(dependencies, inProgressEntry); + return inProgressEntry; + } + }); + + const authToken = tokenPromise + ? await tokenPromise + : (entry.authToken as CompletedAuthToken); + return authToken; +} + +/** + * Call only if FID is registered and Auth Token request is in progress. + * + * Waits until the current pending request finishes. If the request times out, + * tries once in this thread as well. + */ +async function waitUntilAuthTokenRequest( + dependencies: FirebaseDependencies, + forceRefresh: boolean +): Promise { + // Unfortunately, there is no way of reliably observing when a value in + // IndexedDB changes (yet, see https://github.com/WICG/indexed-db-observers), + // so we need to poll. + + let entry = await updateAuthTokenRequest(dependencies.appConfig); + while (entry.authToken.requestStatus === RequestStatus.IN_PROGRESS) { + // generateAuthToken still in progress. + await sleep(100); + + entry = await updateAuthTokenRequest(dependencies.appConfig); + } + + const authToken = entry.authToken; + if (authToken.requestStatus === RequestStatus.NOT_STARTED) { + // The request timed out or failed in a different call. Try again. + return refreshAuthToken(dependencies, forceRefresh); + } else { + return authToken; + } +} + +/** + * Called only if there is a GenerateAuthToken request in progress. + * + * Updates the InstallationEntry in the DB based on the status of the + * GenerateAuthToken request. + * + * Returns the updated InstallationEntry. + */ +function updateAuthTokenRequest( + appConfig: AppConfig +): Promise { + return update(appConfig, oldEntry => { + if (!isEntryRegistered(oldEntry)) { + throw ERROR_FACTORY.create(ErrorCode.NOT_REGISTERED); + } + + const oldAuthToken = oldEntry.authToken; + if (hasAuthTokenRequestTimedOut(oldAuthToken)) { + return { + ...oldEntry, + authToken: { requestStatus: RequestStatus.NOT_STARTED } + }; + } + + return oldEntry; + }); +} + +async function fetchAuthTokenFromServer( + dependencies: FirebaseDependencies, + installationEntry: RegisteredInstallationEntry +): Promise { + try { + const authToken = await generateAuthTokenRequest( + dependencies, + installationEntry + ); + const updatedInstallationEntry: RegisteredInstallationEntry = { + ...installationEntry, + authToken + }; + await set(dependencies.appConfig, updatedInstallationEntry); + return authToken; + } catch (e) { + if (isServerError(e) && (e.serverCode === 401 || e.serverCode === 404)) { + // Server returned a "FID not found" or a "Invalid authentication" error. + // Generate a new ID next time. + await remove(dependencies.appConfig); + } else { + const updatedInstallationEntry: RegisteredInstallationEntry = { + ...installationEntry, + authToken: { requestStatus: RequestStatus.NOT_STARTED } + }; + await set(dependencies.appConfig, updatedInstallationEntry); + } + throw e; + } +} + +function isEntryRegistered( + installationEntry: InstallationEntry | undefined +): installationEntry is RegisteredInstallationEntry { + return ( + installationEntry !== undefined && + installationEntry.registrationStatus === RequestStatus.COMPLETED + ); +} + +function isAuthTokenValid(authToken: AuthToken): boolean { + return ( + authToken.requestStatus === RequestStatus.COMPLETED && + !isAuthTokenExpired(authToken) + ); +} + +function isAuthTokenExpired(authToken: CompletedAuthToken): boolean { + const now = Date.now(); + return ( + now < authToken.creationTime || + authToken.creationTime + authToken.expiresIn < now + TOKEN_EXPIRATION_BUFFER + ); +} + +/** Returns an updated InstallationEntry with an InProgressAuthToken. */ +function makeAuthTokenRequestInProgressEntry( + oldEntry: RegisteredInstallationEntry +): RegisteredInstallationEntry { + const inProgressAuthToken: InProgressAuthToken = { + requestStatus: RequestStatus.IN_PROGRESS, + requestTime: Date.now() + }; + return { + ...oldEntry, + authToken: inProgressAuthToken + }; +} + +function hasAuthTokenRequestTimedOut(authToken: AuthToken): boolean { + return ( + authToken.requestStatus === RequestStatus.IN_PROGRESS && + authToken.requestTime + PENDING_TIMEOUT_MS < Date.now() + ); +} diff --git a/packages-exp/installations-exp/src/index.ts b/packages-exp/installations-exp/src/index.ts new file mode 100644 index 00000000000..a3d9257bb49 --- /dev/null +++ b/packages-exp/installations-exp/src/index.ts @@ -0,0 +1,85 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 firebase from '@firebase/app'; +import { + _FirebaseNamespace, + FirebaseService +} from '@firebase/app-types/private'; +import { Component, ComponentType } from '@firebase/component'; +import { FirebaseInstallations } from '@firebase/installations-types'; +import { + deleteInstallation, + getId, + getToken, + IdChangeCallbackFn, + IdChangeUnsubscribeFn, + onIdChange +} from './functions'; +import { extractAppConfig } from './helpers/extract-app-config'; +import { FirebaseDependencies } from './interfaces/firebase-dependencies'; + +import { name, version } from '../package.json'; + +export function registerInstallations(instance: _FirebaseNamespace): void { + const installationsName = 'installations'; + + instance.INTERNAL.registerComponent( + new Component( + installationsName, + container => { + const app = container.getProvider('app').getImmediate(); + + // Throws if app isn't configured properly. + const appConfig = extractAppConfig(app); + const platformLoggerProvider = container.getProvider('platform-logger'); + const dependencies: FirebaseDependencies = { + appConfig, + platformLoggerProvider + }; + + const installations: FirebaseInstallations & FirebaseService = { + app, + getId: () => getId(dependencies), + getToken: (forceRefresh?: boolean) => + getToken(dependencies, forceRefresh), + delete: () => deleteInstallation(dependencies), + onIdChange: (callback: IdChangeCallbackFn): IdChangeUnsubscribeFn => + onIdChange(dependencies, callback) + }; + return installations; + }, + ComponentType.PUBLIC + ) + ); + + instance.registerVersion(name, version); +} + +registerInstallations(firebase as _FirebaseNamespace); + +/** + * Define extension behavior of `registerInstallations` + */ +declare module '@firebase/app-types' { + interface FirebaseNamespace { + installations(app?: FirebaseApp): FirebaseInstallations; + } + interface FirebaseApp { + installations(): FirebaseInstallations; + } +} diff --git a/packages-exp/installations-exp/src/interfaces/api-response.ts b/packages-exp/installations-exp/src/interfaces/api-response.ts new file mode 100644 index 00000000000..f7560a6925c --- /dev/null +++ b/packages-exp/installations-exp/src/interfaces/api-response.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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. + */ + +export interface CreateInstallationResponse { + readonly refreshToken: string; + readonly authToken: GenerateAuthTokenResponse; + readonly fid?: string; +} + +export interface GenerateAuthTokenResponse { + readonly token: string; + + /** + * Encoded as a string with the suffix 's' (indicating seconds), preceded by + * the number of seconds. + * + * Example: "604800s". + */ + readonly expiresIn: string; +} diff --git a/packages-exp/installations-exp/src/interfaces/app-config.ts b/packages-exp/installations-exp/src/interfaces/app-config.ts new file mode 100644 index 00000000000..007f14f1db4 --- /dev/null +++ b/packages-exp/installations-exp/src/interfaces/app-config.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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. + */ + +export interface AppConfig { + readonly appName: string; + readonly projectId: string; + readonly apiKey: string; + readonly appId: string; +} diff --git a/packages-exp/installations-exp/src/interfaces/firebase-dependencies.ts b/packages-exp/installations-exp/src/interfaces/firebase-dependencies.ts new file mode 100644 index 00000000000..3dd3c2eb03b --- /dev/null +++ b/packages-exp/installations-exp/src/interfaces/firebase-dependencies.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { Provider } from '@firebase/component'; +import { AppConfig } from './app-config'; + +export interface FirebaseDependencies { + readonly appConfig: AppConfig; + readonly platformLoggerProvider: Provider<'platform-logger'>; +} diff --git a/packages-exp/installations-exp/src/interfaces/installation-entry.ts b/packages-exp/installations-exp/src/interfaces/installation-entry.ts new file mode 100644 index 00000000000..4b30aa2486f --- /dev/null +++ b/packages-exp/installations-exp/src/interfaces/installation-entry.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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. + */ + +/** Status of a server request. */ +export const enum RequestStatus { + NOT_STARTED, + IN_PROGRESS, + COMPLETED +} + +export interface NotStartedAuthToken { + readonly requestStatus: RequestStatus.NOT_STARTED; +} + +export interface InProgressAuthToken { + readonly requestStatus: RequestStatus.IN_PROGRESS; + + /** + * Unix timestamp when the current generateAuthRequest was initiated. + * Used for figuring out how long the request status has been IN_PROGRESS. + */ + readonly requestTime: number; +} + +export interface CompletedAuthToken { + readonly requestStatus: RequestStatus.COMPLETED; + + /** + * Firebase Installations Authentication Token. + * Only exists if requestStatus is COMPLETED. + */ + readonly token: string; + + /** + * Unix timestamp when Authentication Token was created. + * Only exists if requestStatus is COMPLETED. + */ + readonly creationTime: number; + + /** + * Authentication Token time to live duration in milliseconds. + * Only exists if requestStatus is COMPLETED. + */ + readonly expiresIn: number; +} + +export type AuthToken = + | NotStartedAuthToken + | InProgressAuthToken + | CompletedAuthToken; + +export interface UnregisteredInstallationEntry { + /** Status of the Firebase Installation registration on the server. */ + readonly registrationStatus: RequestStatus.NOT_STARTED; + + /** Firebase Installation ID */ + readonly fid: string; +} + +export interface InProgressInstallationEntry { + /** Status of the Firebase Installation registration on the server. */ + readonly registrationStatus: RequestStatus.IN_PROGRESS; + + /** + * Unix timestamp that shows the time when the current createInstallation + * request was initiated. + * Used for figuring out how long the registration status has been PENDING. + */ + readonly registrationTime: number; + + /** Firebase Installation ID */ + readonly fid: string; +} + +export interface RegisteredInstallationEntry { + /** Status of the Firebase Installation registration on the server. */ + readonly registrationStatus: RequestStatus.COMPLETED; + + /** Firebase Installation ID */ + readonly fid: string; + + /** + * Refresh Token returned from the server. + * Used for authenticating generateAuthToken requests. + */ + readonly refreshToken: string; + + /** Firebase Installation Authentication Token. */ + readonly authToken: AuthToken; +} + +/** Firebase Installation ID and related data in the database. */ +export type InstallationEntry = + | UnregisteredInstallationEntry + | InProgressInstallationEntry + | RegisteredInstallationEntry; diff --git a/packages-exp/installations-exp/src/testing/compare-headers.test.ts b/packages-exp/installations-exp/src/testing/compare-headers.test.ts new file mode 100644 index 00000000000..8bd6fb81203 --- /dev/null +++ b/packages-exp/installations-exp/src/testing/compare-headers.test.ts @@ -0,0 +1,44 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { AssertionError, expect } from 'chai'; +import '../testing/setup'; +import { compareHeaders } from './compare-headers'; + +describe('compareHeaders', () => { + it("doesn't fail if headers contain the same entries", () => { + const headers1 = new Headers({ a: '123', b: '456' }); + const headers2 = new Headers({ a: '123', b: '456' }); + compareHeaders(headers1, headers2); + }); + + it('fails if headers contain different keys', () => { + const headers1 = new Headers({ a: '123', b: '456', extraKey: '789' }); + const headers2 = new Headers({ a: '123', b: '456' }); + expect(() => { + compareHeaders(headers1, headers2); + }).to.throw(AssertionError); + }); + + it('fails if headers contain different values', () => { + const headers1 = new Headers({ a: '123', b: '456' }); + const headers2 = new Headers({ a: '123', b: 'differentValue' }); + expect(() => { + compareHeaders(headers1, headers2); + }).to.throw(AssertionError); + }); +}); diff --git a/packages-exp/installations-exp/src/testing/compare-headers.ts b/packages-exp/installations-exp/src/testing/compare-headers.ts new file mode 100644 index 00000000000..5b93c14933e --- /dev/null +++ b/packages-exp/installations-exp/src/testing/compare-headers.ts @@ -0,0 +1,41 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { AssertionError, expect } from 'chai'; + +// Trick TS since it's set to target ES5. +declare class HeadersWithEntries extends Headers { + entries?(): Iterable<[string, string]>; +} + +// Chai doesn't check if Headers objects contain the same entries, +// so we need to do that manually. +export function compareHeaders( + expectedHeaders: HeadersWithEntries, + actualHeaders: HeadersWithEntries +): void { + if ( + expectedHeaders.entries === undefined || + actualHeaders.entries === undefined + ) { + throw new AssertionError('Headers object does not have entries method'); + } + + const expected = new Map(Array.from(expectedHeaders.entries())); + const actual = new Map(Array.from(actualHeaders.entries())); + expect(actual).to.deep.equal(expected); +} diff --git a/packages-exp/installations-exp/src/testing/fake-generators.ts b/packages-exp/installations-exp/src/testing/fake-generators.ts new file mode 100644 index 00000000000..c6a0fbec63e --- /dev/null +++ b/packages-exp/installations-exp/src/testing/fake-generators.ts @@ -0,0 +1,68 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { FirebaseApp } from '@firebase/app-types'; +import { + Component, + ComponentContainer, + ComponentType +} from '@firebase/component'; +import { extractAppConfig } from '../helpers/extract-app-config'; +import { AppConfig } from '../interfaces/app-config'; +import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; + +export function getFakeApp(): FirebaseApp { + return { + name: 'appName', + options: { + apiKey: 'apiKey', + projectId: 'projectId', + authDomain: 'authDomain', + messagingSenderId: 'messagingSenderId', + databaseURL: 'databaseUrl', + storageBucket: 'storageBucket', + appId: '1:777777777777:web:d93b5ca1475efe57' + }, + automaticDataCollectionEnabled: true, + delete: async () => {}, + // This won't be used in tests. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + installations: null as any + }; +} + +export function getFakeAppConfig( + customValues: Partial = {} +): AppConfig { + return { ...extractAppConfig(getFakeApp()), ...customValues }; +} + +export function getFakeDependencies(): FirebaseDependencies { + const container = new ComponentContainer('test'); + container.addComponent( + new Component( + 'platform-logger', + () => ({ getPlatformInfoString: () => 'a/1.2.3 b/2.3.4' }), + ComponentType.PRIVATE + ) + ); + + return { + appConfig: getFakeAppConfig(), + platformLoggerProvider: container.getProvider('platform-logger') + }; +} diff --git a/packages-exp/installations-exp/src/testing/setup.ts b/packages-exp/installations-exp/src/testing/setup.ts new file mode 100644 index 00000000000..3db746533e0 --- /dev/null +++ b/packages-exp/installations-exp/src/testing/setup.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import { restore } from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import { clear } from '../helpers/idb-manager'; + +use(chaiAsPromised); +use(sinonChai); + +afterEach(async () => { + restore(); + await clear(); +}); diff --git a/packages-exp/installations-exp/src/util/constants.ts b/packages-exp/installations-exp/src/util/constants.ts new file mode 100644 index 00000000000..c20fa260274 --- /dev/null +++ b/packages-exp/installations-exp/src/util/constants.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { version } from '../../package.json'; + +export const PENDING_TIMEOUT_MS = 10000; + +export const PACKAGE_VERSION = `w:${version}`; +export const INTERNAL_AUTH_VERSION = 'FIS_v2'; + +export const INSTALLATIONS_API_URL = + 'https://firebaseinstallations.googleapis.com/v1'; + +export const TOKEN_EXPIRATION_BUFFER = 60 * 60 * 1000; // One hour + +export const SERVICE = 'installations'; +export const SERVICE_NAME = 'Installations'; diff --git a/packages-exp/installations-exp/src/util/errors.ts b/packages-exp/installations-exp/src/util/errors.ts new file mode 100644 index 00000000000..6332ff65901 --- /dev/null +++ b/packages-exp/installations-exp/src/util/errors.ts @@ -0,0 +1,72 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { ErrorFactory, FirebaseError } from '@firebase/util'; +import { SERVICE, SERVICE_NAME } from './constants'; + +export const enum ErrorCode { + MISSING_APP_CONFIG_VALUES = 'missing-app-config-values', + NOT_REGISTERED = 'not-registered', + INSTALLATION_NOT_FOUND = 'installation-not-found', + REQUEST_FAILED = 'request-failed', + APP_OFFLINE = 'app-offline', + DELETE_PENDING_REGISTRATION = 'delete-pending-registration' +} + +const ERROR_DESCRIPTION_MAP: { readonly [key in ErrorCode]: string } = { + [ErrorCode.MISSING_APP_CONFIG_VALUES]: + 'Missing App configuration value: "{$valueName}"', + [ErrorCode.NOT_REGISTERED]: 'Firebase Installation is not registered.', + [ErrorCode.INSTALLATION_NOT_FOUND]: 'Firebase Installation not found.', + [ErrorCode.REQUEST_FAILED]: + '{$requestName} request failed with error "{$serverCode} {$serverStatus}: {$serverMessage}"', + [ErrorCode.APP_OFFLINE]: 'Could not process request. Application offline.', + [ErrorCode.DELETE_PENDING_REGISTRATION]: + "Can't delete installation while there is a pending registration request." +}; + +interface ErrorParams { + [ErrorCode.MISSING_APP_CONFIG_VALUES]: { + valueName: string; + }; + [ErrorCode.REQUEST_FAILED]: { + requestName: string; + [index: string]: string | number; // to make Typescript 3.8 happy + } & ServerErrorData; +} + +export const ERROR_FACTORY = new ErrorFactory( + SERVICE, + SERVICE_NAME, + ERROR_DESCRIPTION_MAP +); + +export interface ServerErrorData { + serverCode: number; + serverMessage: string; + serverStatus: string; +} + +export type ServerError = FirebaseError & ServerErrorData; + +/** Returns true if error is a FirebaseError that is based on an error from the server. */ +export function isServerError(error: unknown): error is ServerError { + return ( + error instanceof FirebaseError && + error.code.includes(ErrorCode.REQUEST_FAILED) + ); +} diff --git a/packages-exp/installations-exp/src/util/get-key.ts b/packages-exp/installations-exp/src/util/get-key.ts new file mode 100644 index 00000000000..84c1ecd6239 --- /dev/null +++ b/packages-exp/installations-exp/src/util/get-key.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { AppConfig } from '../interfaces/app-config'; + +/** Returns a string key that can be used to identify the app. */ +export function getKey(appConfig: AppConfig): string { + return `${appConfig.appName}!${appConfig.appId}`; +} diff --git a/packages-exp/installations-exp/src/util/sleep.test.ts b/packages-exp/installations-exp/src/util/sleep.test.ts new file mode 100644 index 00000000000..6dfc4b328ee --- /dev/null +++ b/packages-exp/installations-exp/src/util/sleep.test.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { expect } from 'chai'; +import { SinonFakeTimers, useFakeTimers } from 'sinon'; +import '../testing/setup'; +import { sleep } from './sleep'; + +describe('sleep', () => { + let clock: SinonFakeTimers; + + beforeEach(() => { + clock = useFakeTimers({ shouldAdvanceTime: true }); + }); + + it('returns a promise that resolves after a given amount of time', async () => { + const t0 = clock.now; + await sleep(100); + const t1 = clock.now; + + expect(t1 - t0).to.equal(100); + }); +}); diff --git a/packages-exp/installations-exp/src/util/sleep.ts b/packages-exp/installations-exp/src/util/sleep.ts new file mode 100644 index 00000000000..2bd1eb9283b --- /dev/null +++ b/packages-exp/installations-exp/src/util/sleep.ts @@ -0,0 +1,23 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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. + */ + +/** Returns a promise that resolves after given time passes. */ +export function sleep(ms: number): Promise { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} diff --git a/packages-exp/installations-exp/tsconfig.json b/packages-exp/installations-exp/tsconfig.json new file mode 100644 index 00000000000..420eda97a1d --- /dev/null +++ b/packages-exp/installations-exp/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../config/tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "downlevelIteration": true, + "resolveJsonModule": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "exclude": ["dist/**/*"] +} diff --git a/packages-exp/installations-types-exp/index.d.ts b/packages-exp/installations-types-exp/index.d.ts new file mode 100644 index 00000000000..5508464a509 --- /dev/null +++ b/packages-exp/installations-types-exp/index.d.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 { FirebaseApp } from '@firebase/app-types'; + +export interface FirebaseInstallations { + /** + * Creates a Firebase Installation if there isn't one for the app and + * returns the Installation ID. + * + * @return Firebase Installation ID + */ + getId(): Promise; + + /** + * Returns an Authentication Token for the current Firebase Installation. + * + * @return Firebase Installation Authentication Token + */ + getToken(forceRefresh?: boolean): Promise; + + /** + * Deletes the Firebase Installation and all associated data. + */ + delete(): Promise; + + /** + * Sets a new callback that will get called when Installlation ID changes. + * Returns an unsubscribe function that will remove the callback when called. + */ + onIdChange(callback: (installationId: string) => void): () => void; +} + +export type FirebaseInstallationsName = 'installations'; + +declare module '@firebase/component' { + interface NameServiceMapping { + 'installations': FirebaseInstallations; + } +} diff --git a/packages-exp/installations-types-exp/package.json b/packages-exp/installations-types-exp/package.json new file mode 100644 index 00000000000..6f145cc70d1 --- /dev/null +++ b/packages-exp/installations-types-exp/package.json @@ -0,0 +1,29 @@ +{ + "name": "@firebase/installations-types-exp", + "private": true, + "version": "0.1.0", + "description": "@firebase/installations-exp Types", + "author": "Firebase (https://firebase.google.com/)", + "license": "Apache-2.0", + "scripts": { + "test": "tsc", + "test:ci": "node ../../scripts/run_tests_in_ci.js" + }, + "files": [ + "index.d.ts" + ], + "peerDependencies": { + "@firebase/app-types": "0.x" + }, + "repository": { + "directory": "packages/installations-types-exp", + "type": "git", + "url": "https://github.com/firebase/firebase-js-sdk.git" + }, + "bugs": { + "url": "https://github.com/firebase/firebase-js-sdk/issues" + }, + "devDependencies": { + "typescript": "3.9.7" + } +} diff --git a/packages-exp/installations-types-exp/tsconfig.json b/packages-exp/installations-types-exp/tsconfig.json new file mode 100644 index 00000000000..74ca67964dd --- /dev/null +++ b/packages-exp/installations-types-exp/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../installations/tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "exclude": ["dist/**/*"] +} From 41cd275d2e291e28ffe43f9c3572caeb4154f023 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Thu, 13 Aug 2020 14:29:57 -0700 Subject: [PATCH 02/22] migrated test app and fixed a test app bug to make it worked --- .../installations-exp/test-app/.gitignore | 2 + .../installations-exp/test-app/index.html | 43 +++++++ .../installations-exp/test-app/index.js | 113 ++++++++++++++++++ .../test-app/rollup.config.js | 48 ++++++++ 4 files changed, 206 insertions(+) create mode 100644 packages-exp/installations-exp/test-app/.gitignore create mode 100644 packages-exp/installations-exp/test-app/index.html create mode 100644 packages-exp/installations-exp/test-app/index.js create mode 100644 packages-exp/installations-exp/test-app/rollup.config.js diff --git a/packages-exp/installations-exp/test-app/.gitignore b/packages-exp/installations-exp/test-app/.gitignore new file mode 100644 index 00000000000..e706d63f780 --- /dev/null +++ b/packages-exp/installations-exp/test-app/.gitignore @@ -0,0 +1,2 @@ +sdk.js +sdk.js.map diff --git a/packages-exp/installations-exp/test-app/index.html b/packages-exp/installations-exp/test-app/index.html new file mode 100644 index 00000000000..f5e2958cea0 --- /dev/null +++ b/packages-exp/installations-exp/test-app/index.html @@ -0,0 +1,43 @@ + + + + + Test App + + + + +

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ + + + +

+

Requests

+
+

Database Contents

+
+ + + diff --git a/packages-exp/installations-exp/test-app/index.js b/packages-exp/installations-exp/test-app/index.js new file mode 100644 index 00000000000..d0101e3114e --- /dev/null +++ b/packages-exp/installations-exp/test-app/index.js @@ -0,0 +1,113 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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. + */ + +const DATABASE_NAME = 'firebase-installations-database'; +const DATABASE_VERSION = 1; +const OBJECT_STORE_NAME = 'firebase-installations-store'; + +const requestLogs = []; +let db; + +window.indexedDB.open(DATABASE_NAME, DATABASE_VERSION).onsuccess = event => { + db = event.target.result; + setInterval(refreshDatabase, 1000); +}; + +function refreshDatabase() { + const request = db + .transaction(OBJECT_STORE_NAME, 'readwrite') + .objectStore(OBJECT_STORE_NAME) + .getAll(); + + request.onsuccess = () => { + const dbElement = getElement('database'); + dbElement.innerHTML = request.result + .map(v => `

${format(v)}

`) + .join(''); + }; +} + +function clearDb() { + const request = db + .transaction(OBJECT_STORE_NAME, 'readwrite') + .objectStore(OBJECT_STORE_NAME) + .clear(); + request.onsuccess = refreshDatabase; +} + +function getElement(id) { + const element = document.getElementById(id); + if (!element) { + throw new Error(`Element not found: ${id}`); + } + return element; +} + +function getInputValue(elementId) { + const element = getElement(elementId); + return element.value; +} + +function getId() { + printRequest('Get ID', FirebaseInstallations.getId(getApp())); +} + +function getToken() { + printRequest('Get Token', FirebaseInstallations.getToken(getApp())); +} + +function deleteInstallation() { + printRequest( + 'Delete Installation', + FirebaseInstallations.deleteInstallation(getApp()) + ); +} + +async function printRequest(requestInfo, promise) { + const requestsElement = getElement('requests'); + requestsElement.innerHTML = '

Loading...

' + requestLogs.join(''); + let result; + try { + const request = await promise; + result = request ? format(request) : 'Completed successfully'; + } catch (e) { + result = e.toString(); + } + requestLogs.unshift(`

${requestInfo}:
${result}

`); + requestsElement.innerHTML = requestLogs.join(''); +} + +function format(o) { + const escapedString = JSON.stringify(o, null, 2); + return `${escapedString}`; +} + +function getApp() { + const appName = getInputValue('appName'); + const projectId = getInputValue('projectId'); + const apiKey = getInputValue('apiKey'); + const appId = getInputValue('appId'); + return { + name: appName, + appConfig: { appName, projectId, apiKey, appId } + }; +} + +getElement('getId').onclick = getId; +getElement('getToken').onclick = getToken; +getElement('deleteInstallation').onclick = deleteInstallation; +getElement('clearDb').onclick = clearDb; diff --git a/packages-exp/installations-exp/test-app/rollup.config.js b/packages-exp/installations-exp/test-app/rollup.config.js new file mode 100644 index 00000000000..c065abfba59 --- /dev/null +++ b/packages-exp/installations-exp/test-app/rollup.config.js @@ -0,0 +1,48 @@ +/** + * @license + * Copyright 2019 Google LLC + * + * 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 typescriptPlugin from 'rollup-plugin-typescript2'; +import resolve from 'rollup-plugin-node-resolve'; +import commonjs from 'rollup-plugin-commonjs'; +import json from 'rollup-plugin-json'; +import { uglify } from 'rollup-plugin-uglify'; +import typescript from 'typescript'; + +/** + * Creates an iife build to run with the Test App. + */ +export default [ + { + input: 'src/functions/index.ts', + output: { + name: 'FirebaseInstallations', + file: 'test-app/sdk.js', + format: 'iife', + sourcemap: true + }, + plugins: [ + typescriptPlugin({ + typescript, + tsconfigOverride: { compilerOptions: { declaration: false } } + }), + json(), + resolve(), + commonjs(), + uglify() + ] + } +]; From 494792521a0d2772cfb8228c20a96df7748dad25 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Thu, 13 Aug 2020 15:47:52 -0700 Subject: [PATCH 03/22] made installations-exp depend on app-exp --- packages-exp/installations-exp/package.json | 6 +++--- packages-exp/installations-exp/src/index.ts | 18 ++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/packages-exp/installations-exp/package.json b/packages-exp/installations-exp/package.json index 235397dfcb2..e5e46e8b7f8 100644 --- a/packages-exp/installations-exp/package.json +++ b/packages-exp/installations-exp/package.json @@ -15,7 +15,7 @@ "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "build": "rollup -c", - "build:deps": "lerna run --scope @firebase/'{app,installations-exp}' --include-dependencies build", + "build:deps": "lerna run --scope @firebase/'{app-exp,installations-exp}' --include-dependencies build", "dev": "rollup -c -w", "test": "yarn type-check && yarn test:karma && yarn lint", "test:ci": "node ../../scripts/run_tests_in_ci.js", @@ -45,8 +45,8 @@ "typescript": "3.9.7" }, "peerDependencies": { - "@firebase/app": "0.x", - "@firebase/app-types": "0.x" + "@firebase/app-exp": "0.x", + "@firebase/app-types-exp": "0.x" }, "dependencies": { "@firebase/installations-types-exp": "0.1.0", diff --git a/packages-exp/installations-exp/src/index.ts b/packages-exp/installations-exp/src/index.ts index a3d9257bb49..029f9d538fa 100644 --- a/packages-exp/installations-exp/src/index.ts +++ b/packages-exp/installations-exp/src/index.ts @@ -15,11 +15,9 @@ * limitations under the License. */ -import firebase from '@firebase/app'; -import { - _FirebaseNamespace, - FirebaseService -} from '@firebase/app-types/private'; +// import firebase from '@firebase/app-exp'; +import { registerVersion, _registerComponent } from '@firebase/app-exp'; +import { _FirebaseService } from '@firebase/app-types-exp'; import { Component, ComponentType } from '@firebase/component'; import { FirebaseInstallations } from '@firebase/installations-types'; import { @@ -35,10 +33,10 @@ import { FirebaseDependencies } from './interfaces/firebase-dependencies'; import { name, version } from '../package.json'; -export function registerInstallations(instance: _FirebaseNamespace): void { +export function registerInstallations(): void { const installationsName = 'installations'; - instance.INTERNAL.registerComponent( + _registerComponent( new Component( installationsName, container => { @@ -52,7 +50,7 @@ export function registerInstallations(instance: _FirebaseNamespace): void { platformLoggerProvider }; - const installations: FirebaseInstallations & FirebaseService = { + const installations: FirebaseInstallations & _FirebaseService = { app, getId: () => getId(dependencies), getToken: (forceRefresh?: boolean) => @@ -67,10 +65,10 @@ export function registerInstallations(instance: _FirebaseNamespace): void { ) ); - instance.registerVersion(name, version); + registerVersion(name, version); } -registerInstallations(firebase as _FirebaseNamespace); +registerInstallations(); /** * Define extension behavior of `registerInstallations` From a67daa41d40f9f2e950d5d8ab19b3b9aa0a4f51a Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Thu, 13 Aug 2020 15:58:12 -0700 Subject: [PATCH 04/22] Relocated public methods files into folder --- .../src/{functions => api}/delete-installation.test.ts | 2 +- .../src/{functions => api}/delete-installation.ts | 2 +- .../installations-exp/src/{functions => api}/get-id.test.ts | 0 .../installations-exp/src/{functions => api}/get-id.ts | 0 .../src/{functions => api}/get-token.test.ts | 4 ++-- .../installations-exp/src/{functions => api}/get-token.ts | 0 .../installations-exp/src/{functions => api}/index.ts | 0 .../src/{functions => api}/on-id-change.test.ts | 0 .../installations-exp/src/{functions => api}/on-id-change.ts | 0 .../installations-exp/src/{api => functions}/common.test.ts | 0 .../installations-exp/src/{api => functions}/common.ts | 0 .../{api => functions}/create-installation-request.test.ts | 0 .../src/{api => functions}/create-installation-request.ts | 0 .../{api => functions}/delete-installation-request.test.ts | 0 .../src/{api => functions}/delete-installation-request.ts | 0 .../{api => functions}/generate-auth-token-request.test.ts | 0 .../src/{api => functions}/generate-auth-token-request.ts | 0 packages-exp/installations-exp/src/helpers/fid-changed.ts | 2 +- .../src/helpers/get-installation-entry.test.ts | 2 +- .../installations-exp/src/helpers/get-installation-entry.ts | 2 +- .../installations-exp/src/helpers/refresh-auth-token.test.ts | 2 +- .../installations-exp/src/helpers/refresh-auth-token.ts | 2 +- packages-exp/installations-exp/src/index.ts | 2 +- 23 files changed, 10 insertions(+), 10 deletions(-) rename packages-exp/installations-exp/src/{functions => api}/delete-installation.test.ts (97%) rename packages-exp/installations-exp/src/{functions => api}/delete-installation.ts (95%) rename packages-exp/installations-exp/src/{functions => api}/get-id.test.ts (100%) rename packages-exp/installations-exp/src/{functions => api}/get-id.ts (100%) rename packages-exp/installations-exp/src/{functions => api}/get-token.test.ts (98%) rename packages-exp/installations-exp/src/{functions => api}/get-token.ts (100%) rename packages-exp/installations-exp/src/{functions => api}/index.ts (100%) rename packages-exp/installations-exp/src/{functions => api}/on-id-change.test.ts (100%) rename packages-exp/installations-exp/src/{functions => api}/on-id-change.ts (100%) rename packages-exp/installations-exp/src/{api => functions}/common.test.ts (100%) rename packages-exp/installations-exp/src/{api => functions}/common.ts (100%) rename packages-exp/installations-exp/src/{api => functions}/create-installation-request.test.ts (100%) rename packages-exp/installations-exp/src/{api => functions}/create-installation-request.ts (100%) rename packages-exp/installations-exp/src/{api => functions}/delete-installation-request.test.ts (100%) rename packages-exp/installations-exp/src/{api => functions}/delete-installation-request.ts (100%) rename packages-exp/installations-exp/src/{api => functions}/generate-auth-token-request.test.ts (100%) rename packages-exp/installations-exp/src/{api => functions}/generate-auth-token-request.ts (100%) diff --git a/packages-exp/installations-exp/src/functions/delete-installation.test.ts b/packages-exp/installations-exp/src/api/delete-installation.test.ts similarity index 97% rename from packages-exp/installations-exp/src/functions/delete-installation.test.ts rename to packages-exp/installations-exp/src/api/delete-installation.test.ts index 376a35e53bd..ef053afb006 100644 --- a/packages-exp/installations-exp/src/functions/delete-installation.test.ts +++ b/packages-exp/installations-exp/src/api/delete-installation.test.ts @@ -17,7 +17,7 @@ import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; -import * as deleteInstallationRequestModule from '../api/delete-installation-request'; +import * as deleteInstallationRequestModule from '../functions/delete-installation-request'; import { get, set } from '../helpers/idb-manager'; import { AppConfig } from '../interfaces/app-config'; import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; diff --git a/packages-exp/installations-exp/src/functions/delete-installation.ts b/packages-exp/installations-exp/src/api/delete-installation.ts similarity index 95% rename from packages-exp/installations-exp/src/functions/delete-installation.ts rename to packages-exp/installations-exp/src/api/delete-installation.ts index 53f912569cc..eb22fdae91b 100644 --- a/packages-exp/installations-exp/src/functions/delete-installation.ts +++ b/packages-exp/installations-exp/src/api/delete-installation.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { deleteInstallationRequest } from '../api/delete-installation-request'; +import { deleteInstallationRequest } from '../functions/delete-installation-request'; import { remove, update } from '../helpers/idb-manager'; import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; import { RequestStatus } from '../interfaces/installation-entry'; diff --git a/packages-exp/installations-exp/src/functions/get-id.test.ts b/packages-exp/installations-exp/src/api/get-id.test.ts similarity index 100% rename from packages-exp/installations-exp/src/functions/get-id.test.ts rename to packages-exp/installations-exp/src/api/get-id.test.ts diff --git a/packages-exp/installations-exp/src/functions/get-id.ts b/packages-exp/installations-exp/src/api/get-id.ts similarity index 100% rename from packages-exp/installations-exp/src/functions/get-id.ts rename to packages-exp/installations-exp/src/api/get-id.ts diff --git a/packages-exp/installations-exp/src/functions/get-token.test.ts b/packages-exp/installations-exp/src/api/get-token.test.ts similarity index 98% rename from packages-exp/installations-exp/src/functions/get-token.test.ts rename to packages-exp/installations-exp/src/api/get-token.test.ts index be633924e05..30837155825 100644 --- a/packages-exp/installations-exp/src/functions/get-token.test.ts +++ b/packages-exp/installations-exp/src/api/get-token.test.ts @@ -17,8 +17,8 @@ import { expect } from 'chai'; import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; -import * as createInstallationRequestModule from '../api/create-installation-request'; -import * as generateAuthTokenRequestModule from '../api/generate-auth-token-request'; +import * as createInstallationRequestModule from '../functions/create-installation-request'; +import * as generateAuthTokenRequestModule from '../functions/generate-auth-token-request'; import { get, set } from '../helpers/idb-manager'; import { AppConfig } from '../interfaces/app-config'; import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; diff --git a/packages-exp/installations-exp/src/functions/get-token.ts b/packages-exp/installations-exp/src/api/get-token.ts similarity index 100% rename from packages-exp/installations-exp/src/functions/get-token.ts rename to packages-exp/installations-exp/src/api/get-token.ts diff --git a/packages-exp/installations-exp/src/functions/index.ts b/packages-exp/installations-exp/src/api/index.ts similarity index 100% rename from packages-exp/installations-exp/src/functions/index.ts rename to packages-exp/installations-exp/src/api/index.ts diff --git a/packages-exp/installations-exp/src/functions/on-id-change.test.ts b/packages-exp/installations-exp/src/api/on-id-change.test.ts similarity index 100% rename from packages-exp/installations-exp/src/functions/on-id-change.test.ts rename to packages-exp/installations-exp/src/api/on-id-change.test.ts diff --git a/packages-exp/installations-exp/src/functions/on-id-change.ts b/packages-exp/installations-exp/src/api/on-id-change.ts similarity index 100% rename from packages-exp/installations-exp/src/functions/on-id-change.ts rename to packages-exp/installations-exp/src/api/on-id-change.ts diff --git a/packages-exp/installations-exp/src/api/common.test.ts b/packages-exp/installations-exp/src/functions/common.test.ts similarity index 100% rename from packages-exp/installations-exp/src/api/common.test.ts rename to packages-exp/installations-exp/src/functions/common.test.ts diff --git a/packages-exp/installations-exp/src/api/common.ts b/packages-exp/installations-exp/src/functions/common.ts similarity index 100% rename from packages-exp/installations-exp/src/api/common.ts rename to packages-exp/installations-exp/src/functions/common.ts diff --git a/packages-exp/installations-exp/src/api/create-installation-request.test.ts b/packages-exp/installations-exp/src/functions/create-installation-request.test.ts similarity index 100% rename from packages-exp/installations-exp/src/api/create-installation-request.test.ts rename to packages-exp/installations-exp/src/functions/create-installation-request.test.ts diff --git a/packages-exp/installations-exp/src/api/create-installation-request.ts b/packages-exp/installations-exp/src/functions/create-installation-request.ts similarity index 100% rename from packages-exp/installations-exp/src/api/create-installation-request.ts rename to packages-exp/installations-exp/src/functions/create-installation-request.ts diff --git a/packages-exp/installations-exp/src/api/delete-installation-request.test.ts b/packages-exp/installations-exp/src/functions/delete-installation-request.test.ts similarity index 100% rename from packages-exp/installations-exp/src/api/delete-installation-request.test.ts rename to packages-exp/installations-exp/src/functions/delete-installation-request.test.ts diff --git a/packages-exp/installations-exp/src/api/delete-installation-request.ts b/packages-exp/installations-exp/src/functions/delete-installation-request.ts similarity index 100% rename from packages-exp/installations-exp/src/api/delete-installation-request.ts rename to packages-exp/installations-exp/src/functions/delete-installation-request.ts diff --git a/packages-exp/installations-exp/src/api/generate-auth-token-request.test.ts b/packages-exp/installations-exp/src/functions/generate-auth-token-request.test.ts similarity index 100% rename from packages-exp/installations-exp/src/api/generate-auth-token-request.test.ts rename to packages-exp/installations-exp/src/functions/generate-auth-token-request.test.ts diff --git a/packages-exp/installations-exp/src/api/generate-auth-token-request.ts b/packages-exp/installations-exp/src/functions/generate-auth-token-request.ts similarity index 100% rename from packages-exp/installations-exp/src/api/generate-auth-token-request.ts rename to packages-exp/installations-exp/src/functions/generate-auth-token-request.ts diff --git a/packages-exp/installations-exp/src/helpers/fid-changed.ts b/packages-exp/installations-exp/src/helpers/fid-changed.ts index 044950085a6..517f9e04cb2 100644 --- a/packages-exp/installations-exp/src/helpers/fid-changed.ts +++ b/packages-exp/installations-exp/src/helpers/fid-changed.ts @@ -17,7 +17,7 @@ import { getKey } from '../util/get-key'; import { AppConfig } from '../interfaces/app-config'; -import { IdChangeCallbackFn } from '../functions'; +import { IdChangeCallbackFn } from '../api'; const fidChangeCallbacks: Map> = new Map(); diff --git a/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts b/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts index e1b830bd0c6..2f38de4f5e2 100644 --- a/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts +++ b/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts @@ -17,7 +17,7 @@ import { AssertionError, expect } from 'chai'; import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; -import * as createInstallationRequestModule from '../api/create-installation-request'; +import * as createInstallationRequestModule from '../functions/create-installation-request'; import { AppConfig } from '../interfaces/app-config'; import { InProgressInstallationEntry, diff --git a/packages-exp/installations-exp/src/helpers/get-installation-entry.ts b/packages-exp/installations-exp/src/helpers/get-installation-entry.ts index 0edcf8e8b94..5e8068eeb67 100644 --- a/packages-exp/installations-exp/src/helpers/get-installation-entry.ts +++ b/packages-exp/installations-exp/src/helpers/get-installation-entry.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { createInstallationRequest } from '../api/create-installation-request'; +import { createInstallationRequest } from '../functions/create-installation-request'; import { AppConfig } from '../interfaces/app-config'; import { InProgressInstallationEntry, diff --git a/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts b/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts index c3a66782292..2f6ec7e746c 100644 --- a/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts +++ b/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts @@ -17,7 +17,7 @@ import { expect } from 'chai'; import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; -import * as generateAuthTokenRequestModule from '../api/generate-auth-token-request'; +import * as generateAuthTokenRequestModule from '../functions/generate-auth-token-request'; import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; import { CompletedAuthToken, diff --git a/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts b/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts index fa980597342..be858d7f736 100644 --- a/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts +++ b/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { generateAuthTokenRequest } from '../api/generate-auth-token-request'; +import { generateAuthTokenRequest } from '../functions/generate-auth-token-request'; import { AppConfig } from '../interfaces/app-config'; import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; import { diff --git a/packages-exp/installations-exp/src/index.ts b/packages-exp/installations-exp/src/index.ts index 029f9d538fa..70c6dcf1cc4 100644 --- a/packages-exp/installations-exp/src/index.ts +++ b/packages-exp/installations-exp/src/index.ts @@ -27,7 +27,7 @@ import { IdChangeCallbackFn, IdChangeUnsubscribeFn, onIdChange -} from './functions'; +} from './api'; import { extractAppConfig } from './helpers/extract-app-config'; import { FirebaseDependencies } from './interfaces/firebase-dependencies'; From 5d5001a6d2dcab87538957f19e08237d74ba3374 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Thu, 13 Aug 2020 18:08:44 -0700 Subject: [PATCH 05/22] Making installations mudularization step1, build success --- .../src/api/delete-installation.test.ts | 36 +++++---- .../src/api/delete-installation.ts | 6 +- .../installations-exp/src/api/get-id.test.ts | 16 ++-- .../installations-exp/src/api/get-id.ts | 8 +- .../src/api/get-token.test.ts | 80 ++++++++++--------- .../installations-exp/src/api/get-token.ts | 12 +-- .../src/api/on-id-change.test.ts | 16 ++-- .../installations-exp/src/api/on-id-change.ts | 4 +- .../installations-exp/src/functions/common.ts | 2 +- .../installations-exp/src/functions/config.ts | 51 ++++++++++++ .../create-installation-request.test.ts | 2 +- .../functions/create-installation-request.ts | 2 +- .../delete-installation-request.test.ts | 2 +- .../functions/delete-installation-request.ts | 2 +- .../generate-auth-token-request.test.ts | 19 +++-- .../functions/generate-auth-token-request.ts | 8 +- .../src/helpers/extract-app-config.test.ts | 2 +- .../src/helpers/extract-app-config.ts | 2 +- .../src/helpers/fid-changed.test.ts | 2 +- .../src/helpers/fid-changed.ts | 2 +- .../helpers/get-installation-entry.test.ts | 2 +- .../src/helpers/get-installation-entry.ts | 2 +- .../src/helpers/idb-manager.test.ts | 2 +- .../src/helpers/idb-manager.ts | 2 +- .../src/helpers/refresh-auth-token.test.ts | 48 +++++------ .../src/helpers/refresh-auth-token.ts | 32 ++++---- packages-exp/installations-exp/src/index.ts | 64 +-------------- .../src/interfaces/app-config.ts | 23 ------ .../src/interfaces/firebase-dependencies.ts | 24 ------ .../src/testing/fake-generators.ts | 10 ++- .../installations-exp/src/util/get-key.ts | 2 +- .../test-app/rollup.config.js | 2 +- .../installations-types-exp/index.d.ts | 41 +++++----- .../installations-types-exp/tsconfig.json | 2 +- 34 files changed, 247 insertions(+), 283 deletions(-) create mode 100644 packages-exp/installations-exp/src/functions/config.ts delete mode 100644 packages-exp/installations-exp/src/interfaces/app-config.ts delete mode 100644 packages-exp/installations-exp/src/interfaces/firebase-dependencies.ts diff --git a/packages-exp/installations-exp/src/api/delete-installation.test.ts b/packages-exp/installations-exp/src/api/delete-installation.test.ts index ef053afb006..1b1b747b0ea 100644 --- a/packages-exp/installations-exp/src/api/delete-installation.test.ts +++ b/packages-exp/installations-exp/src/api/delete-installation.test.ts @@ -19,15 +19,17 @@ import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; import * as deleteInstallationRequestModule from '../functions/delete-installation-request'; import { get, set } from '../helpers/idb-manager'; -import { AppConfig } from '../interfaces/app-config'; -import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + FirebaseInstallations, + AppConfig +} from '@firebase/installations-types-exp'; import { InProgressInstallationEntry, RegisteredInstallationEntry, RequestStatus, UnregisteredInstallationEntry } from '../interfaces/installation-entry'; -import { getFakeDependencies } from '../testing/fake-generators'; +import { getFakeInstallations } from '../testing/fake-generators'; import '../testing/setup'; import { ErrorCode } from '../util/errors'; import { sleep } from '../util/sleep'; @@ -36,14 +38,14 @@ import { deleteInstallation } from './delete-installation'; const FID = 'children-of-the-damned'; describe('deleteInstallation', () => { - let dependencies: FirebaseDependencies; + let installations: FirebaseInstallations; let deleteInstallationRequestSpy: SinonStub< [AppConfig, RegisteredInstallationEntry], Promise >; beforeEach(() => { - dependencies = getFakeDependencies(); + installations = getFakeInstallations(); deleteInstallationRequestSpy = stub( deleteInstallationRequestModule, @@ -54,7 +56,7 @@ describe('deleteInstallation', () => { }); it('resolves without calling server API if there is no installation', async () => { - await expect(deleteInstallation(dependencies)).to.be.fulfilled; + await expect(deleteInstallation(installations)).to.be.fulfilled; expect(deleteInstallationRequestSpy).not.to.have.been.called; }); @@ -63,11 +65,11 @@ describe('deleteInstallation', () => { registrationStatus: RequestStatus.NOT_STARTED, fid: FID }; - await set(dependencies.appConfig, entry); + await set(installations.appConfig, entry); - await expect(deleteInstallation(dependencies)).to.be.fulfilled; + await expect(deleteInstallation(installations)).to.be.fulfilled; expect(deleteInstallationRequestSpy).not.to.have.been.called; - await expect(get(dependencies.appConfig)).to.eventually.be.undefined; + await expect(get(installations.appConfig)).to.eventually.be.undefined; }); it('rejects without calling server API if the installation is pending', async () => { @@ -76,9 +78,9 @@ describe('deleteInstallation', () => { registrationStatus: RequestStatus.IN_PROGRESS, registrationTime: Date.now() - 3 * 1000 }; - await set(dependencies.appConfig, entry); + await set(installations.appConfig, entry); - await expect(deleteInstallation(dependencies)).to.be.rejectedWith( + await expect(deleteInstallation(installations)).to.be.rejectedWith( ErrorCode.DELETE_PENDING_REGISTRATION ); expect(deleteInstallationRequestSpy).not.to.have.been.called; @@ -96,10 +98,10 @@ describe('deleteInstallation', () => { creationTime: Date.now() } }; - await set(dependencies.appConfig, entry); + await set(installations.appConfig, entry); stub(navigator, 'onLine').value(false); - await expect(deleteInstallation(dependencies)).to.be.rejectedWith( + await expect(deleteInstallation(installations)).to.be.rejectedWith( ErrorCode.APP_OFFLINE ); expect(deleteInstallationRequestSpy).not.to.have.been.called; @@ -117,13 +119,13 @@ describe('deleteInstallation', () => { creationTime: Date.now() } }; - await set(dependencies.appConfig, entry); + await set(installations.appConfig, entry); - await expect(deleteInstallation(dependencies)).to.be.fulfilled; + await expect(deleteInstallation(installations)).to.be.fulfilled; expect(deleteInstallationRequestSpy).to.have.been.calledOnceWith( - dependencies.appConfig, + installations.appConfig, entry ); - await expect(get(dependencies.appConfig)).to.eventually.be.undefined; + await expect(get(installations.appConfig)).to.eventually.be.undefined; }); }); diff --git a/packages-exp/installations-exp/src/api/delete-installation.ts b/packages-exp/installations-exp/src/api/delete-installation.ts index eb22fdae91b..3048dbf3ece 100644 --- a/packages-exp/installations-exp/src/api/delete-installation.ts +++ b/packages-exp/installations-exp/src/api/delete-installation.ts @@ -17,14 +17,14 @@ import { deleteInstallationRequest } from '../functions/delete-installation-request'; import { remove, update } from '../helpers/idb-manager'; -import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; import { RequestStatus } from '../interfaces/installation-entry'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; export async function deleteInstallation( - dependencies: FirebaseDependencies + installations: FirebaseInstallations ): Promise { - const { appConfig } = dependencies; + const { appConfig } = installations; const entry = await update(appConfig, oldEntry => { if (oldEntry && oldEntry.registrationStatus === RequestStatus.NOT_STARTED) { diff --git a/packages-exp/installations-exp/src/api/get-id.test.ts b/packages-exp/installations-exp/src/api/get-id.test.ts index a1c03700b44..7d17852d1c4 100644 --- a/packages-exp/installations-exp/src/api/get-id.test.ts +++ b/packages-exp/installations-exp/src/api/get-id.test.ts @@ -19,27 +19,29 @@ import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; import * as getInstallationEntryModule from '../helpers/get-installation-entry'; import * as refreshAuthTokenModule from '../helpers/refresh-auth-token'; -import { AppConfig } from '../interfaces/app-config'; -import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + FirebaseInstallations, + AppConfig +} from '@firebase/installations-types-exp'; import { RegisteredInstallationEntry, RequestStatus } from '../interfaces/installation-entry'; -import { getFakeDependencies } from '../testing/fake-generators'; +import { getFakeInstallations } from '../testing/fake-generators'; import '../testing/setup'; import { getId } from './get-id'; const FID = 'disciples-of-the-watch'; describe('getId', () => { - let dependencies: FirebaseDependencies; + let installations: FirebaseInstallations; let getInstallationEntrySpy: SinonStub< [AppConfig], Promise >; beforeEach(() => { - dependencies = getFakeDependencies(); + installations = getFakeInstallations(); getInstallationEntrySpy = stub( getInstallationEntryModule, @@ -56,7 +58,7 @@ describe('getId', () => { registrationPromise: Promise.resolve({} as RegisteredInstallationEntry) }); - const fid = await getId(dependencies); + const fid = await getId(installations); expect(fid).to.equal(FID); expect(getInstallationEntrySpy).to.be.calledOnce; }); @@ -83,7 +85,7 @@ describe('getId', () => { creationTime: Date.now() }); - await getId(dependencies); + await getId(installations); expect(refreshAuthTokenSpy).to.be.calledOnce; }); }); diff --git a/packages-exp/installations-exp/src/api/get-id.ts b/packages-exp/installations-exp/src/api/get-id.ts index 28a692af10f..265b72c2d41 100644 --- a/packages-exp/installations-exp/src/api/get-id.ts +++ b/packages-exp/installations-exp/src/api/get-id.ts @@ -17,13 +17,13 @@ import { getInstallationEntry } from '../helpers/get-installation-entry'; import { refreshAuthToken } from '../helpers/refresh-auth-token'; -import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; export async function getId( - dependencies: FirebaseDependencies + installations: FirebaseInstallations ): Promise { const { installationEntry, registrationPromise } = await getInstallationEntry( - dependencies.appConfig + installations.appConfig ); if (registrationPromise) { @@ -31,7 +31,7 @@ export async function getId( } else { // If the installation is already registered, update the authentication // token if needed. - refreshAuthToken(dependencies).catch(console.error); + refreshAuthToken(installations).catch(console.error); } return installationEntry.fid; diff --git a/packages-exp/installations-exp/src/api/get-token.test.ts b/packages-exp/installations-exp/src/api/get-token.test.ts index 30837155825..f8ecd8a4783 100644 --- a/packages-exp/installations-exp/src/api/get-token.test.ts +++ b/packages-exp/installations-exp/src/api/get-token.test.ts @@ -20,8 +20,10 @@ import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; import * as createInstallationRequestModule from '../functions/create-installation-request'; import * as generateAuthTokenRequestModule from '../functions/generate-auth-token-request'; import { get, set } from '../helpers/idb-manager'; -import { AppConfig } from '../interfaces/app-config'; -import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + FirebaseInstallations, + AppConfig +} from '@firebase/installations-types-exp'; import { CompletedAuthToken, InProgressInstallationEntry, @@ -29,7 +31,7 @@ import { RequestStatus, UnregisteredInstallationEntry } from '../interfaces/installation-entry'; -import { getFakeDependencies } from '../testing/fake-generators'; +import { getFakeInstallations } from '../testing/fake-generators'; import '../testing/setup'; import { TOKEN_EXPIRATION_BUFFER } from '../util/constants'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; @@ -171,18 +173,18 @@ const setupInstallationEntryMap: Map< ]); describe('getToken', () => { - let dependencies: FirebaseDependencies; + let installations: FirebaseInstallations; let createInstallationRequestSpy: SinonStub< [AppConfig, InProgressInstallationEntry], Promise >; let generateAuthTokenRequestSpy: SinonStub< - [FirebaseDependencies, RegisteredInstallationEntry], + [FirebaseInstallations, RegisteredInstallationEntry], Promise >; beforeEach(() => { - dependencies = getFakeDependencies(); + installations = getFakeInstallations(); createInstallationRequestSpy = stub( createInstallationRequestModule, @@ -220,17 +222,17 @@ describe('getToken', () => { describe('basic functionality', () => { for (const [title, setup] of setupInstallationEntryMap.entries()) { describe(`when ${title} in the DB`, () => { - beforeEach(() => setup(dependencies.appConfig)); + beforeEach(() => setup(installations.appConfig)); it('resolves with an auth token', async () => { - const token = await getToken(dependencies); + const token = await getToken(installations); expect(token).to.be.oneOf([AUTH_TOKEN, NEW_AUTH_TOKEN]); }); it('saves the token in the DB', async () => { - const token = await getToken(dependencies); + const token = await getToken(installations); const installationEntry = (await get( - dependencies.appConfig + installations.appConfig )) as RegisteredInstallationEntry; expect(installationEntry).not.to.be.undefined; expect(installationEntry.registrationStatus).to.equal( @@ -245,8 +247,8 @@ describe('getToken', () => { }); it('returns the same token on subsequent calls', async () => { - const token1 = await getToken(dependencies); - const token2 = await getToken(dependencies); + const token1 = await getToken(installations); + const token2 = await getToken(installations); expect(token1).to.equal(token2); }); }); @@ -255,21 +257,21 @@ describe('getToken', () => { describe('when there is no FID in the DB', () => { it('gets the token by registering a new FID', async () => { - await getToken(dependencies); + await getToken(installations); expect(createInstallationRequestSpy).to.be.called; expect(generateAuthTokenRequestSpy).not.to.be.called; }); it('does not register a new FID on subsequent calls', async () => { - await getToken(dependencies); - await getToken(dependencies); + await getToken(installations); + await getToken(installations); expect(createInstallationRequestSpy).to.be.calledOnce; }); it('throws if the app is offline', async () => { stub(navigator, 'onLine').value(false); - await expect(getToken(dependencies)).to.be.rejected; + await expect(getToken(installations)).to.be.rejected; }); }); @@ -285,30 +287,30 @@ describe('getToken', () => { requestStatus: RequestStatus.NOT_STARTED } }; - await set(dependencies.appConfig, installationEntry); + await set(installations.appConfig, installationEntry); }); it('gets the token by calling generateAuthToken', async () => { - await getToken(dependencies); + await getToken(installations); expect(generateAuthTokenRequestSpy).to.be.called; expect(createInstallationRequestSpy).not.to.be.called; }); it('does not call generateAuthToken twice on subsequent calls', async () => { - await getToken(dependencies); - await getToken(dependencies); + await getToken(installations); + await getToken(installations); expect(generateAuthTokenRequestSpy).to.be.calledOnce; }); it('does not call generateAuthToken twice on simultaneous calls', async () => { - await Promise.all([getToken(dependencies), getToken(dependencies)]); + await Promise.all([getToken(installations), getToken(installations)]); expect(generateAuthTokenRequestSpy).to.be.calledOnce; }); it('throws if the app is offline', async () => { stub(navigator, 'onLine').value(false); - await expect(getToken(dependencies)).to.be.rejected; + await expect(getToken(installations)).to.be.rejected; }); describe('and the server returns an error', () => { @@ -322,8 +324,8 @@ describe('getToken', () => { }); }); - await expect(getToken(dependencies)).to.be.rejected; - await expect(get(dependencies.appConfig)).to.eventually.be.undefined; + await expect(getToken(installations)).to.be.rejected; + await expect(get(installations.appConfig)).to.eventually.be.undefined; }); it('removes the FID from the DB if the server returns a 404 response', async () => { @@ -336,8 +338,8 @@ describe('getToken', () => { }); }); - await expect(getToken(dependencies)).to.be.rejected; - await expect(get(dependencies.appConfig)).to.eventually.be.undefined; + await expect(getToken(installations)).to.be.rejected; + await expect(get(installations.appConfig)).to.eventually.be.undefined; }); it('does not remove the FID from the DB if the server returns any other response', async () => { @@ -350,8 +352,8 @@ describe('getToken', () => { }); }); - await expect(getToken(dependencies)).to.be.rejected; - await expect(get(dependencies.appConfig)).to.eventually.deep.equal( + await expect(getToken(installations)).to.be.rejected; + await expect(get(installations.appConfig)).to.eventually.deep.equal( installationEntry ); }); @@ -371,17 +373,17 @@ describe('getToken', () => { creationTime: Date.now() } }; - await set(dependencies.appConfig, installationEntry); + await set(installations.appConfig, installationEntry); }); it('does not call any server APIs', async () => { - await getToken(dependencies); + await getToken(installations); expect(createInstallationRequestSpy).not.to.be.called; expect(generateAuthTokenRequestSpy).not.to.be.called; }); it('refreshes the token if forceRefresh is true', async () => { - const token = await getToken(dependencies, true); + const token = await getToken(installations, true); expect(token).to.equal(NEW_AUTH_TOKEN); expect(generateAuthTokenRequestSpy).to.be.called; }); @@ -389,14 +391,14 @@ describe('getToken', () => { it('works even if the app is offline', async () => { stub(navigator, 'onLine').value(false); - const token = await getToken(dependencies); + const token = await getToken(installations); expect(token).to.equal(AUTH_TOKEN); }); it('throws if the app is offline and forceRefresh is true', async () => { stub(navigator, 'onLine').value(false); - await expect(getToken(dependencies, true)).to.be.rejected; + await expect(getToken(installations, true)).to.be.rejected; }); }); @@ -418,17 +420,17 @@ describe('getToken', () => { Date.now() - ONE_WEEK_MS + TOKEN_EXPIRATION_BUFFER + 10 * 60 * 1000 } }; - await set(dependencies.appConfig, installationEntry); + await set(installations.appConfig, installationEntry); }); it('returns a different token after expiration', async () => { - const token1 = await getToken(dependencies); + const token1 = await getToken(installations); expect(token1).to.equal(AUTH_TOKEN); // Wait 30 minutes. clock.tick('30:00'); - const token2 = await getToken(dependencies); + const token2 = await getToken(installations); await expect(token2).to.equal(NEW_AUTH_TOKEN); await expect(token2).not.to.equal(token1); }); @@ -447,11 +449,11 @@ describe('getToken', () => { creationTime: Date.now() - 2 * ONE_WEEK_MS } }; - await set(dependencies.appConfig, installationEntry); + await set(installations.appConfig, installationEntry); }); it('returns a different token', async () => { - const token = await getToken(dependencies); + const token = await getToken(installations); expect(token).to.equal(NEW_AUTH_TOKEN); expect(generateAuthTokenRequestSpy).to.be.called; }); @@ -459,7 +461,7 @@ describe('getToken', () => { it('throws if the app is offline', async () => { stub(navigator, 'onLine').value(false); - await expect(getToken(dependencies)).to.be.rejected; + await expect(getToken(installations)).to.be.rejected; }); }); }); diff --git a/packages-exp/installations-exp/src/api/get-token.ts b/packages-exp/installations-exp/src/api/get-token.ts index a747626f476..f3df9bd5069 100644 --- a/packages-exp/installations-exp/src/api/get-token.ts +++ b/packages-exp/installations-exp/src/api/get-token.ts @@ -17,18 +17,20 @@ import { getInstallationEntry } from '../helpers/get-installation-entry'; import { refreshAuthToken } from '../helpers/refresh-auth-token'; -import { AppConfig } from '../interfaces/app-config'; -import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + FirebaseInstallations, + AppConfig +} from '@firebase/installations-types-exp'; export async function getToken( - dependencies: FirebaseDependencies, + installations: FirebaseInstallations, forceRefresh = false ): Promise { - await completeInstallationRegistration(dependencies.appConfig); + await completeInstallationRegistration(installations.appConfig); // At this point we either have a Registered Installation in the DB, or we've // already thrown an error. - const authToken = await refreshAuthToken(dependencies, forceRefresh); + const authToken = await refreshAuthToken(installations, forceRefresh); return authToken.token; } diff --git a/packages-exp/installations-exp/src/api/on-id-change.test.ts b/packages-exp/installations-exp/src/api/on-id-change.test.ts index 89175b07eb3..21051e3c9ee 100644 --- a/packages-exp/installations-exp/src/api/on-id-change.test.ts +++ b/packages-exp/installations-exp/src/api/on-id-change.test.ts @@ -20,32 +20,32 @@ import { stub } from 'sinon'; import '../testing/setup'; import { onIdChange } from './on-id-change'; import * as FidChangedModule from '../helpers/fid-changed'; -import { getFakeDependencies } from '../testing/fake-generators'; -import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { getFakeInstallations } from '../testing/fake-generators'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; describe('onIdChange', () => { - let dependencies: FirebaseDependencies; + let installations: FirebaseInstallations; beforeEach(() => { - dependencies = getFakeDependencies(); + installations = getFakeInstallations(); stub(FidChangedModule); }); it('calls addCallback with the given callback and app key when called', () => { const callback = stub(); - onIdChange(dependencies, callback); + onIdChange(installations, callback); expect(FidChangedModule.addCallback).to.have.been.calledOnceWith( - dependencies.appConfig, + installations.appConfig, callback ); }); it('calls removeCallback with the given callback and app key when unsubscribe is called', () => { const callback = stub(); - const unsubscribe = onIdChange(dependencies, callback); + const unsubscribe = onIdChange(installations, callback); unsubscribe(); expect(FidChangedModule.removeCallback).to.have.been.calledOnceWith( - dependencies.appConfig, + installations.appConfig, callback ); }); diff --git a/packages-exp/installations-exp/src/api/on-id-change.ts b/packages-exp/installations-exp/src/api/on-id-change.ts index c417b005e43..14e5e2ffe2c 100644 --- a/packages-exp/installations-exp/src/api/on-id-change.ts +++ b/packages-exp/installations-exp/src/api/on-id-change.ts @@ -16,7 +16,7 @@ */ import { addCallback, removeCallback } from '../helpers/fid-changed'; -import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; export type IdChangeCallbackFn = (installationId: string) => void; export type IdChangeUnsubscribeFn = () => void; @@ -26,7 +26,7 @@ export type IdChangeUnsubscribeFn = () => void; * Returns an unsubscribe function that will remove the callback when called. */ export function onIdChange( - { appConfig }: FirebaseDependencies, + { appConfig }: FirebaseInstallations, callback: IdChangeCallbackFn ): IdChangeUnsubscribeFn { addCallback(appConfig, callback); diff --git a/packages-exp/installations-exp/src/functions/common.ts b/packages-exp/installations-exp/src/functions/common.ts index 3283f764723..ee6b26bea9b 100644 --- a/packages-exp/installations-exp/src/functions/common.ts +++ b/packages-exp/installations-exp/src/functions/common.ts @@ -17,7 +17,7 @@ import { FirebaseError } from '@firebase/util'; import { GenerateAuthTokenResponse } from '../interfaces/api-response'; -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types-exp'; import { CompletedAuthToken, RegisteredInstallationEntry, diff --git a/packages-exp/installations-exp/src/functions/config.ts b/packages-exp/installations-exp/src/functions/config.ts new file mode 100644 index 00000000000..41a98bcc6b0 --- /dev/null +++ b/packages-exp/installations-exp/src/functions/config.ts @@ -0,0 +1,51 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 { registerVersion, _registerComponent } from '@firebase/app-exp'; +import { _FirebaseService } from '@firebase/app-types-exp'; +import { Component, ComponentType } from '@firebase/component'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; +import { extractAppConfig } from '../helpers/extract-app-config'; + +import { name, version } from '../../package.json'; + +export function registerInstallations(): void { + const installationsName = 'installations'; + + _registerComponent( + new Component( + installationsName, + container => { + const app = container.getProvider('app').getImmediate(); + + // Throws if app isn't configured properly. + const appConfig = extractAppConfig(app); + const platformLoggerProvider = container.getProvider('platform-logger'); + + const installations: FirebaseInstallations = { + appConfig, + platformLoggerProvider + }; + + return installations; + }, + ComponentType.PUBLIC + ) + ); + + registerVersion(name, version); +} diff --git a/packages-exp/installations-exp/src/functions/create-installation-request.test.ts b/packages-exp/installations-exp/src/functions/create-installation-request.test.ts index 05c0c8dfbbf..39687bb9231 100644 --- a/packages-exp/installations-exp/src/functions/create-installation-request.test.ts +++ b/packages-exp/installations-exp/src/functions/create-installation-request.test.ts @@ -19,7 +19,7 @@ import { FirebaseError } from '@firebase/util'; import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; import { CreateInstallationResponse } from '../interfaces/api-response'; -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types-exp'; import { InProgressInstallationEntry, RequestStatus diff --git a/packages-exp/installations-exp/src/functions/create-installation-request.ts b/packages-exp/installations-exp/src/functions/create-installation-request.ts index 07e0c7bb35c..92b97b778dd 100644 --- a/packages-exp/installations-exp/src/functions/create-installation-request.ts +++ b/packages-exp/installations-exp/src/functions/create-installation-request.ts @@ -16,7 +16,7 @@ */ import { CreateInstallationResponse } from '../interfaces/api-response'; -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types-exp'; import { InProgressInstallationEntry, RegisteredInstallationEntry, diff --git a/packages-exp/installations-exp/src/functions/delete-installation-request.test.ts b/packages-exp/installations-exp/src/functions/delete-installation-request.test.ts index 278b4965882..4841964abbd 100644 --- a/packages-exp/installations-exp/src/functions/delete-installation-request.test.ts +++ b/packages-exp/installations-exp/src/functions/delete-installation-request.test.ts @@ -18,7 +18,7 @@ import { FirebaseError } from '@firebase/util'; import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types-exp'; import { RegisteredInstallationEntry, RequestStatus diff --git a/packages-exp/installations-exp/src/functions/delete-installation-request.ts b/packages-exp/installations-exp/src/functions/delete-installation-request.ts index 5fba2314c5a..8179cbd0bb1 100644 --- a/packages-exp/installations-exp/src/functions/delete-installation-request.ts +++ b/packages-exp/installations-exp/src/functions/delete-installation-request.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types-exp'; import { RegisteredInstallationEntry } from '../interfaces/installation-entry'; import { getErrorFromResponse, diff --git a/packages-exp/installations-exp/src/functions/generate-auth-token-request.test.ts b/packages-exp/installations-exp/src/functions/generate-auth-token-request.test.ts index a7f74115681..d75b68e2610 100644 --- a/packages-exp/installations-exp/src/functions/generate-auth-token-request.test.ts +++ b/packages-exp/installations-exp/src/functions/generate-auth-token-request.test.ts @@ -19,14 +19,14 @@ import { FirebaseError } from '@firebase/util'; import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; import { GenerateAuthTokenResponse } from '../interfaces/api-response'; -import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; import { CompletedAuthToken, RegisteredInstallationEntry, RequestStatus } from '../interfaces/installation-entry'; import { compareHeaders } from '../testing/compare-headers'; -import { getFakeDependencies } from '../testing/fake-generators'; +import { getFakeInstallations } from '../testing/fake-generators'; import '../testing/setup'; import { INSTALLATIONS_API_URL, @@ -39,13 +39,13 @@ import { generateAuthTokenRequest } from './generate-auth-token-request'; const FID = 'evil-has-no-boundaries'; describe('generateAuthTokenRequest', () => { - let dependencies: FirebaseDependencies; + let installations: FirebaseInstallations; let fetchSpy: SinonStub<[RequestInfo, RequestInit?], Promise>; let registeredInstallationEntry: RegisteredInstallationEntry; let response: GenerateAuthTokenResponse; beforeEach(() => { - dependencies = getFakeDependencies(); + installations = getFakeInstallations(); registeredInstallationEntry = { fid: FID, @@ -72,7 +72,7 @@ describe('generateAuthTokenRequest', () => { it('fetches a new Authentication Token', async () => { const completedAuthToken: CompletedAuthToken = await generateAuthTokenRequest( - dependencies, + installations, registeredInstallationEntry ); expect(completedAuthToken.requestStatus).to.equal( @@ -100,7 +100,10 @@ describe('generateAuthTokenRequest', () => { }; const expectedEndpoint = `${INSTALLATIONS_API_URL}/projects/projectId/installations/${FID}/authTokens:generate`; - await generateAuthTokenRequest(dependencies, registeredInstallationEntry); + await generateAuthTokenRequest( + installations, + registeredInstallationEntry + ); expect(fetchSpy).to.be.calledOnceWith(expectedEndpoint, expectedRequest); const actualHeaders = fetchSpy.lastCall.lastArg.headers; @@ -123,7 +126,7 @@ describe('generateAuthTokenRequest', () => { ); await expect( - generateAuthTokenRequest(dependencies, registeredInstallationEntry) + generateAuthTokenRequest(installations, registeredInstallationEntry) ).to.be.rejectedWith(FirebaseError); }); @@ -142,7 +145,7 @@ describe('generateAuthTokenRequest', () => { fetchSpy.onCall(1).resolves(new Response(JSON.stringify(response))); await expect( - generateAuthTokenRequest(dependencies, registeredInstallationEntry) + generateAuthTokenRequest(installations, registeredInstallationEntry) ).to.be.fulfilled; expect(fetchSpy).to.be.calledTwice; }); diff --git a/packages-exp/installations-exp/src/functions/generate-auth-token-request.ts b/packages-exp/installations-exp/src/functions/generate-auth-token-request.ts index f32650179f6..1be0ee1b780 100644 --- a/packages-exp/installations-exp/src/functions/generate-auth-token-request.ts +++ b/packages-exp/installations-exp/src/functions/generate-auth-token-request.ts @@ -16,8 +16,10 @@ */ import { GenerateAuthTokenResponse } from '../interfaces/api-response'; -import { AppConfig } from '../interfaces/app-config'; -import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + AppConfig, + FirebaseInstallations +} from '@firebase/installations-types-exp'; import { CompletedAuthToken, RegisteredInstallationEntry @@ -32,7 +34,7 @@ import { } from './common'; export async function generateAuthTokenRequest( - { appConfig, platformLoggerProvider }: FirebaseDependencies, + { appConfig, platformLoggerProvider }: FirebaseInstallations, installationEntry: RegisteredInstallationEntry ): Promise { const endpoint = getGenerateAuthTokenEndpoint(appConfig, installationEntry); diff --git a/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts b/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts index 110a6d1ab10..9e95496eba2 100644 --- a/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts +++ b/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts @@ -17,7 +17,7 @@ import { FirebaseError } from '@firebase/util'; import { expect } from 'chai'; -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types-exp'; import { getFakeApp } from '../testing/fake-generators'; import '../testing/setup'; import { extractAppConfig } from './extract-app-config'; diff --git a/packages-exp/installations-exp/src/helpers/extract-app-config.ts b/packages-exp/installations-exp/src/helpers/extract-app-config.ts index 51f1bad2e71..f04ede48ac4 100644 --- a/packages-exp/installations-exp/src/helpers/extract-app-config.ts +++ b/packages-exp/installations-exp/src/helpers/extract-app-config.ts @@ -17,7 +17,7 @@ import { FirebaseApp, FirebaseOptions } from '@firebase/app-types'; import { FirebaseError } from '@firebase/util'; -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; export function extractAppConfig(app: FirebaseApp): AppConfig { diff --git a/packages-exp/installations-exp/src/helpers/fid-changed.test.ts b/packages-exp/installations-exp/src/helpers/fid-changed.test.ts index 99a9e5a462d..b4990a3a8da 100644 --- a/packages-exp/installations-exp/src/helpers/fid-changed.test.ts +++ b/packages-exp/installations-exp/src/helpers/fid-changed.test.ts @@ -18,7 +18,7 @@ import { expect } from 'chai'; import { stub } from 'sinon'; import '../testing/setup'; -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types-exp'; import { fidChanged, addCallback, diff --git a/packages-exp/installations-exp/src/helpers/fid-changed.ts b/packages-exp/installations-exp/src/helpers/fid-changed.ts index 517f9e04cb2..6e9819cb52b 100644 --- a/packages-exp/installations-exp/src/helpers/fid-changed.ts +++ b/packages-exp/installations-exp/src/helpers/fid-changed.ts @@ -16,7 +16,7 @@ */ import { getKey } from '../util/get-key'; -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types-exp'; import { IdChangeCallbackFn } from '../api'; const fidChangeCallbacks: Map> = new Map(); diff --git a/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts b/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts index 2f38de4f5e2..9be9a34c8f6 100644 --- a/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts +++ b/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts @@ -18,7 +18,7 @@ import { AssertionError, expect } from 'chai'; import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; import * as createInstallationRequestModule from '../functions/create-installation-request'; -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types-exp'; import { InProgressInstallationEntry, RegisteredInstallationEntry, diff --git a/packages-exp/installations-exp/src/helpers/get-installation-entry.ts b/packages-exp/installations-exp/src/helpers/get-installation-entry.ts index 5e8068eeb67..3ae6be67499 100644 --- a/packages-exp/installations-exp/src/helpers/get-installation-entry.ts +++ b/packages-exp/installations-exp/src/helpers/get-installation-entry.ts @@ -16,7 +16,7 @@ */ import { createInstallationRequest } from '../functions/create-installation-request'; -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types-exp'; import { InProgressInstallationEntry, InstallationEntry, diff --git a/packages-exp/installations-exp/src/helpers/idb-manager.test.ts b/packages-exp/installations-exp/src/helpers/idb-manager.test.ts index 3dbaa9f5091..44d1a7029d0 100644 --- a/packages-exp/installations-exp/src/helpers/idb-manager.test.ts +++ b/packages-exp/installations-exp/src/helpers/idb-manager.test.ts @@ -17,7 +17,7 @@ import { expect } from 'chai'; import { stub } from 'sinon'; -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types-exp'; import { InstallationEntry, RequestStatus diff --git a/packages-exp/installations-exp/src/helpers/idb-manager.ts b/packages-exp/installations-exp/src/helpers/idb-manager.ts index 8bc464fd115..b916b236275 100644 --- a/packages-exp/installations-exp/src/helpers/idb-manager.ts +++ b/packages-exp/installations-exp/src/helpers/idb-manager.ts @@ -16,7 +16,7 @@ */ import { DB, openDb } from 'idb'; -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types-exp'; import { InstallationEntry } from '../interfaces/installation-entry'; import { getKey } from '../util/get-key'; import { fidChanged } from './fid-changed'; diff --git a/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts b/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts index 2f6ec7e746c..3bb656a7db7 100644 --- a/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts +++ b/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts @@ -18,14 +18,14 @@ import { expect } from 'chai'; import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; import * as generateAuthTokenRequestModule from '../functions/generate-auth-token-request'; -import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { FirebaseInstallations } from '@firebase/installations-types'; import { CompletedAuthToken, RegisteredInstallationEntry, RequestStatus, UnregisteredInstallationEntry } from '../interfaces/installation-entry'; -import { getFakeDependencies } from '../testing/fake-generators'; +import { getFakeInstallations } from '../testing/fake-generators'; import '../testing/setup'; import { TOKEN_EXPIRATION_BUFFER } from '../util/constants'; import { sleep } from '../util/sleep'; @@ -38,14 +38,14 @@ const DB_AUTH_TOKEN = 'authTokenFromDB'; const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000; describe('refreshAuthToken', () => { - let dependencies: FirebaseDependencies; + let installations: FirebaseInstallations; let generateAuthTokenRequestSpy: SinonStub< - [FirebaseDependencies, RegisteredInstallationEntry], + [FirebaseInstallations, RegisteredInstallationEntry], Promise >; beforeEach(() => { - dependencies = getFakeDependencies(); + installations = getFakeInstallations(); generateAuthTokenRequestSpy = stub( generateAuthTokenRequestModule, @@ -63,7 +63,7 @@ describe('refreshAuthToken', () => { }); it('throws when there is no installation in the DB', async () => { - await expect(refreshAuthToken(dependencies)).to.be.rejected; + await expect(refreshAuthToken(installations)).to.be.rejected; }); it('throws when there is an unregistered installation in the db', async () => { @@ -71,9 +71,9 @@ describe('refreshAuthToken', () => { fid: FID, registrationStatus: RequestStatus.NOT_STARTED }; - await set(dependencies.appConfig, installationEntry); + await set(installations.appConfig, installationEntry); - await expect(refreshAuthToken(dependencies)).to.be.rejected; + await expect(refreshAuthToken(installations)).to.be.rejected; }); describe('when there is a valid auth token in the DB', () => { @@ -89,23 +89,23 @@ describe('refreshAuthToken', () => { creationTime: Date.now() } }; - await set(dependencies.appConfig, installationEntry); + await set(installations.appConfig, installationEntry); }); it('returns the token from the DB', async () => { - const { token } = await refreshAuthToken(dependencies); + const { token } = await refreshAuthToken(installations); expect(token).to.equal(AUTH_TOKEN); }); it('does not call any server APIs', async () => { - await refreshAuthToken(dependencies); + await refreshAuthToken(installations); expect(generateAuthTokenRequestSpy).not.to.be.called; }); it('works even if the app is offline', async () => { stub(navigator, 'onLine').value(false); - const { token } = await refreshAuthToken(dependencies); + const { token } = await refreshAuthToken(installations); expect(token).to.equal(AUTH_TOKEN); }); }); @@ -129,17 +129,17 @@ describe('refreshAuthToken', () => { Date.now() - ONE_WEEK_MS + TOKEN_EXPIRATION_BUFFER + 10 * 60 * 1000 } }; - await set(dependencies.appConfig, installationEntry); + await set(installations.appConfig, installationEntry); }); it('returns a different token after expiration', async () => { - const token1 = await refreshAuthToken(dependencies); + const token1 = await refreshAuthToken(installations); expect(token1.token).to.equal(DB_AUTH_TOKEN); // Wait 30 minutes. clock.tick('30:00'); - const token2 = await refreshAuthToken(dependencies); + const token2 = await refreshAuthToken(installations); await expect(token2.token).to.equal(AUTH_TOKEN); await expect(token2.token).not.to.equal(DB_AUTH_TOKEN); expect(generateAuthTokenRequestSpy).to.be.calledOnce; @@ -159,25 +159,25 @@ describe('refreshAuthToken', () => { creationTime: Date.now() - 2 * ONE_WEEK_MS } }; - await set(dependencies.appConfig, installationEntry); + await set(installations.appConfig, installationEntry); }); it('does not call generateAuthToken twice on subsequent calls', async () => { - await refreshAuthToken(dependencies); - await refreshAuthToken(dependencies); + await refreshAuthToken(installations); + await refreshAuthToken(installations); expect(generateAuthTokenRequestSpy).to.be.calledOnce; }); it('does not call generateAuthToken twice on simultaneous calls', async () => { await Promise.all([ - refreshAuthToken(dependencies), - refreshAuthToken(dependencies) + refreshAuthToken(installations), + refreshAuthToken(installations) ]); expect(generateAuthTokenRequestSpy).to.be.calledOnce; }); it('returns a new token', async () => { - const { token } = await refreshAuthToken(dependencies); + const { token } = await refreshAuthToken(installations); await expect(token).to.equal(AUTH_TOKEN); await expect(token).not.to.equal(DB_AUTH_TOKEN); expect(generateAuthTokenRequestSpy).to.be.calledOnce; @@ -186,14 +186,14 @@ describe('refreshAuthToken', () => { it('throws if the app is offline', async () => { stub(navigator, 'onLine').value(false); - await expect(refreshAuthToken(dependencies)).to.be.rejected; + await expect(refreshAuthToken(installations)).to.be.rejected; }); it('saves the new token in the DB', async () => { - const { token } = await refreshAuthToken(dependencies); + const { token } = await refreshAuthToken(installations); const installationEntry = (await get( - dependencies.appConfig + installations.appConfig )) as RegisteredInstallationEntry; expect(installationEntry).not.to.be.undefined; expect(installationEntry.registrationStatus).to.equal( diff --git a/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts b/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts index be858d7f736..4decb78b163 100644 --- a/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts +++ b/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts @@ -16,8 +16,10 @@ */ import { generateAuthTokenRequest } from '../functions/generate-auth-token-request'; -import { AppConfig } from '../interfaces/app-config'; -import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + AppConfig, + FirebaseInstallations +} from '@firebase/installations-types-exp'; import { AuthToken, CompletedAuthToken, @@ -38,11 +40,11 @@ import { remove, set, update } from './idb-manager'; * Should only be called if the Firebase Installation is registered. */ export async function refreshAuthToken( - dependencies: FirebaseDependencies, + installations: FirebaseInstallations, forceRefresh = false ): Promise { let tokenPromise: Promise | undefined; - const entry = await update(dependencies.appConfig, oldEntry => { + const entry = await update(installations.appConfig, oldEntry => { if (!isEntryRegistered(oldEntry)) { throw ERROR_FACTORY.create(ErrorCode.NOT_REGISTERED); } @@ -53,7 +55,7 @@ export async function refreshAuthToken( return oldEntry; } else if (oldAuthToken.requestStatus === RequestStatus.IN_PROGRESS) { // There already is a token request in progress. - tokenPromise = waitUntilAuthTokenRequest(dependencies, forceRefresh); + tokenPromise = waitUntilAuthTokenRequest(installations, forceRefresh); return oldEntry; } else { // No token or token expired. @@ -62,7 +64,7 @@ export async function refreshAuthToken( } const inProgressEntry = makeAuthTokenRequestInProgressEntry(oldEntry); - tokenPromise = fetchAuthTokenFromServer(dependencies, inProgressEntry); + tokenPromise = fetchAuthTokenFromServer(installations, inProgressEntry); return inProgressEntry; } }); @@ -80,25 +82,25 @@ export async function refreshAuthToken( * tries once in this thread as well. */ async function waitUntilAuthTokenRequest( - dependencies: FirebaseDependencies, + installations: FirebaseInstallations, forceRefresh: boolean ): Promise { // Unfortunately, there is no way of reliably observing when a value in // IndexedDB changes (yet, see https://github.com/WICG/indexed-db-observers), // so we need to poll. - let entry = await updateAuthTokenRequest(dependencies.appConfig); + let entry = await updateAuthTokenRequest(installations.appConfig); while (entry.authToken.requestStatus === RequestStatus.IN_PROGRESS) { // generateAuthToken still in progress. await sleep(100); - entry = await updateAuthTokenRequest(dependencies.appConfig); + entry = await updateAuthTokenRequest(installations.appConfig); } const authToken = entry.authToken; if (authToken.requestStatus === RequestStatus.NOT_STARTED) { // The request timed out or failed in a different call. Try again. - return refreshAuthToken(dependencies, forceRefresh); + return refreshAuthToken(installations, forceRefresh); } else { return authToken; } @@ -133,31 +135,31 @@ function updateAuthTokenRequest( } async function fetchAuthTokenFromServer( - dependencies: FirebaseDependencies, + installations: FirebaseInstallations, installationEntry: RegisteredInstallationEntry ): Promise { try { const authToken = await generateAuthTokenRequest( - dependencies, + installations, installationEntry ); const updatedInstallationEntry: RegisteredInstallationEntry = { ...installationEntry, authToken }; - await set(dependencies.appConfig, updatedInstallationEntry); + await set(installations.appConfig, updatedInstallationEntry); return authToken; } catch (e) { if (isServerError(e) && (e.serverCode === 401 || e.serverCode === 404)) { // Server returned a "FID not found" or a "Invalid authentication" error. // Generate a new ID next time. - await remove(dependencies.appConfig); + await remove(installations.appConfig); } else { const updatedInstallationEntry: RegisteredInstallationEntry = { ...installationEntry, authToken: { requestStatus: RequestStatus.NOT_STARTED } }; - await set(dependencies.appConfig, updatedInstallationEntry); + await set(installations.appConfig, updatedInstallationEntry); } throw e; } diff --git a/packages-exp/installations-exp/src/index.ts b/packages-exp/installations-exp/src/index.ts index 70c6dcf1cc4..fe64136858e 100644 --- a/packages-exp/installations-exp/src/index.ts +++ b/packages-exp/installations-exp/src/index.ts @@ -16,68 +16,8 @@ */ // import firebase from '@firebase/app-exp'; -import { registerVersion, _registerComponent } from '@firebase/app-exp'; -import { _FirebaseService } from '@firebase/app-types-exp'; -import { Component, ComponentType } from '@firebase/component'; -import { FirebaseInstallations } from '@firebase/installations-types'; -import { - deleteInstallation, - getId, - getToken, - IdChangeCallbackFn, - IdChangeUnsubscribeFn, - onIdChange -} from './api'; -import { extractAppConfig } from './helpers/extract-app-config'; -import { FirebaseDependencies } from './interfaces/firebase-dependencies'; +import { registerInstallations } from './functions/config'; -import { name, version } from '../package.json'; - -export function registerInstallations(): void { - const installationsName = 'installations'; - - _registerComponent( - new Component( - installationsName, - container => { - const app = container.getProvider('app').getImmediate(); - - // Throws if app isn't configured properly. - const appConfig = extractAppConfig(app); - const platformLoggerProvider = container.getProvider('platform-logger'); - const dependencies: FirebaseDependencies = { - appConfig, - platformLoggerProvider - }; - - const installations: FirebaseInstallations & _FirebaseService = { - app, - getId: () => getId(dependencies), - getToken: (forceRefresh?: boolean) => - getToken(dependencies, forceRefresh), - delete: () => deleteInstallation(dependencies), - onIdChange: (callback: IdChangeCallbackFn): IdChangeUnsubscribeFn => - onIdChange(dependencies, callback) - }; - return installations; - }, - ComponentType.PUBLIC - ) - ); - - registerVersion(name, version); -} +export * from './api'; registerInstallations(); - -/** - * Define extension behavior of `registerInstallations` - */ -declare module '@firebase/app-types' { - interface FirebaseNamespace { - installations(app?: FirebaseApp): FirebaseInstallations; - } - interface FirebaseApp { - installations(): FirebaseInstallations; - } -} diff --git a/packages-exp/installations-exp/src/interfaces/app-config.ts b/packages-exp/installations-exp/src/interfaces/app-config.ts deleted file mode 100644 index 007f14f1db4..00000000000 --- a/packages-exp/installations-exp/src/interfaces/app-config.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * 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. - */ - -export interface AppConfig { - readonly appName: string; - readonly projectId: string; - readonly apiKey: string; - readonly appId: string; -} diff --git a/packages-exp/installations-exp/src/interfaces/firebase-dependencies.ts b/packages-exp/installations-exp/src/interfaces/firebase-dependencies.ts deleted file mode 100644 index 3dd3c2eb03b..00000000000 --- a/packages-exp/installations-exp/src/interfaces/firebase-dependencies.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @license - * Copyright 2019 Google LLC - * - * 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 { Provider } from '@firebase/component'; -import { AppConfig } from './app-config'; - -export interface FirebaseDependencies { - readonly appConfig: AppConfig; - readonly platformLoggerProvider: Provider<'platform-logger'>; -} diff --git a/packages-exp/installations-exp/src/testing/fake-generators.ts b/packages-exp/installations-exp/src/testing/fake-generators.ts index c6a0fbec63e..d37ddf3f84f 100644 --- a/packages-exp/installations-exp/src/testing/fake-generators.ts +++ b/packages-exp/installations-exp/src/testing/fake-generators.ts @@ -15,15 +15,17 @@ * limitations under the License. */ -import { FirebaseApp } from '@firebase/app-types'; +import { FirebaseApp } from '@firebase/app-types-exp'; import { Component, ComponentContainer, ComponentType } from '@firebase/component'; import { extractAppConfig } from '../helpers/extract-app-config'; -import { AppConfig } from '../interfaces/app-config'; -import { FirebaseDependencies } from '../interfaces/firebase-dependencies'; +import { + FirebaseInstallations, + AppConfig +} from '@firebase/installations-types-exp'; export function getFakeApp(): FirebaseApp { return { @@ -51,7 +53,7 @@ export function getFakeAppConfig( return { ...extractAppConfig(getFakeApp()), ...customValues }; } -export function getFakeDependencies(): FirebaseDependencies { +export function getFakeInstallations(): FirebaseInstallations { const container = new ComponentContainer('test'); container.addComponent( new Component( diff --git a/packages-exp/installations-exp/src/util/get-key.ts b/packages-exp/installations-exp/src/util/get-key.ts index 84c1ecd6239..baed6850952 100644 --- a/packages-exp/installations-exp/src/util/get-key.ts +++ b/packages-exp/installations-exp/src/util/get-key.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { AppConfig } from '../interfaces/app-config'; +import { AppConfig } from '@firebase/installations-types-exp'; /** Returns a string key that can be used to identify the app. */ export function getKey(appConfig: AppConfig): string { diff --git a/packages-exp/installations-exp/test-app/rollup.config.js b/packages-exp/installations-exp/test-app/rollup.config.js index c065abfba59..c5c39d4e239 100644 --- a/packages-exp/installations-exp/test-app/rollup.config.js +++ b/packages-exp/installations-exp/test-app/rollup.config.js @@ -27,7 +27,7 @@ import typescript from 'typescript'; */ export default [ { - input: 'src/functions/index.ts', + input: 'src/api/index.ts', output: { name: 'FirebaseInstallations', file: 'test-app/sdk.js', diff --git a/packages-exp/installations-types-exp/index.d.ts b/packages-exp/installations-types-exp/index.d.ts index 5508464a509..e01e802b188 100644 --- a/packages-exp/installations-types-exp/index.d.ts +++ b/packages-exp/installations-types-exp/index.d.ts @@ -15,40 +15,43 @@ * limitations under the License. */ -import { FirebaseApp } from '@firebase/app-types'; +import { FirebaseApp } from '@firebase/app-types-exp'; +import { Provider } from '@firebase/component'; export interface FirebaseInstallations { /** - * Creates a Firebase Installation if there isn't one for the app and - * returns the Installation ID. - * - * @return Firebase Installation ID + * Firebase APP configurations */ - getId(): Promise; - + readonly appConfig: AppConfig; /** - * Returns an Authentication Token for the current Firebase Installation. - * - * @return Firebase Installation Authentication Token + * Firebase platform logging util. */ - getToken(forceRefresh?: boolean): Promise; + readonly platformLoggerProvider: Provider<'platform-logger'>; +} +export interface AppConfig { /** - * Deletes the Firebase Installation and all associated data. + * Firebase APP name. */ - delete(): Promise; - + readonly appName: string; + /** + * Firebase project ID. + */ + readonly projectId: string; + /** + * Firebase API key. + */ + readonly apiKey: string; /** - * Sets a new callback that will get called when Installlation ID changes. - * Returns an unsubscribe function that will remove the callback when called. + * Firebase APP ID. */ - onIdChange(callback: (installationId: string) => void): () => void; + readonly appId: string; } -export type FirebaseInstallationsName = 'installations'; +export type FirebaseInstallationsName = 'installations-exp'; declare module '@firebase/component' { interface NameServiceMapping { - 'installations': FirebaseInstallations; + 'installations-exp': FirebaseInstallations; } } diff --git a/packages-exp/installations-types-exp/tsconfig.json b/packages-exp/installations-types-exp/tsconfig.json index 74ca67964dd..9ec79aa816b 100644 --- a/packages-exp/installations-types-exp/tsconfig.json +++ b/packages-exp/installations-types-exp/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../installations/tsconfig.json", + "extends": "../installations-exp/tsconfig.json", "compilerOptions": { "noEmit": true }, From 3aa24ddedc72445be7883ea3672ca5cefbfeb658 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Fri, 14 Aug 2020 10:54:11 -0700 Subject: [PATCH 06/22] Making installations mudularization step2, unit tests pass --- packages-exp/installations-exp/src/functions/config.ts | 8 ++++---- .../src/helpers/extract-app-config.test.ts | 4 ++-- .../installations-exp/src/helpers/extract-app-config.ts | 4 ++-- .../src/helpers/refresh-auth-token.test.ts | 2 +- .../installations-exp/src/testing/fake-generators.ts | 6 +----- 5 files changed, 10 insertions(+), 14 deletions(-) diff --git a/packages-exp/installations-exp/src/functions/config.ts b/packages-exp/installations-exp/src/functions/config.ts index 41a98bcc6b0..c98ff22d383 100644 --- a/packages-exp/installations-exp/src/functions/config.ts +++ b/packages-exp/installations-exp/src/functions/config.ts @@ -24,13 +24,13 @@ import { extractAppConfig } from '../helpers/extract-app-config'; import { name, version } from '../../package.json'; export function registerInstallations(): void { - const installationsName = 'installations'; + const installationsName = 'installations-exp'; _registerComponent( new Component( installationsName, container => { - const app = container.getProvider('app').getImmediate(); + const app = container.getProvider('app-exp').getImmediate(); // Throws if app isn't configured properly. const appConfig = extractAppConfig(app); @@ -46,6 +46,6 @@ export function registerInstallations(): void { ComponentType.PUBLIC ) ); - - registerVersion(name, version); } + +registerVersion(name, version); diff --git a/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts b/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts index 9e95496eba2..c11b035e6f2 100644 --- a/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts +++ b/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts @@ -38,11 +38,11 @@ describe('extractAppConfig', () => { expect(() => extractAppConfig(undefined as any)).to.throw(FirebaseError); let firebaseApp = getFakeApp(); - delete firebaseApp.name; + delete (firebaseApp as any).name; expect(() => extractAppConfig(firebaseApp)).to.throw(FirebaseError); firebaseApp = getFakeApp(); - delete firebaseApp.options; + delete (firebaseApp as any).options; expect(() => extractAppConfig(firebaseApp)).to.throw(FirebaseError); firebaseApp = getFakeApp(); diff --git a/packages-exp/installations-exp/src/helpers/extract-app-config.ts b/packages-exp/installations-exp/src/helpers/extract-app-config.ts index f04ede48ac4..321d13dbd0c 100644 --- a/packages-exp/installations-exp/src/helpers/extract-app-config.ts +++ b/packages-exp/installations-exp/src/helpers/extract-app-config.ts @@ -15,9 +15,9 @@ * limitations under the License. */ -import { FirebaseApp, FirebaseOptions } from '@firebase/app-types'; +import { FirebaseApp, FirebaseOptions } from '@firebase/app-types-exp'; import { FirebaseError } from '@firebase/util'; -import { AppConfig } from '@firebase/installations-types'; +import { AppConfig } from '@firebase/installations-types-exp'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; export function extractAppConfig(app: FirebaseApp): AppConfig { diff --git a/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts b/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts index 3bb656a7db7..8b4ee46415a 100644 --- a/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts +++ b/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts @@ -18,7 +18,7 @@ import { expect } from 'chai'; import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; import * as generateAuthTokenRequestModule from '../functions/generate-auth-token-request'; -import { FirebaseInstallations } from '@firebase/installations-types'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; import { CompletedAuthToken, RegisteredInstallationEntry, diff --git a/packages-exp/installations-exp/src/testing/fake-generators.ts b/packages-exp/installations-exp/src/testing/fake-generators.ts index d37ddf3f84f..61f092e75e3 100644 --- a/packages-exp/installations-exp/src/testing/fake-generators.ts +++ b/packages-exp/installations-exp/src/testing/fake-generators.ts @@ -39,11 +39,7 @@ export function getFakeApp(): FirebaseApp { storageBucket: 'storageBucket', appId: '1:777777777777:web:d93b5ca1475efe57' }, - automaticDataCollectionEnabled: true, - delete: async () => {}, - // This won't be used in tests. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - installations: null as any + automaticDataCollectionEnabled: true }; } From 39c39eb396ba50c72f8e624d25edcfdfb955d782 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Fri, 14 Aug 2020 13:46:52 -0700 Subject: [PATCH 07/22] update dependency version, merge master --- packages-exp/installations-exp/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages-exp/installations-exp/package.json b/packages-exp/installations-exp/package.json index e5e46e8b7f8..a6d0fa8b8b7 100644 --- a/packages-exp/installations-exp/package.json +++ b/packages-exp/installations-exp/package.json @@ -50,8 +50,8 @@ }, "dependencies": { "@firebase/installations-types-exp": "0.1.0", - "@firebase/util": "0.3.0", - "@firebase/component": "0.1.17", + "@firebase/util": "0.3.1", + "@firebase/component": "0.1.18", "idb": "3.0.2", "tslib": "^1.11.1" } From bbc121cfa7f74a41fb3bda1a85d0bcca29393588 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Tue, 25 Aug 2020 14:36:53 -0700 Subject: [PATCH 08/22] Apply suggestions from code review Co-authored-by: Feiyang --- packages-exp/installations-exp/package.json | 5 +++-- packages-exp/installations-types-exp/package.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages-exp/installations-exp/package.json b/packages-exp/installations-exp/package.json index a6d0fa8b8b7..fd19a1717ae 100644 --- a/packages-exp/installations-exp/package.json +++ b/packages-exp/installations-exp/package.json @@ -1,6 +1,6 @@ { "name": "@firebase/installations-exp", - "version": "0.1.0", + "version": "0.0.800", "private": true, "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", @@ -15,7 +15,7 @@ "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "build": "rollup -c", - "build:deps": "lerna run --scope @firebase/'{app-exp,installations-exp}' --include-dependencies build", + "build:deps": "lerna run --scope @firebase/installations-exp --include-dependencies build", "dev": "rollup -c -w", "test": "yarn type-check && yarn test:karma && yarn lint", "test:ci": "node ../../scripts/run_tests_in_ci.js", @@ -36,6 +36,7 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { + "@firebase/app-exp": "0.0.800", "rollup": "2.23.0", "rollup-plugin-commonjs": "10.1.0", "rollup-plugin-json": "4.0.0", diff --git a/packages-exp/installations-types-exp/package.json b/packages-exp/installations-types-exp/package.json index 6f145cc70d1..85abb459202 100644 --- a/packages-exp/installations-types-exp/package.json +++ b/packages-exp/installations-types-exp/package.json @@ -1,7 +1,7 @@ { "name": "@firebase/installations-types-exp", "private": true, - "version": "0.1.0", + "version": "0.0.800", "description": "@firebase/installations-exp Types", "author": "Firebase (https://firebase.google.com/)", "license": "Apache-2.0", From 93e1215220ca7c66e467ddbd7eddcc8e1e60d6e3 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Tue, 25 Aug 2020 14:56:01 -0700 Subject: [PATCH 09/22] update dependencies' version --- packages-exp/installations-exp/package.json | 8 ++++---- packages-exp/installations-types-exp/package.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages-exp/installations-exp/package.json b/packages-exp/installations-exp/package.json index fd19a1717ae..1f152b35056 100644 --- a/packages-exp/installations-exp/package.json +++ b/packages-exp/installations-exp/package.json @@ -37,23 +37,23 @@ }, "devDependencies": { "@firebase/app-exp": "0.0.800", - "rollup": "2.23.0", + "rollup": "2.26.5", "rollup-plugin-commonjs": "10.1.0", "rollup-plugin-json": "4.0.0", "rollup-plugin-node-resolve": "5.2.0", "rollup-plugin-typescript2": "0.27.1", "rollup-plugin-uglify": "6.0.4", - "typescript": "3.9.7" + "typescript": "4.0.2" }, "peerDependencies": { "@firebase/app-exp": "0.x", "@firebase/app-types-exp": "0.x" }, "dependencies": { - "@firebase/installations-types-exp": "0.1.0", + "@firebase/installations-types-exp": "0.0.800", "@firebase/util": "0.3.1", "@firebase/component": "0.1.18", "idb": "3.0.2", "tslib": "^1.11.1" } -} +} \ No newline at end of file diff --git a/packages-exp/installations-types-exp/package.json b/packages-exp/installations-types-exp/package.json index 85abb459202..950db08ce96 100644 --- a/packages-exp/installations-types-exp/package.json +++ b/packages-exp/installations-types-exp/package.json @@ -24,6 +24,6 @@ "url": "https://github.com/firebase/firebase-js-sdk/issues" }, "devDependencies": { - "typescript": "3.9.7" + "typescript": "4.0.2" } -} +} \ No newline at end of file From aa2c9a077e7f660c50fa349dc023a9f503693217 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Tue, 25 Aug 2020 17:05:50 -0700 Subject: [PATCH 10/22] add getInstallation(app) --- .../src/api/get-installations.ts | 31 +++++++++++++++++++ .../installations-exp/src/api/index.ts | 1 + .../installations-exp/src/functions/config.ts | 15 ++++----- .../installations-types-exp/index.d.ts | 4 +-- 4 files changed, 40 insertions(+), 11 deletions(-) create mode 100644 packages-exp/installations-exp/src/api/get-installations.ts diff --git a/packages-exp/installations-exp/src/api/get-installations.ts b/packages-exp/installations-exp/src/api/get-installations.ts new file mode 100644 index 00000000000..1ea9b83edd6 --- /dev/null +++ b/packages-exp/installations-exp/src/api/get-installations.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 { FirebaseApp } from '@firebase/app-types-exp'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; +import { _getProvider } from '@firebase/app-exp'; +import { extractAppConfig } from '../helpers/extract-app-config'; + +export function getInstallations(app: FirebaseApp): FirebaseInstallations { + const appConfig = extractAppConfig(app); + const platformLoggerProvider = _getProvider(app, 'platform-logger'); + const installations: FirebaseInstallations = { + appConfig, + platformLoggerProvider + }; + return installations; +} diff --git a/packages-exp/installations-exp/src/api/index.ts b/packages-exp/installations-exp/src/api/index.ts index 7952e267a45..e2b0459bda6 100644 --- a/packages-exp/installations-exp/src/api/index.ts +++ b/packages-exp/installations-exp/src/api/index.ts @@ -19,3 +19,4 @@ export * from './get-id'; export * from './get-token'; export * from './delete-installation'; export * from './on-id-change'; +export * from './get-installations'; diff --git a/packages-exp/installations-exp/src/functions/config.ts b/packages-exp/installations-exp/src/functions/config.ts index c98ff22d383..1e62021577f 100644 --- a/packages-exp/installations-exp/src/functions/config.ts +++ b/packages-exp/installations-exp/src/functions/config.ts @@ -18,8 +18,7 @@ import { registerVersion, _registerComponent } from '@firebase/app-exp'; import { _FirebaseService } from '@firebase/app-types-exp'; import { Component, ComponentType } from '@firebase/component'; -import { FirebaseInstallations } from '@firebase/installations-types-exp'; -import { extractAppConfig } from '../helpers/extract-app-config'; +import { getInstallations, deleteInstallation } from '../api/index'; import { name, version } from '../../package.json'; @@ -33,15 +32,13 @@ export function registerInstallations(): void { const app = container.getProvider('app-exp').getImmediate(); // Throws if app isn't configured properly. - const appConfig = extractAppConfig(app); - const platformLoggerProvider = container.getProvider('platform-logger'); - - const installations: FirebaseInstallations = { - appConfig, - platformLoggerProvider + const installations = getInstallations(app); + const installationsService: _FirebaseService = { + app, + delete: () => deleteInstallation(installations) }; - return installations; + return installationsService; }, ComponentType.PUBLIC ) diff --git a/packages-exp/installations-types-exp/index.d.ts b/packages-exp/installations-types-exp/index.d.ts index e01e802b188..517fb56ea76 100644 --- a/packages-exp/installations-types-exp/index.d.ts +++ b/packages-exp/installations-types-exp/index.d.ts @@ -15,8 +15,8 @@ * limitations under the License. */ -import { FirebaseApp } from '@firebase/app-types-exp'; import { Provider } from '@firebase/component'; +import { _FirebaseService } from '@firebase/app-types-exp'; export interface FirebaseInstallations { /** @@ -52,6 +52,6 @@ export type FirebaseInstallationsName = 'installations-exp'; declare module '@firebase/component' { interface NameServiceMapping { - 'installations-exp': FirebaseInstallations; + 'installations-exp': _FirebaseService; } } From e2a193056c0dcaa7f683e47e484e3c4a5abb153a Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Tue, 25 Aug 2020 17:17:27 -0700 Subject: [PATCH 11/22] correct deleteInstallations funciton name --- ...allation.test.ts => delete-installations.test.ts} | 12 ++++++------ ...elete-installation.ts => delete-installations.ts} | 2 +- packages-exp/installations-exp/src/api/index.ts | 2 +- .../installations-exp/src/functions/config.ts | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) rename packages-exp/installations-exp/src/api/{delete-installation.test.ts => delete-installations.test.ts} (90%) rename packages-exp/installations-exp/src/api/{delete-installation.ts => delete-installations.ts} (97%) diff --git a/packages-exp/installations-exp/src/api/delete-installation.test.ts b/packages-exp/installations-exp/src/api/delete-installations.test.ts similarity index 90% rename from packages-exp/installations-exp/src/api/delete-installation.test.ts rename to packages-exp/installations-exp/src/api/delete-installations.test.ts index 1b1b747b0ea..fbe3b25fcf0 100644 --- a/packages-exp/installations-exp/src/api/delete-installation.test.ts +++ b/packages-exp/installations-exp/src/api/delete-installations.test.ts @@ -33,7 +33,7 @@ import { getFakeInstallations } from '../testing/fake-generators'; import '../testing/setup'; import { ErrorCode } from '../util/errors'; import { sleep } from '../util/sleep'; -import { deleteInstallation } from './delete-installation'; +import { deleteInstallations } from './delete-installations'; const FID = 'children-of-the-damned'; @@ -56,7 +56,7 @@ describe('deleteInstallation', () => { }); it('resolves without calling server API if there is no installation', async () => { - await expect(deleteInstallation(installations)).to.be.fulfilled; + await expect(deleteInstallations(installations)).to.be.fulfilled; expect(deleteInstallationRequestSpy).not.to.have.been.called; }); @@ -67,7 +67,7 @@ describe('deleteInstallation', () => { }; await set(installations.appConfig, entry); - await expect(deleteInstallation(installations)).to.be.fulfilled; + await expect(deleteInstallations(installations)).to.be.fulfilled; expect(deleteInstallationRequestSpy).not.to.have.been.called; await expect(get(installations.appConfig)).to.eventually.be.undefined; }); @@ -80,7 +80,7 @@ describe('deleteInstallation', () => { }; await set(installations.appConfig, entry); - await expect(deleteInstallation(installations)).to.be.rejectedWith( + await expect(deleteInstallations(installations)).to.be.rejectedWith( ErrorCode.DELETE_PENDING_REGISTRATION ); expect(deleteInstallationRequestSpy).not.to.have.been.called; @@ -101,7 +101,7 @@ describe('deleteInstallation', () => { await set(installations.appConfig, entry); stub(navigator, 'onLine').value(false); - await expect(deleteInstallation(installations)).to.be.rejectedWith( + await expect(deleteInstallations(installations)).to.be.rejectedWith( ErrorCode.APP_OFFLINE ); expect(deleteInstallationRequestSpy).not.to.have.been.called; @@ -121,7 +121,7 @@ describe('deleteInstallation', () => { }; await set(installations.appConfig, entry); - await expect(deleteInstallation(installations)).to.be.fulfilled; + await expect(deleteInstallations(installations)).to.be.fulfilled; expect(deleteInstallationRequestSpy).to.have.been.calledOnceWith( installations.appConfig, entry diff --git a/packages-exp/installations-exp/src/api/delete-installation.ts b/packages-exp/installations-exp/src/api/delete-installations.ts similarity index 97% rename from packages-exp/installations-exp/src/api/delete-installation.ts rename to packages-exp/installations-exp/src/api/delete-installations.ts index 3048dbf3ece..1316774a1bd 100644 --- a/packages-exp/installations-exp/src/api/delete-installation.ts +++ b/packages-exp/installations-exp/src/api/delete-installations.ts @@ -21,7 +21,7 @@ import { RequestStatus } from '../interfaces/installation-entry'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; import { FirebaseInstallations } from '@firebase/installations-types-exp'; -export async function deleteInstallation( +export async function deleteInstallations( installations: FirebaseInstallations ): Promise { const { appConfig } = installations; diff --git a/packages-exp/installations-exp/src/api/index.ts b/packages-exp/installations-exp/src/api/index.ts index e2b0459bda6..7a21b629036 100644 --- a/packages-exp/installations-exp/src/api/index.ts +++ b/packages-exp/installations-exp/src/api/index.ts @@ -17,6 +17,6 @@ export * from './get-id'; export * from './get-token'; -export * from './delete-installation'; +export * from './delete-installations'; export * from './on-id-change'; export * from './get-installations'; diff --git a/packages-exp/installations-exp/src/functions/config.ts b/packages-exp/installations-exp/src/functions/config.ts index 1e62021577f..c593665a411 100644 --- a/packages-exp/installations-exp/src/functions/config.ts +++ b/packages-exp/installations-exp/src/functions/config.ts @@ -18,7 +18,7 @@ import { registerVersion, _registerComponent } from '@firebase/app-exp'; import { _FirebaseService } from '@firebase/app-types-exp'; import { Component, ComponentType } from '@firebase/component'; -import { getInstallations, deleteInstallation } from '../api/index'; +import { getInstallations, deleteInstallations } from '../api/index'; import { name, version } from '../../package.json'; @@ -35,7 +35,7 @@ export function registerInstallations(): void { const installations = getInstallations(app); const installationsService: _FirebaseService = { app, - delete: () => deleteInstallation(installations) + delete: () => deleteInstallations(installations) }; return installationsService; From 9b46a8fbb57e822a8bc47911e43873ede614714d Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Tue, 25 Aug 2020 17:43:08 -0700 Subject: [PATCH 12/22] Place the call to registerVerion and registerInstallations at the same place --- packages-exp/installations-exp/src/functions/config.ts | 6 +----- packages-exp/installations-exp/src/index.ts | 3 +++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages-exp/installations-exp/src/functions/config.ts b/packages-exp/installations-exp/src/functions/config.ts index c593665a411..44d529c25fa 100644 --- a/packages-exp/installations-exp/src/functions/config.ts +++ b/packages-exp/installations-exp/src/functions/config.ts @@ -15,13 +15,11 @@ * limitations under the License. */ -import { registerVersion, _registerComponent } from '@firebase/app-exp'; +import { _registerComponent } from '@firebase/app-exp'; import { _FirebaseService } from '@firebase/app-types-exp'; import { Component, ComponentType } from '@firebase/component'; import { getInstallations, deleteInstallations } from '../api/index'; -import { name, version } from '../../package.json'; - export function registerInstallations(): void { const installationsName = 'installations-exp'; @@ -44,5 +42,3 @@ export function registerInstallations(): void { ) ); } - -registerVersion(name, version); diff --git a/packages-exp/installations-exp/src/index.ts b/packages-exp/installations-exp/src/index.ts index fe64136858e..5e1850f71c6 100644 --- a/packages-exp/installations-exp/src/index.ts +++ b/packages-exp/installations-exp/src/index.ts @@ -17,7 +17,10 @@ // import firebase from '@firebase/app-exp'; import { registerInstallations } from './functions/config'; +import { registerVersion } from '@firebase/app-exp'; +import { name, version } from '../package.json'; export * from './api'; registerInstallations(); +registerVersion(name, version); From 36e93da699eec91be235f3d718fa7114a3615e4e Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Tue, 25 Aug 2020 18:14:42 -0700 Subject: [PATCH 13/22] remove dead code --- packages-exp/installations-exp/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages-exp/installations-exp/src/index.ts b/packages-exp/installations-exp/src/index.ts index 5e1850f71c6..98351b54056 100644 --- a/packages-exp/installations-exp/src/index.ts +++ b/packages-exp/installations-exp/src/index.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -// import firebase from '@firebase/app-exp'; import { registerInstallations } from './functions/config'; import { registerVersion } from '@firebase/app-exp'; import { name, version } from '../package.json'; From 3d3d36ca71b0f483c5afaa8137815ead7313648a Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Wed, 26 Aug 2020 09:30:11 -0700 Subject: [PATCH 14/22] add api extractor config --- packages-exp/installations-types-exp/api-extractor.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages-exp/installations-types-exp/api-extractor.json diff --git a/packages-exp/installations-types-exp/api-extractor.json b/packages-exp/installations-types-exp/api-extractor.json new file mode 100644 index 00000000000..42f37a88c4b --- /dev/null +++ b/packages-exp/installations-types-exp/api-extractor.json @@ -0,0 +1,5 @@ +{ + "extends": "../../config/api-extractor.json", + // Point it to your entry point d.ts file. + "mainEntryPointFilePath": "/index.d.ts" +} \ No newline at end of file From 8e36c93fbf8df66cc114fdc94b62d86221a2e680 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Wed, 26 Aug 2020 14:46:36 -0700 Subject: [PATCH 15/22] rewrite the internal interface --- .../installations-exp/src/functions/config.ts | 53 ++++++++++++------- .../src/interfaces/installation-internal.ts | 50 +++++++++++++++++ .../installations-types-exp/index.d.ts | 4 +- 3 files changed, 86 insertions(+), 21 deletions(-) create mode 100644 packages-exp/installations-exp/src/interfaces/installation-internal.ts diff --git a/packages-exp/installations-exp/src/functions/config.ts b/packages-exp/installations-exp/src/functions/config.ts index 44d529c25fa..c93728ef870 100644 --- a/packages-exp/installations-exp/src/functions/config.ts +++ b/packages-exp/installations-exp/src/functions/config.ts @@ -17,28 +17,43 @@ import { _registerComponent } from '@firebase/app-exp'; import { _FirebaseService } from '@firebase/app-types-exp'; -import { Component, ComponentType } from '@firebase/component'; -import { getInstallations, deleteInstallations } from '../api/index'; +import { + Component, + ComponentType, + InstanceFactory, + ComponentContainer +} from '@firebase/component'; +import { + getInstallations, + deleteInstallations, + getId, + getToken, + onIdChange +} from '../api/index'; +import { FirebaseInstallationsInternal } from '../interfaces/installation-internal'; -export function registerInstallations(): void { - const installationsName = 'installations-exp'; +const installationsName = 'installations-exp'; - _registerComponent( - new Component( - installationsName, - container => { - const app = container.getProvider('app-exp').getImmediate(); +const factory: InstanceFactory<'installations-exp'> = ( + container: ComponentContainer +) => { + const app = container.getProvider('app-exp').getImmediate(); - // Throws if app isn't configured properly. - const installations = getInstallations(app); - const installationsService: _FirebaseService = { - app, - delete: () => deleteInstallations(installations) - }; + // Throws if app isn't configured properly. + const installations = getInstallations(app); + const installationsService: FirebaseInstallationsInternal = { + app, + getId: () => getId(installations), + getToken: (forceRefresh?: boolean) => getToken(installations, forceRefresh), + delete: () => deleteInstallations(installations), + onIdChange: (callback: (fid: string) => void) => + onIdChange(installations, callback) + }; + return installationsService; +}; - return installationsService; - }, - ComponentType.PUBLIC - ) +export function registerInstallations(): void { + _registerComponent( + new Component(installationsName, factory, ComponentType.PUBLIC) ); } diff --git a/packages-exp/installations-exp/src/interfaces/installation-internal.ts b/packages-exp/installations-exp/src/interfaces/installation-internal.ts new file mode 100644 index 00000000000..4231b763965 --- /dev/null +++ b/packages-exp/installations-exp/src/interfaces/installation-internal.ts @@ -0,0 +1,50 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 { _FirebaseService, FirebaseApp } from '@firebase/app-types-exp'; + +/** + * An interface for Firebase internal SDKs use only. + */ +export interface FirebaseInstallationsInternal extends _FirebaseService { + /** + * FirebaseApp instance which carries Firebase app configurations. + */ + app: FirebaseApp; + + /** + * Creates a Firebase Installation if there isn't one for the app and + * returns the Installation ID. + */ + getId(): Promise; + + /** + * Returns an Authentication Token for the current Firebase Installation. + */ + getToken(forceRefresh?: boolean): Promise; + + /** + * Deletes the Firebase Installation and all associated data. + */ + delete(): Promise; + + /** + * Sets a new callback that will get called when Installlation ID changes. + * Returns an unsubscribe function that will remove the callback when called. + */ + onIdChange(callback: (installationId: string) => void): () => void; +} diff --git a/packages-exp/installations-types-exp/index.d.ts b/packages-exp/installations-types-exp/index.d.ts index 517fb56ea76..3351d657276 100644 --- a/packages-exp/installations-types-exp/index.d.ts +++ b/packages-exp/installations-types-exp/index.d.ts @@ -16,7 +16,7 @@ */ import { Provider } from '@firebase/component'; -import { _FirebaseService } from '@firebase/app-types-exp'; +import { FirebaseInstallationsInternal } from '../installations-exp/src/interfaces/installation-internal'; export interface FirebaseInstallations { /** @@ -52,6 +52,6 @@ export type FirebaseInstallationsName = 'installations-exp'; declare module '@firebase/component' { interface NameServiceMapping { - 'installations-exp': _FirebaseService; + 'installations-exp': FirebaseInstallationsInternal; } } From 4aa04bcd85674e5886a8a00e94cdb3f689e7ae57 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Wed, 26 Aug 2020 15:32:58 -0700 Subject: [PATCH 16/22] fix build error --- packages-exp/installations-exp/src/functions/config.ts | 2 +- .../installations-exp/src/interfaces/installation-internal.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages-exp/installations-exp/src/functions/config.ts b/packages-exp/installations-exp/src/functions/config.ts index c93728ef870..ab3dea96c58 100644 --- a/packages-exp/installations-exp/src/functions/config.ts +++ b/packages-exp/installations-exp/src/functions/config.ts @@ -45,7 +45,7 @@ const factory: InstanceFactory<'installations-exp'> = ( app, getId: () => getId(installations), getToken: (forceRefresh?: boolean) => getToken(installations, forceRefresh), - delete: () => deleteInstallations(installations), + _delete: () => deleteInstallations(installations), onIdChange: (callback: (fid: string) => void) => onIdChange(installations, callback) }; diff --git a/packages-exp/installations-exp/src/interfaces/installation-internal.ts b/packages-exp/installations-exp/src/interfaces/installation-internal.ts index 4231b763965..fd910fbb4d1 100644 --- a/packages-exp/installations-exp/src/interfaces/installation-internal.ts +++ b/packages-exp/installations-exp/src/interfaces/installation-internal.ts @@ -40,7 +40,7 @@ export interface FirebaseInstallationsInternal extends _FirebaseService { /** * Deletes the Firebase Installation and all associated data. */ - delete(): Promise; + _delete(): Promise; /** * Sets a new callback that will get called when Installlation ID changes. From ce48439226214a8b48cdb247a61cb818c9f7acd7 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Mon, 31 Aug 2020 18:00:12 -0700 Subject: [PATCH 17/22] Seperate internal interface from public interface. --- .../src/api/delete-installations.test.ts | 10 ++-- .../src/api/delete-installations.ts | 4 +- .../installations-exp/src/api/get-id.test.ts | 10 ++-- .../installations-exp/src/api/get-id.ts | 4 +- .../src/api/get-installations.ts | 13 ++--- .../src/api/get-token.test.ts | 12 ++-- .../installations-exp/src/api/get-token.ts | 6 +- .../src/api/on-id-change.test.ts | 4 +- .../installations-exp/src/api/on-id-change.ts | 4 +- .../installations-exp/src/functions/common.ts | 2 +- .../installations-exp/src/functions/config.ts | 58 ++++++++++++------- .../create-installation-request.test.ts | 2 +- .../functions/create-installation-request.ts | 2 +- .../delete-installation-request.test.ts | 2 +- .../functions/delete-installation-request.ts | 2 +- .../generate-auth-token-request.test.ts | 4 +- .../functions/generate-auth-token-request.ts | 10 ++-- .../src/helpers/extract-app-config.test.ts | 2 +- .../src/helpers/extract-app-config.ts | 2 +- .../src/helpers/fid-changed.test.ts | 2 +- .../src/helpers/fid-changed.ts | 2 +- .../helpers/get-installation-entry.test.ts | 2 +- .../src/helpers/get-installation-entry.ts | 2 +- .../src/helpers/idb-manager.test.ts | 2 +- .../src/helpers/idb-manager.ts | 2 +- .../src/helpers/refresh-auth-token.test.ts | 6 +- .../src/helpers/refresh-auth-token.ts | 10 ++-- .../src/interfaces/installation-impl.ts | 34 +++++++++++ .../src/interfaces/installation-internal.ts | 50 ---------------- .../src/testing/fake-generators.ts | 12 ++-- .../installations-exp/src/util/get-key.ts | 2 +- .../installations-types-exp/index.d.ts | 42 +++++--------- 32 files changed, 154 insertions(+), 167 deletions(-) create mode 100644 packages-exp/installations-exp/src/interfaces/installation-impl.ts delete mode 100644 packages-exp/installations-exp/src/interfaces/installation-internal.ts diff --git a/packages-exp/installations-exp/src/api/delete-installations.test.ts b/packages-exp/installations-exp/src/api/delete-installations.test.ts index fbe3b25fcf0..ccae7f9c2cd 100644 --- a/packages-exp/installations-exp/src/api/delete-installations.test.ts +++ b/packages-exp/installations-exp/src/api/delete-installations.test.ts @@ -19,10 +19,6 @@ import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; import * as deleteInstallationRequestModule from '../functions/delete-installation-request'; import { get, set } from '../helpers/idb-manager'; -import { - FirebaseInstallations, - AppConfig -} from '@firebase/installations-types-exp'; import { InProgressInstallationEntry, RegisteredInstallationEntry, @@ -34,11 +30,15 @@ import '../testing/setup'; import { ErrorCode } from '../util/errors'; import { sleep } from '../util/sleep'; import { deleteInstallations } from './delete-installations'; +import { + FirebaseInstallationsImpl, + AppConfig +} from '../interfaces/installation-impl'; const FID = 'children-of-the-damned'; describe('deleteInstallation', () => { - let installations: FirebaseInstallations; + let installations: FirebaseInstallationsImpl; let deleteInstallationRequestSpy: SinonStub< [AppConfig, RegisteredInstallationEntry], Promise diff --git a/packages-exp/installations-exp/src/api/delete-installations.ts b/packages-exp/installations-exp/src/api/delete-installations.ts index 1316774a1bd..b075edb8110 100644 --- a/packages-exp/installations-exp/src/api/delete-installations.ts +++ b/packages-exp/installations-exp/src/api/delete-installations.ts @@ -19,10 +19,10 @@ import { deleteInstallationRequest } from '../functions/delete-installation-requ import { remove, update } from '../helpers/idb-manager'; import { RequestStatus } from '../interfaces/installation-entry'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; -import { FirebaseInstallations } from '@firebase/installations-types-exp'; +import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; export async function deleteInstallations( - installations: FirebaseInstallations + installations: FirebaseInstallationsImpl ): Promise { const { appConfig } = installations; diff --git a/packages-exp/installations-exp/src/api/get-id.test.ts b/packages-exp/installations-exp/src/api/get-id.test.ts index 7d17852d1c4..77f14a03cb8 100644 --- a/packages-exp/installations-exp/src/api/get-id.test.ts +++ b/packages-exp/installations-exp/src/api/get-id.test.ts @@ -19,10 +19,6 @@ import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; import * as getInstallationEntryModule from '../helpers/get-installation-entry'; import * as refreshAuthTokenModule from '../helpers/refresh-auth-token'; -import { - FirebaseInstallations, - AppConfig -} from '@firebase/installations-types-exp'; import { RegisteredInstallationEntry, RequestStatus @@ -30,11 +26,15 @@ import { import { getFakeInstallations } from '../testing/fake-generators'; import '../testing/setup'; import { getId } from './get-id'; +import { + FirebaseInstallationsImpl, + AppConfig +} from '../interfaces/installation-impl'; const FID = 'disciples-of-the-watch'; describe('getId', () => { - let installations: FirebaseInstallations; + let installations: FirebaseInstallationsImpl; let getInstallationEntrySpy: SinonStub< [AppConfig], Promise diff --git a/packages-exp/installations-exp/src/api/get-id.ts b/packages-exp/installations-exp/src/api/get-id.ts index 265b72c2d41..52c899c840a 100644 --- a/packages-exp/installations-exp/src/api/get-id.ts +++ b/packages-exp/installations-exp/src/api/get-id.ts @@ -17,10 +17,10 @@ import { getInstallationEntry } from '../helpers/get-installation-entry'; import { refreshAuthToken } from '../helpers/refresh-auth-token'; -import { FirebaseInstallations } from '@firebase/installations-types-exp'; +import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; export async function getId( - installations: FirebaseInstallations + installations: FirebaseInstallationsImpl ): Promise { const { installationEntry, registrationPromise } = await getInstallationEntry( installations.appConfig diff --git a/packages-exp/installations-exp/src/api/get-installations.ts b/packages-exp/installations-exp/src/api/get-installations.ts index 1ea9b83edd6..1dcd2f1a3e4 100644 --- a/packages-exp/installations-exp/src/api/get-installations.ts +++ b/packages-exp/installations-exp/src/api/get-installations.ts @@ -18,14 +18,11 @@ import { FirebaseApp } from '@firebase/app-types-exp'; import { FirebaseInstallations } from '@firebase/installations-types-exp'; import { _getProvider } from '@firebase/app-exp'; -import { extractAppConfig } from '../helpers/extract-app-config'; export function getInstallations(app: FirebaseApp): FirebaseInstallations { - const appConfig = extractAppConfig(app); - const platformLoggerProvider = _getProvider(app, 'platform-logger'); - const installations: FirebaseInstallations = { - appConfig, - platformLoggerProvider - }; - return installations; + const installationsImpl = _getProvider( + app, + 'installations-exp' + ).getImmediate(); + return installationsImpl; } diff --git a/packages-exp/installations-exp/src/api/get-token.test.ts b/packages-exp/installations-exp/src/api/get-token.test.ts index f8ecd8a4783..430d341b3c2 100644 --- a/packages-exp/installations-exp/src/api/get-token.test.ts +++ b/packages-exp/installations-exp/src/api/get-token.test.ts @@ -20,10 +20,6 @@ import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; import * as createInstallationRequestModule from '../functions/create-installation-request'; import * as generateAuthTokenRequestModule from '../functions/generate-auth-token-request'; import { get, set } from '../helpers/idb-manager'; -import { - FirebaseInstallations, - AppConfig -} from '@firebase/installations-types-exp'; import { CompletedAuthToken, InProgressInstallationEntry, @@ -37,6 +33,10 @@ import { TOKEN_EXPIRATION_BUFFER } from '../util/constants'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; import { sleep } from '../util/sleep'; import { getToken } from './get-token'; +import { + AppConfig, + FirebaseInstallationsImpl +} from '../interfaces/installation-impl'; const FID = 'dont-talk-to-strangers'; const AUTH_TOKEN = 'authToken'; @@ -173,13 +173,13 @@ const setupInstallationEntryMap: Map< ]); describe('getToken', () => { - let installations: FirebaseInstallations; + let installations: FirebaseInstallationsImpl; let createInstallationRequestSpy: SinonStub< [AppConfig, InProgressInstallationEntry], Promise >; let generateAuthTokenRequestSpy: SinonStub< - [FirebaseInstallations, RegisteredInstallationEntry], + [FirebaseInstallationsImpl, RegisteredInstallationEntry], Promise >; diff --git a/packages-exp/installations-exp/src/api/get-token.ts b/packages-exp/installations-exp/src/api/get-token.ts index f3df9bd5069..7fd14603a98 100644 --- a/packages-exp/installations-exp/src/api/get-token.ts +++ b/packages-exp/installations-exp/src/api/get-token.ts @@ -18,12 +18,12 @@ import { getInstallationEntry } from '../helpers/get-installation-entry'; import { refreshAuthToken } from '../helpers/refresh-auth-token'; import { - FirebaseInstallations, + FirebaseInstallationsImpl, AppConfig -} from '@firebase/installations-types-exp'; +} from '../interfaces/installation-impl'; export async function getToken( - installations: FirebaseInstallations, + installations: FirebaseInstallationsImpl, forceRefresh = false ): Promise { await completeInstallationRegistration(installations.appConfig); diff --git a/packages-exp/installations-exp/src/api/on-id-change.test.ts b/packages-exp/installations-exp/src/api/on-id-change.test.ts index 21051e3c9ee..219532a0233 100644 --- a/packages-exp/installations-exp/src/api/on-id-change.test.ts +++ b/packages-exp/installations-exp/src/api/on-id-change.test.ts @@ -21,10 +21,10 @@ import '../testing/setup'; import { onIdChange } from './on-id-change'; import * as FidChangedModule from '../helpers/fid-changed'; import { getFakeInstallations } from '../testing/fake-generators'; -import { FirebaseInstallations } from '@firebase/installations-types-exp'; +import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; describe('onIdChange', () => { - let installations: FirebaseInstallations; + let installations: FirebaseInstallationsImpl; beforeEach(() => { installations = getFakeInstallations(); diff --git a/packages-exp/installations-exp/src/api/on-id-change.ts b/packages-exp/installations-exp/src/api/on-id-change.ts index 14e5e2ffe2c..a088a7ae71a 100644 --- a/packages-exp/installations-exp/src/api/on-id-change.ts +++ b/packages-exp/installations-exp/src/api/on-id-change.ts @@ -16,7 +16,7 @@ */ import { addCallback, removeCallback } from '../helpers/fid-changed'; -import { FirebaseInstallations } from '@firebase/installations-types-exp'; +import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; export type IdChangeCallbackFn = (installationId: string) => void; export type IdChangeUnsubscribeFn = () => void; @@ -26,7 +26,7 @@ export type IdChangeUnsubscribeFn = () => void; * Returns an unsubscribe function that will remove the callback when called. */ export function onIdChange( - { appConfig }: FirebaseInstallations, + { appConfig }: FirebaseInstallationsImpl, callback: IdChangeCallbackFn ): IdChangeUnsubscribeFn { addCallback(appConfig, callback); diff --git a/packages-exp/installations-exp/src/functions/common.ts b/packages-exp/installations-exp/src/functions/common.ts index ee6b26bea9b..507296da392 100644 --- a/packages-exp/installations-exp/src/functions/common.ts +++ b/packages-exp/installations-exp/src/functions/common.ts @@ -17,7 +17,6 @@ import { FirebaseError } from '@firebase/util'; import { GenerateAuthTokenResponse } from '../interfaces/api-response'; -import { AppConfig } from '@firebase/installations-types-exp'; import { CompletedAuthToken, RegisteredInstallationEntry, @@ -28,6 +27,7 @@ import { INTERNAL_AUTH_VERSION } from '../util/constants'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; +import { AppConfig } from '../interfaces/installation-impl'; export function getInstallationsEndpoint({ projectId }: AppConfig): string { return `${INSTALLATIONS_API_URL}/projects/${projectId}/installations`; diff --git a/packages-exp/installations-exp/src/functions/config.ts b/packages-exp/installations-exp/src/functions/config.ts index ab3dea96c58..1a1e0c55814 100644 --- a/packages-exp/installations-exp/src/functions/config.ts +++ b/packages-exp/installations-exp/src/functions/config.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { _registerComponent } from '@firebase/app-exp'; +import { _registerComponent, _getProvider } from '@firebase/app-exp'; import { _FirebaseService } from '@firebase/app-types-exp'; import { Component, @@ -23,37 +23,53 @@ import { InstanceFactory, ComponentContainer } from '@firebase/component'; -import { - getInstallations, - deleteInstallations, - getId, - getToken, - onIdChange -} from '../api/index'; -import { FirebaseInstallationsInternal } from '../interfaces/installation-internal'; +import { deleteInstallations, getId, getToken } from '../api/index'; +import { FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; +import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; +import { extractAppConfig } from '../helpers/extract-app-config'; -const installationsName = 'installations-exp'; +const INSTALLATIONS_NAME = 'installations-exp'; -const factory: InstanceFactory<'installations-exp'> = ( +const publicFactory: InstanceFactory<'installations-exp'> = ( container: ComponentContainer ) => { const app = container.getProvider('app-exp').getImmediate(); - // Throws if app isn't configured properly. - const installations = getInstallations(app); - const installationsService: FirebaseInstallationsInternal = { + const appConfig = extractAppConfig(app); + const platformLoggerProvider = _getProvider(app, 'platform-logger'); + + const installationsImpl: FirebaseInstallationsImpl = { app, - getId: () => getId(installations), - getToken: (forceRefresh?: boolean) => getToken(installations, forceRefresh), - _delete: () => deleteInstallations(installations), - onIdChange: (callback: (fid: string) => void) => - onIdChange(installations, callback) + appConfig, + platformLoggerProvider, + _delete: () => deleteInstallations(installationsImpl) }; - return installationsService; + return installationsImpl; +}; + +const internalFactory: InstanceFactory<'installations-exp-internal'> = ( + container: ComponentContainer +) => { + const app = container.getProvider('app-exp').getImmediate(); + // Internal FIS instance relies on public FIS instance. + const installationsImpl = _getProvider( + app, + INSTALLATIONS_NAME + ).getImmediate(); + + const installationsInternal: FirebaseInstallationsInternal = { + getId: () => getId(installationsImpl), + getToken: (forceRefresh?: boolean) => + getToken(installationsImpl, forceRefresh) + }; + return installationsInternal; }; export function registerInstallations(): void { _registerComponent( - new Component(installationsName, factory, ComponentType.PUBLIC) + new Component(INSTALLATIONS_NAME, publicFactory, ComponentType.PUBLIC) + ); + _registerComponent( + new Component(INSTALLATIONS_NAME, internalFactory, ComponentType.PRIVATE) ); } diff --git a/packages-exp/installations-exp/src/functions/create-installation-request.test.ts b/packages-exp/installations-exp/src/functions/create-installation-request.test.ts index 39687bb9231..67f00585595 100644 --- a/packages-exp/installations-exp/src/functions/create-installation-request.test.ts +++ b/packages-exp/installations-exp/src/functions/create-installation-request.test.ts @@ -19,7 +19,7 @@ import { FirebaseError } from '@firebase/util'; import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; import { CreateInstallationResponse } from '../interfaces/api-response'; -import { AppConfig } from '@firebase/installations-types-exp'; +import { AppConfig } from '../interfaces/installation-impl'; import { InProgressInstallationEntry, RequestStatus diff --git a/packages-exp/installations-exp/src/functions/create-installation-request.ts b/packages-exp/installations-exp/src/functions/create-installation-request.ts index 92b97b778dd..fe8242613f6 100644 --- a/packages-exp/installations-exp/src/functions/create-installation-request.ts +++ b/packages-exp/installations-exp/src/functions/create-installation-request.ts @@ -16,7 +16,6 @@ */ import { CreateInstallationResponse } from '../interfaces/api-response'; -import { AppConfig } from '@firebase/installations-types-exp'; import { InProgressInstallationEntry, RegisteredInstallationEntry, @@ -30,6 +29,7 @@ import { getInstallationsEndpoint, retryIfServerError } from './common'; +import { AppConfig } from '../interfaces/installation-impl'; export async function createInstallationRequest( appConfig: AppConfig, diff --git a/packages-exp/installations-exp/src/functions/delete-installation-request.test.ts b/packages-exp/installations-exp/src/functions/delete-installation-request.test.ts index 4841964abbd..68c069e6bf4 100644 --- a/packages-exp/installations-exp/src/functions/delete-installation-request.test.ts +++ b/packages-exp/installations-exp/src/functions/delete-installation-request.test.ts @@ -18,7 +18,7 @@ import { FirebaseError } from '@firebase/util'; import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; -import { AppConfig } from '@firebase/installations-types-exp'; +import { AppConfig } from '../interfaces/installation-impl'; import { RegisteredInstallationEntry, RequestStatus diff --git a/packages-exp/installations-exp/src/functions/delete-installation-request.ts b/packages-exp/installations-exp/src/functions/delete-installation-request.ts index 8179cbd0bb1..0cab8595ee0 100644 --- a/packages-exp/installations-exp/src/functions/delete-installation-request.ts +++ b/packages-exp/installations-exp/src/functions/delete-installation-request.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { AppConfig } from '@firebase/installations-types-exp'; +import { AppConfig } from '../interfaces/installation-impl'; import { RegisteredInstallationEntry } from '../interfaces/installation-entry'; import { getErrorFromResponse, diff --git a/packages-exp/installations-exp/src/functions/generate-auth-token-request.test.ts b/packages-exp/installations-exp/src/functions/generate-auth-token-request.test.ts index d75b68e2610..cf1d4231ef4 100644 --- a/packages-exp/installations-exp/src/functions/generate-auth-token-request.test.ts +++ b/packages-exp/installations-exp/src/functions/generate-auth-token-request.test.ts @@ -19,7 +19,6 @@ import { FirebaseError } from '@firebase/util'; import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; import { GenerateAuthTokenResponse } from '../interfaces/api-response'; -import { FirebaseInstallations } from '@firebase/installations-types-exp'; import { CompletedAuthToken, RegisteredInstallationEntry, @@ -35,11 +34,12 @@ import { } from '../util/constants'; import { ErrorResponse } from './common'; import { generateAuthTokenRequest } from './generate-auth-token-request'; +import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; const FID = 'evil-has-no-boundaries'; describe('generateAuthTokenRequest', () => { - let installations: FirebaseInstallations; + let installations: FirebaseInstallationsImpl; let fetchSpy: SinonStub<[RequestInfo, RequestInit?], Promise>; let registeredInstallationEntry: RegisteredInstallationEntry; let response: GenerateAuthTokenResponse; diff --git a/packages-exp/installations-exp/src/functions/generate-auth-token-request.ts b/packages-exp/installations-exp/src/functions/generate-auth-token-request.ts index 1be0ee1b780..d662745d300 100644 --- a/packages-exp/installations-exp/src/functions/generate-auth-token-request.ts +++ b/packages-exp/installations-exp/src/functions/generate-auth-token-request.ts @@ -16,10 +16,6 @@ */ import { GenerateAuthTokenResponse } from '../interfaces/api-response'; -import { - AppConfig, - FirebaseInstallations -} from '@firebase/installations-types-exp'; import { CompletedAuthToken, RegisteredInstallationEntry @@ -32,9 +28,13 @@ import { getInstallationsEndpoint, retryIfServerError } from './common'; +import { + FirebaseInstallationsImpl, + AppConfig +} from '../interfaces/installation-impl'; export async function generateAuthTokenRequest( - { appConfig, platformLoggerProvider }: FirebaseInstallations, + { appConfig, platformLoggerProvider }: FirebaseInstallationsImpl, installationEntry: RegisteredInstallationEntry ): Promise { const endpoint = getGenerateAuthTokenEndpoint(appConfig, installationEntry); diff --git a/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts b/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts index c11b035e6f2..06a7ec2dcb6 100644 --- a/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts +++ b/packages-exp/installations-exp/src/helpers/extract-app-config.test.ts @@ -17,7 +17,7 @@ import { FirebaseError } from '@firebase/util'; import { expect } from 'chai'; -import { AppConfig } from '@firebase/installations-types-exp'; +import { AppConfig } from '../interfaces/installation-impl'; import { getFakeApp } from '../testing/fake-generators'; import '../testing/setup'; import { extractAppConfig } from './extract-app-config'; diff --git a/packages-exp/installations-exp/src/helpers/extract-app-config.ts b/packages-exp/installations-exp/src/helpers/extract-app-config.ts index 321d13dbd0c..e1390d4ab83 100644 --- a/packages-exp/installations-exp/src/helpers/extract-app-config.ts +++ b/packages-exp/installations-exp/src/helpers/extract-app-config.ts @@ -17,7 +17,7 @@ import { FirebaseApp, FirebaseOptions } from '@firebase/app-types-exp'; import { FirebaseError } from '@firebase/util'; -import { AppConfig } from '@firebase/installations-types-exp'; +import { AppConfig } from '../interfaces/installation-impl'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; export function extractAppConfig(app: FirebaseApp): AppConfig { diff --git a/packages-exp/installations-exp/src/helpers/fid-changed.test.ts b/packages-exp/installations-exp/src/helpers/fid-changed.test.ts index b4990a3a8da..458bc1d097d 100644 --- a/packages-exp/installations-exp/src/helpers/fid-changed.test.ts +++ b/packages-exp/installations-exp/src/helpers/fid-changed.test.ts @@ -18,7 +18,7 @@ import { expect } from 'chai'; import { stub } from 'sinon'; import '../testing/setup'; -import { AppConfig } from '@firebase/installations-types-exp'; +import { AppConfig } from '../interfaces/installation-impl'; import { fidChanged, addCallback, diff --git a/packages-exp/installations-exp/src/helpers/fid-changed.ts b/packages-exp/installations-exp/src/helpers/fid-changed.ts index 6e9819cb52b..63f04d75cea 100644 --- a/packages-exp/installations-exp/src/helpers/fid-changed.ts +++ b/packages-exp/installations-exp/src/helpers/fid-changed.ts @@ -16,7 +16,7 @@ */ import { getKey } from '../util/get-key'; -import { AppConfig } from '@firebase/installations-types-exp'; +import { AppConfig } from '../interfaces/installation-impl'; import { IdChangeCallbackFn } from '../api'; const fidChangeCallbacks: Map> = new Map(); diff --git a/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts b/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts index 9be9a34c8f6..b19ec6ea037 100644 --- a/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts +++ b/packages-exp/installations-exp/src/helpers/get-installation-entry.test.ts @@ -18,7 +18,7 @@ import { AssertionError, expect } from 'chai'; import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; import * as createInstallationRequestModule from '../functions/create-installation-request'; -import { AppConfig } from '@firebase/installations-types-exp'; +import { AppConfig } from '../interfaces/installation-impl'; import { InProgressInstallationEntry, RegisteredInstallationEntry, diff --git a/packages-exp/installations-exp/src/helpers/get-installation-entry.ts b/packages-exp/installations-exp/src/helpers/get-installation-entry.ts index 3ae6be67499..a0e1cc425ce 100644 --- a/packages-exp/installations-exp/src/helpers/get-installation-entry.ts +++ b/packages-exp/installations-exp/src/helpers/get-installation-entry.ts @@ -16,7 +16,7 @@ */ import { createInstallationRequest } from '../functions/create-installation-request'; -import { AppConfig } from '@firebase/installations-types-exp'; +import { AppConfig } from '../interfaces/installation-impl'; import { InProgressInstallationEntry, InstallationEntry, diff --git a/packages-exp/installations-exp/src/helpers/idb-manager.test.ts b/packages-exp/installations-exp/src/helpers/idb-manager.test.ts index 44d1a7029d0..db7eaca58f9 100644 --- a/packages-exp/installations-exp/src/helpers/idb-manager.test.ts +++ b/packages-exp/installations-exp/src/helpers/idb-manager.test.ts @@ -17,7 +17,7 @@ import { expect } from 'chai'; import { stub } from 'sinon'; -import { AppConfig } from '@firebase/installations-types-exp'; +import { AppConfig } from '../interfaces/installation-impl'; import { InstallationEntry, RequestStatus diff --git a/packages-exp/installations-exp/src/helpers/idb-manager.ts b/packages-exp/installations-exp/src/helpers/idb-manager.ts index b916b236275..bc30563fa06 100644 --- a/packages-exp/installations-exp/src/helpers/idb-manager.ts +++ b/packages-exp/installations-exp/src/helpers/idb-manager.ts @@ -16,7 +16,7 @@ */ import { DB, openDb } from 'idb'; -import { AppConfig } from '@firebase/installations-types-exp'; +import { AppConfig } from '../interfaces/installation-impl'; import { InstallationEntry } from '../interfaces/installation-entry'; import { getKey } from '../util/get-key'; import { fidChanged } from './fid-changed'; diff --git a/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts b/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts index 8b4ee46415a..3bdd859a6b0 100644 --- a/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts +++ b/packages-exp/installations-exp/src/helpers/refresh-auth-token.test.ts @@ -18,7 +18,6 @@ import { expect } from 'chai'; import { SinonFakeTimers, SinonStub, stub, useFakeTimers } from 'sinon'; import * as generateAuthTokenRequestModule from '../functions/generate-auth-token-request'; -import { FirebaseInstallations } from '@firebase/installations-types-exp'; import { CompletedAuthToken, RegisteredInstallationEntry, @@ -31,6 +30,7 @@ import { TOKEN_EXPIRATION_BUFFER } from '../util/constants'; import { sleep } from '../util/sleep'; import { get, set } from './idb-manager'; import { refreshAuthToken } from './refresh-auth-token'; +import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; const FID = 'carry-the-blessed-home'; const AUTH_TOKEN = 'authTokenFromServer'; @@ -38,9 +38,9 @@ const DB_AUTH_TOKEN = 'authTokenFromDB'; const ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1000; describe('refreshAuthToken', () => { - let installations: FirebaseInstallations; + let installations: FirebaseInstallationsImpl; let generateAuthTokenRequestSpy: SinonStub< - [FirebaseInstallations, RegisteredInstallationEntry], + [FirebaseInstallationsImpl, RegisteredInstallationEntry], Promise >; diff --git a/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts b/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts index 4decb78b163..1ad5dc5da50 100644 --- a/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts +++ b/packages-exp/installations-exp/src/helpers/refresh-auth-token.ts @@ -18,8 +18,8 @@ import { generateAuthTokenRequest } from '../functions/generate-auth-token-request'; import { AppConfig, - FirebaseInstallations -} from '@firebase/installations-types-exp'; + FirebaseInstallationsImpl +} from '../interfaces/installation-impl'; import { AuthToken, CompletedAuthToken, @@ -40,7 +40,7 @@ import { remove, set, update } from './idb-manager'; * Should only be called if the Firebase Installation is registered. */ export async function refreshAuthToken( - installations: FirebaseInstallations, + installations: FirebaseInstallationsImpl, forceRefresh = false ): Promise { let tokenPromise: Promise | undefined; @@ -82,7 +82,7 @@ export async function refreshAuthToken( * tries once in this thread as well. */ async function waitUntilAuthTokenRequest( - installations: FirebaseInstallations, + installations: FirebaseInstallationsImpl, forceRefresh: boolean ): Promise { // Unfortunately, there is no way of reliably observing when a value in @@ -135,7 +135,7 @@ function updateAuthTokenRequest( } async function fetchAuthTokenFromServer( - installations: FirebaseInstallations, + installations: FirebaseInstallationsImpl, installationEntry: RegisteredInstallationEntry ): Promise { try { diff --git a/packages-exp/installations-exp/src/interfaces/installation-impl.ts b/packages-exp/installations-exp/src/interfaces/installation-impl.ts new file mode 100644 index 00000000000..280e51a7478 --- /dev/null +++ b/packages-exp/installations-exp/src/interfaces/installation-impl.ts @@ -0,0 +1,34 @@ +/** + * @license + * Copyright 2020 Google LLC + * + * 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 { Provider } from '@firebase/component'; +import { _FirebaseService } from '@firebase/app-types-exp'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; + +export interface FirebaseInstallationsImpl + extends FirebaseInstallations, + _FirebaseService { + readonly appConfig: AppConfig; + readonly platformLoggerProvider: Provider<'platform-logger'>; +} + +export interface AppConfig { + readonly appName: string; + readonly projectId: string; + readonly apiKey: string; + readonly appId: string; +} diff --git a/packages-exp/installations-exp/src/interfaces/installation-internal.ts b/packages-exp/installations-exp/src/interfaces/installation-internal.ts deleted file mode 100644 index fd910fbb4d1..00000000000 --- a/packages-exp/installations-exp/src/interfaces/installation-internal.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * @license - * Copyright 2020 Google LLC - * - * 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 { _FirebaseService, FirebaseApp } from '@firebase/app-types-exp'; - -/** - * An interface for Firebase internal SDKs use only. - */ -export interface FirebaseInstallationsInternal extends _FirebaseService { - /** - * FirebaseApp instance which carries Firebase app configurations. - */ - app: FirebaseApp; - - /** - * Creates a Firebase Installation if there isn't one for the app and - * returns the Installation ID. - */ - getId(): Promise; - - /** - * Returns an Authentication Token for the current Firebase Installation. - */ - getToken(forceRefresh?: boolean): Promise; - - /** - * Deletes the Firebase Installation and all associated data. - */ - _delete(): Promise; - - /** - * Sets a new callback that will get called when Installlation ID changes. - * Returns an unsubscribe function that will remove the callback when called. - */ - onIdChange(callback: (installationId: string) => void): () => void; -} diff --git a/packages-exp/installations-exp/src/testing/fake-generators.ts b/packages-exp/installations-exp/src/testing/fake-generators.ts index 61f092e75e3..5d8a59a6fa6 100644 --- a/packages-exp/installations-exp/src/testing/fake-generators.ts +++ b/packages-exp/installations-exp/src/testing/fake-generators.ts @@ -23,9 +23,9 @@ import { } from '@firebase/component'; import { extractAppConfig } from '../helpers/extract-app-config'; import { - FirebaseInstallations, + FirebaseInstallationsImpl, AppConfig -} from '@firebase/installations-types-exp'; +} from '../interfaces/installation-impl'; export function getFakeApp(): FirebaseApp { return { @@ -49,7 +49,7 @@ export function getFakeAppConfig( return { ...extractAppConfig(getFakeApp()), ...customValues }; } -export function getFakeInstallations(): FirebaseInstallations { +export function getFakeInstallations(): FirebaseInstallationsImpl { const container = new ComponentContainer('test'); container.addComponent( new Component( @@ -60,7 +60,11 @@ export function getFakeInstallations(): FirebaseInstallations { ); return { + app: getFakeApp(), appConfig: getFakeAppConfig(), - platformLoggerProvider: container.getProvider('platform-logger') + platformLoggerProvider: container.getProvider('platform-logger'), + _delete: () => { + return Promise.resolve(); + } }; } diff --git a/packages-exp/installations-exp/src/util/get-key.ts b/packages-exp/installations-exp/src/util/get-key.ts index baed6850952..272d342d366 100644 --- a/packages-exp/installations-exp/src/util/get-key.ts +++ b/packages-exp/installations-exp/src/util/get-key.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { AppConfig } from '@firebase/installations-types-exp'; +import { AppConfig } from '../interfaces/installation-impl'; /** Returns a string key that can be used to identify the app. */ export function getKey(appConfig: AppConfig): string { diff --git a/packages-exp/installations-types-exp/index.d.ts b/packages-exp/installations-types-exp/index.d.ts index 3351d657276..37cb716b7f3 100644 --- a/packages-exp/installations-types-exp/index.d.ts +++ b/packages-exp/installations-types-exp/index.d.ts @@ -15,43 +15,29 @@ * limitations under the License. */ -import { Provider } from '@firebase/component'; -import { FirebaseInstallationsInternal } from '../installations-exp/src/interfaces/installation-internal'; +import { FirebaseInstallationsImpl } from '../installations-exp/src/interfaces/installation-impl'; -export interface FirebaseInstallations { - /** - * Firebase APP configurations - */ - readonly appConfig: AppConfig; - /** - * Firebase platform logging util. - */ - readonly platformLoggerProvider: Provider<'platform-logger'>; -} +export interface FirebaseInstallations {} -export interface AppConfig { - /** - * Firebase APP name. - */ - readonly appName: string; - /** - * Firebase project ID. - */ - readonly projectId: string; +/** + * An interface for Firebase internal SDKs use only. + */ +export interface FirebaseInstallationsInternal { /** - * Firebase API key. + * Creates a Firebase Installation if there isn't one for the app and + * returns the Installation ID. */ - readonly apiKey: string; + getId(): Promise; + /** - * Firebase APP ID. + * Returns an Authentication Token for the current Firebase Installation. */ - readonly appId: string; + getToken(forceRefresh?: boolean): Promise; } -export type FirebaseInstallationsName = 'installations-exp'; - declare module '@firebase/component' { interface NameServiceMapping { - 'installations-exp': FirebaseInstallationsInternal; + 'installations-exp': FirebaseInstallationsImpl; + 'installations-exp-internal': FirebaseInstallationsInternal; } } From 1df33cc824ad2706c4260c3a87265d569eaa1901 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Tue, 1 Sep 2020 17:06:15 -0700 Subject: [PATCH 18/22] Change public methods to accept public interface --- .../src/api/delete-installations.ts | 5 +++-- .../installations-exp/src/api/get-id.ts | 8 ++++--- .../installations-exp/src/api/get-token.ts | 8 ++++--- .../installations-exp/src/api/on-id-change.ts | 6 ++++-- .../installations-exp/src/functions/config.ts | 21 ++++++++++--------- .../installations-types-exp/index.d.ts | 2 +- 6 files changed, 29 insertions(+), 21 deletions(-) diff --git a/packages-exp/installations-exp/src/api/delete-installations.ts b/packages-exp/installations-exp/src/api/delete-installations.ts index b075edb8110..32e689028aa 100644 --- a/packages-exp/installations-exp/src/api/delete-installations.ts +++ b/packages-exp/installations-exp/src/api/delete-installations.ts @@ -20,11 +20,12 @@ import { remove, update } from '../helpers/idb-manager'; import { RequestStatus } from '../interfaces/installation-entry'; import { ERROR_FACTORY, ErrorCode } from '../util/errors'; import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; export async function deleteInstallations( - installations: FirebaseInstallationsImpl + installations: FirebaseInstallations ): Promise { - const { appConfig } = installations; + const { appConfig } = installations as FirebaseInstallationsImpl; const entry = await update(appConfig, oldEntry => { if (oldEntry && oldEntry.registrationStatus === RequestStatus.NOT_STARTED) { diff --git a/packages-exp/installations-exp/src/api/get-id.ts b/packages-exp/installations-exp/src/api/get-id.ts index 52c899c840a..dffb35cb26d 100644 --- a/packages-exp/installations-exp/src/api/get-id.ts +++ b/packages-exp/installations-exp/src/api/get-id.ts @@ -18,12 +18,14 @@ import { getInstallationEntry } from '../helpers/get-installation-entry'; import { refreshAuthToken } from '../helpers/refresh-auth-token'; import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; export async function getId( - installations: FirebaseInstallationsImpl + installations: FirebaseInstallations ): Promise { + const installationsImpl = installations as FirebaseInstallationsImpl; const { installationEntry, registrationPromise } = await getInstallationEntry( - installations.appConfig + installationsImpl.appConfig ); if (registrationPromise) { @@ -31,7 +33,7 @@ export async function getId( } else { // If the installation is already registered, update the authentication // token if needed. - refreshAuthToken(installations).catch(console.error); + refreshAuthToken(installationsImpl).catch(console.error); } return installationEntry.fid; diff --git a/packages-exp/installations-exp/src/api/get-token.ts b/packages-exp/installations-exp/src/api/get-token.ts index 7fd14603a98..9014625d9a5 100644 --- a/packages-exp/installations-exp/src/api/get-token.ts +++ b/packages-exp/installations-exp/src/api/get-token.ts @@ -21,16 +21,18 @@ import { FirebaseInstallationsImpl, AppConfig } from '../interfaces/installation-impl'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; export async function getToken( - installations: FirebaseInstallationsImpl, + installations: FirebaseInstallations, forceRefresh = false ): Promise { - await completeInstallationRegistration(installations.appConfig); + const installationsImpl = installations as FirebaseInstallationsImpl; + await completeInstallationRegistration(installationsImpl.appConfig); // At this point we either have a Registered Installation in the DB, or we've // already thrown an error. - const authToken = await refreshAuthToken(installations, forceRefresh); + const authToken = await refreshAuthToken(installationsImpl, forceRefresh); return authToken.token; } diff --git a/packages-exp/installations-exp/src/api/on-id-change.ts b/packages-exp/installations-exp/src/api/on-id-change.ts index a088a7ae71a..514a0e786fd 100644 --- a/packages-exp/installations-exp/src/api/on-id-change.ts +++ b/packages-exp/installations-exp/src/api/on-id-change.ts @@ -17,6 +17,7 @@ import { addCallback, removeCallback } from '../helpers/fid-changed'; import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; export type IdChangeCallbackFn = (installationId: string) => void; export type IdChangeUnsubscribeFn = () => void; @@ -26,11 +27,12 @@ export type IdChangeUnsubscribeFn = () => void; * Returns an unsubscribe function that will remove the callback when called. */ export function onIdChange( - { appConfig }: FirebaseInstallationsImpl, + installations: FirebaseInstallations, callback: IdChangeCallbackFn ): IdChangeUnsubscribeFn { - addCallback(appConfig, callback); + const { appConfig } = installations as FirebaseInstallationsImpl; + addCallback(appConfig, callback); return () => { removeCallback(appConfig, callback); }; diff --git a/packages-exp/installations-exp/src/functions/config.ts b/packages-exp/installations-exp/src/functions/config.ts index 1a1e0c55814..eab85a27e59 100644 --- a/packages-exp/installations-exp/src/functions/config.ts +++ b/packages-exp/installations-exp/src/functions/config.ts @@ -23,12 +23,13 @@ import { InstanceFactory, ComponentContainer } from '@firebase/component'; -import { deleteInstallations, getId, getToken } from '../api/index'; +import { getId, getToken } from '../api/index'; import { FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; import { extractAppConfig } from '../helpers/extract-app-config'; const INSTALLATIONS_NAME = 'installations-exp'; +const INSTALLATIONS_NAME_INTERNAL = 'installations-exp-internal'; const publicFactory: InstanceFactory<'installations-exp'> = ( container: ComponentContainer @@ -42,7 +43,7 @@ const publicFactory: InstanceFactory<'installations-exp'> = ( app, appConfig, platformLoggerProvider, - _delete: () => deleteInstallations(installationsImpl) + _delete: () => Promise.resolve() }; return installationsImpl; }; @@ -52,15 +53,11 @@ const internalFactory: InstanceFactory<'installations-exp-internal'> = ( ) => { const app = container.getProvider('app-exp').getImmediate(); // Internal FIS instance relies on public FIS instance. - const installationsImpl = _getProvider( - app, - INSTALLATIONS_NAME - ).getImmediate(); + const installations = _getProvider(app, INSTALLATIONS_NAME).getImmediate(); const installationsInternal: FirebaseInstallationsInternal = { - getId: () => getId(installationsImpl), - getToken: (forceRefresh?: boolean) => - getToken(installationsImpl, forceRefresh) + getId: () => getId(installations), + getToken: (forceRefresh?: boolean) => getToken(installations, forceRefresh) }; return installationsInternal; }; @@ -70,6 +67,10 @@ export function registerInstallations(): void { new Component(INSTALLATIONS_NAME, publicFactory, ComponentType.PUBLIC) ); _registerComponent( - new Component(INSTALLATIONS_NAME, internalFactory, ComponentType.PRIVATE) + new Component( + INSTALLATIONS_NAME_INTERNAL, + internalFactory, + ComponentType.PRIVATE + ) ); } diff --git a/packages-exp/installations-types-exp/index.d.ts b/packages-exp/installations-types-exp/index.d.ts index 37cb716b7f3..50b4cd4072e 100644 --- a/packages-exp/installations-types-exp/index.d.ts +++ b/packages-exp/installations-types-exp/index.d.ts @@ -37,7 +37,7 @@ export interface FirebaseInstallationsInternal { declare module '@firebase/component' { interface NameServiceMapping { - 'installations-exp': FirebaseInstallationsImpl; + 'installations-exp': FirebaseInstallations; 'installations-exp-internal': FirebaseInstallationsInternal; } } From 5876ea8f0709ce73611279d5979f6c356183d128 Mon Sep 17 00:00:00 2001 From: Feiyang Date: Wed, 2 Sep 2020 16:52:28 -0700 Subject: [PATCH 19/22] Fixes and api extractor integration for @firebase/installations-exp (#3733) * integrate api-extractor into installations-exp * add release tag to APIs * add _ prefix to the internal interface * update api report --- common/api-review/installations-exp.api.md | 34 +++++++++++++++++++ .../api-review/installations-types-exp.api.md | 20 +++++++++++ .../installations-exp/api-extractor.json | 8 +++++ packages-exp/installations-exp/package.json | 15 +++++--- .../src/api/delete-installations.ts | 3 ++ .../installations-exp/src/api/get-id.ts | 3 ++ .../src/api/get-installations.ts | 3 ++ .../installations-exp/src/api/get-token.ts | 3 ++ .../installations-exp/src/api/on-id-change.ts | 8 +++++ .../installations-exp/src/functions/config.ts | 4 +-- .../installations-types-exp/index.d.ts | 11 +++--- .../installations-types-exp/package.json | 6 +++- scripts/exp/remove-exp.js | 2 +- 13 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 common/api-review/installations-exp.api.md create mode 100644 common/api-review/installations-types-exp.api.md create mode 100644 packages-exp/installations-exp/api-extractor.json diff --git a/common/api-review/installations-exp.api.md b/common/api-review/installations-exp.api.md new file mode 100644 index 00000000000..9e3db393ef6 --- /dev/null +++ b/common/api-review/installations-exp.api.md @@ -0,0 +1,34 @@ +## API Report File for "@firebase/installations-exp" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { FirebaseApp } from '@firebase/app-types-exp'; +import { FirebaseInstallations } from '@firebase/installations-types-exp'; + +// @public (undocumented) +export function deleteInstallations(installations: FirebaseInstallations): Promise; + +// @public (undocumented) +export function getId(installations: FirebaseInstallations): Promise; + +// @public (undocumented) +export function getInstallations(app: FirebaseApp): FirebaseInstallations; + +// @public (undocumented) +export function getToken(installations: FirebaseInstallations, forceRefresh?: boolean): Promise; + +// @public (undocumented) +export type IdChangeCallbackFn = (installationId: string) => void; + +// @public (undocumented) +export type IdChangeUnsubscribeFn = () => void; + +// @public +export function onIdChange(installations: FirebaseInstallations, callback: IdChangeCallbackFn): IdChangeUnsubscribeFn; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/common/api-review/installations-types-exp.api.md b/common/api-review/installations-types-exp.api.md new file mode 100644 index 00000000000..f7925dd7a9f --- /dev/null +++ b/common/api-review/installations-types-exp.api.md @@ -0,0 +1,20 @@ +## API Report File for "@firebase/installations-types-exp" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public (undocumented) +export interface FirebaseInstallations {} + +// @internal +export interface _FirebaseInstallationsInternal { + getId(): Promise; + + getToken(forceRefresh?: boolean): Promise; +} + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/packages-exp/installations-exp/api-extractor.json b/packages-exp/installations-exp/api-extractor.json new file mode 100644 index 00000000000..f291311f711 --- /dev/null +++ b/packages-exp/installations-exp/api-extractor.json @@ -0,0 +1,8 @@ +{ + "extends": "../../config/api-extractor.json", + // Point it to your entry point d.ts file. + "mainEntryPointFilePath": "/dist/src/index.d.ts", + "dtsRollup": { + "enabled": true + } +} \ No newline at end of file diff --git a/packages-exp/installations-exp/package.json b/packages-exp/installations-exp/package.json index 1f152b35056..f715dd022b3 100644 --- a/packages-exp/installations-exp/package.json +++ b/packages-exp/installations-exp/package.json @@ -5,8 +5,9 @@ "author": "Firebase (https://firebase.google.com/)", "main": "dist/index.cjs.js", "module": "dist/index.esm.js", + "browser": "dist/index.esm.js", "esm2017": "dist/index.esm2017.js", - "types": "dist/src/index.d.ts", + "typings": "dist/installations-exp.d.ts", "license": "Apache-2.0", "files": [ "dist" @@ -14,7 +15,7 @@ "scripts": { "lint": "eslint -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", "lint:fix": "eslint --fix -c .eslintrc.js '**/*.ts' --ignore-path '../../.gitignore'", - "build": "rollup -c", + "build": "rollup -c && yarn api-report", "build:deps": "lerna run --scope @firebase/installations-exp --include-dependencies build", "dev": "rollup -c -w", "test": "yarn type-check && yarn test:karma && yarn lint", @@ -25,7 +26,11 @@ "serve": "yarn serve:build && yarn serve:host", "serve:build": "rollup -c test-app/rollup.config.js", "serve:host": "http-server -c-1 test-app", - "prepare": "yarn build" + "prepare": "yarn build", + "api-report": "api-extractor run --local --verbose", + "predoc": "node ../../scripts/exp/remove-exp.js temp", + "doc": "api-documenter markdown --input temp --output docs", + "build:doc": "yarn build && yarn doc" }, "repository": { "directory": "packages-exp/installations-exp", @@ -37,11 +42,11 @@ }, "devDependencies": { "@firebase/app-exp": "0.0.800", - "rollup": "2.26.5", + "rollup": "2.26.7", "rollup-plugin-commonjs": "10.1.0", "rollup-plugin-json": "4.0.0", "rollup-plugin-node-resolve": "5.2.0", - "rollup-plugin-typescript2": "0.27.1", + "rollup-plugin-typescript2": "0.27.2", "rollup-plugin-uglify": "6.0.4", "typescript": "4.0.2" }, diff --git a/packages-exp/installations-exp/src/api/delete-installations.ts b/packages-exp/installations-exp/src/api/delete-installations.ts index 32e689028aa..adaf51c3dde 100644 --- a/packages-exp/installations-exp/src/api/delete-installations.ts +++ b/packages-exp/installations-exp/src/api/delete-installations.ts @@ -22,6 +22,9 @@ import { ERROR_FACTORY, ErrorCode } from '../util/errors'; import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; import { FirebaseInstallations } from '@firebase/installations-types-exp'; +/** + * @public + */ export async function deleteInstallations( installations: FirebaseInstallations ): Promise { diff --git a/packages-exp/installations-exp/src/api/get-id.ts b/packages-exp/installations-exp/src/api/get-id.ts index dffb35cb26d..b8ae95090e0 100644 --- a/packages-exp/installations-exp/src/api/get-id.ts +++ b/packages-exp/installations-exp/src/api/get-id.ts @@ -20,6 +20,9 @@ import { refreshAuthToken } from '../helpers/refresh-auth-token'; import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; import { FirebaseInstallations } from '@firebase/installations-types-exp'; +/** + * @public + */ export async function getId( installations: FirebaseInstallations ): Promise { diff --git a/packages-exp/installations-exp/src/api/get-installations.ts b/packages-exp/installations-exp/src/api/get-installations.ts index 1dcd2f1a3e4..b6d0839c36c 100644 --- a/packages-exp/installations-exp/src/api/get-installations.ts +++ b/packages-exp/installations-exp/src/api/get-installations.ts @@ -19,6 +19,9 @@ import { FirebaseApp } from '@firebase/app-types-exp'; import { FirebaseInstallations } from '@firebase/installations-types-exp'; import { _getProvider } from '@firebase/app-exp'; +/** + * @public + */ export function getInstallations(app: FirebaseApp): FirebaseInstallations { const installationsImpl = _getProvider( app, diff --git a/packages-exp/installations-exp/src/api/get-token.ts b/packages-exp/installations-exp/src/api/get-token.ts index 9014625d9a5..622193759ea 100644 --- a/packages-exp/installations-exp/src/api/get-token.ts +++ b/packages-exp/installations-exp/src/api/get-token.ts @@ -23,6 +23,9 @@ import { } from '../interfaces/installation-impl'; import { FirebaseInstallations } from '@firebase/installations-types-exp'; +/** + * @public + */ export async function getToken( installations: FirebaseInstallations, forceRefresh = false diff --git a/packages-exp/installations-exp/src/api/on-id-change.ts b/packages-exp/installations-exp/src/api/on-id-change.ts index 514a0e786fd..b3133e21eca 100644 --- a/packages-exp/installations-exp/src/api/on-id-change.ts +++ b/packages-exp/installations-exp/src/api/on-id-change.ts @@ -19,12 +19,20 @@ import { addCallback, removeCallback } from '../helpers/fid-changed'; import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; import { FirebaseInstallations } from '@firebase/installations-types-exp'; +/** + * @public + */ export type IdChangeCallbackFn = (installationId: string) => void; +/** + * @public + */ export type IdChangeUnsubscribeFn = () => void; /** * Sets a new callback that will get called when Installation ID changes. * Returns an unsubscribe function that will remove the callback when called. + * + * @public */ export function onIdChange( installations: FirebaseInstallations, diff --git a/packages-exp/installations-exp/src/functions/config.ts b/packages-exp/installations-exp/src/functions/config.ts index eab85a27e59..9a0544b4ba6 100644 --- a/packages-exp/installations-exp/src/functions/config.ts +++ b/packages-exp/installations-exp/src/functions/config.ts @@ -24,7 +24,7 @@ import { ComponentContainer } from '@firebase/component'; import { getId, getToken } from '../api/index'; -import { FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; +import { _FirebaseInstallationsInternal } from '@firebase/installations-types-exp'; import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; import { extractAppConfig } from '../helpers/extract-app-config'; @@ -55,7 +55,7 @@ const internalFactory: InstanceFactory<'installations-exp-internal'> = ( // Internal FIS instance relies on public FIS instance. const installations = _getProvider(app, INSTALLATIONS_NAME).getImmediate(); - const installationsInternal: FirebaseInstallationsInternal = { + const installationsInternal: _FirebaseInstallationsInternal = { getId: () => getId(installations), getToken: (forceRefresh?: boolean) => getToken(installations, forceRefresh) }; diff --git a/packages-exp/installations-types-exp/index.d.ts b/packages-exp/installations-types-exp/index.d.ts index 50b4cd4072e..fc784509307 100644 --- a/packages-exp/installations-types-exp/index.d.ts +++ b/packages-exp/installations-types-exp/index.d.ts @@ -15,14 +15,17 @@ * limitations under the License. */ -import { FirebaseInstallationsImpl } from '../installations-exp/src/interfaces/installation-impl'; - +/** + * @public + */ export interface FirebaseInstallations {} /** * An interface for Firebase internal SDKs use only. + * + * @internal */ -export interface FirebaseInstallationsInternal { +export interface _FirebaseInstallationsInternal { /** * Creates a Firebase Installation if there isn't one for the app and * returns the Installation ID. @@ -38,6 +41,6 @@ export interface FirebaseInstallationsInternal { declare module '@firebase/component' { interface NameServiceMapping { 'installations-exp': FirebaseInstallations; - 'installations-exp-internal': FirebaseInstallationsInternal; + 'installations-exp-internal': _FirebaseInstallationsInternal; } } diff --git a/packages-exp/installations-types-exp/package.json b/packages-exp/installations-types-exp/package.json index 950db08ce96..95d69e2c00a 100644 --- a/packages-exp/installations-types-exp/package.json +++ b/packages-exp/installations-types-exp/package.json @@ -7,7 +7,11 @@ "license": "Apache-2.0", "scripts": { "test": "tsc", - "test:ci": "node ../../scripts/run_tests_in_ci.js" + "test:ci": "node ../../scripts/run_tests_in_ci.js", + "api-report": "api-extractor run --local --verbose", + "predoc": "node ../../scripts/exp/remove-exp.js temp", + "doc": "api-documenter markdown --input temp --output docs", + "build:doc": "yarn api-report && yarn doc" }, "files": [ "index.d.ts" diff --git a/scripts/exp/remove-exp.js b/scripts/exp/remove-exp.js index 33937805906..cadcd5ab51b 100644 --- a/scripts/exp/remove-exp.js +++ b/scripts/exp/remove-exp.js @@ -29,7 +29,7 @@ if (argv._[0]) { if (statSync(dirOrFile).isFile()) { removeExpSuffixFromFile(dirOrFile); } else { - removeExpSuffix(dir); + removeExpSuffix(dirOrFile); } } From 5682334781d5dde167b37f19c8655f1f5cb96f0a Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Tue, 8 Sep 2020 15:24:57 -0700 Subject: [PATCH 20/22] add documentations to public apis --- common/api-review/installations-exp.api.md | 12 ++++++------ .../src/api/delete-installations.ts | 2 ++ packages-exp/installations-exp/src/api/get-id.ts | 3 +++ .../installations-exp/src/api/get-installations.ts | 2 ++ packages-exp/installations-exp/src/api/get-token.ts | 3 +++ .../installations-exp/src/api/on-id-change.ts | 4 ++++ packages-exp/installations-types-exp/index.d.ts | 2 ++ 7 files changed, 22 insertions(+), 6 deletions(-) diff --git a/common/api-review/installations-exp.api.md b/common/api-review/installations-exp.api.md index 9e3db393ef6..4759f5dceaf 100644 --- a/common/api-review/installations-exp.api.md +++ b/common/api-review/installations-exp.api.md @@ -7,22 +7,22 @@ import { FirebaseApp } from '@firebase/app-types-exp'; import { FirebaseInstallations } from '@firebase/installations-types-exp'; -// @public (undocumented) +// @public export function deleteInstallations(installations: FirebaseInstallations): Promise; -// @public (undocumented) +// @public export function getId(installations: FirebaseInstallations): Promise; -// @public (undocumented) +// @public export function getInstallations(app: FirebaseApp): FirebaseInstallations; -// @public (undocumented) +// @public export function getToken(installations: FirebaseInstallations, forceRefresh?: boolean): Promise; -// @public (undocumented) +// @public export type IdChangeCallbackFn = (installationId: string) => void; -// @public (undocumented) +// @public export type IdChangeUnsubscribeFn = () => void; // @public diff --git a/packages-exp/installations-exp/src/api/delete-installations.ts b/packages-exp/installations-exp/src/api/delete-installations.ts index adaf51c3dde..5e3d6b21123 100644 --- a/packages-exp/installations-exp/src/api/delete-installations.ts +++ b/packages-exp/installations-exp/src/api/delete-installations.ts @@ -23,6 +23,8 @@ import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; import { FirebaseInstallations } from '@firebase/installations-types-exp'; /** + * Deletes the Firebase Installation and all associated data. + * * @public */ export async function deleteInstallations( diff --git a/packages-exp/installations-exp/src/api/get-id.ts b/packages-exp/installations-exp/src/api/get-id.ts index b8ae95090e0..83a3871d78d 100644 --- a/packages-exp/installations-exp/src/api/get-id.ts +++ b/packages-exp/installations-exp/src/api/get-id.ts @@ -21,6 +21,9 @@ import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; import { FirebaseInstallations } from '@firebase/installations-types-exp'; /** + * Creates a Firebase Installation if there isn't one for the app and + * returns the Installation ID. + * * @public */ export async function getId( diff --git a/packages-exp/installations-exp/src/api/get-installations.ts b/packages-exp/installations-exp/src/api/get-installations.ts index b6d0839c36c..c1e124296e0 100644 --- a/packages-exp/installations-exp/src/api/get-installations.ts +++ b/packages-exp/installations-exp/src/api/get-installations.ts @@ -20,6 +20,8 @@ import { FirebaseInstallations } from '@firebase/installations-types-exp'; import { _getProvider } from '@firebase/app-exp'; /** + * Returns a Firebase Installation instance for the given app. + * * @public */ export function getInstallations(app: FirebaseApp): FirebaseInstallations { diff --git a/packages-exp/installations-exp/src/api/get-token.ts b/packages-exp/installations-exp/src/api/get-token.ts index 622193759ea..8e12ed2a0a8 100644 --- a/packages-exp/installations-exp/src/api/get-token.ts +++ b/packages-exp/installations-exp/src/api/get-token.ts @@ -24,6 +24,9 @@ import { import { FirebaseInstallations } from '@firebase/installations-types-exp'; /** + * + * Returns an authentication token for the current Firebase Installation. + * * @public */ export async function getToken( diff --git a/packages-exp/installations-exp/src/api/on-id-change.ts b/packages-exp/installations-exp/src/api/on-id-change.ts index b1ece84e1f0..4f3694c85ff 100644 --- a/packages-exp/installations-exp/src/api/on-id-change.ts +++ b/packages-exp/installations-exp/src/api/on-id-change.ts @@ -20,10 +20,14 @@ import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; import { FirebaseInstallations } from '@firebase/installations-types-exp'; /** + * An user defined callback function that takes action when Installations ID changes. + * * @public */ export type IdChangeCallbackFn = (installationId: string) => void; /** + * An unsubscribe function that will remove the onIdChange callback when called. + * * @public */ export type IdChangeUnsubscribeFn = () => void; diff --git a/packages-exp/installations-types-exp/index.d.ts b/packages-exp/installations-types-exp/index.d.ts index 8ce0bc18bef..fecca33e651 100644 --- a/packages-exp/installations-types-exp/index.d.ts +++ b/packages-exp/installations-types-exp/index.d.ts @@ -16,6 +16,8 @@ */ /** + * A Firebase Installation instance which is a required argument for all Firebase Installations operations. + * * @public */ export interface FirebaseInstallations {} From 60ab3eca878da2c802b27b590cb7dda2565ef07f Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Thu, 10 Sep 2020 16:18:49 -0700 Subject: [PATCH 21/22] refine public documentations --- packages-exp/installations-exp/src/api/get-installations.ts | 2 +- packages-exp/installations-exp/src/api/get-token.ts | 2 +- packages-exp/installations-exp/src/api/on-id-change.ts | 2 +- packages-exp/installations-types-exp/index.d.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages-exp/installations-exp/src/api/get-installations.ts b/packages-exp/installations-exp/src/api/get-installations.ts index c1e124296e0..d2032710ab8 100644 --- a/packages-exp/installations-exp/src/api/get-installations.ts +++ b/packages-exp/installations-exp/src/api/get-installations.ts @@ -20,7 +20,7 @@ import { FirebaseInstallations } from '@firebase/installations-types-exp'; import { _getProvider } from '@firebase/app-exp'; /** - * Returns a Firebase Installation instance for the given app. + * Returns an instance of FirebaseInstallations associated with the given FirebaseApp instance. * * @public */ diff --git a/packages-exp/installations-exp/src/api/get-token.ts b/packages-exp/installations-exp/src/api/get-token.ts index 82bbf729623..53cb89899dd 100644 --- a/packages-exp/installations-exp/src/api/get-token.ts +++ b/packages-exp/installations-exp/src/api/get-token.ts @@ -24,7 +24,7 @@ import { import { FirebaseInstallations } from '@firebase/installations-types-exp'; /** - * Returns an authentication token for the current Firebase Installation. + * Returns an Installation auth token, identifying the current Firebase Installation. * * @public */ diff --git a/packages-exp/installations-exp/src/api/on-id-change.ts b/packages-exp/installations-exp/src/api/on-id-change.ts index 4f3694c85ff..757c2e90ae0 100644 --- a/packages-exp/installations-exp/src/api/on-id-change.ts +++ b/packages-exp/installations-exp/src/api/on-id-change.ts @@ -20,7 +20,7 @@ import { FirebaseInstallationsImpl } from '../interfaces/installation-impl'; import { FirebaseInstallations } from '@firebase/installations-types-exp'; /** - * An user defined callback function that takes action when Installations ID changes. + * An user defined callback function that gets called when Installations ID changes. * * @public */ diff --git a/packages-exp/installations-types-exp/index.d.ts b/packages-exp/installations-types-exp/index.d.ts index fecca33e651..1803c45d422 100644 --- a/packages-exp/installations-types-exp/index.d.ts +++ b/packages-exp/installations-types-exp/index.d.ts @@ -16,7 +16,7 @@ */ /** - * A Firebase Installation instance which is a required argument for all Firebase Installations operations. + * Public interface of the FirebaseInstallations SDK. * * @public */ From d9beaa65a1213e63b8dc9f8457a7ea72424d0366 Mon Sep 17 00:00:00 2001 From: ChaoqunCHEN Date: Thu, 10 Sep 2020 16:21:36 -0700 Subject: [PATCH 22/22] refine public documentations --- packages-exp/installations-exp/src/api/on-id-change.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages-exp/installations-exp/src/api/on-id-change.ts b/packages-exp/installations-exp/src/api/on-id-change.ts index 757c2e90ae0..a9e4360dfd4 100644 --- a/packages-exp/installations-exp/src/api/on-id-change.ts +++ b/packages-exp/installations-exp/src/api/on-id-change.ts @@ -26,7 +26,7 @@ import { FirebaseInstallations } from '@firebase/installations-types-exp'; */ export type IdChangeCallbackFn = (installationId: string) => void; /** - * An unsubscribe function that will remove the onIdChange callback when called. + * Unsubscribe a callback function previously added via {@link #IdChangeCallbackFn}. * * @public */