diff --git a/README.md b/README.md
index 91b8795c3..6eda04895 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,10 @@ export class MyApp {
## Compatibility
+### Angular and Firebase versions
+
+AngularFire doesn't follow Angular's versioning as Firebase also has breaking changes throughout the year. Instead we try to maintain compatability with both Firebase and Angular majors for as long as possible, only breaking when we need to support a new major of one or the other. We've been forunate that the APIs we rely on have been fairly stable, but we understand this can lead to some confusion so we've created the following table for advice.
+
| Angular | Firebase | AngularFire |
| --------|----------|--------------|
| 11 | 7,8 | @next |
@@ -53,6 +57,18 @@ export class MyApp {
Version combinations not documented here __may__ work but are untested and you will see NPM peer warnings.
+### Polyfills
+
+Neither AngularFire or Firebase ship with polyfills & we tend to use modern ES features in our development. To have compatability across as wide-range of environments we suggest the following polyfills be added to your application:
+
+| API | Environments | Suggested Polyfill | License |
+|-----|--------------|--------------------|---------|
+| Various ES5+ features | IE 11 Safari < 10 Node < 6.5 | [`core-js/stable`](https://github.com/zloirock/core-js#readme) | MIT |
+| `globalThis` | [most](https://caniuse.com/mdn-javascript_builtins_globalthis) | [`globalThis`](https://github.com/es-shims/globalThis#readme) | MIT |
+| `Proxy` | [IE 11 Safari < 10](https://caniuse.com/proxy) | [`proxy-polyfill`](https://github.com/GoogleChrome/proxy-polyfill#readme) | Apache 2.0 |
+| `fetch` | [IE 11 Node Safari < 10.1 iOS < 10.3](https://caniuse.com/fetch) | [`cross-fetch`](https://github.com/lquixada/cross-fetch#readme) | MIT |
+| `Headers` | [IE 11 Node Safari < 10.1 iOS Safari](https://caniuse.com/mdn-api_headers) | [`cross-fetch`](https://github.com/lquixada/cross-fetch#readme) | MIT |
+
## Resources
[Quickstart](docs/install-and-setup.md) - Get your first application up and running by following our quickstart guide.
diff --git a/sample/package.json b/sample/package.json
index 8b6f29f81..dd5383d56 100644
--- a/sample/package.json
+++ b/sample/package.json
@@ -4,7 +4,7 @@
"scripts": {
"ng": "ng",
"start": "ng serve",
- "started:emulated": "concurrently -n ng,firebase -c red,yellow \"ng serve -c emulated\" \"firebase emulators:start\"",
+ "start:emulated": "concurrently -n ng,firebase -c red,yellow \"ng serve -c emulated\" \"firebase emulators:start\"",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
diff --git a/sample/src/app/app.module.ts b/sample/src/app/app.module.ts
index 7dd0c4eb3..8f08ff34b 100644
--- a/sample/src/app/app.module.ts
+++ b/sample/src/app/app.module.ts
@@ -35,12 +35,15 @@ import { HomeComponent } from './home/home.component';
import { AuthComponent } from './auth/auth.component';
import { MessagingComponent } from './messaging/messaging.component';
import { FunctionsComponent } from './functions/functions.component';
+import { FirestoreOfflineComponent } from './firestore-offline/firestore-offline.component';
+import { FirestoreOfflineModule } from './firestore-offline/firestore-offline.module';
@NgModule({
declarations: [
AppComponent,
StorageComponent,
FirestoreComponent,
+ FirestoreOfflineComponent,
DatabaseComponent,
RemoteConfigComponent,
HomeComponent,
@@ -56,7 +59,7 @@ import { FunctionsComponent } from './functions/functions.component';
AngularFireModule.initializeApp(environment.firebase),
AngularFireStorageModule,
AngularFireDatabaseModule,
- AngularFirestoreModule.enablePersistence({ synchronizeTabs: true }),
+ AngularFirestoreModule,
AngularFireAuthModule,
AngularFireAuthGuardModule,
AngularFireRemoteConfigModule,
@@ -64,13 +67,14 @@ import { FunctionsComponent } from './functions/functions.component';
AngularFireAnalyticsModule,
AngularFireFunctionsModule,
AngularFirePerformanceModule,
- AngularFireAuthGuardModule
+ AngularFireAuthGuardModule,
+ FirestoreOfflineModule
],
providers: [
UserTrackingService,
ScreenTrackingService,
PerformanceMonitoringService,
- { provide: ANALYTICS_DEBUG_MODE, useValue: false },
+ { provide: ANALYTICS_DEBUG_MODE, useValue: true },
{ provide: COLLECTION_ENABLED, useValue: true },
{ provide: USE_AUTH_EMULATOR, useValue: environment.useEmulators ? ['localhost', 9099] : undefined },
{ provide: USE_DATABASE_EMULATOR, useValue: environment.useEmulators ? ['localhost', 9000] : undefined },
diff --git a/sample/src/app/auth/auth.component.ts b/sample/src/app/auth/auth.component.ts
index adeaff729..284f90f04 100644
--- a/sample/src/app/auth/auth.component.ts
+++ b/sample/src/app/auth/auth.component.ts
@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, PLATFORM_ID } from '@angular/core';
import { AngularFireAuth } from '@angular/fire/auth';
import firebase from 'firebase/app';
import { Subscription } from 'rxjs';
-import { map, tap } from 'rxjs/operators';
+import { map } from 'rxjs/operators';
import { trace } from '@angular/fire/performance';
import { Inject } from '@angular/core';
import { isPlatformServer } from '@angular/common';
@@ -13,7 +13,9 @@ import { isPlatformServer } from '@angular/common';
Auth!
{{ (auth.user | async)?.uid | json }}
+ {{ (auth.credential | async)?.operationType | json }}
Log in with Google
+ Log in anonymously
Log out
`,
@@ -46,12 +48,19 @@ export class AuthComponent implements OnInit, OnDestroy {
}
}
- login() {
- this.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider());
+ async login() {
+ const user = await this.auth.signInWithPopup(new firebase.auth.GoogleAuthProvider());
+ // TODO sign into offline app
+ }
+
+ async loginAnonymously() {
+ const user = await this.auth.signInAnonymously();
+ // TODO sign into offline app
}
logout() {
this.auth.signOut();
+ // TODO sign out of offline app
}
}
diff --git a/sample/src/app/firestore-offline/firestore-offline.component.spec.ts b/sample/src/app/firestore-offline/firestore-offline.component.spec.ts
new file mode 100644
index 000000000..7c95a29de
--- /dev/null
+++ b/sample/src/app/firestore-offline/firestore-offline.component.spec.ts
@@ -0,0 +1,25 @@
+import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { FirestoreOfflineComponent } from './firestore-offline.component';
+
+describe('FirestoreComponent', () => {
+ let component: FirestoreOfflineComponent;
+ let fixture: ComponentFixture;
+
+ beforeEach(waitForAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [ FirestoreOfflineComponent ]
+ })
+ .compileComponents();
+ }));
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(FirestoreOfflineComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/sample/src/app/firestore-offline/firestore-offline.component.ts b/sample/src/app/firestore-offline/firestore-offline.component.ts
new file mode 100644
index 000000000..cb21df1fd
--- /dev/null
+++ b/sample/src/app/firestore-offline/firestore-offline.component.ts
@@ -0,0 +1,37 @@
+import { Component, OnInit } from '@angular/core';
+import { AngularFirestore } from '@angular/fire/firestore';
+import { Observable } from 'rxjs';
+import { startWith, tap } from 'rxjs/operators';
+import { makeStateKey, TransferState } from '@angular/platform-browser';
+import { trace } from '@angular/fire/performance';
+import { AngularFirestoreOffline } from './firestore-offline.module';
+
+@Component({
+ selector: 'app-firestore-offline',
+ template: `
+ Firestore Offline!
+ {{ testDocValue$ | async | json }}
+ {{ persistenceEnabled$ | async }}
+
`,
+ styles: [``]
+})
+export class FirestoreOfflineComponent implements OnInit {
+
+ public readonly persistenceEnabled$: Observable;
+ public readonly testDocValue$: Observable;
+
+ constructor(state: TransferState, firestore: AngularFirestoreOffline) {
+ const doc = firestore.doc('test/1');
+ const key = makeStateKey(doc.ref.path);
+ const existing = state.get(key, undefined);
+ this.testDocValue$ = firestore.doc('test/1').valueChanges().pipe(
+ trace('firestore'),
+ existing ? startWith(existing) : tap(it => state.set(key, it))
+ );
+ this.persistenceEnabled$ = firestore.persistenceEnabled$;
+ }
+
+ ngOnInit(): void {
+ }
+
+}
diff --git a/sample/src/app/firestore-offline/firestore-offline.module.ts b/sample/src/app/firestore-offline/firestore-offline.module.ts
new file mode 100644
index 000000000..7bd3bc678
--- /dev/null
+++ b/sample/src/app/firestore-offline/firestore-offline.module.ts
@@ -0,0 +1,28 @@
+import { Inject, Injectable, InjectionToken, NgModule, NgZone, Optional, PLATFORM_ID } from '@angular/core';
+import { FirebaseOptions, FIREBASE_OPTIONS } from '@angular/fire';
+import { USE_EMULATOR } from '@angular/fire/firestore';
+import { AngularFirestore, SETTINGS, Settings } from '@angular/fire/firestore';
+import { USE_EMULATOR as USE_AUTH_EMULATOR } from '@angular/fire/auth';
+
+export const FIRESTORE_OFFLINE = new InjectionToken('my.firestore');
+
+@Injectable()
+export class AngularFirestoreOffline extends AngularFirestore {
+ constructor(
+ @Inject(FIREBASE_OPTIONS) options: FirebaseOptions,
+ @Optional() @Inject(SETTINGS) settings: Settings | null,
+ // tslint:disable-next-line:ban-types
+ @Inject(PLATFORM_ID) platformId: Object,
+ zone: NgZone,
+ @Optional() @Inject(USE_EMULATOR) useEmulator: any,
+ @Optional() @Inject(USE_AUTH_EMULATOR) useAuthEmulator: any,
+ ) {
+ super(options, 'offline', true, settings, platformId, zone, { synchronizeTabs: true }, useEmulator, useAuthEmulator);
+ }
+}
+
+@NgModule({
+ providers: [ AngularFirestoreOffline ]
+}) export class FirestoreOfflineModule {
+
+}
diff --git a/sample/src/app/home/home.component.ts b/sample/src/app/home/home.component.ts
index f7b5e3089..bac1a2da2 100644
--- a/sample/src/app/home/home.component.ts
+++ b/sample/src/app/home/home.component.ts
@@ -8,6 +8,7 @@ import { FirebaseApp } from '@angular/fire';
{{ firebaseApp.name }}
+
diff --git a/sample/yarn.lock b/sample/yarn.lock
index 0d75b5e00..a91db3508 100644
--- a/sample/yarn.lock
+++ b/sample/yarn.lock
@@ -2295,9 +2295,9 @@ acorn-walk@^7.1.1:
integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==
acorn@^6.4.1:
- version "6.4.1"
- resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
- integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
+ version "6.4.2"
+ resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
+ integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
acorn@^7.1.1:
version "7.4.0"
@@ -2373,7 +2373,7 @@ ajv@6.12.4:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
-ajv@6.12.6, ajv@^6.12.5:
+ajv@6.12.6, ajv@^6.10.2, ajv@^6.12.5:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -2383,7 +2383,7 @@ ajv@6.12.6, ajv@^6.12.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
-ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.0:
+ajv@^6.1.0, ajv@^6.12.0:
version "6.12.0"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7"
integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==
@@ -2959,7 +2959,7 @@ bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
-bn.js@^5.1.1:
+bn.js@^5.0.0, bn.js@^5.1.1:
version "5.1.3"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b"
integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==
@@ -3142,11 +3142,11 @@ browserify-des@^1.0.0:
safe-buffer "^5.1.2"
browserify-rsa@^4.0.0, browserify-rsa@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524"
- integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=
+ version "4.1.0"
+ resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d"
+ integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==
dependencies:
- bn.js "^4.1.0"
+ bn.js "^5.0.0"
randombytes "^2.0.1"
browserify-sign@^4.0.0:
@@ -11265,7 +11265,7 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2:
source-map-url "^0.4.0"
urix "^0.1.0"
-source-map-support@0.5.19, source-map-support@^0.5.17, source-map-support@~0.5.19:
+source-map-support@0.5.19, source-map-support@^0.5.17, source-map-support@~0.5.12, source-map-support@~0.5.19:
version "0.5.19"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
@@ -11273,7 +11273,7 @@ source-map-support@0.5.19, source-map-support@^0.5.17, source-map-support@~0.5.1
buffer-from "^1.0.0"
source-map "^0.6.0"
-source-map-support@^0.5.5, source-map-support@~0.5.12:
+source-map-support@^0.5.5:
version "0.5.16"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042"
integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==
@@ -11972,9 +11972,9 @@ thunky@^1.0.2:
integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
timers-browserify@^2.0.4:
- version "2.0.11"
- resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.11.tgz#800b1f3eee272e5bc53ee465a04d0e804c31211f"
- integrity sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==
+ version "2.0.12"
+ resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee"
+ integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==
dependencies:
setimmediate "^1.0.4"
@@ -12579,23 +12579,23 @@ walkdir@^0.4.0:
resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.4.1.tgz#dc119f83f4421df52e3061e514228a2db20afa39"
integrity sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==
-watchpack-chokidar2@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0"
- integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA==
+watchpack-chokidar2@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"
+ integrity sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==
dependencies:
chokidar "^2.1.8"
watchpack@^1.7.4:
- version "1.7.4"
- resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.4.tgz#6e9da53b3c80bb2d6508188f5b200410866cd30b"
- integrity sha512-aWAgTW4MoSJzZPAicljkO1hsi1oKj/RRq/OJQh2PKI2UKL04c2Bs+MBOB+BBABHTXJpf9mCwHN7ANCvYsvY2sg==
+ version "1.7.5"
+ resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.5.tgz#1267e6c55e0b9b5be44c2023aed5437a2c26c453"
+ integrity sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==
dependencies:
graceful-fs "^4.1.2"
neo-async "^2.5.0"
optionalDependencies:
chokidar "^3.4.1"
- watchpack-chokidar2 "^2.0.0"
+ watchpack-chokidar2 "^2.0.1"
wbuf@^1.1.0, wbuf@^1.7.3:
version "1.7.3"
diff --git a/src/analytics/analytics.module.ts b/src/analytics/analytics.module.ts
index f59ae3cac..5adb87980 100644
--- a/src/analytics/analytics.module.ts
+++ b/src/analytics/analytics.module.ts
@@ -1,6 +1,7 @@
import { NgModule, Optional } from '@angular/core';
-import { ScreenTrackingService, UserTrackingService } from './analytics.service';
+import { ScreenTrackingService } from './screen-tracking.service';
import { AngularFireAnalytics } from './analytics';
+import { UserTrackingService } from './user-tracking.service';
@NgModule({
providers: [ AngularFireAnalytics ]
diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts
index 0116c610a..296f0e398 100644
--- a/src/analytics/analytics.ts
+++ b/src/analytics/analytics.ts
@@ -1,20 +1,17 @@
import { Inject, Injectable, InjectionToken, NgZone, Optional, PLATFORM_ID } from '@angular/core';
import { EMPTY, of } from 'rxjs';
import { isPlatformBrowser } from '@angular/common';
-import { map, tap, shareReplay, switchMap, observeOn } from 'rxjs/operators';
+import { map, shareReplay, switchMap, observeOn } from 'rxjs/operators';
import {
- FirebaseAppConfig,
- FirebaseOptions,
ɵAngularFireSchedulers,
ɵlazySDKProxy,
- FIREBASE_OPTIONS,
- FIREBASE_APP_NAME,
- ɵfirebaseAppFactory,
ɵPromiseProxy,
- ɵapplyMixins
+ ɵapplyMixins,
+ FirebaseApp
} from '@angular/fire';
import firebase from 'firebase/app';
import { proxyPolyfillCompat } from './base';
+import { ɵfetchInstance } from '@angular/fire';
export interface Config {
[key: string]: any;
@@ -32,30 +29,26 @@ const DEBUG_MODE_KEY = 'debug_mode';
const GTAG_CONFIG_COMMAND = 'config';
const GTAG_FUNCTION_NAME = 'gtag'; // TODO rename these
const DATA_LAYER_NAME = 'dataLayer';
+const SEND_TO_KEY = 'send_to';
export interface AngularFireAnalytics extends ɵPromiseProxy {
}
-let gtag: (...args: any[]) => void;
-
-// tslint:disable-next-line
-var __analyticsInitialized: Promise;
-
@Injectable({
providedIn: 'any'
})
export class AngularFireAnalytics {
private measurementId: string;
+ private analyticsInitialized: Promise = new Promise(() => {});
async updateConfig(config: Config) {
- await __analyticsInitialized;
+ await this.analyticsInitialized;
window[GTAG_FUNCTION_NAME](GTAG_CONFIG_COMMAND, this.measurementId, { ...config, update: true });
}
constructor(
- @Inject(FIREBASE_OPTIONS) private options: FirebaseOptions,
- @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig: string | FirebaseAppConfig | null | undefined,
+ app: FirebaseApp,
@Optional() @Inject(COLLECTION_ENABLED) analyticsCollectionEnabled: boolean | null,
@Optional() @Inject(APP_VERSION) providedAppVersion: string | null,
@Optional() @Inject(APP_NAME) providedAppName: string | null,
@@ -66,72 +59,107 @@ export class AngularFireAnalytics {
zone: NgZone
) {
- if (!__analyticsInitialized) {
- if (isPlatformBrowser(platformId)) {
- window[DATA_LAYER_NAME] = window[DATA_LAYER_NAME] || [];
-
- __analyticsInitialized = new Promise(resolve => {
- window[GTAG_FUNCTION_NAME] = (...args: any[]) => {
- // wait to initialize until we know the measurementId as the one in config is unstable
- if (args[0] === 'config' && args[2].origin === 'firebase') {
- this.measurementId = args[1];
- resolve();
+ if (isPlatformBrowser(platformId)) {
+
+ window[DATA_LAYER_NAME] = window[DATA_LAYER_NAME] || [];
+
+ // It turns out we can't rely on the measurementId in the Firebase config JSON
+ // this identifier is not stable. firebase/analytics does a call to get a fresh value
+ // falling back on the one in the config. Rather than do that ourselves we should listen
+ // on our gtag function for a analytics config command
+ // e.g, ['config', measurementId, { origin: 'firebase', firebase_id }]
+ const parseMeasurementId = (...args: any[]) => {
+ if (args[0] === 'config' && args[2].origin === 'firebase') {
+ this.measurementId = args[1];
+ return true;
+ } else {
+ return false;
+ }
+ };
+
+ const patchGtag = (fn?: (...args: any[]) => void) => {
+ window[GTAG_FUNCTION_NAME] = (...args: any[]) => {
+ if (fn) {
+ fn(args);
+ }
+ // Inject app_name and app_version into events
+ // TODO(jamesdaniels): I'm doing this as documented but it's still not
+ // showing up in the console. Investigate. Guessing it's just part of the
+ // whole GA4 transition mess.
+ if (args[0] === 'event' && args[2][SEND_TO_KEY] === this.measurementId) {
+ if (providedAppName) {
+ args[2][APP_NAME_KEY] = providedAppName;
}
- if (args[0] === 'event') {
- if (providedAppName) {
- args[2][APP_NAME_KEY] = providedAppName;
- }
- if (providedAppVersion) {
- args[2][APP_VERSION_KEY] = providedAppVersion;
- }
+ if (providedAppVersion) {
+ args[2][APP_VERSION_KEY] = providedAppVersion;
}
- if (debugModeEnabled && typeof console !== 'undefined') {
- // tslint:disable-next-line:no-console
- console.info(...args);
+ }
+ if (debugModeEnabled && typeof console !== 'undefined') {
+ // tslint:disable-next-line:no-console
+ console.info(...args);
+ }
+ /**
+ * According to the gtag documentation, this function that defines a custom data layer cannot be
+ * an arrow function because 'arguments' is not an array. It is actually an object that behaves
+ * like an array and contains more information then just indexes. Transforming this into arrow function
+ * caused issue #2505 where analytics no longer sent any data.
+ */
+ // tslint:disable-next-line: only-arrow-functions
+ (function(..._args: any[]) {
+ window[DATA_LAYER_NAME].push(arguments);
+ })(...args);
+ };
+ };
+
+ // Unclear if we still need to but I was running into config/events I passed
+ // to gtag before ['js' timestamp] weren't getting parsed, so let's make a promise
+ // that resolves when firebase/analytics has configured gtag.js that we wait on
+ // before sending anything
+ const firebaseAnalyticsAlreadyInitialized = window[DATA_LAYER_NAME].some(parseMeasurementId);
+ if (firebaseAnalyticsAlreadyInitialized) {
+ this.analyticsInitialized = Promise.resolve();
+ patchGtag();
+ } else {
+ this.analyticsInitialized = new Promise(resolve => {
+ patchGtag((...args) => {
+ if (parseMeasurementId(...args)) {
+ resolve();
}
- /**
- * According to the gtag documentation, this function that defines a custom data layer cannot be
- * an arrow function because 'arguments' is not an array. It is actually an object that behaves
- * like an array and contains more information then just indexes. Transforming this into arrow function
- * caused issue #2505 where analytics no longer sent any data.
- */
- // tslint:disable-next-line: only-arrow-functions
- (function(..._args: any[]) {
- window[DATA_LAYER_NAME].push(arguments);
- })(...args);
- };
+ });
});
- } else {
- gtag = () => {};
- __analyticsInitialized = Promise.resolve();
}
+
+ if (providedConfig) {
+ this.updateConfig(providedConfig);
+ }
+ if (debugModeEnabled) {
+ this.updateConfig({ [DEBUG_MODE_KEY]: 1 });
+ }
+
+ } else {
+
+ this.analyticsInitialized = Promise.reject();
+
}
const analytics = of(undefined).pipe(
observeOn(new ɵAngularFireSchedulers(zone).outsideAngular),
- switchMap(() => isPlatformBrowser(platformId) ? import('firebase/analytics') : EMPTY),
- map(() => ɵfirebaseAppFactory(options, zone, nameOrConfig)),
- // app.analytics doesn't expose settings, which is odd... bug?
- /* tap((app: any) => app.analytics.settings({
- dataLayerName: DATA_LAYER_NAME,
- gtagName: GTAG_FUNCTION_NAME,
- })), */
- map(app => app.analytics()),
- tap(analytics => {
- if (analyticsCollectionEnabled === false) {
- analytics.setAnalyticsCollectionEnabled(false);
- }
+ switchMap(() => import('firebase/analytics')),
+ switchMap(() => firebase.analytics.isSupported().then(it => it, () => false)),
+ // TODO server-side investigate use of the Universal Analytics API
+ switchMap(supported => supported ? of(undefined) : EMPTY),
+ map(() => {
+ return ɵfetchInstance(`analytics`, 'AngularFireAnalytics', app, () => {
+ const analytics = app.analytics();
+ if (analyticsCollectionEnabled === false) {
+ analytics.setAnalyticsCollectionEnabled(false);
+ }
+ return analytics;
+ }, [app, analyticsCollectionEnabled, providedConfig, debugModeEnabled]);
}),
shareReplay({ bufferSize: 1, refCount: false })
);
- if (providedConfig) {
- this.updateConfig(providedConfig);
- }
- if (debugModeEnabled) {
- this.updateConfig({ [DEBUG_MODE_KEY]: 1 });
- }
-
return ɵlazySDKProxy(this, analytics, zone);
}
diff --git a/src/analytics/public_api.ts b/src/analytics/public_api.ts
index 8ce1376e6..4601131aa 100644
--- a/src/analytics/public_api.ts
+++ b/src/analytics/public_api.ts
@@ -1,3 +1,4 @@
export * from './analytics';
export * from './analytics.module';
-export * from './analytics.service';
+export * from './screen-tracking.service';
+export * from './user-tracking.service';
diff --git a/src/analytics/analytics.service.ts b/src/analytics/screen-tracking.service.ts
similarity index 78%
rename from src/analytics/analytics.service.ts
rename to src/analytics/screen-tracking.service.ts
index 9223d4d00..c217ec726 100644
--- a/src/analytics/analytics.service.ts
+++ b/src/analytics/screen-tracking.service.ts
@@ -2,20 +2,18 @@ import {
ComponentFactoryResolver,
Inject,
Injectable,
- Injector,
NgZone,
OnDestroy,
Optional,
PLATFORM_ID
} from '@angular/core';
-import { from, Observable, of, Subscription } from 'rxjs';
-import { distinctUntilChanged, filter, groupBy, map, mergeMap, observeOn, pairwise, startWith, switchMap, tap } from 'rxjs/operators';
+import { of, Subscription } from 'rxjs';
+import { distinctUntilChanged, filter, groupBy, map, mergeMap, pairwise, startWith, switchMap } from 'rxjs/operators';
import { ActivationEnd, Router, ɵEmptyOutletComponent } from '@angular/router';
-import { ɵAngularFireSchedulers } from '@angular/fire';
import { AngularFireAnalytics } from './analytics';
-import firebase from 'firebase/app';
import { Title } from '@angular/platform-browser';
-import { isPlatformBrowser, isPlatformServer } from '@angular/common';
+import { isPlatformBrowser } from '@angular/common';
+import { UserTrackingService } from './user-tracking.service';
const FIREBASE_EVENT_ORIGIN_KEY = 'firebase_event_origin';
const FIREBASE_PREVIOUS_SCREEN_CLASS_KEY = 'firebase_previous_class';
@@ -31,7 +29,6 @@ const SCREEN_CLASS_KEY = 'screen_class';
const SCREEN_NAME_KEY = 'screen_name';
const SCREEN_VIEW_EVENT = 'screen_view';
const EVENT_ORIGIN_AUTO = 'auto';
-const DEFAULT_SCREEN_CLASS = '???';
const SCREEN_INSTANCE_DELIMITER = '#';
// this is an INT64 in iOS/Android but use INT32 cause javascript
@@ -67,7 +64,7 @@ export class ScreenTrackingService implements OnDestroy {
// tslint:disable-next-line:ban-types
@Inject(PLATFORM_ID) platformId: Object,
zone: NgZone,
- injector: Injector,
+ @Optional() userTrackingService: UserTrackingService,
) {
if (!router || !isPlatformBrowser(platformId)) {
return this;
@@ -141,7 +138,12 @@ export class ScreenTrackingService implements OnDestroy {
...current
} : current
),
- tap(params => analytics.logEvent(SCREEN_VIEW_EVENT, params))
+ switchMap(async params => {
+ if (userTrackingService) {
+ await userTrackingService.initialized;
+ }
+ return await analytics.logEvent(SCREEN_VIEW_EVENT, params);
+ })
))
).subscribe();
});
@@ -154,38 +156,3 @@ export class ScreenTrackingService implements OnDestroy {
}
}
-
-@Injectable()
-export class UserTrackingService implements OnDestroy {
-
- private disposable: Subscription | undefined;
-
- // TODO a user properties injector
- constructor(
- analytics: AngularFireAnalytics,
- zone: NgZone,
- // tslint:disable-next-line:ban-types
- @Inject(PLATFORM_ID) platformId: Object
- ) {
- const schedulers = new ɵAngularFireSchedulers(zone);
-
- if (!isPlatformServer(platformId)) {
- zone.runOutsideAngular(() => {
- // @ts-ignore zap the import in the UMD
- this.disposable = from(import('firebase/auth')).pipe(
- observeOn(schedulers.outsideAngular),
- switchMap(() => analytics.app),
- map(app => app.auth()),
- switchMap(auth => new Observable(auth.onAuthStateChanged.bind(auth))),
- switchMap(user => analytics.setUserId(user ? user.uid : null))
- ).subscribe();
- });
- }
- }
-
- ngOnDestroy() {
- if (this.disposable) {
- this.disposable.unsubscribe();
- }
- }
-}
diff --git a/src/analytics/user-tracking.service.ts b/src/analytics/user-tracking.service.ts
new file mode 100644
index 000000000..d908e41e2
--- /dev/null
+++ b/src/analytics/user-tracking.service.ts
@@ -0,0 +1,49 @@
+import { isPlatformServer } from '@angular/common';
+import { Inject, Injectable, OnDestroy, PLATFORM_ID } from '@angular/core';
+import { AngularFireAnalytics } from './analytics';
+import { AngularFireAuth } from '@angular/fire/auth';
+import { Subscription } from 'rxjs';
+import { credential } from 'firebase-admin';
+
+@Injectable()
+export class UserTrackingService implements OnDestroy {
+
+ initialized: Promise;
+ private disposables: Subscription[] = [];
+
+ // TODO a user properties injector
+ constructor(
+ analytics: AngularFireAnalytics,
+ // tslint:disable-next-line:ban-types
+ @Inject(PLATFORM_ID) platformId: Object,
+ auth: AngularFireAuth,
+ ) {
+
+ this.initialized = new Promise(resolve => {
+ if (!isPlatformServer(platformId)) {
+ this.disposables = [
+ auth.authState.subscribe(user => {
+ analytics.setUserId(user?.uid);
+ resolve();
+ }),
+ auth.credential.subscribe(credential => {
+ if (credential) {
+ const method = credential.user.isAnonymous ? 'anonymous' : credential.additionalUserInfo.providerId;
+ if (credential.additionalUserInfo.isNewUser) {
+ analytics.logEvent('sign_up', { method });
+ }
+ analytics.logEvent('login', { method });
+ }
+ })
+ ];
+ } else {
+ resolve();
+ }
+ });
+
+ }
+
+ ngOnDestroy() {
+ this.disposables.forEach(it => it.unsubscribe());
+ }
+}
diff --git a/src/auth/auth.ts b/src/auth/auth.ts
index f5ba93411..ebbd65916 100644
--- a/src/auth/auth.ts
+++ b/src/auth/auth.ts
@@ -1,6 +1,6 @@
import { Injectable, Inject, Optional, NgZone, PLATFORM_ID, InjectionToken } from '@angular/core';
-import { Observable, of, from } from 'rxjs';
-import { switchMap, map, observeOn, shareReplay, first } from 'rxjs/operators';
+import { Observable, of, from, merge, Subject } from 'rxjs';
+import { switchMap, map, observeOn, shareReplay, first, filter } from 'rxjs/operators';
import {
FIREBASE_OPTIONS,
FIREBASE_APP_NAME,
@@ -16,6 +16,7 @@ import {
import firebase from 'firebase/app';
import { isPlatformServer } from '@angular/common';
import { proxyPolyfillCompat } from './base';
+import { ɵfetchInstance } from '@angular/fire';
export interface AngularFireAuth extends ɵPromiseProxy {}
@@ -55,6 +56,11 @@ export class AngularFireAuth {
*/
public readonly idTokenResult: Observable;
+ /**
+ * Observable of the currently signed-in user's credential, or null
+ */
+ public readonly credential: Observable|null>;
+
constructor(
@Inject(FIREBASE_OPTIONS) options: FirebaseOptions,
@Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig: string|FirebaseAppConfig|null|undefined,
@@ -70,40 +76,43 @@ export class AngularFireAuth {
) {
const schedulers = new ɵAngularFireSchedulers(zone);
const keepUnstableUntilFirst = ɵkeepUnstableUntilFirstFactory(schedulers);
+ const logins = new Subject();
const auth = of(undefined).pipe(
observeOn(schedulers.outsideAngular),
switchMap(() => zone.runOutsideAngular(() => import('firebase/auth'))),
map(() => ɵfirebaseAppFactory(options, zone, nameOrConfig)),
map(app => zone.runOutsideAngular(() => {
- const auth = app.auth();
const useEmulator: UseEmulatorArguments | null = _useEmulator;
- if (useEmulator) {
- // Firebase Auth doesn't conform to the useEmulator convention, let's smooth that over
- auth.useEmulator(`http://${useEmulator.join(':')}`);
- }
- if (tenantId) {
- auth.tenantId = tenantId;
- }
- auth.languageCode = languageCode;
- if (useDeviceLanguage) {
- auth.useDeviceLanguage();
- }
const settings: firebase.auth.AuthSettings | null = _settings;
- if (settings) {
- auth.settings = settings;
- }
- if (persistence) {
- auth.setPersistence(persistence);
- }
- return auth;
+ return ɵfetchInstance(`${app.name}.auth`, 'AngularFireAuth', app, () => {
+ const auth = app.auth();
+ if (useEmulator) {
+ // Firebase Auth doesn't conform to the useEmulator convention, let's smooth that over
+ auth.useEmulator(`http://${useEmulator.join(':')}`);
+ }
+ if (tenantId) {
+ auth.tenantId = tenantId;
+ }
+ auth.languageCode = languageCode;
+ if (useDeviceLanguage) {
+ auth.useDeviceLanguage();
+ }
+ if (settings) {
+ auth.settings = settings;
+ }
+ if (persistence) {
+ auth.setPersistence(persistence);
+ }
+ return auth;
+ }, [useEmulator, tenantId, languageCode, useDeviceLanguage, settings, persistence]);
})),
shareReplay({ bufferSize: 1, refCount: false }),
);
if (isPlatformServer(platformId)) {
- this.authState = this.user = this.idToken = this.idTokenResult = of(null);
+ this.authState = this.user = this.idToken = this.idTokenResult = this.credential = of(null);
} else {
@@ -115,12 +124,15 @@ export class AngularFireAuth {
const _ = auth.pipe(first()).subscribe();
this.authState = auth.pipe(
+ // wait for getRedirectResult otherwise we often get extraneous nulls firing on page load even if
+ // a user is signed in
switchMap(auth => auth.getRedirectResult().then(() => auth, () => auth)),
switchMap(auth => zone.runOutsideAngular(() => new Observable(auth.onAuthStateChanged.bind(auth)))),
keepUnstableUntilFirst
);
this.user = auth.pipe(
+ // see comment on authState
switchMap(auth => auth.getRedirectResult().then(() => auth, () => auth)),
switchMap(auth => zone.runOutsideAngular(() => new Observable(auth.onIdTokenChanged.bind(auth)))),
keepUnstableUntilFirst
@@ -134,9 +146,30 @@ export class AngularFireAuth {
switchMap(user => user ? from(user.getIdTokenResult()) : of(null))
);
+ this.credential = auth.pipe(
+ switchMap(auth => merge(
+ auth.getRedirectResult().then(it => it, () => null),
+ logins,
+ // pipe in null authState to make credential zipable, just a weird devexp if
+ // authState and user go null to still have a credential
+ this.authState.pipe(filter(it => !it)))
+ ),
+ // handle the { user: { } } when a user is already logged in, rather have null
+ map(credential => credential?.user ? credential : null),
+ );
+
}
- return ɵlazySDKProxy(this, auth, zone);
+ return ɵlazySDKProxy(this, auth, zone, { spy: {
+ apply: (name, _, val) => {
+ // If they call a signIn or createUser function listen into the promise
+ // this will give us the user credential, push onto the logins Subject
+ // to be consumed in .credential
+ if (name.startsWith('signIn') || name.startsWith('createUser')) {
+ val.then((user: firebase.auth.UserCredential) => logins.next(user));
+ }
+ }
+ }});
}
diff --git a/src/core/angularfire2.ts b/src/core/angularfire2.ts
index 45e452e9e..614162bfd 100644
--- a/src/core/angularfire2.ts
+++ b/src/core/angularfire2.ts
@@ -134,10 +134,18 @@ const noopFunctions = ['ngOnDestroy'];
// INVESTIGATE should we make the Proxy revokable and do some cleanup?
// right now it's fairly simple but I'm sure this will grow in complexity
-export const ɵlazySDKProxy = (klass: any, observable: Observable, zone: NgZone) => {
+export const ɵlazySDKProxy = (klass: any, observable: Observable, zone: NgZone, options: {
+ spy?: {
+ get?: ((name: string, it: any) => void),
+ apply?: ((name: string, args: any[], it: any) => void)
+ }
+} = {}) => {
return new Proxy(klass, {
get: (_, name: string) => zone.runOutsideAngular(() => {
if (klass[name]) {
+ if (options?.spy?.get) {
+ options.spy.get(name, klass[name]);
+ }
return klass[name];
}
if (noopFunctions.indexOf(name) > -1) {
@@ -159,7 +167,13 @@ export const ɵlazySDKProxy = (klass: any, observable: Observable, zone: Ng
return new Proxy(() => {}, {
get: (_, name) => promise[name],
// TODO handle callbacks as transparently as I can
- apply: (self, _, args) => promise.then(it => it && it(...args))
+ apply: (self, _, args) => promise.then(it => {
+ const res = it && it(...args);
+ if (options?.spy?.apply) {
+ options.spy.apply(name, args, res);
+ }
+ return res;
+ })
}
);
})
diff --git a/src/core/firebase.app.module.ts b/src/core/firebase.app.module.ts
index f452d348a..e1942e7e0 100644
--- a/src/core/firebase.app.module.ts
+++ b/src/core/firebase.app.module.ts
@@ -1,5 +1,5 @@
import {
- Inject, InjectionToken, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, VERSION as NG_VERSION, Version
+ Inject, InjectionToken, isDevMode, ModuleWithProviders, NgModule, NgZone, Optional, PLATFORM_ID, VERSION as NG_VERSION, Version
} from '@angular/core';
import firebase from 'firebase/app';
@@ -42,7 +42,45 @@ export function ɵfirebaseAppFactory(options: FirebaseOptions, zone: NgZone, nam
const existingApp = firebase.apps.filter(app => app && app.name === config.name)[0] as any;
// We support FirebaseConfig, initializeApp's public type only accepts string; need to cast as any
// Could be solved with https://github.com/firebase/firebase-js-sdk/pull/1206
- return (existingApp || zone.runOutsideAngular(() => firebase.initializeApp(options, config as any))) as FirebaseApp;
+ const app = (existingApp || zone.runOutsideAngular(() => firebase.initializeApp(options, config as any))) as FirebaseApp;
+ if (JSON.stringify(options) !== JSON.stringify(app.options)) {
+ const hmr = !!(module as any).hot;
+ log('error', `${app.toString()} already initialized with different options${hmr ? ', you may need to reload as Firebase is not HMR aware.' : '.'}`);
+ }
+ return app;
+}
+
+export const ɵlogAuthEmulatorError = () => {
+ // TODO sort this out, https://github.com/angular/angularfire/issues/2656
+ log('warn', 'You may need to import \'firebase/auth\' manually in your component rather than rely on AngularFireAuth\'s dynamic import, when using the emulator suite https://github.com/angular/angularfire/issues/2656');
+};
+
+const log = (level: 'log'|'error'|'info'|'warn', ...args: any) => {
+ if (isDevMode() && typeof console !== 'undefined') {
+ console[level](...args);
+ }
+};
+
+globalThis.ɵAngularfireInstanceCache ||= new Map();
+
+export function ɵfetchInstance(cacheKey: any, moduleName: string, app: FirebaseApp, fn: () => T, args: any[]): T {
+ const [instance, ...cachedArgs] = globalThis.ɵAngularfireInstanceCache.get(cacheKey) || [];
+ if (instance && args.some((arg, i) => {
+ const cachedArg = cachedArgs[i];
+ if (arg && typeof arg === 'object') {
+ return JSON.stringify(arg) !== JSON.stringify(cachedArg);
+ } else {
+ return arg !== cachedArg;
+ }
+ })) {
+ const hmr = !!(module as any).hot;
+ log('error', `${moduleName} was already initialized on the ${app.name} Firebase App instance with different settings.${hmr ? ' You may need to reload as Firebase is not HMR aware.' : ''}`);
+ return instance;
+ } else {
+ const newInstance = fn();
+ globalThis.ɵAngularfireInstanceCache.set(cacheKey, [newInstance, ...args]);
+ return newInstance;
+ }
}
const FIREBASE_APP_PROVIDER = {
diff --git a/src/database/database.spec.ts b/src/database/database.spec.ts
index f64d1a68c..3ec1c7ea9 100644
--- a/src/database/database.spec.ts
+++ b/src/database/database.spec.ts
@@ -44,7 +44,7 @@ describe('AngularFireDatabase', () => {
});
it('should accept a Firebase App in the constructor', (done) => {
- const database = new AngularFireDatabase(app.options, rando(), undefined, {}, zone, undefined);
+ const database = new AngularFireDatabase(app.options, rando(), undefined, {}, zone, undefined, undefined);
expect(database instanceof AngularFireDatabase).toEqual(true);
database.database.app.delete().then(done, done);
});
diff --git a/src/database/database.ts b/src/database/database.ts
index bf1a69ff8..7107b2d1a 100644
--- a/src/database/database.ts
+++ b/src/database/database.ts
@@ -10,12 +10,13 @@ import {
FirebaseOptions,
ɵAngularFireSchedulers,
ɵfirebaseAppFactory,
- ɵkeepUnstableUntilFirstFactory
+ ɵkeepUnstableUntilFirstFactory,
} from '@angular/fire';
import { Observable } from 'rxjs';
import 'firebase/database';
-import { registerDatabase } from '@firebase/database';
+import { USE_EMULATOR as USE_AUTH_EMULATOR } from '@angular/fire/auth';
import firebase from 'firebase/app';
+import { ɵfetchInstance, ɵlogAuthEmulatorError } from '@angular/fire';
export const URL = new InjectionToken('angularfire2.realtimeDatabaseURL');
@@ -42,22 +43,25 @@ export class AngularFireDatabase {
@Inject(PLATFORM_ID) platformId: Object,
zone: NgZone,
@Optional() @Inject(USE_EMULATOR) _useEmulator: any, // tuple isn't working here
+ @Optional() @Inject(USE_AUTH_EMULATOR) useAuthEmulator: any,
) {
this.schedulers = new ɵAngularFireSchedulers(zone);
this.keepUnstableUntilFirst = ɵkeepUnstableUntilFirstFactory(this.schedulers);
- this.database = zone.runOutsideAngular(() => {
- const app = ɵfirebaseAppFactory(options, zone, nameOrConfig);
- if (registerDatabase) {
- registerDatabase(firebase as any);
- }
- const database = app.database(databaseURL || undefined);
- const useEmulator: UseEmulatorArguments | null = _useEmulator;
+ const useEmulator: UseEmulatorArguments | null = _useEmulator;
+ const app = ɵfirebaseAppFactory(options, zone, nameOrConfig);
+
+ if (!firebase.auth && useAuthEmulator) {
+ ɵlogAuthEmulatorError();
+ }
+
+ this.database = ɵfetchInstance(`${app.name}.database.${databaseURL}`, 'AngularFireDatabase', app, () => {
+ const database = zone.runOutsideAngular(() => app.database(databaseURL || undefined));
if (useEmulator) {
database.useEmulator(...useEmulator);
}
return database;
- });
+ }, [useEmulator]);
}
list(pathOrRef: PathReference, queryFn?: QueryFn): AngularFireList {
diff --git a/src/firestore/firestore.ts b/src/firestore/firestore.ts
index a123714e5..4d7a9b974 100644
--- a/src/firestore/firestore.ts
+++ b/src/firestore/firestore.ts
@@ -20,12 +20,14 @@ import {
FirebaseOptions,
ɵAngularFireSchedulers,
ɵfirebaseAppFactory,
- ɵkeepUnstableUntilFirstFactory
+ ɵkeepUnstableUntilFirstFactory,
+ FirebaseApp
} from '@angular/fire';
import { isPlatformServer } from '@angular/common';
import 'firebase/firestore';
-const atFirestore = require('@firebase/firestore');
import firebase from 'firebase/app';
+import { USE_EMULATOR as USE_AUTH_EMULATOR } from '@angular/fire/auth';
+import { ɵfetchInstance, ɵlogAuthEmulatorError } from '@angular/fire';
/**
* The value of this token determines whether or not the firestore will have persistance enabled
@@ -57,6 +59,13 @@ export function associateQuery(collectionRef: CollectionReference, queryFn
return { query, ref };
}
+type InstanceCache = Map;
+
/**
* AngularFirestore Service
*
@@ -136,43 +145,43 @@ export class AngularFirestore {
zone: NgZone,
@Optional() @Inject(PERSISTENCE_SETTINGS) persistenceSettings: PersistenceSettings | null,
@Optional() @Inject(USE_EMULATOR) _useEmulator: any,
+ @Optional() @Inject(USE_AUTH_EMULATOR) useAuthEmulator: any,
) {
this.schedulers = new ɵAngularFireSchedulers(zone);
this.keepUnstableUntilFirst = ɵkeepUnstableUntilFirstFactory(this.schedulers);
- this.firestore = zone.runOutsideAngular(() => {
- const app = ɵfirebaseAppFactory(options, zone, nameOrConfig);
- // INVESTIGATE this seems to be required because in the browser build registerFirestore is an Object?
- // seems like we're fighting ngcc. In the server firestore() isn't available, so I have to register
- // in the browser registerFirestore is not a function... maybe this is an underlying firebase-js-sdk issue
- if ('registerFirestore' in atFirestore) {
- (atFirestore as any).registerFirestore(firebase as any);
- }
- const firestore = app.firestore();
+ const app = ɵfirebaseAppFactory(options, zone, nameOrConfig);
+ if (!firebase.auth && useAuthEmulator) {
+ ɵlogAuthEmulatorError();
+ }
+ const useEmulator: UseEmulatorArguments | null = _useEmulator;
+
+ [this.firestore, this.persistenceEnabled$] = ɵfetchInstance(`${app.name}.firestore`, 'AngularFirestore', app, () => {
+ const firestore = zone.runOutsideAngular(() => app.firestore());
if (settings) {
firestore.settings(settings);
}
- const useEmulator: UseEmulatorArguments | null = _useEmulator;
if (useEmulator) {
firestore.useEmulator(...useEmulator);
}
- return firestore;
- });
- if (shouldEnablePersistence && !isPlatformServer(platformId)) {
- // We need to try/catch here because not all enablePersistence() failures are caught
- // https://github.com/firebase/firebase-js-sdk/issues/608
- const enablePersistence = () => {
- try {
- return from(this.firestore.enablePersistence(persistenceSettings || undefined).then(() => true, () => false));
- } catch (e) {
- return of(false);
- }
- };
- this.persistenceEnabled$ = zone.runOutsideAngular(enablePersistence);
- } else {
- this.persistenceEnabled$ = of(false);
- }
+ if (shouldEnablePersistence && !isPlatformServer(platformId)) {
+ // We need to try/catch here because not all enablePersistence() failures are caught
+ // https://github.com/firebase/firebase-js-sdk/issues/608
+ const enablePersistence = () => {
+ try {
+ return from(firestore.enablePersistence(persistenceSettings || undefined).then(() => true, () => false));
+ } catch (e) {
+ if (typeof console !== 'undefined') { console.warn(e); }
+ return of(false);
+ }
+ };
+ return [firestore, zone.runOutsideAngular(enablePersistence)];
+ } else {
+ return [firestore, of(false)];
+ }
+
+ }, [settings, useEmulator, shouldEnablePersistence]);
}
/**
diff --git a/src/functions/functions.ts b/src/functions/functions.ts
index e22579122..a6083f4b4 100644
--- a/src/functions/functions.ts
+++ b/src/functions/functions.ts
@@ -1,6 +1,6 @@
import { Inject, Injectable, InjectionToken, NgZone, Optional } from '@angular/core';
import { from, Observable, of } from 'rxjs';
-import { map, observeOn, shareReplay, switchMap, tap } from 'rxjs/operators';
+import { map, observeOn, shareReplay, switchMap } from 'rxjs/operators';
import {
FIREBASE_APP_NAME,
FIREBASE_OPTIONS,
@@ -15,6 +15,7 @@ import {
import firebase from 'firebase/app';
import { proxyPolyfillCompat } from './base';
import { HttpsCallableOptions } from '@firebase/functions-types';
+import { ɵfetchInstance } from '@angular/fire';
export const ORIGIN = new InjectionToken('angularfire2.functions.origin');
export const REGION = new InjectionToken('angularfire2.functions.region');
@@ -46,30 +47,30 @@ export class AngularFireFunctions {
@Optional() @Inject(USE_EMULATOR) _useEmulator: any, // can't use the tuple here
) {
const schedulers = new ɵAngularFireSchedulers(zone);
+ const useEmulator: UseEmulatorArguments | null = _useEmulator;
const functions = of(undefined).pipe(
observeOn(schedulers.outsideAngular),
switchMap(() => import('firebase/functions')),
map(() => ɵfirebaseAppFactory(options, zone, nameOrConfig)),
- map(app => {
+ map(app => ɵfetchInstance(`${app.name}.functions.${region || origin}`, 'AngularFireFunctions', app, () => {
+ let functions: firebase.functions.Functions;
if (newOriginBehavior) {
if (region && origin) {
throw new Error('REGION and ORIGIN can\'t be used at the same time.');
}
- return app.functions(region || origin || undefined);
+ functions = app.functions(region || origin || undefined);
} else {
- return app.functions(region || undefined);
+ functions = app.functions(region || undefined);
}
- }),
- tap(functions => {
- const useEmulator: UseEmulatorArguments | null = _useEmulator;
if (!newOriginBehavior && !useEmulator && origin) {
functions.useFunctionsEmulator(origin);
}
if (useEmulator) {
functions.useEmulator(...useEmulator);
}
- }),
+ return functions;
+ }, [region, origin, useEmulator])),
shareReplay({ bufferSize: 1, refCount: false })
);
diff --git a/src/messaging/messaging.ts b/src/messaging/messaging.ts
index 75948c7c4..b8006314f 100644
--- a/src/messaging/messaging.ts
+++ b/src/messaging/messaging.ts
@@ -15,6 +15,7 @@ import {
} from '@angular/fire';
import { isPlatformServer } from '@angular/common';
import { proxyPolyfillCompat } from './base';
+import { ɵfetchInstance } from '@angular/fire';
export const VAPID_KEY = new InjectionToken('angularfire2.messaging.vapid-key');
export const SERVICE_WORKER = new InjectionToken>('angularfire2.messaging.service-worker-registeration');
@@ -54,7 +55,7 @@ export class AngularFireMessaging {
observeOn(schedulers.insideAngular),
switchMap(() => isPlatformServer(platformId) ? EMPTY : import('firebase/messaging')),
map(() => ɵfirebaseAppFactory(options, zone, nameOrConfig)),
- switchMap(async app => {
+ switchMap(app => ɵfetchInstance(`${app.name}.messaging`, 'AngularFireMessaging', app, async () => {
const messaging = app.messaging();
if (firebaseLTv8) {
if (vapidKey) {
@@ -65,7 +66,7 @@ export class AngularFireMessaging {
}
}
return messaging;
- }),
+ }, [vapidKey, serviceWorker])),
shareReplay({ bufferSize: 1, refCount: false })
);
diff --git a/src/performance/performance.ts b/src/performance/performance.ts
index 965867a75..6bc4dd958 100644
--- a/src/performance/performance.ts
+++ b/src/performance/performance.ts
@@ -5,6 +5,7 @@ import firebase from 'firebase/app';
import { FirebaseApp, ɵapplyMixins, ɵlazySDKProxy, ɵPromiseProxy } from '@angular/fire';
import { isPlatformBrowser } from '@angular/common';
import { proxyPolyfillCompat } from './base';
+import { ɵfetchInstance } from '@angular/fire';
// SEMVER @ v6, drop and move core ng metrics to a service
export const AUTOMATICALLY_TRACE_CORE_NG_METRICS = new InjectionToken('angularfire2.performance.auto_trace');
@@ -32,15 +33,16 @@ export class AngularFirePerformance {
this.performance = of(undefined).pipe(
switchMap(() => isPlatformBrowser(platformId) ? zone.runOutsideAngular(() => import('firebase/performance')) : EMPTY),
- map(() => zone.runOutsideAngular(() => app.performance())),
- tap(performance => {
+ map(() => ɵfetchInstance(`performance`, 'AngularFirePerformance', app, () => {
+ const performance = zone.runOutsideAngular(() => app.performance());
if (instrumentationEnabled === false) {
performance.instrumentationEnabled = false;
}
if (dataCollectionEnabled === false) {
performance.dataCollectionEnabled = false;
}
- }),
+ return performance;
+ }, [instrumentationEnabled, dataCollectionEnabled])),
shareReplay({ bufferSize: 1, refCount: false })
);
diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts
index bc0cea926..2ee7c5c69 100644
--- a/src/remote-config/remote-config.ts
+++ b/src/remote-config/remote-config.ts
@@ -31,6 +31,7 @@ import { isPlatformBrowser } from '@angular/common';
import firebase from 'firebase/app';
import { Settings } from './interfaces';
import { proxyPolyfillCompat } from './base';
+import { ɵfetchInstance } from '@angular/fire';
export interface ConfigTemplate {
[key: string]: string | number | boolean;
@@ -149,15 +150,16 @@ export class AngularFireRemoteConfig {
switchMap(() => import('@firebase/remote-config')),
tap(rc => rc.registerRemoteConfig && rc.registerRemoteConfig(firebase as any)),
map(() => ɵfirebaseAppFactory(options, zone, nameOrConfig)),
- map(app => app.remoteConfig()),
- tap(rc => {
+ map(app => ɵfetchInstance(`${app.name}.remote-config`, 'AngularFireRemoteConfig', app, () => {
+ const rc = app.remoteConfig();
if (settings) {
rc.settings = settings;
}
if (defaultConfig) {
rc.defaultConfig = defaultConfig;
}
- }),
+ return rc;
+ }, [settings, defaultConfig])),
// tslint:disable-next-line
startWith(undefined),
shareReplay({ bufferSize: 1, refCount: false })
diff --git a/src/storage/storage.ts b/src/storage/storage.ts
index c204ebb18..e5e906acf 100644
--- a/src/storage/storage.ts
+++ b/src/storage/storage.ts
@@ -7,13 +7,13 @@ import {
FirebaseAppConfig,
FirebaseOptions,
ɵAngularFireSchedulers,
+ ɵfetchInstance,
ɵfirebaseAppFactory,
ɵkeepUnstableUntilFirstFactory
} from '@angular/fire';
import { UploadMetadata } from './interfaces';
import 'firebase/storage';
import firebase from 'firebase/app';
-import { registerStorage } from '@firebase/storage';
export const BUCKET = new InjectionToken('angularfire2.storageBucket');
export const MAX_UPLOAD_RETRY_TIME = new InjectionToken('angularfire2.storage.maxUploadRetryTime');
@@ -47,13 +47,10 @@ export class AngularFireStorage {
) {
this.schedulers = new ɵAngularFireSchedulers(zone);
this.keepUnstableUntilFirst = ɵkeepUnstableUntilFirstFactory(this.schedulers);
+ const app = ɵfirebaseAppFactory(options, zone, nameOrConfig);
- this.storage = zone.runOutsideAngular(() => {
- const app = ɵfirebaseAppFactory(options, zone, nameOrConfig);
- if (registerStorage) {
- registerStorage(firebase as any);
- }
- const storage = app.storage(storageBucket || undefined);
+ this.storage = ɵfetchInstance(`${app.name}.storage.${storageBucket}`, 'AngularFireStorage', app, () => {
+ const storage = zone.runOutsideAngular(() => app.storage(storageBucket || undefined));
if (maxUploadRetryTime) {
storage.setMaxUploadRetryTime(maxUploadRetryTime);
}
@@ -61,7 +58,7 @@ export class AngularFireStorage {
storage.setMaxOperationRetryTime(maxOperationRetryTime);
}
return storage;
- });
+ }, [maxUploadRetryTime, maxOperationRetryTime]);
}
ref(path: string) {