From e5be92725ddf18665e308deb6eaae5b8d023bdc3 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Wed, 2 Oct 2019 23:00:38 +0200 Subject: [PATCH 01/37] Firebase v7, analytics and remote-config --- package.json | 2 +- src/analytics/analytics.module.ts | 11 + src/analytics/analytics.spec.ts | 168 ++++++++++ src/analytics/analytics.ts | 72 +++++ src/analytics/index.spec.ts | 1 + src/analytics/index.ts | 1 + src/analytics/package.json | 8 + src/analytics/public_api.ts | 2 + src/analytics/test-config.ts | 7 + src/analytics/tsconfig-build.json | 33 ++ src/analytics/tsconfig-esm.json | 19 ++ src/analytics/tsconfig-test.json | 14 + src/core/firebase.app.module.ts | 6 +- src/remote-config/index.spec.ts | 1 + src/remote-config/index.ts | 1 + src/remote-config/package.json | 8 + src/remote-config/public_api.ts | 2 + src/remote-config/remote-config.module.ts | 7 + src/remote-config/remote-config.spec.ts | 168 ++++++++++ src/remote-config/remote-config.ts | 59 ++++ src/remote-config/test-config.ts | 7 + src/remote-config/tsconfig-build.json | 33 ++ src/remote-config/tsconfig-esm.json | 19 ++ src/remote-config/tsconfig-test.json | 14 + src/tsconfig.json | 2 + tools/build.js | 28 +- yarn.lock | 355 ++++++++++++---------- 27 files changed, 890 insertions(+), 158 deletions(-) create mode 100644 src/analytics/analytics.module.ts create mode 100644 src/analytics/analytics.spec.ts create mode 100644 src/analytics/analytics.ts create mode 100644 src/analytics/index.spec.ts create mode 100644 src/analytics/index.ts create mode 100644 src/analytics/package.json create mode 100644 src/analytics/public_api.ts create mode 100644 src/analytics/test-config.ts create mode 100644 src/analytics/tsconfig-build.json create mode 100644 src/analytics/tsconfig-esm.json create mode 100644 src/analytics/tsconfig-test.json create mode 100644 src/remote-config/index.spec.ts create mode 100644 src/remote-config/index.ts create mode 100644 src/remote-config/package.json create mode 100644 src/remote-config/public_api.ts create mode 100644 src/remote-config/remote-config.module.ts create mode 100644 src/remote-config/remote-config.spec.ts create mode 100644 src/remote-config/remote-config.ts create mode 100644 src/remote-config/test-config.ts create mode 100644 src/remote-config/tsconfig-build.json create mode 100644 src/remote-config/tsconfig-esm.json create mode 100644 src/remote-config/tsconfig-test.json diff --git a/package.json b/package.json index 8a0502b46..1f00cd3f0 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@angular/platform-browser": ">=6.0.0 <9 || ^9.0.0-0", "@angular/platform-browser-dynamic": ">=6.0.0 <9 || ^9.0.0-0", "@angular/router": ">=6.0.0 <9 || ^9.0.0-0", - "firebase": ">= 5.5.7 <7", + "firebase": ">= 5.5.7 <8", "firebase-tools": "^6.10.0", "fuzzy": "^0.1.3", "inquirer": "^6.2.2", diff --git a/src/analytics/analytics.module.ts b/src/analytics/analytics.module.ts new file mode 100644 index 000000000..31371ef46 --- /dev/null +++ b/src/analytics/analytics.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { AngularFireAnalytics } from './analytics'; + +@NgModule({ + providers: [ AngularFireAnalytics ] +}) +export class AngularFireAnalyticsModule { + constructor(_: AngularFireAnalytics) { + // DI inject Analytics here for the automatic data collection + } +} diff --git a/src/analytics/analytics.spec.ts b/src/analytics/analytics.spec.ts new file mode 100644 index 000000000..30e9b28c6 --- /dev/null +++ b/src/analytics/analytics.spec.ts @@ -0,0 +1,168 @@ +import { User } from 'firebase/app'; +import { Observable, Subject } from 'rxjs' +import { TestBed, inject } from '@angular/core/testing'; +import { FirebaseApp, FirebaseOptionsToken, AngularFireModule, FirebaseNameOrConfigToken } from '@angular/fire'; +import { AngularFireAuth, AngularFireAuthModule } from '@angular/fire/auth'; +import { COMMON_CONFIG } from './test-config'; +import { take, skip } from 'rxjs/operators'; + +function authTake(auth: Observable, count: number): Observable { + return take.call(auth, 1); +} + +function authSkip(auth: Observable, count: number): Observable { + return skip.call(auth, 1); +} + +const firebaseUser = { + uid: '12345', + providerData: [{ displayName: 'jeffbcrossyface' }] +}; + +describe('AngularFireAuth', () => { + let app: FirebaseApp; + let afAuth: AngularFireAuth; + let authSpy: jasmine.Spy; + let mockAuthState: Subject; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG), + AngularFireAuthModule + ] + }); + inject([FirebaseApp, AngularFireAuth], (app_: FirebaseApp, _auth: AngularFireAuth) => { + app = app_; + afAuth = _auth; + })(); + + mockAuthState = new Subject(); + spyOn(afAuth, 'authState').and.returnValue(mockAuthState); + spyOn(afAuth, 'idToken').and.returnValue(mockAuthState); + afAuth.authState = mockAuthState as Observable; + afAuth.idToken = mockAuthState as Observable; + }); + + afterEach(done => { + afAuth.auth.app.delete().then(done, done.fail); + }); + + describe('Zones', () => { + it('should call operators and subscriber in the same zone as when service was initialized', (done) => { + // Initialize the app outside of the zone, to mimick real life behavior. + let ngZone = Zone.current.fork({ + name: 'ngZone' + }); + ngZone.run(() => { + const subs = [ + afAuth.authState.subscribe(user => { + expect(Zone.current.name).toBe('ngZone'); + done(); + }, done.fail), + afAuth.authState.subscribe(user => { + expect(Zone.current.name).toBe('ngZone'); + done(); + }, done.fail) + ]; + mockAuthState.next(firebaseUser); + subs.forEach(s => s.unsubscribe()); + }); + }); + }); + + it('should be exist', () => { + expect(afAuth instanceof AngularFireAuth).toBe(true); + }); + + it('should have the Firebase Auth instance', () => { + expect(afAuth.auth).toBeDefined(); + }); + + it('should have an initialized Firebase app', () => { + expect(afAuth.auth.app).toBeDefined(); + expect(afAuth.auth.app).toEqual(app); + }); + + it('should emit auth updates through authState', (done: any) => { + let count = 0; + + // Check that the first value is null and second is the auth user + const subs = afAuth.authState.subscribe(user => { + if (count === 0) { + expect(user).toBe(null!); + count = count + 1; + mockAuthState.next(firebaseUser); + } else { + expect(user).toEqual(firebaseUser); + subs.unsubscribe(); + done(); + } + }, done, done.fail); + mockAuthState.next(null!); + }); + + it('should emit auth updates through idToken', (done: any) => { + let count = 0; + + // Check that the first value is null and second is the auth user + const subs = afAuth.idToken.subscribe(user => { + if (count === 0) { + expect(user).toBe(null!); + count = count + 1; + mockAuthState.next(firebaseUser); + } else { + expect(user).toEqual(firebaseUser); + subs.unsubscribe(); + done(); + } + }, done, done.fail); + mockAuthState.next(null!); + }); + +}); + +const FIREBASE_APP_NAME_TOO = (Math.random() + 1).toString(36).substring(7); + +describe('AngularFireAuth with different app', () => { + let app: FirebaseApp; + let afAuth: AngularFireAuth; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG), + AngularFireAuthModule + ], + providers: [ + { provide: FirebaseNameOrConfigToken, useValue: FIREBASE_APP_NAME_TOO }, + { provide: FirebaseOptionsToken, useValue: COMMON_CONFIG } + ] + }); + inject([FirebaseApp, AngularFireAuth], (app_: FirebaseApp, _afAuth: AngularFireAuth) => { + app = app_; + afAuth = _afAuth; + })(); + }); + + afterEach(done => { + app.delete().then(done, done.fail); + }); + + describe('', () => { + + it('should be an AngularFireAuth type', () => { + expect(afAuth instanceof AngularFireAuth).toEqual(true); + }); + + it('should have an initialized Firebase app', () => { + expect(afAuth.auth.app).toBeDefined(); + expect(afAuth.auth.app).toEqual(app); + }); + + it('should have an initialized Firebase app instance member', () => { + expect(afAuth.auth.app.name).toEqual(FIREBASE_APP_NAME_TOO); + }); + }); + +}); diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts new file mode 100644 index 000000000..1607f3f0b --- /dev/null +++ b/src/analytics/analytics.ts @@ -0,0 +1,72 @@ +import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; +import { Observable, from, of } from 'rxjs'; +import { map, switchMap, tap, filter } from 'rxjs/operators'; +import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular } from '@angular/fire'; +import { Router, NavigationEnd } from '@angular/router'; +import { FirebaseAnalytics, FirebaseOptionsToken, FirebaseNameOrConfigToken, _firebaseAppFactory } from '@angular/fire'; + +export const AUTOMATICALLY_SET_CURRENT_SCREEN = new InjectionToken('angularfire2.analytics.setCurrentScreen'); +export const ANALYTICS_COLLECTION_ENABLED = new InjectionToken('angularfire2.analytics.analyticsCollectionEnabled'); +export const AUTOMATICALLY_TRACK_USER_IDENTIFIER = new InjectionToken('angularfire2.analytics.trackUserIdentifier'); + +@Injectable() +export class AngularFireAnalytics { + + /** + * Firebase Analytics instance + */ + public readonly analytics: Observable; + + constructor( + @Inject(FirebaseOptionsToken) options:FirebaseOptions, + @Optional() @Inject(FirebaseNameOrConfigToken) nameOrConfig:string|FirebaseAppConfig|null|undefined, + @Optional() router:Router, + @Optional() @Inject(AUTOMATICALLY_SET_CURRENT_SCREEN) automaticallySetCurrentScreen:boolean|null, + @Optional() @Inject(ANALYTICS_COLLECTION_ENABLED) analyticsCollectionEnabled:boolean|null, + @Optional() @Inject(AUTOMATICALLY_TRACK_USER_IDENTIFIER) automaticallyTrackUserIdentifier:boolean|null, + private zone: NgZone + ) { + // @ts-ignore zapping in the UMD in the build script + const requireAnalytics = from(import('firebase/analytics')); + + this.analytics = requireAnalytics.pipe( + map(() => _firebaseAppFactory(options, nameOrConfig)), + map(app => app.analytics()), + tap(analytics => { + if (analyticsCollectionEnabled == false) { analytics.setAnalyticsCollectionEnabled(false) } + }), + runOutsideAngular(zone) + ); + + if (router && automaticallySetCurrentScreen !== false) { + this.analytics.pipe( + switchMap(analytics => + router.events.pipe( + filter(e => e instanceof NavigationEnd), + tap(e => console.log(e)), + tap(e => analytics.setCurrentScreen(e.url)) + ) + ) + ).subscribe(); + } + + if (automaticallyTrackUserIdentifier !== false) { + this.analytics.pipe( + switchMap(analytics => { + if (analytics.app.auth) { + const auth = analytics.app.auth(); + return new Observable(auth.onAuthStateChanged.bind(auth)).pipe( + tap(user => console.log("uid", user && user.uid)), + tap(user => user ? analytics.setUserId(user.uid) : analytics.setUserId(null!)) + ) + } else { + return of() + } + }), + runOutsideAngular(zone) + ).subscribe() + } + + } + +} diff --git a/src/analytics/index.spec.ts b/src/analytics/index.spec.ts new file mode 100644 index 000000000..f5d1cb076 --- /dev/null +++ b/src/analytics/index.spec.ts @@ -0,0 +1 @@ +import './analytics.spec'; diff --git a/src/analytics/index.ts b/src/analytics/index.ts new file mode 100644 index 000000000..4aaf8f92e --- /dev/null +++ b/src/analytics/index.ts @@ -0,0 +1 @@ +export * from './public_api'; diff --git a/src/analytics/package.json b/src/analytics/package.json new file mode 100644 index 000000000..8557582d6 --- /dev/null +++ b/src/analytics/package.json @@ -0,0 +1,8 @@ +{ + "name": "@angular/fire/analytics", + "main": "../bundles/analytics.umd.js", + "module": "index.js", + "es2015": "./es2015/index.js", + "typings": "index.d.ts", + "sideEffects": false +} diff --git a/src/analytics/public_api.ts b/src/analytics/public_api.ts new file mode 100644 index 000000000..aee785b0e --- /dev/null +++ b/src/analytics/public_api.ts @@ -0,0 +1,2 @@ +export * from './analytics'; +export * from './analytics.module'; diff --git a/src/analytics/test-config.ts b/src/analytics/test-config.ts new file mode 100644 index 000000000..4b69c98dd --- /dev/null +++ b/src/analytics/test-config.ts @@ -0,0 +1,7 @@ + +export const COMMON_CONFIG = { + apiKey: "AIzaSyBVSy3YpkVGiKXbbxeK0qBnu3-MNZ9UIjA", + authDomain: "angularfire2-test.firebaseapp.com", + databaseURL: "https://angularfire2-test.firebaseio.com", + storageBucket: "angularfire2-test.appspot.com", +}; diff --git a/src/analytics/tsconfig-build.json b/src/analytics/tsconfig-build.json new file mode 100644 index 000000000..6277bf530 --- /dev/null +++ b/src/analytics/tsconfig-build.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "module": "es2015", + "target": "es2015", + "noImplicitAny": false, + "outDir": "../../dist/packages-dist/analytics/es2015", + "rootDir": ".", + "sourceMap": true, + "inlineSources": true, + "declaration": false, + "removeComments": true, + "strictNullChecks": true, + "lib": ["es2015", "dom", "es2015.promise", "es2015.collection", "es2015.iterable"], + "skipLibCheck": true, + "moduleResolution": "node", + "paths": { + "@angular/fire": ["../../dist/packages-dist"] + } + }, + "files": [ + "index.ts", + "../../node_modules/zone.js/dist/zone.js.d.ts" + ], + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableSummariesForJit": false + } +} + diff --git a/src/analytics/tsconfig-esm.json b/src/analytics/tsconfig-esm.json new file mode 100644 index 000000000..413beb9d0 --- /dev/null +++ b/src/analytics/tsconfig-esm.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig-build.json", + "compilerOptions": { + "target": "es5", + "outDir": "../../dist/packages-dist/analytics", + "declaration": true + }, + "files": [ + "public_api.ts", + "../../node_modules/zone.js/dist/zone.js.d.ts" + ], + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableSummariesForJit": false, + "flatModuleOutFile": "index.js", + "flatModuleId": "@angular/fire/analytics" + } +} diff --git a/src/analytics/tsconfig-test.json b/src/analytics/tsconfig-test.json new file mode 100644 index 000000000..dfed897cc --- /dev/null +++ b/src/analytics/tsconfig-test.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig-esm.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@angular/fire": ["../../dist/packages-dist"], + "@angular/fire/analytics": ["../../dist/packages-dist/analytics"] + } + }, + "files": [ + "index.spec.ts", + "../../node_modules/zone.js/dist/zone.js.d.ts" + ] +} diff --git a/src/core/firebase.app.module.ts b/src/core/firebase.app.module.ts index ed35ff51d..63c72980d 100644 --- a/src/core/firebase.app.module.ts +++ b/src/core/firebase.app.module.ts @@ -1,5 +1,5 @@ import { InjectionToken, NgModule, Optional } from '@angular/core'; -import { auth, database, firestore, functions, messaging, storage } from 'firebase/app'; +import { auth, database, firestore, functions, messaging, storage, analytics, remoteConfig } from 'firebase/app'; // @ts-ignore (https://github.com/firebase/firebase-js-sdk/pull/1206) import firebase from 'firebase/app'; // once fixed can pull in as "default as firebase" above @@ -12,16 +12,19 @@ export const FirebaseNameOrConfigToken = new InjectionToken FirebaseAnalytics; auth: () => FirebaseAuth; database: (databaseURL?: string) => FirebaseDatabase; messaging: () => FirebaseMessaging; @@ -30,6 +33,7 @@ export class FirebaseApp { delete: () => Promise; firestore: () => FirebaseFirestore; functions: (region?: string) => FirebaseFunctions; + remoteConfig: () => FirebaseRemoteConfig; } export function _firebaseAppFactory(options: FirebaseOptions, nameOrConfig?: string|FirebaseAppConfig|null) { diff --git a/src/remote-config/index.spec.ts b/src/remote-config/index.spec.ts new file mode 100644 index 000000000..e79a978ab --- /dev/null +++ b/src/remote-config/index.spec.ts @@ -0,0 +1 @@ +import './remote-config.spec'; diff --git a/src/remote-config/index.ts b/src/remote-config/index.ts new file mode 100644 index 000000000..4aaf8f92e --- /dev/null +++ b/src/remote-config/index.ts @@ -0,0 +1 @@ +export * from './public_api'; diff --git a/src/remote-config/package.json b/src/remote-config/package.json new file mode 100644 index 000000000..d1a1f1a66 --- /dev/null +++ b/src/remote-config/package.json @@ -0,0 +1,8 @@ +{ + "name": "@angular/fire/remote-config", + "main": "../bundles/remote-config.umd.js", + "module": "index.js", + "es2015": "./es2015/index.js", + "typings": "index.d.ts", + "sideEffects": false +} diff --git a/src/remote-config/public_api.ts b/src/remote-config/public_api.ts new file mode 100644 index 000000000..f3239f665 --- /dev/null +++ b/src/remote-config/public_api.ts @@ -0,0 +1,2 @@ +export * from './remote-config'; +export * from './remote-config.module'; diff --git a/src/remote-config/remote-config.module.ts b/src/remote-config/remote-config.module.ts new file mode 100644 index 000000000..fd0fe7ac5 --- /dev/null +++ b/src/remote-config/remote-config.module.ts @@ -0,0 +1,7 @@ +import { NgModule } from '@angular/core'; +import { AngularFireRemoteConfig } from './remote-config'; + +@NgModule({ + providers: [ AngularFireRemoteConfig ] +}) +export class AngularFireRemoteConfigModule { } diff --git a/src/remote-config/remote-config.spec.ts b/src/remote-config/remote-config.spec.ts new file mode 100644 index 000000000..30e9b28c6 --- /dev/null +++ b/src/remote-config/remote-config.spec.ts @@ -0,0 +1,168 @@ +import { User } from 'firebase/app'; +import { Observable, Subject } from 'rxjs' +import { TestBed, inject } from '@angular/core/testing'; +import { FirebaseApp, FirebaseOptionsToken, AngularFireModule, FirebaseNameOrConfigToken } from '@angular/fire'; +import { AngularFireAuth, AngularFireAuthModule } from '@angular/fire/auth'; +import { COMMON_CONFIG } from './test-config'; +import { take, skip } from 'rxjs/operators'; + +function authTake(auth: Observable, count: number): Observable { + return take.call(auth, 1); +} + +function authSkip(auth: Observable, count: number): Observable { + return skip.call(auth, 1); +} + +const firebaseUser = { + uid: '12345', + providerData: [{ displayName: 'jeffbcrossyface' }] +}; + +describe('AngularFireAuth', () => { + let app: FirebaseApp; + let afAuth: AngularFireAuth; + let authSpy: jasmine.Spy; + let mockAuthState: Subject; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG), + AngularFireAuthModule + ] + }); + inject([FirebaseApp, AngularFireAuth], (app_: FirebaseApp, _auth: AngularFireAuth) => { + app = app_; + afAuth = _auth; + })(); + + mockAuthState = new Subject(); + spyOn(afAuth, 'authState').and.returnValue(mockAuthState); + spyOn(afAuth, 'idToken').and.returnValue(mockAuthState); + afAuth.authState = mockAuthState as Observable; + afAuth.idToken = mockAuthState as Observable; + }); + + afterEach(done => { + afAuth.auth.app.delete().then(done, done.fail); + }); + + describe('Zones', () => { + it('should call operators and subscriber in the same zone as when service was initialized', (done) => { + // Initialize the app outside of the zone, to mimick real life behavior. + let ngZone = Zone.current.fork({ + name: 'ngZone' + }); + ngZone.run(() => { + const subs = [ + afAuth.authState.subscribe(user => { + expect(Zone.current.name).toBe('ngZone'); + done(); + }, done.fail), + afAuth.authState.subscribe(user => { + expect(Zone.current.name).toBe('ngZone'); + done(); + }, done.fail) + ]; + mockAuthState.next(firebaseUser); + subs.forEach(s => s.unsubscribe()); + }); + }); + }); + + it('should be exist', () => { + expect(afAuth instanceof AngularFireAuth).toBe(true); + }); + + it('should have the Firebase Auth instance', () => { + expect(afAuth.auth).toBeDefined(); + }); + + it('should have an initialized Firebase app', () => { + expect(afAuth.auth.app).toBeDefined(); + expect(afAuth.auth.app).toEqual(app); + }); + + it('should emit auth updates through authState', (done: any) => { + let count = 0; + + // Check that the first value is null and second is the auth user + const subs = afAuth.authState.subscribe(user => { + if (count === 0) { + expect(user).toBe(null!); + count = count + 1; + mockAuthState.next(firebaseUser); + } else { + expect(user).toEqual(firebaseUser); + subs.unsubscribe(); + done(); + } + }, done, done.fail); + mockAuthState.next(null!); + }); + + it('should emit auth updates through idToken', (done: any) => { + let count = 0; + + // Check that the first value is null and second is the auth user + const subs = afAuth.idToken.subscribe(user => { + if (count === 0) { + expect(user).toBe(null!); + count = count + 1; + mockAuthState.next(firebaseUser); + } else { + expect(user).toEqual(firebaseUser); + subs.unsubscribe(); + done(); + } + }, done, done.fail); + mockAuthState.next(null!); + }); + +}); + +const FIREBASE_APP_NAME_TOO = (Math.random() + 1).toString(36).substring(7); + +describe('AngularFireAuth with different app', () => { + let app: FirebaseApp; + let afAuth: AngularFireAuth; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG), + AngularFireAuthModule + ], + providers: [ + { provide: FirebaseNameOrConfigToken, useValue: FIREBASE_APP_NAME_TOO }, + { provide: FirebaseOptionsToken, useValue: COMMON_CONFIG } + ] + }); + inject([FirebaseApp, AngularFireAuth], (app_: FirebaseApp, _afAuth: AngularFireAuth) => { + app = app_; + afAuth = _afAuth; + })(); + }); + + afterEach(done => { + app.delete().then(done, done.fail); + }); + + describe('', () => { + + it('should be an AngularFireAuth type', () => { + expect(afAuth instanceof AngularFireAuth).toEqual(true); + }); + + it('should have an initialized Firebase app', () => { + expect(afAuth.auth.app).toBeDefined(); + expect(afAuth.auth.app).toEqual(app); + }); + + it('should have an initialized Firebase app instance member', () => { + expect(afAuth.auth.app.name).toEqual(FIREBASE_APP_NAME_TOO); + }); + }); + +}); diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts new file mode 100644 index 000000000..87ce631bc --- /dev/null +++ b/src/remote-config/remote-config.ts @@ -0,0 +1,59 @@ +import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; +import { Observable, from } from 'rxjs'; +import { map, switchMap, tap } from 'rxjs/operators'; +import { FirebaseAppConfig, FirebaseOptions } from '@angular/fire'; +import { remoteConfig } from 'firebase/app'; + +export interface DefaultConfig {[key:string]: string|number|boolean}; + +export const SETTINGS = new InjectionToken('angularfire2.remoteConfig.settings'); +export const DEFAULT_CONFIG = new InjectionToken('angularfire2.remoteConfig.defaultConfig'); + +import { FirebaseRemoteConfig, FirebaseOptionsToken, FirebaseNameOrConfigToken, _firebaseAppFactory, runOutsideAngular } from '@angular/fire'; + +@Injectable() +export class AngularFireRemoteConfig { + + /** + * Firebase RemoteConfig instance + */ + public readonly remoteConfig: Observable; + + public readonly freshConfiguration: Observable<{[key:string]: remoteConfig.Value}>; + + public readonly configuration: Observable<{[key:string]: remoteConfig.Value}>; + + constructor( + @Inject(FirebaseOptionsToken) options:FirebaseOptions, + @Optional() @Inject(FirebaseNameOrConfigToken) nameOrConfig:string|FirebaseAppConfig|null|undefined, + @Optional() @Inject(SETTINGS) settings:remoteConfig.Settings|null, + @Optional() @Inject(DEFAULT_CONFIG) defaultConfig:DefaultConfig|null, + private zone: NgZone + ) { + // @ts-ignore zapping in the UMD in the build script + const requireRemoteConfig = from(import('firebase/remote-config')); + + this.remoteConfig = requireRemoteConfig.pipe( + map(() => _firebaseAppFactory(options, nameOrConfig)), + map(app => app.remoteConfig()), + tap(rc => { + if (settings) { rc.settings = settings } + if (defaultConfig) { rc.defaultConfig = defaultConfig } + }), + runOutsideAngular(zone) + ); + + this.freshConfiguration = this.remoteConfig.pipe( + switchMap(rc => rc.fetchAndActivate().then(() => rc.getAll())), + runOutsideAngular(zone) + ) + + this.configuration = this.remoteConfig.pipe( + switchMap(rc => rc.activate().then(() => rc)), + tap(rc => rc.fetch()), + map(rc => rc.getAll()), + runOutsideAngular(zone) + ) + } + +} diff --git a/src/remote-config/test-config.ts b/src/remote-config/test-config.ts new file mode 100644 index 000000000..4b69c98dd --- /dev/null +++ b/src/remote-config/test-config.ts @@ -0,0 +1,7 @@ + +export const COMMON_CONFIG = { + apiKey: "AIzaSyBVSy3YpkVGiKXbbxeK0qBnu3-MNZ9UIjA", + authDomain: "angularfire2-test.firebaseapp.com", + databaseURL: "https://angularfire2-test.firebaseio.com", + storageBucket: "angularfire2-test.appspot.com", +}; diff --git a/src/remote-config/tsconfig-build.json b/src/remote-config/tsconfig-build.json new file mode 100644 index 000000000..4a9c81f39 --- /dev/null +++ b/src/remote-config/tsconfig-build.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "module": "es2015", + "target": "es2015", + "noImplicitAny": false, + "outDir": "../../dist/packages-dist/remote-config/es2015", + "rootDir": ".", + "sourceMap": true, + "inlineSources": true, + "declaration": false, + "removeComments": true, + "strictNullChecks": true, + "lib": ["es2015", "dom", "es2015.promise", "es2015.collection", "es2015.iterable"], + "skipLibCheck": true, + "moduleResolution": "node", + "paths": { + "@angular/fire": ["../../dist/packages-dist"] + } + }, + "files": [ + "index.ts", + "../../node_modules/zone.js/dist/zone.js.d.ts" + ], + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableSummariesForJit": false + } +} + diff --git a/src/remote-config/tsconfig-esm.json b/src/remote-config/tsconfig-esm.json new file mode 100644 index 000000000..88dbfecf0 --- /dev/null +++ b/src/remote-config/tsconfig-esm.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig-build.json", + "compilerOptions": { + "target": "es5", + "outDir": "../../dist/packages-dist/remote-config", + "declaration": true + }, + "files": [ + "public_api.ts", + "../../node_modules/zone.js/dist/zone.js.d.ts" + ], + "angularCompilerOptions": { + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableSummariesForJit": false, + "flatModuleOutFile": "index.js", + "flatModuleId": "@angular/fire/remote-config" + } +} diff --git a/src/remote-config/tsconfig-test.json b/src/remote-config/tsconfig-test.json new file mode 100644 index 000000000..29d4b4f44 --- /dev/null +++ b/src/remote-config/tsconfig-test.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig-esm.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@angular/fire": ["../../dist/packages-dist"], + "@angular/fire/auth": ["../../dist/packages-dist/auth"] + } + }, + "files": [ + "index.spec.ts", + "../../node_modules/zone.js/dist/zone.js.d.ts" + ] +} diff --git a/src/tsconfig.json b/src/tsconfig.json index 6978e6669..9ec8d1ee1 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -11,6 +11,7 @@ "noFallthroughCasesInSwitch": true, "paths": { "@angular/fire": ["./core"], + "@angular/fire/analytics": ["./analytics"], "@angular/fire/auth": ["./auth"], "@angular/fire/auth-guard": ["./auth-guard"], "@angular/fire/database": ["./database"], @@ -19,6 +20,7 @@ "@angular/fire/storage": ["./storage"], "@angular/fire/messaging": ["./messaging"], "@angular/fire/performance": ["./performance"], + "@angular/fire/remote-config": ["./remote-config"], "@angular/fire/database-deprecated": ["./database-deprecated"] }, "rootDir": ".", diff --git a/tools/build.js b/tools/build.js index fca00c273..769da1a38 100644 --- a/tools/build.js +++ b/tools/build.js @@ -21,6 +21,7 @@ const GLOBALS = { '@angular-devkit/core/node': 'ng-devkit.core.node', '@angular-devkit/architect': 'ng-devkit.architect', 'firebase': 'firebase', + 'firebase/analytics': 'firebase', 'firebase/app': 'firebase', 'firebase/auth': 'firebase', 'firebase/database': 'firebase', @@ -28,9 +29,11 @@ const GLOBALS = { 'firebase/firestore': 'firebase', 'firebase/functions': 'firebase', 'firebase/performance': 'firebase', + 'firebase/remote-config': 'firebase', 'firebase/storage': 'firebase', '@angular/fire': 'angularfire2', '@angular/fire/auth': 'angularfire2.auth', + '@angular/fire/analytics': 'angularfire2.analytics', '@angular/fire/auth-guard': 'angularfire2.auth_guard', '@angular/fire/database': 'angularfire2.database', '@angular/fire/database-deprecated': 'angularfire2.database_deprecated', @@ -39,6 +42,7 @@ const GLOBALS = { '@angular/fire/storage': 'angularfire2.storage', '@angular/fire/messaging': 'angularfire2.messaging', '@angular/fire/performance': 'angularfire2.performance', + '@angular/fire/remote-config': 'angularfire2.remote_config', 'fs': 'node.fs', 'path': 'node.path', 'inquirer': 'inquirer' @@ -65,6 +69,7 @@ const VERSIONS = { const MODULE_NAMES = { core: 'angularfire2', + analytics: 'angularfire2.analytics', auth: 'angularfire2.auth', "auth-guard": 'angularfire2.auth_guard', database: 'angularfire2.database', @@ -74,11 +79,13 @@ const MODULE_NAMES = { schematics: 'angularfire2.schematics', storage: 'angularfire2.storage', messaging: 'angularfire2.messaging', - performance: 'angularfire2.performance' + performance: 'angularfire2.performance', + "remote-config": 'angularfire2.remote_config' }; const ENTRIES = { core: `${process.cwd()}/dist/packages-dist/index.js`, + analytics: `${process.cwd()}/dist/packages-dist/analytics/index.js`, auth: `${process.cwd()}/dist/packages-dist/auth/index.js`, "auth-guard": `${process.cwd()}/dist/packages-dist/auth-guard/index.js`, database: `${process.cwd()}/dist/packages-dist/database/index.js`, @@ -88,11 +95,13 @@ const ENTRIES = { schematics: `${process.cwd()}/dist/packages-dist/schematics/index.js`, storage: `${process.cwd()}/dist/packages-dist/storage/index.js`, messaging: `${process.cwd()}/dist/packages-dist/messaging/index.js`, - performance: `${process.cwd()}/dist/packages-dist/performance/index.js` + performance: `${process.cwd()}/dist/packages-dist/performance/index.js`, + "remote-config": `${process.cwd()}/dist/packages-dist/remote-config/index.js` }; const SRC_PKG_PATHS = { core: `${process.cwd()}/src/core/package.json`, + analytics: `${process.cwd()}/src/analytics/package.json`, auth: `${process.cwd()}/src/auth/package.json`, "auth-guard": `${process.cwd()}/src/auth-guard/package.json`, database: `${process.cwd()}/src/database/package.json`, @@ -103,12 +112,14 @@ const SRC_PKG_PATHS = { storage: `${process.cwd()}/src/storage/package.json`, messaging: `${process.cwd()}/src/messaging/package.json`, performance: `${process.cwd()}/src/performance/package.json`, + "remote-config": `${process.cwd()}/src/remote-config/package.json`, schematics: `${process.cwd()}/dist/packages-dist/schematics/versions.js`, "schematics-es2015": `${process.cwd()}/dist/packages-dist/schematics/es2015/versions.js` }; const DEST_PKG_PATHS = { core: `${process.cwd()}/dist/packages-dist/package.json`, + analytics: `${process.cwd()}/dist/packages-dist/analytics/package.json`, auth: `${process.cwd()}/dist/packages-dist/auth/package.json`, "auth-guard": `${process.cwd()}/dist/packages-dist/auth-guard/package.json`, database: `${process.cwd()}/dist/packages-dist/database/package.json`, @@ -119,6 +130,7 @@ const DEST_PKG_PATHS = { storage: `${process.cwd()}/dist/packages-dist/storage/package.json`, messaging: `${process.cwd()}/dist/packages-dist/messaging/package.json`, performance: `${process.cwd()}/dist/packages-dist/performance/package.json`, + "remote-config": `${process.cwd()}/dist/packages-dist/remote-config/package.json`, schematics: `${process.cwd()}/dist/packages-dist/schematics/versions.js`, "schematics-es2015": `${process.cwd()}/dist/packages-dist/schematics/es2015/versions.js` }; @@ -285,6 +297,8 @@ function copySchematicFiles() { function replaceDynamicImportsForUMD() { writeFileSync('./dist/packages-dist/bundles/performance.umd.js', readFileSync('./dist/packages-dist/bundles/performance.umd.js', 'utf8').replace("rxjs.from(import('firebase/performance'))", "rxjs.empty()")); writeFileSync('./dist/packages-dist/bundles/messaging.umd.js', readFileSync('./dist/packages-dist/bundles/messaging.umd.js', 'utf8').replace("rxjs.from(import('firebase/messaging'))", "rxjs.empty()")); + writeFileSync('./dist/packages-dist/bundles/remote-config.umd.js', readFileSync('./dist/packages-dist/bundles/remote-config.umd.js', 'utf8').replace("rxjs.from(import('firebase/remote-config'))", "rxjs.empty()")); + writeFileSync('./dist/packages-dist/bundles/analytics.umd.js', readFileSync('./dist/packages-dist/bundles/analytics.umd.js', 'utf8').replace("rxjs.from(import('firebase/analytics'))", "rxjs.empty()")); } function measure(module) { @@ -302,6 +316,7 @@ function measure(module) { function getVersions() { const paths = [ getDestPackageFile('core'), + getDestPackageFile('analytics'), getDestPackageFile('auth'), getDestPackageFile('auth-guard'), getDestPackageFile('database'), @@ -311,6 +326,7 @@ function getVersions() { getDestPackageFile('storage'), getDestPackageFile('messaging'), getDestPackageFile('performance'), + getDestPackageFile('remote-config'), getDestPackageFile('database-deprecated') ]; return paths @@ -344,6 +360,7 @@ function buildModule(name, globals) { */ function buildModules(globals) { const core$ = buildModule('core', globals); + const analytics$ = buildModule('analytics', globals); const auth$ = buildModule('auth', globals); const authGuard$ = buildModule('auth-guard', globals); const db$ = buildModule('database', globals); @@ -353,9 +370,11 @@ function buildModules(globals) { const storage$ = buildModule('storage', globals); const messaging$ = buildModule('messaging', globals); const performance$ = buildModule('performance', globals); + const remoteConfig$ = buildModule('remote-config', globals); const dbdep$ = buildModule('database-deprecated', globals); return forkJoin(core$, from(copyRootTest())).pipe( switchMapTo(schematics$), + switchMapTo(analytics$), switchMapTo(auth$), switchMapTo(authGuard$), switchMapTo(db$), @@ -364,6 +383,7 @@ function buildModules(globals) { switchMapTo(storage$), switchMapTo(messaging$), switchMapTo(performance$), + switchMapTo(remoteConfig$), switchMapTo(dbdep$) ); } @@ -383,6 +403,7 @@ function buildLibrary(globals) { tap(() => { replaceDynamicImportsForUMD(); const coreStats = measure('core'); + const analyticsStats = measure('analytics'); const authStats = measure('auth'); const authGuardStats = measure('auth-guard'); const dbStats = measure('database'); @@ -391,9 +412,11 @@ function buildLibrary(globals) { const storageStats = measure('storage'); const messagingStats = measure('messaging'); const performanceStats = measure('performance'); + const remoteConfigStats = measure('remote-config'); const dbdepStats = measure('database-deprecated'); console.log(` core.umd.js - ${coreStats.size}, ${coreStats.gzip} +analytics.umd.js - ${analyticsStats.size}, ${analyticsStats.gzip} auth.umd.js - ${authStats.size}, ${authStats.gzip} auth-guard.umd.js - ${authGuardStats.size}, ${authGuardStats.gzip} database.umd.js - ${dbStats.size}, ${dbStats.gzip} @@ -402,6 +425,7 @@ functions.umd.js - ${functionsStats.size}, ${functionsStats.gzip} storage.umd.js - ${storageStats.size}, ${storageStats.gzip} messaging.umd.js - ${messagingStats.size}, ${messagingStats.gzip} performance.umd.js - ${performanceStats.size}, ${performanceStats.gzip} +remote-config.umd.js - ${remoteConfigStats.size}, ${remoteConfigStats.gzip} database-deprecated.umd.js - ${dbdepStats.size}, ${dbdepStats.gzip} `); verifyVersions(); diff --git a/yarn.lock b/yarn.lock index 8ad43d34a..0bbec818b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -123,165 +123,187 @@ dependencies: tslib "^1.9.0" -"@firebase/app-types@0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.4.0.tgz#bb2c651f3b275fef549050cff28af752839c75c0" - integrity sha512-8erNMHc0V26gA6Nj4W9laVrQrXHsj9K2TEM7eL2IQogGSHLL4vet3UNekYfcGQ2cjfvwUjMzd+BNS/8S7GnfiA== +"@firebase/app-types@0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.4.4.tgz#6423e9b284018109d3658a6ecf7f82ca0e288bbb" + integrity sha512-s3rKk6aGnzNTx6bzdtOdJpg2XXMiadLk/hv/PjiFEI/m+8MEKz+wkl+eGwvzxFyOd6BD5GVy637boGFqRt20FQ== -"@firebase/app@0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.4.1.tgz#07df341e1a71ffe2ef07988f8377cf4b053a374a" - integrity sha512-NHzEMPXWRXmDMMhFbCqElOkoVo5mScw81KzhqiEkU7e0v1Ia0P0fztfwqIzawvJtYW158UFSU7Q+EbpzthImqQ== +"@firebase/app@0.4.18": + version "0.4.18" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.4.18.tgz#16b87afa1339fe42ad218db3ab88a3e51798bf27" + integrity sha512-2q/1DU2eYS7Iuepszd0cddTRQdRdwuKZ8Somf0LollorzkMNGM1IoGIsRu2vO1xmY8rgKthLCdTey9WE03JhmQ== dependencies: - "@firebase/app-types" "0.4.0" - "@firebase/logger" "0.1.14" - "@firebase/util" "0.2.15" + "@firebase/app-types" "0.4.4" + "@firebase/logger" "0.1.25" + "@firebase/util" "0.2.28" dom-storage "2.1.0" - tslib "1.9.3" + tslib "1.10.0" xmlhttprequest "1.8.0" -"@firebase/auth-types@0.7.0": - version "0.7.0" - resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.7.0.tgz#8aac4b9c04aff61362827c35b5ad36db16a837ba" - integrity sha512-QEG9azYwssGWcb4NaKFHe3Piez0SG46nRlu76HM4/ob0sjjNpNTY1Z5C3IoeJYknp2kMzuQi0TTW8tjEgkUAUA== +"@firebase/auth-types@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.8.0.tgz#10c0657847f6c311ef42e8a550bdbbb678ec86e4" + integrity sha512-foQHhvyB0RR+mb/+wmHXd/VOU+D8fruFEW1k79Q9wzyTPpovMBa1Mcns5fwEWBhUfi8bmoEtaGB8RSAHnTFzTg== -"@firebase/auth@0.11.2": - version "0.11.2" - resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.11.2.tgz#a75fd7d7be11d670c23d56395458f92d0cc205bd" - integrity sha512-vx8rP85rxKg4oOhPROrQqXvFhFNCy2jCHd359mugfm3VBezBGqs15KHTFGL+agQY9hdhVbGw+EIGk3Y/ou/9zw== +"@firebase/auth@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.12.0.tgz#e721323f8753f03b7387d899406c6f9e72f65876" + integrity sha512-DGYvAmz2aUmrWYS3ADw/UmsuicxJi6G+X38XITqNPUrd1YxmM5SBzX19oEb9WCrJZXcr4JaESg6hQkT2yEPaCA== dependencies: - "@firebase/auth-types" "0.7.0" - -"@firebase/database-types@0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.4.0.tgz#71a711a3f666fac905422e130731930e2bcca582" - integrity sha512-2piRYW7t+2s/P1NPpcI/3+8Y5l2WnJhm9KACoXW5zmoAPlya8R1aEaR2dNHLNePTMHdg04miEDD9fEz4xUqzZA== + "@firebase/auth-types" "0.8.0" -"@firebase/database@0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.4.1.tgz#272bc1ffb71f122436eaa4f00d18e6735b2c1f9f" - integrity sha512-3hCq5m1JOvg037Yci471LA+1B7PnmiZDiwztNZ9a2U7t28VXGXkf4ECriG7UyxWoR6yTFWUUQeaBTr4SJ78fhA== +"@firebase/database-types@0.4.4": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.4.4.tgz#d2337413a5d88b326c199cb3008a5fccc9626001" + integrity sha512-n+CKrqfxKII6VMl44HFCPIB8xS78isRiA2V6SYJe4Mo6vauKggRBm8Tdt1SKmBco5zHhhYkhaYzU7bfPYm54Ag== dependencies: - "@firebase/database-types" "0.4.0" - "@firebase/logger" "0.1.14" - "@firebase/util" "0.2.15" - faye-websocket "0.11.1" - tslib "1.9.3" + "@firebase/app-types" "0.4.4" -"@firebase/firestore-types@1.3.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-1.3.0.tgz#a32c132fff2bc77d36b6e864a3cc76c9cb75c965" - integrity sha512-XPnfAaYsKgYivgl/U1+M5ulBG9Hxv52zrZR5TuaoKCU791t/E3K85rT1ZGtEHu9Fj4CPTep2NSl8I30MQpUlHA== - -"@firebase/firestore@1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-1.3.1.tgz#dcb7e18bdc9d9a5cdb7bc285e01c3804f5aa0650" - integrity sha512-cr7qpGIphewqB4uMi/JeXFaHaqA+v0VeHRkTn/f8LC4npQgbq9r1yOumQpcJEJH0POfkMGZjVdpb+knzjM8qUQ== +"@firebase/database@0.5.5": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.5.5.tgz#f636dd04f955136e1f406f35b1dfe66375a1c7a4" + integrity sha512-rsOQCXoNLSKw7V2RL7xJhGAMS5bj8d/Y4/KfRwoaMsKUdfaogbZEdSgxMjKd00PKIiYa7TSAch0KziEujtAfuA== dependencies: - "@firebase/firestore-types" "1.3.0" - "@firebase/logger" "0.1.14" - "@firebase/webchannel-wrapper" "0.2.20" + "@firebase/database-types" "0.4.4" + "@firebase/logger" "0.1.25" + "@firebase/util" "0.2.28" + faye-websocket "0.11.3" + tslib "1.10.0" + +"@firebase/firestore-types@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-1.5.0.tgz#f05700220f882773ca01e818bf10b1dc456ee5be" + integrity sha512-VhRHNbEbak+R2iK8e1ir2Lec7eaHMZpGTRy6LMtzATYthlkwNHF9tO8JU8l6d1/kYkI4+DWzX++i3HhTziHEWA== + +"@firebase/firestore@1.5.4": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-1.5.4.tgz#43728c66d0a1846f33cad147cfc0003cbd9f580b" + integrity sha512-F3zVLTMVdCcUuQNlveVw6FsRBeZlMNQuaR/fXg5eGSidz5W3NdMgQCGd7fRUSP5SsHWlG6KPLh6j+A6LG3p5Xw== + dependencies: + "@firebase/firestore-types" "1.5.0" + "@firebase/logger" "0.1.25" + "@firebase/util" "0.2.28" + "@firebase/webchannel-wrapper" "0.2.26" "@grpc/proto-loader" "^0.5.0" - grpc "1.20.3" - tslib "1.9.3" + grpc "1.23.3" + tslib "1.10.0" -"@firebase/functions-types@0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.3.5.tgz#1fae28b0bbb89fd0629a353eafbc53e8d6e073e2" - integrity sha512-3hTMqfSugCfxzT6vZPbzQ58G4941rsFr99fWKXGKFAl2QpdMBCnKmEKdg/p5M47xIPyzIQn6NMF5kCo/eICXhA== +"@firebase/functions-types@0.3.8": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.3.8.tgz#c01f670bbca04365e680d048b0fe5770a946f643" + integrity sha512-9hajHxA4UWVCGFmoL8PBYHpamE3JTNjObieMmnvZw3cMRTP2EwipMpzZi+GPbMlA/9swF9yHCY/XFAEkwbvdgQ== -"@firebase/functions@0.4.7": - version "0.4.7" - resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.4.7.tgz#e4dc4994adf01ceb0ede11bf953ceb13761c23fa" - integrity sha512-k0Rn2c1Nsb90Qwk9O2JCSPoH6ozxyp0WZgIPU2OoLzYrrg+fVdcEnG3Sb1YhlOU5yRA9Nuxh1pPBQA3/e/A98g== +"@firebase/functions@0.4.19": + version "0.4.19" + resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.4.19.tgz#449710e4c49fa653113b6587012e8a8cc07bf4b7" + integrity sha512-fK1IIDn8ZjdiKO8H1ol9kJ6K0U7411Nodn1am2XuwsvFW8OvI3ufbDZ7eceL0OpZgDHzIQQP1o/VCyuW5dVoew== dependencies: - "@firebase/functions-types" "0.3.5" - "@firebase/messaging-types" "0.2.11" + "@firebase/functions-types" "0.3.8" + "@firebase/messaging-types" "0.3.2" isomorphic-fetch "2.2.1" - tslib "1.9.3" + tslib "1.10.0" -"@firebase/installations-types@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.1.0.tgz#51c2d93d91ae6539f1a292f8d58d3d83e98cc9a2" - integrity sha512-cw2UIvPa3+umy6w7dGj0LqQQ9v7WEOro5s+6B+v54Tw25WyLnR6cBIkyyahv3Uu9QPnGZCIsemlQwxIaIOMb9g== +"@firebase/installations-types@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.1.2.tgz#ac2a912e078282fd270b03b571b4639ed88d871a" + integrity sha512-fQaWIW8hyX1XUN7+FCSPjvM1agFjGidVuF4Sxi7aFwfyh5t+4fD2VpM4wCQbWmodnx4fZLvsuQd9mkxxU+lGYQ== -"@firebase/installations@0.1.1": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.1.1.tgz#eb8419eb0e0cbea1f9796148c8ccf3079e57d29a" - integrity sha512-7qg0iSYs/XhvfXF8m8fMc44ViEPbaTpKpvD6A6bN3VUjaTg+un7bROEGtfP8xboTbqdN80gjiKnHI+Ibs5KxCw== +"@firebase/installations@0.2.7": + version "0.2.7" + resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.2.7.tgz#b439e134c94c423b7aa45efb1bf835fd1762e7e0" + integrity sha512-67tzowHVwRBtEuB1HLMD+fCdoRyinOQlMKBes7UwrtZIVd0CPDUqAKxNqup5EypWZb7O2tqFtRzK7POajfSNMA== dependencies: - "@firebase/installations-types" "0.1.0" - "@firebase/util" "0.2.15" + "@firebase/installations-types" "0.1.2" + "@firebase/util" "0.2.28" idb "3.0.2" + tslib "1.10.0" -"@firebase/logger@0.1.14": - version "0.1.14" - resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.1.14.tgz#e65e1f6bf86225b98a57018a2037b18dc4d79fae" - integrity sha512-WREaY2n6HzypeoovOjYefjLJqT9+zlI1wQlLMXnkSPhwuM+udIQ87orjVL6tfmuHW++u5bZh3JJAyvuRv/nciA== +"@firebase/logger@0.1.25": + version "0.1.25" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.1.25.tgz#2f95832a62ae634a10242bdeedd84b62d9e5c8ef" + integrity sha512-/lRhuepVcCCnQ2jcO5Hr08SYdmZDTQU9fdPdzg+qXJ9k/QnIrD2RbswXQcL6mmae3uPpX7fFXQAoScJ9pzp50w== -"@firebase/messaging-types@0.2.11": - version "0.2.11" - resolved "https://registry.yarnpkg.com/@firebase/messaging-types/-/messaging-types-0.2.11.tgz#b81d09c0aa6be7dbac421edff8100971c56d64e0" - integrity sha512-uWtzPMj1mAX8EbG68SnxE12Waz+hRuO7vtosUFePGBfxVNNmPx5vJyKZTz+hbM4P77XddshAiaEkyduro4gNgA== +"@firebase/messaging-types@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@firebase/messaging-types/-/messaging-types-0.3.2.tgz#cf802617c161434a02fe029290a79f422821d12f" + integrity sha512-2qa2qNKqpalmtwaUV3+wQqfCm5myP/dViIBv+pXF8HinemIfO1IPQtr9pCNfsSYyus78qEhtfldnPWXxUH5v0w== -"@firebase/messaging@0.3.20": - version "0.3.20" - resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.3.20.tgz#84dc288d10aa8c8b820f5985b7f52c57417b1ffd" - integrity sha512-0mJpaSFoineVFnaYTHVP4k/PANEEmZhES9fPd44Ir6b9mR4FhJip5ojZl0vlyMe5rZWCLuxYkfFcyzEn2afNKw== +"@firebase/messaging@0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.5.0.tgz#88188e669e4833f73ce3106f9063f099da32cfd0" + integrity sha512-bk1W0eOah2js/DMI9hrilQqTBFijJNu617K0y1hXfJnGrJCTZRcHLX/6FUQ0S95YfaT3+j1NxS4ZuEgnnXKkjA== dependencies: - "@firebase/messaging-types" "0.2.11" - "@firebase/util" "0.2.15" - tslib "1.9.3" + "@firebase/installations" "0.2.7" + "@firebase/messaging-types" "0.3.2" + "@firebase/util" "0.2.28" + tslib "1.10.0" -"@firebase/performance-types@0.0.1": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.0.1.tgz#749b6351f5f802ec7a9be5737546eeda18e7ac4a" - integrity sha512-U45GbVAnPyz7wPLd3FrWdTeaFSvgsnGfGK58VojfEMmFnMAixCM3qBv1XJ0xfhyKbK1xZN4+usWAR8F3CwRAXw== +"@firebase/performance-types@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.0.3.tgz#bdd37975cd5f12a55d3951f4942c3fa2661b354f" + integrity sha512-RuC63nYJPJU65AsrNMc3fTRcRgHiyNcQLh9ufeKUT1mEsFgpxr167gMb+tpzNU4jsbvM6+c6nQAFdHpqcGkRlQ== + +"@firebase/performance@0.2.19": + version "0.2.19" + resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.2.19.tgz#aed889539f8f8af022191a92067cacbc67a8980b" + integrity sha512-dINWwR/XcSiSnFNNX7QWfec8bymiXk1Zp6mPyPN+R9ONMrpDbygQUy06oT/6r/xx9nHG4Za6KMUJag3sWNKqnQ== + dependencies: + "@firebase/installations" "0.2.7" + "@firebase/logger" "0.1.25" + "@firebase/performance-types" "0.0.3" + "@firebase/util" "0.2.28" + tslib "1.10.0" + +"@firebase/polyfill@0.3.23": + version "0.3.23" + resolved "https://registry.yarnpkg.com/@firebase/polyfill/-/polyfill-0.3.23.tgz#abf1a4050a2d66fc3ae5e64e959da0f8881174b6" + integrity sha512-CAbNJUK7NGjkS1txCCm0ymIlHVSrf6ssALKlk//2h3KOEYBngTsmQ3odbVX4yqfxTPhx9V2by+ycY5HhSjVv6Q== + dependencies: + core-js "3.2.1" + promise-polyfill "8.1.3" + whatwg-fetch "2.0.4" -"@firebase/performance@0.2.2": - version "0.2.2" - resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.2.2.tgz#8d9ed48c3276828269e050b289d134d0b39ac51c" - integrity sha512-wpH9Nac8CadEfyrr8u8QQO+HfQ7ndkrOSesXDtIbbVOify9A2sCdwWuIWPxMAclevgSp99zVj2yjd7sv6M3jVQ== - dependencies: - "@firebase/installations" "0.1.1" - "@firebase/logger" "0.1.14" - "@firebase/performance-types" "0.0.1" - "@firebase/util" "0.2.15" - tslib "1.9.3" +"@firebase/remote-config-types@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.1.0.tgz#8f4d06f3ca0f6f18b1942f00d9666023740c8ad3" + integrity sha512-ER6MHeUBF74j20Jau1OtxCoN7DXigJwO3NPz3uax3CBsM/dLNHMjr9iywF1yCs4UnjkOFyDqb1EDWqAE12cGjg== -"@firebase/polyfill@0.3.14": - version "0.3.14" - resolved "https://registry.yarnpkg.com/@firebase/polyfill/-/polyfill-0.3.14.tgz#2a3fe0bff207b5145902fe98d86098980eeb6704" - integrity sha512-MnJRIS2iqGfQ4SGFFZ441B1VBHgmHiGznpA3gN+FzSdqg9di4sIHw2gM0VOGS6e7jRJxYeyHL3rwzzU43kP+UQ== +"@firebase/remote-config@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.1.0.tgz#9098781122980834a98dc65ab660c479a6afb0df" + integrity sha512-ly8uIm/txjXO4+A2qWfc3JiiQjMqHOkww5rtAGflraFxZm50wp8Adc35bNDtDjUwS19AldrUVzcf6AZqfqN3pQ== dependencies: - core-js "3.0.1" - promise-polyfill "8.1.0" - whatwg-fetch "2.0.4" + "@firebase/installations" "0.2.7" + "@firebase/logger" "0.1.25" + "@firebase/remote-config-types" "0.1.0" + "@firebase/util" "0.2.28" + tslib "1.10.0" -"@firebase/storage-types@0.2.11": - version "0.2.11" - resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.2.11.tgz#cbbcdca9bbd68c527ca2c2be2241d619126cb5b3" - integrity sha512-vGTFJmKbfScmCAVUamREIBTopr5Uusxd8xPAgNDxMZwICvdCjHO0UH0pZZj6iBQuwxLe/NEtFycPnu1kKT+TPw== +"@firebase/storage-types@0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.3.3.tgz#179ad38485d450023406cf9731560d690ef39d19" + integrity sha512-fUp4kpbxwDiWs/aIBJqBvXgFHZvgoND2JA0gJYSEsXtWtVwfgzY/710plErgZDeQKopX5eOR1sHskZkQUy0U6w== -"@firebase/storage@0.2.16": - version "0.2.16" - resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.2.16.tgz#dee8c4aa2a1b25974687d763622300668ad384b0" - integrity sha512-R/qLIqp7ht7T0P2w3LWB5JvXUZMCWzrHOyublAa4iSVcuqN2SLMIpqnYXNKYk+o/NcW7jO8DE8c62mnpvsS+pw== +"@firebase/storage@0.3.12": + version "0.3.12" + resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.3.12.tgz#6e48bac57c0a6e3c9e4678d722109a5ff04c3d3d" + integrity sha512-8hXt3qPZlVH+yPF4W9Dc15/gBiTPGUJUgYs3dH9WnO41QWl1o4aNlZpZK/pdnpCIO1GmN0+PxJW9TCNb0H0Hqw== dependencies: - "@firebase/storage-types" "0.2.11" - tslib "1.9.3" + "@firebase/storage-types" "0.3.3" + "@firebase/util" "0.2.28" + tslib "1.10.0" -"@firebase/util@0.2.15": - version "0.2.15" - resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.2.15.tgz#727799e7c018cc946f8809cd5cc53b0ea32b4ed7" - integrity sha512-XA6C4HkBOcUfDyvp/clq0MCFSuWSm3bPkolP689RCrGzjBs+bnCzC5a7by6Fq106zAQYeASwULJjTfVtZjKaaQ== +"@firebase/util@0.2.28": + version "0.2.28" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.2.28.tgz#9a17198b6ea416f1fb9edaea9f353d9a1f517f51" + integrity sha512-ZQMAWtXj8y5kvB6izs0aTM/jG+WO8HpqhXA/EwD6LckJ+1P5LnAhaLZt1zR4HpuCE+jeP5I32Id5RJ/aifFs6A== dependencies: - tslib "1.9.3" + tslib "1.10.0" -"@firebase/webchannel-wrapper@0.2.20": - version "0.2.20" - resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.2.20.tgz#d8b25b23e532c67bd56c614025b24cb2a65e25db" - integrity sha512-TpqR1qCn117fY4mrxSGqv/CT/iAM58sHdlS8ujj0Roa7DoleAHJzqOhNNoHCNncwvNDWcvygLsEiTBuDQZsv3A== +"@firebase/webchannel-wrapper@0.2.26": + version "0.2.26" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.2.26.tgz#4fe0e0a878d26af98901b29f051deed8017d6237" + integrity sha512-VlTurkvs4v7EVFWESBZGOPghFEokQhU5au5CP9WqA8B2/PcQRDsaaQlQCA6VATuEnW+vtSiSBvTiOc4004f8xg== "@grpc/proto-loader@^0.5.0": version "0.5.0" @@ -344,6 +366,14 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= +"@types/bytebuffer@^5.0.40": + version "5.0.40" + resolved "https://registry.yarnpkg.com/@types/bytebuffer/-/bytebuffer-5.0.40.tgz#d6faac40dcfb09cd856cdc4c01d3690ba536d3ee" + integrity sha512-h48dyzZrPMz25K6Q4+NCwWaxwXany2FhQg/ErOcdZS1ZpsaDnDMZg8JYLMTGz7uvXKrcKGJUZJlZObyfgdaN9g== + dependencies: + "@types/long" "*" + "@types/node" "*" + "@types/estree@0.0.39": version "0.0.39" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" @@ -369,7 +399,7 @@ resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.8.16.tgz#a6cb24b1149d65293bd616923500014838e14e7d" integrity sha512-056oRlBBp7MDzr+HoU5su099s/s7wjZ3KcHxLfv+Byqb9MwdLUvsfLgw1VS97hsh3ddxSPyQu+olHMnoVTUY6g== -"@types/long@^4.0.0": +"@types/long@*", "@types/long@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef" integrity sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q== @@ -2187,10 +2217,10 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-js@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.1.tgz#1343182634298f7f38622f95e73f54e48ddf4738" - integrity sha512-sco40rF+2KlE0ROMvydjkrVMMG1vYilP2ALoRXcYR4obqbYIuV3Bg+51GEDW+HF8n7NRA+iaA4qD0nD9lo9mew== +core-js@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09" + integrity sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw== core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0: version "2.6.5" @@ -3082,7 +3112,14 @@ fast-url-parser@^1.1.3: dependencies: punycode "^1.3.2" -faye-websocket@0.11.1, faye-websocket@>=0.6.0: +faye-websocket@0.11.3: + version "0.11.3" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" + integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== + dependencies: + websocket-driver ">=0.5.1" + +faye-websocket@>=0.6.0: version "0.11.1" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38" integrity sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg= @@ -3254,20 +3291,24 @@ firebase@2.x.x: dependencies: faye-websocket ">=0.6.0" -"firebase@>= 5.5.7 <7": - version "6.0.2" - resolved "https://registry.yarnpkg.com/firebase/-/firebase-6.0.2.tgz#a2d1daa9a3f329aac2974349d521fbd923ee0f35" - integrity sha512-KA4VviZQJCzCIkCEvt3sJEsNe/HpqQdNE+ajy3wELHCxNV8PK8eBa10qxWDQhNgEtorHHltgYw3VwS0rbSvWrQ== - dependencies: - "@firebase/app" "0.4.1" - "@firebase/auth" "0.11.2" - "@firebase/database" "0.4.1" - "@firebase/firestore" "1.3.1" - "@firebase/functions" "0.4.7" - "@firebase/messaging" "0.3.20" - "@firebase/performance" "0.2.2" - "@firebase/polyfill" "0.3.14" - "@firebase/storage" "0.2.16" +"firebase@>= 5.5.7 <8ss": + version "7.0.0" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-7.0.0.tgz#3919ed3c1f9441f0465d35b2f3bedf56f206a8a5" + integrity sha512-XLuIsv5IBNILZ1jKxvZah+2p+ZvA1jpAC45TUlobMKhpYp6TN9KkEK+8J11qNxkJG6PittHW3TjxJqejU8xg9Q== + dependencies: + "@firebase/app" "0.4.18" + "@firebase/app-types" "0.4.4" + "@firebase/auth" "0.12.0" + "@firebase/database" "0.5.5" + "@firebase/firestore" "1.5.4" + "@firebase/functions" "0.4.19" + "@firebase/installations" "0.2.7" + "@firebase/messaging" "0.5.0" + "@firebase/performance" "0.2.19" + "@firebase/polyfill" "0.3.23" + "@firebase/remote-config" "0.1.0" + "@firebase/storage" "0.3.12" + "@firebase/util" "0.2.28" first-chunk-stream@^1.0.0: version "1.0.0" @@ -3836,11 +3877,12 @@ graceful-fs@~1.2.0: resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= -grpc@1.20.3: - version "1.20.3" - resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.20.3.tgz#a74d36718f1e89c4a64f2fb9441199c3c8f78978" - integrity sha512-GsEsi0NVj6usS/xor8pF/xDbDiwZQR59aZl5NUZ59Sy2bdPQFZ3UePr5wevZjHboirRCIQCKRI1cCgvSWUe2ag== +grpc@1.23.3: + version "1.23.3" + resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.23.3.tgz#30a013ca2cd7e350b0ffc0034be5380ddef3ae7f" + integrity sha512-7vdzxPw9s5UYch4aUn4hyM5tMaouaxUUkwkgJlwbR4AXMxiYZJOv19N2ps2eKiuUbJovo5fnGF9hg/X91gWYjw== dependencies: + "@types/bytebuffer" "^5.0.40" lodash.camelcase "^4.3.0" lodash.clone "^4.5.0" nan "^2.13.2" @@ -6803,10 +6845,10 @@ progress@^2.0.0: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -promise-polyfill@8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.0.tgz#30059da54d1358ce905ac581f287e184aedf995d" - integrity sha512-OzSf6gcCUQ01byV4BgwyUCswlaQQ6gzXc23aLQWhicvfX9kfsUiUhgt3CCQej8jDnl8/PhGF31JdHX2/MzF3WA== +promise-polyfill@8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116" + integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g== promisify-call@^2.0.2: version "2.0.4" @@ -8534,7 +8576,12 @@ try-require@^1.0.0: resolved "https://registry.yarnpkg.com/try-require/-/try-require-1.2.1.tgz#34489a2cac0c09c1cc10ed91ba011594d4333be2" integrity sha1-NEiaLKwMCcHMEO2RugEVlNQzO+I= -tslib@1.9.3, tslib@^1.9.0: +tslib@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +tslib@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== From 1cf961bd2185c988916aa3741d7764a556d3a027 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Wed, 2 Oct 2019 23:20:58 +0200 Subject: [PATCH 02/37] Cleaning up the DI tokens --- src/analytics/analytics.ts | 6 +++--- src/auth/auth.ts | 8 +++----- src/core/angularfire2.ts | 1 + src/core/firebase.app.module.ts | 13 ++++++++----- src/database-deprecated/database.ts | 10 +++++----- src/database/database.ts | 10 +++++----- src/firestore/firestore.ts | 16 ++++++++++------ src/functions/functions.ts | 8 ++++---- src/messaging/messaging.ts | 8 ++++---- src/remote-config/remote-config.ts | 12 ++++++------ src/storage/storage.ts | 10 ++++++---- 11 files changed, 55 insertions(+), 47 deletions(-) diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index 1607f3f0b..debe2e286 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -3,7 +3,7 @@ import { Observable, from, of } from 'rxjs'; import { map, switchMap, tap, filter } from 'rxjs/operators'; import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular } from '@angular/fire'; import { Router, NavigationEnd } from '@angular/router'; -import { FirebaseAnalytics, FirebaseOptionsToken, FirebaseNameOrConfigToken, _firebaseAppFactory } from '@angular/fire'; +import { FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire'; export const AUTOMATICALLY_SET_CURRENT_SCREEN = new InjectionToken('angularfire2.analytics.setCurrentScreen'); export const ANALYTICS_COLLECTION_ENABLED = new InjectionToken('angularfire2.analytics.analyticsCollectionEnabled'); @@ -18,8 +18,8 @@ export class AngularFireAnalytics { public readonly analytics: Observable; constructor( - @Inject(FirebaseOptionsToken) options:FirebaseOptions, - @Optional() @Inject(FirebaseNameOrConfigToken) nameOrConfig:string|FirebaseAppConfig|null|undefined, + @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, + @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, @Optional() router:Router, @Optional() @Inject(AUTOMATICALLY_SET_CURRENT_SCREEN) automaticallySetCurrentScreen:boolean|null, @Optional() @Inject(ANALYTICS_COLLECTION_ENABLED) analyticsCollectionEnabled:boolean|null, diff --git a/src/auth/auth.ts b/src/auth/auth.ts index 648f7c317..d05857155 100644 --- a/src/auth/auth.ts +++ b/src/auth/auth.ts @@ -1,11 +1,9 @@ import { Injectable, Inject, Optional, NgZone, PLATFORM_ID } from '@angular/core'; import { Observable, of, from } from 'rxjs'; import { switchMap } from 'rxjs/operators'; -import { FirebaseAppConfig, FirebaseOptions } from '@angular/fire'; +import { FIREBASE_OPTIONS, FIREBASE_APP_NAME, FirebaseOptions, FirebaseAppConfig, FirebaseAuth, _firebaseAppFactory, FirebaseZoneScheduler } from '@angular/fire'; import { User, auth } from 'firebase/app'; -import { FirebaseAuth, FirebaseOptionsToken, FirebaseNameOrConfigToken, _firebaseAppFactory, FirebaseZoneScheduler } from '@angular/fire'; - @Injectable() export class AngularFireAuth { @@ -37,8 +35,8 @@ export class AngularFireAuth { public readonly idTokenResult: Observable; constructor( - @Inject(FirebaseOptionsToken) options:FirebaseOptions, - @Optional() @Inject(FirebaseNameOrConfigToken) nameOrConfig:string|FirebaseAppConfig|null|undefined, + @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, + @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, @Inject(PLATFORM_ID) platformId: Object, private zone: NgZone ) { diff --git a/src/core/angularfire2.ts b/src/core/angularfire2.ts index 0da33106a..e95292bfd 100644 --- a/src/core/angularfire2.ts +++ b/src/core/angularfire2.ts @@ -4,6 +4,7 @@ import { Observable, Subscription, queueScheduler as queue } from 'rxjs'; // Put in database.ts when we drop database-depreciated export const RealtimeDatabaseURL = new InjectionToken('angularfire2.realtimeDatabaseURL'); +export const DATABASE_URL = RealtimeDatabaseURL; export class FirebaseZoneScheduler { constructor(public zone: NgZone, private platformId: Object) {} diff --git a/src/core/firebase.app.module.ts b/src/core/firebase.app.module.ts index 63c72980d..e87a783b9 100644 --- a/src/core/firebase.app.module.ts +++ b/src/core/firebase.app.module.ts @@ -8,7 +8,10 @@ export type FirebaseOptions = {[key:string]: any}; export type FirebaseAppConfig = {[key:string]: any}; export const FirebaseOptionsToken = new InjectionToken('angularfire2.app.options'); -export const FirebaseNameOrConfigToken = new InjectionToken('angularfire2.app.nameOrConfig') +export const FirebaseNameOrConfigToken = new InjectionToken('angularfire2.app.nameOrConfig'); + +export const FIREBASE_OPTIONS = FirebaseOptionsToken; +export const FIREBASE_APP_NAME = FirebaseNameOrConfigToken; export type FirebaseDatabase = database.Database; export type FirebaseAuth = auth.Auth; @@ -51,8 +54,8 @@ const FirebaseAppProvider = { provide: FirebaseApp, useFactory: _firebaseAppFactory, deps: [ - FirebaseOptionsToken, - [new Optional(), FirebaseNameOrConfigToken] + FIREBASE_OPTIONS, + [new Optional(), FIREBASE_APP_NAME] ] }; @@ -64,8 +67,8 @@ export class AngularFireModule { return { ngModule: AngularFireModule, providers: [ - { provide: FirebaseOptionsToken, useValue: options }, - { provide: FirebaseNameOrConfigToken, useValue: nameOrConfig } + { provide: FIREBASE_OPTIONS, useValue: options }, + { provide: FIREBASE_APP_NAME, useValue: nameOrConfig } ] } } diff --git a/src/database-deprecated/database.ts b/src/database-deprecated/database.ts index 19e80eec4..cc0c0b130 100644 --- a/src/database-deprecated/database.ts +++ b/src/database-deprecated/database.ts @@ -5,7 +5,7 @@ import { FirebaseListFactoryOpts, FirebaseObjectFactoryOpts, PathReference } fro import { FirebaseObjectFactory } from './firebase_object_factory'; import { FirebaseObjectObservable } from './firebase_object_observable'; import * as utils from './utils'; -import { FirebaseDatabase, FirebaseOptions, FirebaseAppConfig, FirebaseOptionsToken, FirebaseNameOrConfigToken, RealtimeDatabaseURL, _firebaseAppFactory } from '@angular/fire'; +import { FirebaseDatabase, FirebaseOptions, FirebaseAppConfig, RealtimeDatabaseURL, FIREBASE_OPTIONS, FIREBASE_APP_NAME, DATABASE_URL, _firebaseAppFactory } from '@angular/fire'; @Injectable() export class AngularFireDatabase { @@ -16,9 +16,9 @@ export class AngularFireDatabase { database: FirebaseDatabase; constructor( - @Inject(FirebaseOptionsToken) options:FirebaseOptions, - @Optional() @Inject(FirebaseNameOrConfigToken) nameOrConfig:string|FirebaseAppConfig|null|undefined, - @Optional() @Inject(RealtimeDatabaseURL) databaseURL:string|null, + @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, + @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, + @Optional() @Inject(DATABASE_URL) databaseURL:string|null, zone: NgZone ) { this.database = zone.runOutsideAngular(() => { @@ -39,4 +39,4 @@ export class AngularFireDatabase { } -export { RealtimeDatabaseURL }; \ No newline at end of file +export { DATABASE_URL }; \ No newline at end of file diff --git a/src/database/database.ts b/src/database/database.ts index 852e9b414..4264760f8 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -4,7 +4,7 @@ import { getRef } from './utils'; import { InjectionToken } from '@angular/core'; import { createListReference } from './list/create-reference'; import { createObjectReference } from './object/create-reference'; -import { FirebaseDatabase, FirebaseOptions, FirebaseAppConfig, FirebaseOptionsToken, FirebaseNameOrConfigToken, RealtimeDatabaseURL, _firebaseAppFactory, FirebaseZoneScheduler } from '@angular/fire'; +import { FirebaseDatabase, FirebaseOptions, FirebaseAppConfig, RealtimeDatabaseURL, FIREBASE_OPTIONS, FIREBASE_APP_NAME, DATABASE_URL, _firebaseAppFactory, FirebaseZoneScheduler } from '@angular/fire'; @Injectable() export class AngularFireDatabase { @@ -12,9 +12,9 @@ export class AngularFireDatabase { public readonly scheduler: FirebaseZoneScheduler; constructor( - @Inject(FirebaseOptionsToken) options:FirebaseOptions, - @Optional() @Inject(FirebaseNameOrConfigToken) nameOrConfig:string|FirebaseAppConfig|null|undefined, - @Optional() @Inject(RealtimeDatabaseURL) databaseURL:string|null, + @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, + @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, + @Optional() @Inject(DATABASE_URL) databaseURL:string|null, @Inject(PLATFORM_ID) platformId: Object, zone: NgZone ) { @@ -58,4 +58,4 @@ export { SnapshotAction } from './interfaces'; -export { RealtimeDatabaseURL }; \ No newline at end of file +export { RealtimeDatabaseURL, DATABASE_URL }; \ No newline at end of file diff --git a/src/firestore/firestore.ts b/src/firestore/firestore.ts index c860a57f5..5f038b6ff 100644 --- a/src/firestore/firestore.ts +++ b/src/firestore/firestore.ts @@ -7,7 +7,7 @@ import { AngularFirestoreDocument } from './document/document'; import { AngularFirestoreCollection } from './collection/collection'; import { AngularFirestoreCollectionGroup } from './collection-group/collection-group'; -import { FirebaseFirestore, FirebaseOptions, FirebaseAppConfig, FirebaseOptionsToken, FirebaseNameOrConfigToken, _firebaseAppFactory, FirebaseZoneScheduler } from '@angular/fire'; +import { FirebaseFirestore, FirebaseOptions, FirebaseAppConfig, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory, FirebaseZoneScheduler } from '@angular/fire'; import { isPlatformServer } from '@angular/common'; // Workaround for Nodejs build @@ -24,6 +24,10 @@ export const EnablePersistenceToken = new InjectionToken('angularfire2. export const PersistenceSettingsToken = new InjectionToken('angularfire2.firestore.persistenceSettings'); export const FirestoreSettingsToken = new InjectionToken('angularfire2.firestore.settings'); +export const ENABLE_PERSISTENCE = EnablePersistenceToken; +export const PERSISTENCE_SETTINGS = PersistenceSettingsToken +export const FIRESTORE_SETTINGS = FirestoreSettingsToken; + // timestampsInSnapshots was depreciated in 5.8.0 const major = parseInt(firebase.SDK_VERSION.split('.')[0]); const minor = parseInt(firebase.SDK_VERSION.split('.')[1]); @@ -115,13 +119,13 @@ export class AngularFirestore { * @param app */ constructor( - @Inject(FirebaseOptionsToken) options:FirebaseOptions, - @Optional() @Inject(FirebaseNameOrConfigToken) nameOrConfig:string|FirebaseAppConfig|null|undefined, - @Optional() @Inject(EnablePersistenceToken) shouldEnablePersistence: boolean|null, - @Optional() @Inject(FirestoreSettingsToken) settings: Settings|null, + @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, + @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, + @Optional() @Inject(ENABLE_PERSISTENCE) shouldEnablePersistence: boolean|null, + @Optional() @Inject(FIRESTORE_SETTINGS) settings: Settings|null, @Inject(PLATFORM_ID) platformId: Object, zone: NgZone, - @Optional() @Inject(PersistenceSettingsToken) persistenceSettings: PersistenceSettings|null, + @Optional() @Inject(PERSISTENCE_SETTINGS) persistenceSettings: PersistenceSettings|null, ) { this.scheduler = new FirebaseZoneScheduler(zone, platformId); this.firestore = zone.runOutsideAngular(() => { diff --git a/src/functions/functions.ts b/src/functions/functions.ts index 99c7c3813..ac7963d76 100644 --- a/src/functions/functions.ts +++ b/src/functions/functions.ts @@ -1,8 +1,8 @@ import { Injectable, Inject, Optional, NgZone, PLATFORM_ID, InjectionToken } from '@angular/core'; import { Observable, from } from 'rxjs'; import { map } from 'rxjs/operators'; -import { FirebaseOptions, FirebaseAppConfig } from '@angular/fire'; -import { FirebaseFunctions, FirebaseOptionsToken, FirebaseNameOrConfigToken, _firebaseAppFactory, FirebaseZoneScheduler } from '@angular/fire'; +import { FirebaseOptions, FirebaseAppConfig, FIREBASE_APP_NAME } from '@angular/fire'; +import { FirebaseFunctions, FIREBASE_OPTIONS, _firebaseAppFactory, FirebaseZoneScheduler } from '@angular/fire'; // SEMVER: @ v6 remove FunctionsRegionToken in favor of FUNCTIONS_REGION export const FunctionsRegionToken = new InjectionToken('angularfire2.functions.region'); @@ -20,8 +20,8 @@ export class AngularFireFunctions { public readonly scheduler: FirebaseZoneScheduler; constructor( - @Inject(FirebaseOptionsToken) options:FirebaseOptions, - @Optional() @Inject(FirebaseNameOrConfigToken) nameOrConfig:string|FirebaseAppConfig|null|undefined, + @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, + @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, @Inject(PLATFORM_ID) platformId: Object, zone: NgZone, @Optional() @Inject(FUNCTIONS_REGION) region:string|null, diff --git a/src/messaging/messaging.ts b/src/messaging/messaging.ts index eaeff011a..bf3fa1877 100644 --- a/src/messaging/messaging.ts +++ b/src/messaging/messaging.ts @@ -3,8 +3,8 @@ import { isPlatformServer } from '@angular/common'; import { messaging } from 'firebase/app'; import { Observable, empty, from, of, throwError } from 'rxjs'; import { mergeMap, catchError, map, switchMap, concat, defaultIfEmpty } from 'rxjs/operators'; -import { FirebaseOptions, FirebaseAppConfig, runOutsideAngular } from '@angular/fire'; -import { FirebaseOptionsToken, FirebaseNameOrConfigToken, _firebaseAppFactory, FirebaseZoneScheduler } from '@angular/fire'; +import { FirebaseOptions, FirebaseAppConfig, runOutsideAngular, FIREBASE_APP_NAME, FIREBASE_OPTIONS } from '@angular/fire'; +import { _firebaseAppFactory, FirebaseZoneScheduler } from '@angular/fire'; @Injectable() export class AngularFireMessaging { @@ -17,8 +17,8 @@ export class AngularFireMessaging { deleteToken: (token: string) => Observable; constructor( - @Inject(FirebaseOptionsToken) options:FirebaseOptions, - @Optional() @Inject(FirebaseNameOrConfigToken) nameOrConfig:string|FirebaseAppConfig|null|undefined, + @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, + @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, @Inject(PLATFORM_ID) platformId: Object, zone: NgZone ) { diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index 87ce631bc..7c66812f7 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -1,15 +1,15 @@ import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; import { Observable, from } from 'rxjs'; import { map, switchMap, tap } from 'rxjs/operators'; -import { FirebaseAppConfig, FirebaseOptions } from '@angular/fire'; +import { FirebaseAppConfig, FirebaseOptions, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; import { remoteConfig } from 'firebase/app'; export interface DefaultConfig {[key:string]: string|number|boolean}; -export const SETTINGS = new InjectionToken('angularfire2.remoteConfig.settings'); +export const REMOTE_CONFIG_SETTINGS = new InjectionToken('angularfire2.remoteConfig.settings'); export const DEFAULT_CONFIG = new InjectionToken('angularfire2.remoteConfig.defaultConfig'); -import { FirebaseRemoteConfig, FirebaseOptionsToken, FirebaseNameOrConfigToken, _firebaseAppFactory, runOutsideAngular } from '@angular/fire'; +import { FirebaseRemoteConfig, _firebaseAppFactory, runOutsideAngular } from '@angular/fire'; @Injectable() export class AngularFireRemoteConfig { @@ -24,9 +24,9 @@ export class AngularFireRemoteConfig { public readonly configuration: Observable<{[key:string]: remoteConfig.Value}>; constructor( - @Inject(FirebaseOptionsToken) options:FirebaseOptions, - @Optional() @Inject(FirebaseNameOrConfigToken) nameOrConfig:string|FirebaseAppConfig|null|undefined, - @Optional() @Inject(SETTINGS) settings:remoteConfig.Settings|null, + @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, + @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, + @Optional() @Inject(REMOTE_CONFIG_SETTINGS) settings:remoteConfig.Settings|null, @Optional() @Inject(DEFAULT_CONFIG) defaultConfig:DefaultConfig|null, private zone: NgZone ) { diff --git a/src/storage/storage.ts b/src/storage/storage.ts index 958177168..d16dd6326 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -2,12 +2,14 @@ import { Injectable, Inject, Optional, InjectionToken, NgZone, PLATFORM_ID } fro import { createStorageRef, AngularFireStorageReference } from './ref'; import { createUploadTask, AngularFireUploadTask } from './task'; import { Observable } from 'rxjs'; -import { FirebaseStorage, FirebaseOptions, FirebaseAppConfig, FirebaseOptionsToken, FirebaseNameOrConfigToken, FirebaseZoneScheduler, _firebaseAppFactory } from '@angular/fire'; +import { FirebaseStorage, FirebaseOptions, FirebaseAppConfig, FirebaseZoneScheduler, _firebaseAppFactory, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; import { UploadMetadata } from './interfaces'; export const StorageBucket = new InjectionToken('angularfire2.storageBucket'); +export const STORAGE_BUCKET = StorageBucket; + /** * AngularFireStorage Service * @@ -21,9 +23,9 @@ export class AngularFireStorage { public readonly scheduler: FirebaseZoneScheduler; constructor( - @Inject(FirebaseOptionsToken) options:FirebaseOptions, - @Optional() @Inject(FirebaseNameOrConfigToken) nameOrConfig:string|FirebaseAppConfig|null|undefined, - @Optional() @Inject(StorageBucket) storageBucket:string|null, + @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, + @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, + @Optional() @Inject(STORAGE_BUCKET) storageBucket:string|null, @Inject(PLATFORM_ID) platformId: Object, zone: NgZone ) { From f5f67adc1e264742f0952ef35e2fdae792eaf8a8 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Fri, 4 Oct 2019 17:27:03 +0100 Subject: [PATCH 03/37] Cleaning things up --- package.json | 2 +- src/analytics/analytics.ts | 62 ++++++++++++++++-------------- src/remote-config/remote-config.ts | 9 ++++- tools/build.js | 1 + 4 files changed, 44 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 1f00cd3f0..dc7f99cfa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@angular/fire", - "version": "5.2.1", + "version": "5.3.0", "description": "The official library of Firebase and Angular.", "private": true, "scripts": { diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index debe2e286..bf1267e41 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -1,13 +1,18 @@ import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; -import { Observable, from, of } from 'rxjs'; -import { map, switchMap, tap, filter } from 'rxjs/operators'; +import { Observable, from } from 'rxjs'; +import { map, tap, filter, withLatestFrom } from 'rxjs/operators'; import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular } from '@angular/fire'; -import { Router, NavigationEnd } from '@angular/router'; +import { Router, NavigationEnd, ActivationEnd } from '@angular/router'; import { FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire'; export const AUTOMATICALLY_SET_CURRENT_SCREEN = new InjectionToken('angularfire2.analytics.setCurrentScreen'); export const ANALYTICS_COLLECTION_ENABLED = new InjectionToken('angularfire2.analytics.analyticsCollectionEnabled'); export const AUTOMATICALLY_TRACK_USER_IDENTIFIER = new InjectionToken('angularfire2.analytics.trackUserIdentifier'); +export const APP_VERSION = new InjectionToken('angularfire2.analytics.appVersion'); +export const APP_NAME = new InjectionToken('angularfire2.analytics.appName'); + +export const DEFAULT_APP_VERSION = '?'; +export const DEFAULT_APP_NAME = 'Angular App'; @Injectable() export class AngularFireAnalytics { @@ -24,45 +29,46 @@ export class AngularFireAnalytics { @Optional() @Inject(AUTOMATICALLY_SET_CURRENT_SCREEN) automaticallySetCurrentScreen:boolean|null, @Optional() @Inject(ANALYTICS_COLLECTION_ENABLED) analyticsCollectionEnabled:boolean|null, @Optional() @Inject(AUTOMATICALLY_TRACK_USER_IDENTIFIER) automaticallyTrackUserIdentifier:boolean|null, - private zone: NgZone + @Optional() @Inject(APP_VERSION) providedAppVersion:string|null, + @Optional() @Inject(APP_NAME) providedAppName:string|null, + zone: NgZone ) { // @ts-ignore zapping in the UMD in the build script const requireAnalytics = from(import('firebase/analytics')); + const app = _firebaseAppFactory(options, nameOrConfig); this.analytics = requireAnalytics.pipe( - map(() => _firebaseAppFactory(options, nameOrConfig)), - map(app => app.analytics()), + map(() => app.analytics()), tap(analytics => { - if (analyticsCollectionEnabled == false) { analytics.setAnalyticsCollectionEnabled(false) } + if (analyticsCollectionEnabled === false) { analytics.setAnalyticsCollectionEnabled(false) } }), runOutsideAngular(zone) ); if (router && automaticallySetCurrentScreen !== false) { - this.analytics.pipe( - switchMap(analytics => - router.events.pipe( - filter(e => e instanceof NavigationEnd), - tap(e => console.log(e)), - tap(e => analytics.setCurrentScreen(e.url)) - ) - ) + const app_name = providedAppName || DEFAULT_APP_NAME; + const app_version = providedAppVersion || DEFAULT_APP_VERSION; + const activationEndEvents = router.events.pipe(filter(e => e instanceof ActivationEnd)); + const navigationEndEvents = router.events.pipe(filter(e => e instanceof NavigationEnd)); + navigationEndEvents.pipe( + withLatestFrom(activationEndEvents, this.analytics), + tap(([navigationEnd, activationEnd, analytics]) => { + const url = navigationEnd.url; + const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || undefined; + const outlet = activationEnd.snapshot.outlet; + analytics.logEvent("screen_view", { app_name, app_version, screen_name, outlet, url }); + // TODO when is screen_name undefined? + analytics.setCurrentScreen(screen_name || url, { global: outlet == "primary" }) + }), + runOutsideAngular(zone) ).subscribe(); } - if (automaticallyTrackUserIdentifier !== false) { - this.analytics.pipe( - switchMap(analytics => { - if (analytics.app.auth) { - const auth = analytics.app.auth(); - return new Observable(auth.onAuthStateChanged.bind(auth)).pipe( - tap(user => console.log("uid", user && user.uid)), - tap(user => user ? analytics.setUserId(user.uid) : analytics.setUserId(null!)) - ) - } else { - return of() - } - }), + // TODO do something other than just check auth presence, what if it's lazy loaded? + if (app.auth && automaticallyTrackUserIdentifier !== false) { + new Observable(app.auth().onAuthStateChanged.bind(app.auth())).pipe( + withLatestFrom(this.analytics), + tap(([user, analytics]) => analytics.setUserId(user ? user.uid : null!, { global: true })), runOutsideAngular(zone) ).subscribe() } diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index 7c66812f7..f098db6be 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -4,6 +4,9 @@ import { map, switchMap, tap } from 'rxjs/operators'; import { FirebaseAppConfig, FirebaseOptions, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; import { remoteConfig } from 'firebase/app'; +// @ts-ignore +import firebase from 'firebase/app'; + export interface DefaultConfig {[key:string]: string|number|boolean}; export const REMOTE_CONFIG_SETTINGS = new InjectionToken('angularfire2.remoteConfig.settings'); @@ -30,10 +33,14 @@ export class AngularFireRemoteConfig { @Optional() @Inject(DEFAULT_CONFIG) defaultConfig:DefaultConfig|null, private zone: NgZone ) { + // import('firebase/remote-config') isn't working for some reason... + // it might have something to do with https://github.com/firebase/firebase-js-sdk/pull/2229 + // import from @firebase/remote-config so I can manually register on the Firebase instance // @ts-ignore zapping in the UMD in the build script - const requireRemoteConfig = from(import('firebase/remote-config')); + const requireRemoteConfig = from(import('@firebase/remote-config')); this.remoteConfig = requireRemoteConfig.pipe( + map(rc => rc.registerRemoteConfig(firebase)), map(() => _firebaseAppFactory(options, nameOrConfig)), map(app => app.remoteConfig()), tap(rc => { diff --git a/tools/build.js b/tools/build.js index 769da1a38..e571f1676 100644 --- a/tools/build.js +++ b/tools/build.js @@ -30,6 +30,7 @@ const GLOBALS = { 'firebase/functions': 'firebase', 'firebase/performance': 'firebase', 'firebase/remote-config': 'firebase', + '@firebase/remote-config': 'firebase', 'firebase/storage': 'firebase', '@angular/fire': 'angularfire2', '@angular/fire/auth': 'angularfire2.auth', From 73413e81906682ce54ced80b0447db6899499f08 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Fri, 4 Oct 2019 18:14:22 +0100 Subject: [PATCH 04/37] DI and jazz --- src/analytics/analytics.ts | 14 ++++++++++---- src/remote-config/remote-config.ts | 24 ++++++++++++++++++------ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index bf1267e41..1e9665a7a 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -6,6 +6,7 @@ import { Router, NavigationEnd, ActivationEnd } from '@angular/router'; import { FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire'; export const AUTOMATICALLY_SET_CURRENT_SCREEN = new InjectionToken('angularfire2.analytics.setCurrentScreen'); +export const AUTOMATICALLY_LOG_SCREEN_VIEWS = new InjectionToken('angularfire2.analytics.logScreenViews'); export const ANALYTICS_COLLECTION_ENABLED = new InjectionToken('angularfire2.analytics.analyticsCollectionEnabled'); export const AUTOMATICALLY_TRACK_USER_IDENTIFIER = new InjectionToken('angularfire2.analytics.trackUserIdentifier'); export const APP_VERSION = new InjectionToken('angularfire2.analytics.appVersion'); @@ -27,6 +28,7 @@ export class AngularFireAnalytics { @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, @Optional() router:Router, @Optional() @Inject(AUTOMATICALLY_SET_CURRENT_SCREEN) automaticallySetCurrentScreen:boolean|null, + @Optional() @Inject(AUTOMATICALLY_LOG_SCREEN_VIEWS) automaticallyLogScreenViews:boolean|null, @Optional() @Inject(ANALYTICS_COLLECTION_ENABLED) analyticsCollectionEnabled:boolean|null, @Optional() @Inject(AUTOMATICALLY_TRACK_USER_IDENTIFIER) automaticallyTrackUserIdentifier:boolean|null, @Optional() @Inject(APP_VERSION) providedAppVersion:string|null, @@ -45,7 +47,7 @@ export class AngularFireAnalytics { runOutsideAngular(zone) ); - if (router && automaticallySetCurrentScreen !== false) { + if (router && (automaticallySetCurrentScreen !== false || automaticallyLogScreenViews !== false)) { const app_name = providedAppName || DEFAULT_APP_NAME; const app_version = providedAppVersion || DEFAULT_APP_VERSION; const activationEndEvents = router.events.pipe(filter(e => e instanceof ActivationEnd)); @@ -56,9 +58,13 @@ export class AngularFireAnalytics { const url = navigationEnd.url; const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || undefined; const outlet = activationEnd.snapshot.outlet; - analytics.logEvent("screen_view", { app_name, app_version, screen_name, outlet, url }); - // TODO when is screen_name undefined? - analytics.setCurrentScreen(screen_name || url, { global: outlet == "primary" }) + if (automaticallyLogScreenViews !== false) { + analytics.logEvent("screen_view", { app_name, app_version, screen_name, outlet, url }); + } + if (automaticallySetCurrentScreen !== false) { + // TODO when is screen_name undefined? + analytics.setCurrentScreen(screen_name || url, { global: outlet == "primary" }) + } }), runOutsideAngular(zone) ).subscribe(); diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index f098db6be..f7dd98e2c 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -1,6 +1,6 @@ import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; -import { Observable, from } from 'rxjs'; -import { map, switchMap, tap } from 'rxjs/operators'; +import { Observable, from, concat } from 'rxjs'; +import { map, switchMap, tap, take } from 'rxjs/operators'; import { FirebaseAppConfig, FirebaseOptions, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; import { remoteConfig } from 'firebase/app'; @@ -26,6 +26,8 @@ export class AngularFireRemoteConfig { public readonly configuration: Observable<{[key:string]: remoteConfig.Value}>; + public readonly activate: Observable<{[key:string]: remoteConfig.Value}>; + constructor( @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, @@ -50,15 +52,25 @@ export class AngularFireRemoteConfig { runOutsideAngular(zone) ); + this.activate = this.remoteConfig.pipe( + switchMap(rc => rc.activate().then(() => rc)), + tap(rc => rc.fetch()), + map(rc => rc.getAll()), + runOutsideAngular(zone), + take(1) + ) + this.freshConfiguration = this.remoteConfig.pipe( switchMap(rc => rc.fetchAndActivate().then(() => rc.getAll())), - runOutsideAngular(zone) + runOutsideAngular(zone), + take(1) ) this.configuration = this.remoteConfig.pipe( - switchMap(rc => rc.activate().then(() => rc)), - tap(rc => rc.fetch()), - map(rc => rc.getAll()), + switchMap(rc => concat( + rc.activate().then(() => rc.getAll()), + rc.fetchAndActivate().then(() => rc.getAll()) + )), runOutsideAngular(zone) ) } From dd0efb11976d3ec91b293c28c889d41a0a69a588 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Fri, 8 Nov 2019 15:53:54 -0800 Subject: [PATCH 05/37] Bumping the tests --- src/analytics/analytics.spec.ts | 140 ++------- src/remote-config/remote-config.spec.ts | 137 ++------- yarn.lock | 375 ++++++++++++++---------- 3 files changed, 266 insertions(+), 386 deletions(-) diff --git a/src/analytics/analytics.spec.ts b/src/analytics/analytics.spec.ts index 30e9b28c6..da486fe61 100644 --- a/src/analytics/analytics.spec.ts +++ b/src/analytics/analytics.spec.ts @@ -1,168 +1,86 @@ -import { User } from 'firebase/app'; -import { Observable, Subject } from 'rxjs' +import { ReflectiveInjector, Provider } from '@angular/core'; import { TestBed, inject } from '@angular/core/testing'; import { FirebaseApp, FirebaseOptionsToken, AngularFireModule, FirebaseNameOrConfigToken } from '@angular/fire'; -import { AngularFireAuth, AngularFireAuthModule } from '@angular/fire/auth'; +import { AngularFireAnalytics, AngularFireAnalyticsModule, AUTOMATICALLY_SET_CURRENT_SCREEN, AUTOMATICALLY_LOG_SCREEN_VIEWS, ANALYTICS_COLLECTION_ENABLED, AUTOMATICALLY_TRACK_USER_IDENTIFIER, APP_VERSION, APP_NAME } from '@angular/fire/analytics'; import { COMMON_CONFIG } from './test-config'; -import { take, skip } from 'rxjs/operators'; -function authTake(auth: Observable, count: number): Observable { - return take.call(auth, 1); -} -function authSkip(auth: Observable, count: number): Observable { - return skip.call(auth, 1); -} - -const firebaseUser = { - uid: '12345', - providerData: [{ displayName: 'jeffbcrossyface' }] -}; - -describe('AngularFireAuth', () => { +describe('AngularFireAnalytics', () => { let app: FirebaseApp; - let afAuth: AngularFireAuth; - let authSpy: jasmine.Spy; - let mockAuthState: Subject; + let analytics: AngularFireAnalytics; beforeEach(() => { TestBed.configureTestingModule({ imports: [ AngularFireModule.initializeApp(COMMON_CONFIG), - AngularFireAuthModule + AngularFireAnalyticsModule ] }); - inject([FirebaseApp, AngularFireAuth], (app_: FirebaseApp, _auth: AngularFireAuth) => { + inject([FirebaseApp, AngularFireAnalytics], (app_: FirebaseApp, _analytics: AngularFireAnalytics) => { app = app_; - afAuth = _auth; + analytics = _analytics; })(); - - mockAuthState = new Subject(); - spyOn(afAuth, 'authState').and.returnValue(mockAuthState); - spyOn(afAuth, 'idToken').and.returnValue(mockAuthState); - afAuth.authState = mockAuthState as Observable; - afAuth.idToken = mockAuthState as Observable; }); afterEach(done => { - afAuth.auth.app.delete().then(done, done.fail); - }); - - describe('Zones', () => { - it('should call operators and subscriber in the same zone as when service was initialized', (done) => { - // Initialize the app outside of the zone, to mimick real life behavior. - let ngZone = Zone.current.fork({ - name: 'ngZone' - }); - ngZone.run(() => { - const subs = [ - afAuth.authState.subscribe(user => { - expect(Zone.current.name).toBe('ngZone'); - done(); - }, done.fail), - afAuth.authState.subscribe(user => { - expect(Zone.current.name).toBe('ngZone'); - done(); - }, done.fail) - ]; - mockAuthState.next(firebaseUser); - subs.forEach(s => s.unsubscribe()); - }); - }); + app.delete(); + done(); }); it('should be exist', () => { - expect(afAuth instanceof AngularFireAuth).toBe(true); + expect(analytics instanceof AngularFireAnalytics).toBe(true); }); - it('should have the Firebase Auth instance', () => { - expect(afAuth.auth).toBeDefined(); - }); - - it('should have an initialized Firebase app', () => { - expect(afAuth.auth.app).toBeDefined(); - expect(afAuth.auth.app).toEqual(app); - }); - - it('should emit auth updates through authState', (done: any) => { - let count = 0; - - // Check that the first value is null and second is the auth user - const subs = afAuth.authState.subscribe(user => { - if (count === 0) { - expect(user).toBe(null!); - count = count + 1; - mockAuthState.next(firebaseUser); - } else { - expect(user).toEqual(firebaseUser); - subs.unsubscribe(); - done(); - } - }, done, done.fail); - mockAuthState.next(null!); - }); - - it('should emit auth updates through idToken', (done: any) => { - let count = 0; - - // Check that the first value is null and second is the auth user - const subs = afAuth.idToken.subscribe(user => { - if (count === 0) { - expect(user).toBe(null!); - count = count + 1; - mockAuthState.next(firebaseUser); - } else { - expect(user).toEqual(firebaseUser); - subs.unsubscribe(); - done(); - } - }, done, done.fail); - mockAuthState.next(null!); + it('should have the Firebase Functions instance', () => { + expect(analytics.analytics).toBeDefined(); }); }); const FIREBASE_APP_NAME_TOO = (Math.random() + 1).toString(36).substring(7); -describe('AngularFireAuth with different app', () => { +describe('AngularFireAnalytics with different app', () => { let app: FirebaseApp; - let afAuth: AngularFireAuth; + let analytics: AngularFireAnalytics; beforeEach(() => { TestBed.configureTestingModule({ imports: [ AngularFireModule.initializeApp(COMMON_CONFIG), - AngularFireAuthModule + AngularFireAnalyticsModule ], providers: [ { provide: FirebaseNameOrConfigToken, useValue: FIREBASE_APP_NAME_TOO }, - { provide: FirebaseOptionsToken, useValue: COMMON_CONFIG } + { provide: FirebaseOptionsToken, useValue: COMMON_CONFIG }, + { provide: AUTOMATICALLY_SET_CURRENT_SCREEN, useValue: true } + { provide: AUTOMATICALLY_LOG_SCREEN_VIEWS, useValue: true }, + { provide: ANALYTICS_COLLECTION_ENABLED, useValue: true }, + { provide: AUTOMATICALLY_TRACK_USER_IDENTIFIER, useValue: true }, + { provide: APP_VERSION, useValue: '0.0' }, + { provide: APP_NAME, useValue: 'Test App!' } ] }); - inject([FirebaseApp, AngularFireAuth], (app_: FirebaseApp, _afAuth: AngularFireAuth) => { + inject([FirebaseApp, AngularFireAnalytics], (app_: FirebaseApp, _analytics: AngularFireAnalytics) => { app = app_; - afAuth = _afAuth; + analytics = _analytics; })(); }); afterEach(done => { - app.delete().then(done, done.fail); + app.delete(); + done(); }); describe('', () => { it('should be an AngularFireAuth type', () => { - expect(afAuth instanceof AngularFireAuth).toEqual(true); + expect(analytics instanceof AngularFireAnalytics).toEqual(true); }); - it('should have an initialized Firebase app', () => { - expect(afAuth.auth.app).toBeDefined(); - expect(afAuth.auth.app).toEqual(app); + it('should have the Firebase Functions instance', () => { + expect(analytics.analytics).toBeDefined(); }); - it('should have an initialized Firebase app instance member', () => { - expect(afAuth.auth.app.name).toEqual(FIREBASE_APP_NAME_TOO); - }); }); }); diff --git a/src/remote-config/remote-config.spec.ts b/src/remote-config/remote-config.spec.ts index 30e9b28c6..a7023d5a7 100644 --- a/src/remote-config/remote-config.spec.ts +++ b/src/remote-config/remote-config.spec.ts @@ -1,168 +1,81 @@ -import { User } from 'firebase/app'; -import { Observable, Subject } from 'rxjs' +import { ReflectiveInjector, Provider } from '@angular/core'; import { TestBed, inject } from '@angular/core/testing'; import { FirebaseApp, FirebaseOptionsToken, AngularFireModule, FirebaseNameOrConfigToken } from '@angular/fire'; -import { AngularFireAuth, AngularFireAuthModule } from '@angular/fire/auth'; +import { AngularFireRemoteConfig, AngularFireRemoteConfigModule, REMOTE_CONFIG_SETTINGS, DEFAULT_CONFIG } from '@angular/fire/remote-config'; import { COMMON_CONFIG } from './test-config'; -import { take, skip } from 'rxjs/operators'; -function authTake(auth: Observable, count: number): Observable { - return take.call(auth, 1); -} - -function authSkip(auth: Observable, count: number): Observable { - return skip.call(auth, 1); -} - -const firebaseUser = { - uid: '12345', - providerData: [{ displayName: 'jeffbcrossyface' }] -}; - -describe('AngularFireAuth', () => { +describe('AngularFireRemoteConfig', () => { let app: FirebaseApp; - let afAuth: AngularFireAuth; - let authSpy: jasmine.Spy; - let mockAuthState: Subject; + let rc: AngularFireRemoteConfig; beforeEach(() => { TestBed.configureTestingModule({ imports: [ AngularFireModule.initializeApp(COMMON_CONFIG), - AngularFireAuthModule + AngularFireRemoteConfigModule ] }); - inject([FirebaseApp, AngularFireAuth], (app_: FirebaseApp, _auth: AngularFireAuth) => { + inject([FirebaseApp, AngularFireRemoteConfig], (app_: FirebaseApp, _rc: AngularFireRemoteConfig) => { app = app_; - afAuth = _auth; + rc = _rc; })(); - - mockAuthState = new Subject(); - spyOn(afAuth, 'authState').and.returnValue(mockAuthState); - spyOn(afAuth, 'idToken').and.returnValue(mockAuthState); - afAuth.authState = mockAuthState as Observable; - afAuth.idToken = mockAuthState as Observable; }); afterEach(done => { - afAuth.auth.app.delete().then(done, done.fail); - }); - - describe('Zones', () => { - it('should call operators and subscriber in the same zone as when service was initialized', (done) => { - // Initialize the app outside of the zone, to mimick real life behavior. - let ngZone = Zone.current.fork({ - name: 'ngZone' - }); - ngZone.run(() => { - const subs = [ - afAuth.authState.subscribe(user => { - expect(Zone.current.name).toBe('ngZone'); - done(); - }, done.fail), - afAuth.authState.subscribe(user => { - expect(Zone.current.name).toBe('ngZone'); - done(); - }, done.fail) - ]; - mockAuthState.next(firebaseUser); - subs.forEach(s => s.unsubscribe()); - }); - }); + app.delete(); + done(); }); it('should be exist', () => { - expect(afAuth instanceof AngularFireAuth).toBe(true); - }); - - it('should have the Firebase Auth instance', () => { - expect(afAuth.auth).toBeDefined(); - }); - - it('should have an initialized Firebase app', () => { - expect(afAuth.auth.app).toBeDefined(); - expect(afAuth.auth.app).toEqual(app); - }); - - it('should emit auth updates through authState', (done: any) => { - let count = 0; - - // Check that the first value is null and second is the auth user - const subs = afAuth.authState.subscribe(user => { - if (count === 0) { - expect(user).toBe(null!); - count = count + 1; - mockAuthState.next(firebaseUser); - } else { - expect(user).toEqual(firebaseUser); - subs.unsubscribe(); - done(); - } - }, done, done.fail); - mockAuthState.next(null!); + expect(rc instanceof AngularFireRemoteConfig).toBe(true); }); - it('should emit auth updates through idToken', (done: any) => { - let count = 0; - - // Check that the first value is null and second is the auth user - const subs = afAuth.idToken.subscribe(user => { - if (count === 0) { - expect(user).toBe(null!); - count = count + 1; - mockAuthState.next(firebaseUser); - } else { - expect(user).toEqual(firebaseUser); - subs.unsubscribe(); - done(); - } - }, done, done.fail); - mockAuthState.next(null!); + it('should have the Firebase Functions instance', () => { + expect(rc.remoteConfig).toBeDefined(); }); }); const FIREBASE_APP_NAME_TOO = (Math.random() + 1).toString(36).substring(7); -describe('AngularFireAuth with different app', () => { +describe('AngularFireRemoteConfig with different app', () => { let app: FirebaseApp; - let afAuth: AngularFireAuth; + let rc: AngularFireRemoteConfig; beforeEach(() => { TestBed.configureTestingModule({ imports: [ AngularFireModule.initializeApp(COMMON_CONFIG), - AngularFireAuthModule + AngularFireRemoteConfigModule ], providers: [ { provide: FirebaseNameOrConfigToken, useValue: FIREBASE_APP_NAME_TOO }, - { provide: FirebaseOptionsToken, useValue: COMMON_CONFIG } + { provide: FirebaseOptionsToken, useValue: COMMON_CONFIG }, + { provide: REMOTE_CONFIG_SETTINGS, useValue: {} }, + { provide: DEFAULT_CONFIG, useValue: {} } ] }); - inject([FirebaseApp, AngularFireAuth], (app_: FirebaseApp, _afAuth: AngularFireAuth) => { + inject([FirebaseApp, AngularFireRemoteConfig], (app_: FirebaseApp, _rc: AngularFireRemoteConfig) => { app = app_; - afAuth = _afAuth; + rc = _rc; })(); }); afterEach(done => { - app.delete().then(done, done.fail); + app.delete(); + done(); }); describe('', () => { it('should be an AngularFireAuth type', () => { - expect(afAuth instanceof AngularFireAuth).toEqual(true); + expect(rc instanceof AngularFireRemoteConfig).toEqual(true); }); - it('should have an initialized Firebase app', () => { - expect(afAuth.auth.app).toBeDefined(); - expect(afAuth.auth.app).toEqual(app); + it('should have the Firebase Functions instance', () => { + expect(rc.remoteConfig).toBeDefined(); }); - it('should have an initialized Firebase app instance member', () => { - expect(afAuth.auth.app.name).toEqual(FIREBASE_APP_NAME_TOO); - }); }); }); diff --git a/yarn.lock b/yarn.lock index 0bbec818b..041feb7c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -123,187 +123,202 @@ dependencies: tslib "^1.9.0" -"@firebase/app-types@0.4.4": - version "0.4.4" - resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.4.4.tgz#6423e9b284018109d3658a6ecf7f82ca0e288bbb" - integrity sha512-s3rKk6aGnzNTx6bzdtOdJpg2XXMiadLk/hv/PjiFEI/m+8MEKz+wkl+eGwvzxFyOd6BD5GVy637boGFqRt20FQ== +"@firebase/analytics-types@0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.2.2.tgz#d442d0cda78dd5a43fe08441423d391b713987c8" + integrity sha512-5qLwkztNdRiLMQCsxmol1FxfGddb9xpensM2y++3rv1e0L4yI+MRR9SHSC00i847tGZDrMt2u67Dyko/xmrgCA== -"@firebase/app@0.4.18": - version "0.4.18" - resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.4.18.tgz#16b87afa1339fe42ad218db3ab88a3e51798bf27" - integrity sha512-2q/1DU2eYS7Iuepszd0cddTRQdRdwuKZ8Somf0LollorzkMNGM1IoGIsRu2vO1xmY8rgKthLCdTey9WE03JhmQ== +"@firebase/analytics@0.2.5": + version "0.2.5" + resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.2.5.tgz#b2d991d55da43de6813ecbdc42a3a4e10f027997" + integrity sha512-m31eUpADtJmD3fBbtIvmBrsN/iqaj31eEjnmljsrnZSYhF2mr8f4soMvA5ciE4gvlgN9/z//frJJgZpCb//PLA== + dependencies: + "@firebase/analytics-types" "0.2.2" + "@firebase/installations" "0.3.4" + "@firebase/util" "0.2.32" + tslib "1.10.0" + +"@firebase/app-types@0.4.7": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.4.7.tgz#792a0f117185e42ec5a247f6bedc94a921711110" + integrity sha512-4LnhDYsUhgxMBnCfQtWvrmMy9XxeZo059HiRbpt3ufdpUcZZOBDOouQdjkODwHLhcnNrB7LeyiqYpS2jrLT8Mw== + +"@firebase/app@0.4.23": + version "0.4.23" + resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.4.23.tgz#102dc131aefa8bb65bb0502b8a1363e23a13bc95" + integrity sha512-0CSfdo0o4NGvdownwcOIpMWpnxyx8M4Ucp0vovBLnJkK3qoLo1AXTvt5Q/C3Rla1kLG3nygE0vF6jue18qDJsA== dependencies: - "@firebase/app-types" "0.4.4" - "@firebase/logger" "0.1.25" - "@firebase/util" "0.2.28" + "@firebase/app-types" "0.4.7" + "@firebase/logger" "0.1.29" + "@firebase/util" "0.2.32" dom-storage "2.1.0" tslib "1.10.0" xmlhttprequest "1.8.0" -"@firebase/auth-types@0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.8.0.tgz#10c0657847f6c311ef42e8a550bdbbb678ec86e4" - integrity sha512-foQHhvyB0RR+mb/+wmHXd/VOU+D8fruFEW1k79Q9wzyTPpovMBa1Mcns5fwEWBhUfi8bmoEtaGB8RSAHnTFzTg== +"@firebase/auth-types@0.8.2": + version "0.8.2" + resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.8.2.tgz#9722138481724a16a79f7d645e9c5598cc932269" + integrity sha512-qcP7wZ76CIb7IN+K544GomA42cCS36KZmQ3n9Ou1JsYplEaMo52x4UuQTZFqlRoMaUWi61oQ9jiuE5tOAMJwDA== -"@firebase/auth@0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.12.0.tgz#e721323f8753f03b7387d899406c6f9e72f65876" - integrity sha512-DGYvAmz2aUmrWYS3ADw/UmsuicxJi6G+X38XITqNPUrd1YxmM5SBzX19oEb9WCrJZXcr4JaESg6hQkT2yEPaCA== +"@firebase/auth@0.12.4": + version "0.12.4" + resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.12.4.tgz#85e54957aa2b8a03b489fb9f59cf2897d0bd498a" + integrity sha512-nGzXJDB6NlGnd4JH16Myl2n+vQKRlJ5Wmjk10CB5ZTJu5NGs65uRf4wLBB6P2VyK0cGD/WcE+mfE34RxY/26hA== dependencies: - "@firebase/auth-types" "0.8.0" + "@firebase/auth-types" "0.8.2" -"@firebase/database-types@0.4.4": - version "0.4.4" - resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.4.4.tgz#d2337413a5d88b326c199cb3008a5fccc9626001" - integrity sha512-n+CKrqfxKII6VMl44HFCPIB8xS78isRiA2V6SYJe4Mo6vauKggRBm8Tdt1SKmBco5zHhhYkhaYzU7bfPYm54Ag== +"@firebase/database-types@0.4.7": + version "0.4.7" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.4.7.tgz#9fc82b3895fc096f073e37dd66d6e5b32f233a54" + integrity sha512-7UHZ0n6aj3sR5W4HsU18dysHMSIS6348xWTMypoA0G4mORaQSuleCSL6zJLaCosarDEojnncy06yW69fyFxZtA== dependencies: - "@firebase/app-types" "0.4.4" + "@firebase/app-types" "0.4.7" -"@firebase/database@0.5.5": - version "0.5.5" - resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.5.5.tgz#f636dd04f955136e1f406f35b1dfe66375a1c7a4" - integrity sha512-rsOQCXoNLSKw7V2RL7xJhGAMS5bj8d/Y4/KfRwoaMsKUdfaogbZEdSgxMjKd00PKIiYa7TSAch0KziEujtAfuA== +"@firebase/database@0.5.11": + version "0.5.11" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.5.11.tgz#ce64eaccfe8b824b5e7d8a3ae120b1460e663e72" + integrity sha512-YEakG5uILYkZ3qEDU4F9pe1HyvPlPG2Zk1FJ5RN2Yt564lTNJTrnltRELoutWoSCAtgEUXEfiTDV+864qFSG9g== dependencies: - "@firebase/database-types" "0.4.4" - "@firebase/logger" "0.1.25" - "@firebase/util" "0.2.28" + "@firebase/database-types" "0.4.7" + "@firebase/logger" "0.1.29" + "@firebase/util" "0.2.32" faye-websocket "0.11.3" tslib "1.10.0" -"@firebase/firestore-types@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-1.5.0.tgz#f05700220f882773ca01e818bf10b1dc456ee5be" - integrity sha512-VhRHNbEbak+R2iK8e1ir2Lec7eaHMZpGTRy6LMtzATYthlkwNHF9tO8JU8l6d1/kYkI4+DWzX++i3HhTziHEWA== - -"@firebase/firestore@1.5.4": - version "1.5.4" - resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-1.5.4.tgz#43728c66d0a1846f33cad147cfc0003cbd9f580b" - integrity sha512-F3zVLTMVdCcUuQNlveVw6FsRBeZlMNQuaR/fXg5eGSidz5W3NdMgQCGd7fRUSP5SsHWlG6KPLh6j+A6LG3p5Xw== - dependencies: - "@firebase/firestore-types" "1.5.0" - "@firebase/logger" "0.1.25" - "@firebase/util" "0.2.28" - "@firebase/webchannel-wrapper" "0.2.26" +"@firebase/firestore-types@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-1.7.0.tgz#796c2d9579ed1a81ce36c216266c40232dbc3d0d" + integrity sha512-mmWeP6XRZXYHC4LIBm69LMk6QXJDp2nVct8Xs1yH3gz517w6S7KAj0NcM7ZvvGwcUoXsm79eGukkvzQWnceTJw== + +"@firebase/firestore@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-1.7.0.tgz#d0ebaa6c9c7fbee2ed0faa71923f6954813e1465" + integrity sha512-/UWkJxXGrTMPG3KVruuW7SR+knyioKWfLi0jkW5ayoQ0lPyIuQ5/SUeOLT3PsEcmJFtHAGQK/eyGoOq9mkaT9A== + dependencies: + "@firebase/firestore-types" "1.7.0" + "@firebase/logger" "0.1.29" + "@firebase/util" "0.2.32" + "@firebase/webchannel-wrapper" "0.2.30" "@grpc/proto-loader" "^0.5.0" - grpc "1.23.3" + grpc "1.24.2" tslib "1.10.0" -"@firebase/functions-types@0.3.8": - version "0.3.8" - resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.3.8.tgz#c01f670bbca04365e680d048b0fe5770a946f643" - integrity sha512-9hajHxA4UWVCGFmoL8PBYHpamE3JTNjObieMmnvZw3cMRTP2EwipMpzZi+GPbMlA/9swF9yHCY/XFAEkwbvdgQ== +"@firebase/functions-types@0.3.10": + version "0.3.10" + resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.3.10.tgz#9ac1bfed56bf4e968d8c2daa12d7d935bcac9fc4" + integrity sha512-QDh7HOxfUzLxj49szekgXxdkXz6ZwwK/63DKeDbRYMKzxo17I01+2GVisdBmUW5NkllVLgVvtOfqKbbEfndqdw== -"@firebase/functions@0.4.19": - version "0.4.19" - resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.4.19.tgz#449710e4c49fa653113b6587012e8a8cc07bf4b7" - integrity sha512-fK1IIDn8ZjdiKO8H1ol9kJ6K0U7411Nodn1am2XuwsvFW8OvI3ufbDZ7eceL0OpZgDHzIQQP1o/VCyuW5dVoew== +"@firebase/functions@0.4.24": + version "0.4.24" + resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.4.24.tgz#84cc60d741bf0f7cc14b189e71e8a079e6226c6a" + integrity sha512-W0kK/kOpJKPDvsIx67pQNbsyh54l2xr8uRQepcLOuMVZTgCj7bh3t7niRW8QGz8WdB/ItKf3Zz3DO6+LTGQLgQ== dependencies: - "@firebase/functions-types" "0.3.8" - "@firebase/messaging-types" "0.3.2" + "@firebase/functions-types" "0.3.10" + "@firebase/messaging-types" "0.3.4" isomorphic-fetch "2.2.1" tslib "1.10.0" -"@firebase/installations-types@0.1.2": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.1.2.tgz#ac2a912e078282fd270b03b571b4639ed88d871a" - integrity sha512-fQaWIW8hyX1XUN7+FCSPjvM1agFjGidVuF4Sxi7aFwfyh5t+4fD2VpM4wCQbWmodnx4fZLvsuQd9mkxxU+lGYQ== +"@firebase/installations-types@0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.2.1.tgz#83f79ed575f1fd3d973ab3b5180f16cb10647712" + integrity sha512-647hwBhMKOjoLndCyi9XmT1wnrRc2T8aGIAoZBqy7rxFqu3c9jZRk3THfufLy1xvAJC3qqYavETrRYF3VigM7Q== -"@firebase/installations@0.2.7": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.2.7.tgz#b439e134c94c423b7aa45efb1bf835fd1762e7e0" - integrity sha512-67tzowHVwRBtEuB1HLMD+fCdoRyinOQlMKBes7UwrtZIVd0CPDUqAKxNqup5EypWZb7O2tqFtRzK7POajfSNMA== +"@firebase/installations@0.3.4": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.3.4.tgz#222a67a357a6081acfef61dc088179d44257dabd" + integrity sha512-uYPoe4ALzoY/1XffEfR0hHnbHQdTLxCCKq7hGe9Y0QBYwVSsC1DivbEXuV63vCLjDLz1kTfNbOnpSVHmqmK/vQ== dependencies: - "@firebase/installations-types" "0.1.2" - "@firebase/util" "0.2.28" + "@firebase/installations-types" "0.2.1" + "@firebase/util" "0.2.32" idb "3.0.2" tslib "1.10.0" -"@firebase/logger@0.1.25": - version "0.1.25" - resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.1.25.tgz#2f95832a62ae634a10242bdeedd84b62d9e5c8ef" - integrity sha512-/lRhuepVcCCnQ2jcO5Hr08SYdmZDTQU9fdPdzg+qXJ9k/QnIrD2RbswXQcL6mmae3uPpX7fFXQAoScJ9pzp50w== +"@firebase/logger@0.1.29": + version "0.1.29" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.1.29.tgz#74f54ee98ec3e719f5df3b4b7c934ded036138bb" + integrity sha512-0GDGHT0eCskNMnDwB1Bx85lHzux9zrf7OJmG/0+kdVkQYFmqJpKwEJnb0mAxLVIVdhYmcYZXPBxUGnN/cQzHNQ== -"@firebase/messaging-types@0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@firebase/messaging-types/-/messaging-types-0.3.2.tgz#cf802617c161434a02fe029290a79f422821d12f" - integrity sha512-2qa2qNKqpalmtwaUV3+wQqfCm5myP/dViIBv+pXF8HinemIfO1IPQtr9pCNfsSYyus78qEhtfldnPWXxUH5v0w== +"@firebase/messaging-types@0.3.4": + version "0.3.4" + resolved "https://registry.yarnpkg.com/@firebase/messaging-types/-/messaging-types-0.3.4.tgz#8154d731355194f8de9a6cbcd33b84c696b5f2ba" + integrity sha512-ytOzB08u8Z15clbq9cAjhFzUs4WRy13A7gRphjnf5rQ+gsrqb4mpcsGIp0KeOcu7MjcN9fqjO5nbVaw0t2KJpQ== -"@firebase/messaging@0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.5.0.tgz#88188e669e4833f73ce3106f9063f099da32cfd0" - integrity sha512-bk1W0eOah2js/DMI9hrilQqTBFijJNu617K0y1hXfJnGrJCTZRcHLX/6FUQ0S95YfaT3+j1NxS4ZuEgnnXKkjA== +"@firebase/messaging@0.5.5": + version "0.5.5" + resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.5.5.tgz#dcead035ec09c61e1b0d3a6488d1049ae1edb299" + integrity sha512-NhF2Mf19qec5hpXtwnwilQGLYcoCN8hvorjuilyrPfeeZEtH1EXOSHMEoSzgJCFBOfvCf+5/Gxug+RZrZV7Hjw== dependencies: - "@firebase/installations" "0.2.7" - "@firebase/messaging-types" "0.3.2" - "@firebase/util" "0.2.28" + "@firebase/installations" "0.3.4" + "@firebase/messaging-types" "0.3.4" + "@firebase/util" "0.2.32" tslib "1.10.0" -"@firebase/performance-types@0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.0.3.tgz#bdd37975cd5f12a55d3951f4942c3fa2661b354f" - integrity sha512-RuC63nYJPJU65AsrNMc3fTRcRgHiyNcQLh9ufeKUT1mEsFgpxr167gMb+tpzNU4jsbvM6+c6nQAFdHpqcGkRlQ== - -"@firebase/performance@0.2.19": - version "0.2.19" - resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.2.19.tgz#aed889539f8f8af022191a92067cacbc67a8980b" - integrity sha512-dINWwR/XcSiSnFNNX7QWfec8bymiXk1Zp6mPyPN+R9ONMrpDbygQUy06oT/6r/xx9nHG4Za6KMUJag3sWNKqnQ== - dependencies: - "@firebase/installations" "0.2.7" - "@firebase/logger" "0.1.25" - "@firebase/performance-types" "0.0.3" - "@firebase/util" "0.2.28" +"@firebase/performance-types@0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.0.5.tgz#8576af338ef1890bce57fbd7cd1c99ef65cedf4b" + integrity sha512-7BOQ2sZZzaIvxscgan/uP8GcH2FZXzqGcQnXvUZBXfXignAM80hJ7UOjVRETa4UjtDehWcD/pZDDivupKtKUyg== + +"@firebase/performance@0.2.24": + version "0.2.24" + resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.2.24.tgz#2ba8709d5a638bc92a9be05f416fb5886c069e95" + integrity sha512-jsTwP1RogWLVZNNUjMhmOZbwhwVMKxZie3tfTaGR2+Co752EQuzo/2+RxWk8Pgs3dj8XyJvPVtkXFrWRpshGiA== + dependencies: + "@firebase/installations" "0.3.4" + "@firebase/logger" "0.1.29" + "@firebase/performance-types" "0.0.5" + "@firebase/util" "0.2.32" tslib "1.10.0" -"@firebase/polyfill@0.3.23": - version "0.3.23" - resolved "https://registry.yarnpkg.com/@firebase/polyfill/-/polyfill-0.3.23.tgz#abf1a4050a2d66fc3ae5e64e959da0f8881174b6" - integrity sha512-CAbNJUK7NGjkS1txCCm0ymIlHVSrf6ssALKlk//2h3KOEYBngTsmQ3odbVX4yqfxTPhx9V2by+ycY5HhSjVv6Q== +"@firebase/polyfill@0.3.27": + version "0.3.27" + resolved "https://registry.yarnpkg.com/@firebase/polyfill/-/polyfill-0.3.27.tgz#88130523c5fa739386aee4c2662cff902d0aaea6" + integrity sha512-dtPtPbpFqSW0T5u31/Qn1Kp4E3xNq6sxLYT7DZ86U4mdg5t0BJtmQRQLEMOd248XNW6zSkloxzJG7oT0b9VFyQ== dependencies: - core-js "3.2.1" + core-js "3.3.6" promise-polyfill "8.1.3" whatwg-fetch "2.0.4" -"@firebase/remote-config-types@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.1.0.tgz#8f4d06f3ca0f6f18b1942f00d9666023740c8ad3" - integrity sha512-ER6MHeUBF74j20Jau1OtxCoN7DXigJwO3NPz3uax3CBsM/dLNHMjr9iywF1yCs4UnjkOFyDqb1EDWqAE12cGjg== +"@firebase/remote-config-types@0.1.2": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.1.2.tgz#056f30e9ba400cc191978bf5138ee4e11ab0d123" + integrity sha512-V1LutCn9fHC5ckkjJFGGB7z72xdg50RVaCkR80x4iJXqac0IpbEr1/k29HSPKiAlf+aIYyj6gFzl7gn4ZzsUcA== -"@firebase/remote-config@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.1.0.tgz#9098781122980834a98dc65ab660c479a6afb0df" - integrity sha512-ly8uIm/txjXO4+A2qWfc3JiiQjMqHOkww5rtAGflraFxZm50wp8Adc35bNDtDjUwS19AldrUVzcf6AZqfqN3pQ== +"@firebase/remote-config@0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.1.5.tgz#fe8ec247c298fdec59a63be9880fdb7d20311f7f" + integrity sha512-gK9P5B+RdZvqW7YrGcZpaZK50e+VaVFY4hrulIIg73uvK1QeBLqY6h4ARzLyqFQDc3dCM2MJzn56z+hc7lz4pA== dependencies: - "@firebase/installations" "0.2.7" - "@firebase/logger" "0.1.25" - "@firebase/remote-config-types" "0.1.0" - "@firebase/util" "0.2.28" + "@firebase/installations" "0.3.4" + "@firebase/logger" "0.1.29" + "@firebase/remote-config-types" "0.1.2" + "@firebase/util" "0.2.32" tslib "1.10.0" -"@firebase/storage-types@0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.3.3.tgz#179ad38485d450023406cf9731560d690ef39d19" - integrity sha512-fUp4kpbxwDiWs/aIBJqBvXgFHZvgoND2JA0gJYSEsXtWtVwfgzY/710plErgZDeQKopX5eOR1sHskZkQUy0U6w== +"@firebase/storage-types@0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.3.5.tgz#c8bce8b09fa5937cb55a77b5549237ebc66fd996" + integrity sha512-rbG6ge3xmte0nUt0asMToPqmNRU4tCJuu73Uy0UJV4uMBQA7e27EXbPza3nBpeOrgASBV9eWPD5AzecjBvTyzQ== -"@firebase/storage@0.3.12": - version "0.3.12" - resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.3.12.tgz#6e48bac57c0a6e3c9e4678d722109a5ff04c3d3d" - integrity sha512-8hXt3qPZlVH+yPF4W9Dc15/gBiTPGUJUgYs3dH9WnO41QWl1o4aNlZpZK/pdnpCIO1GmN0+PxJW9TCNb0H0Hqw== +"@firebase/storage@0.3.18": + version "0.3.18" + resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.3.18.tgz#63ce56a0c0082f342a544f93cc3e378691c5e394" + integrity sha512-+xG7j7w7JkScZK7m7KYUd/l3Z8CrvbpXZbD3WAv5BzFncbMYcDjDtdWmK8tC9xly/GU56fxg8HPDYoeI52bvXw== dependencies: - "@firebase/storage-types" "0.3.3" - "@firebase/util" "0.2.28" + "@firebase/storage-types" "0.3.5" + "@firebase/util" "0.2.32" tslib "1.10.0" -"@firebase/util@0.2.28": - version "0.2.28" - resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.2.28.tgz#9a17198b6ea416f1fb9edaea9f353d9a1f517f51" - integrity sha512-ZQMAWtXj8y5kvB6izs0aTM/jG+WO8HpqhXA/EwD6LckJ+1P5LnAhaLZt1zR4HpuCE+jeP5I32Id5RJ/aifFs6A== +"@firebase/util@0.2.32": + version "0.2.32" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.2.32.tgz#7a7a9c4518551de1175ee5a84ab78d0473af67b5" + integrity sha512-n5l1RDxzhQeLOFWRPdatyGt3ig1NLEmtO1wnG4x3Z5rOZAb09aBp+kYBu5HExJ4o6e+36lJ6l3nwdRnsJWaUlQ== dependencies: tslib "1.10.0" -"@firebase/webchannel-wrapper@0.2.26": - version "0.2.26" - resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.2.26.tgz#4fe0e0a878d26af98901b29f051deed8017d6237" - integrity sha512-VlTurkvs4v7EVFWESBZGOPghFEokQhU5au5CP9WqA8B2/PcQRDsaaQlQCA6VATuEnW+vtSiSBvTiOc4004f8xg== +"@firebase/webchannel-wrapper@0.2.30": + version "0.2.30" + resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.2.30.tgz#dd6878dffb09b6aedcbbfb226ce6461375b7c354" + integrity sha512-tja+VVbHFN2q6k+n1Mgu/hFuips9UVCYTLzwbRL6Uu398OKPo57RoT4+UFrZx1iqb84ATw7ZEn7TtDH7IjtjJQ== "@grpc/proto-loader@^0.5.0": version "0.5.0" @@ -2217,10 +2232,10 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-js@3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09" - integrity sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw== +core-js@3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.3.6.tgz#6ad1650323c441f45379e176ed175c0d021eac92" + integrity sha512-u4oM8SHwmDuh5mWZdDg9UwNVq5s1uqq6ZDLLIs07VY+VJU91i3h4f3K/pgFvtUQPGdeStrZ+odKyfyt4EnKHfA== core-js@^2.2.0, core-js@^2.4.0, core-js@^2.5.0: version "2.6.5" @@ -3291,24 +3306,25 @@ firebase@2.x.x: dependencies: faye-websocket ">=0.6.0" -"firebase@>= 5.5.7 <8ss": - version "7.0.0" - resolved "https://registry.yarnpkg.com/firebase/-/firebase-7.0.0.tgz#3919ed3c1f9441f0465d35b2f3bedf56f206a8a5" - integrity sha512-XLuIsv5IBNILZ1jKxvZah+2p+ZvA1jpAC45TUlobMKhpYp6TN9KkEK+8J11qNxkJG6PittHW3TjxJqejU8xg9Q== - dependencies: - "@firebase/app" "0.4.18" - "@firebase/app-types" "0.4.4" - "@firebase/auth" "0.12.0" - "@firebase/database" "0.5.5" - "@firebase/firestore" "1.5.4" - "@firebase/functions" "0.4.19" - "@firebase/installations" "0.2.7" - "@firebase/messaging" "0.5.0" - "@firebase/performance" "0.2.19" - "@firebase/polyfill" "0.3.23" - "@firebase/remote-config" "0.1.0" - "@firebase/storage" "0.3.12" - "@firebase/util" "0.2.28" +"firebase@>= 5.5.7 <8": + version "7.3.0" + resolved "https://registry.yarnpkg.com/firebase/-/firebase-7.3.0.tgz#b81540de8f5115349868ff1ad9208d9371747996" + integrity sha512-AlCwtFuqLE2b1RkCXV9YKw7kSbIJW6r8n/pBDajORKDnCeQsiSmT+nDmcDtXgXOyLgLtJ0p/q5Mt3VHZ4kmxEg== + dependencies: + "@firebase/analytics" "0.2.5" + "@firebase/app" "0.4.23" + "@firebase/app-types" "0.4.7" + "@firebase/auth" "0.12.4" + "@firebase/database" "0.5.11" + "@firebase/firestore" "1.7.0" + "@firebase/functions" "0.4.24" + "@firebase/installations" "0.3.4" + "@firebase/messaging" "0.5.5" + "@firebase/performance" "0.2.24" + "@firebase/polyfill" "0.3.27" + "@firebase/remote-config" "0.1.5" + "@firebase/storage" "0.3.18" + "@firebase/util" "0.2.32" first-chunk-stream@^1.0.0: version "1.0.0" @@ -3877,16 +3893,16 @@ graceful-fs@~1.2.0: resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= -grpc@1.23.3: - version "1.23.3" - resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.23.3.tgz#30a013ca2cd7e350b0ffc0034be5380ddef3ae7f" - integrity sha512-7vdzxPw9s5UYch4aUn4hyM5tMaouaxUUkwkgJlwbR4AXMxiYZJOv19N2ps2eKiuUbJovo5fnGF9hg/X91gWYjw== +grpc@1.24.2: + version "1.24.2" + resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.24.2.tgz#76d047bfa7b05b607cbbe3abb99065dcefe0c099" + integrity sha512-EG3WH6AWMVvAiV15d+lr+K77HJ/KV/3FvMpjKjulXHbTwgDZkhkcWbwhxFAoTdxTkQvy0WFcO3Nog50QBbHZWw== dependencies: "@types/bytebuffer" "^5.0.40" lodash.camelcase "^4.3.0" lodash.clone "^4.5.0" nan "^2.13.2" - node-pre-gyp "^0.13.0" + node-pre-gyp "^0.14.0" protobufjs "^5.0.3" gtoken@^1.2.1: @@ -5867,6 +5883,14 @@ minipass@^2.2.1, minipass@^2.3.4: safe-buffer "^5.1.2" yallist "^3.0.0" +minipass@^2.8.6, minipass@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + minizlib@^1.1.1: version "1.2.1" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" @@ -5874,6 +5898,13 @@ minizlib@^1.1.1: dependencies: minipass "^2.2.1" +minizlib@^1.2.1: + version "1.3.3" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== + dependencies: + minipass "^2.9.0" + mixin-deep@^1.2.0: version "1.3.1" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" @@ -6064,10 +6095,10 @@ node-pre-gyp@^0.12.0: semver "^5.3.0" tar "^4" -node-pre-gyp@^0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.13.0.tgz#df9ab7b68dd6498137717838e4f92a33fc9daa42" - integrity sha512-Md1D3xnEne8b/HGVQkZZwV27WUi1ZRuZBij24TNaZwUPU3ZAFtvT6xxJGaUVillfmMKnn5oD1HoGsp2Ftik7SQ== +node-pre-gyp@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz#9a0596533b877289bcad4e143982ca3d904ddc83" + integrity sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA== dependencies: detect-libc "^1.0.2" mkdirp "^0.5.1" @@ -6078,7 +6109,7 @@ node-pre-gyp@^0.13.0: rc "^1.2.7" rimraf "^2.6.1" semver "^5.3.0" - tar "^4" + tar "^4.4.2" node-uuid@~1.4.0, node-uuid@~1.4.7: version "1.4.8" @@ -8334,6 +8365,19 @@ tar@^4, tar@^4.3.0: safe-buffer "^5.1.2" yallist "^3.0.2" +tar@^4.4.2: + version "4.4.13" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" + integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== + dependencies: + chownr "^1.1.1" + fs-minipass "^1.2.5" + minipass "^2.8.6" + minizlib "^1.2.1" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.3" + tempfile@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-1.1.1.tgz#5bcc4eaecc4ab2c707d8bc11d99ccc9a2cb287f2" @@ -9200,6 +9244,11 @@ yallist@^3.0.0, yallist@^3.0.2: resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== +yallist@^3.0.3: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + yargs-parser@^13.0.0: version "13.1.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.0.tgz#7016b6dd03e28e1418a510e258be4bff5a31138f" From dffca53823d3bf2704bda8d49ccac84eed032a20 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Fri, 8 Nov 2019 16:09:06 -0800 Subject: [PATCH 06/37] Adding to the root-spec --- src/messaging/messaing.spec.ts | 79 ++++++++++++++++++++++++++++++++++ src/root.spec.js | 4 +- 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 src/messaging/messaing.spec.ts diff --git a/src/messaging/messaing.spec.ts b/src/messaging/messaing.spec.ts new file mode 100644 index 000000000..d99434a18 --- /dev/null +++ b/src/messaging/messaing.spec.ts @@ -0,0 +1,79 @@ +import { ReflectiveInjector, Provider } from '@angular/core'; +import { TestBed, inject } from '@angular/core/testing'; +import { FirebaseApp, FirebaseOptionsToken, AngularFireModule, FirebaseNameOrConfigToken } from '@angular/fire'; +import { AngularFireMessaging, AngularFireMessagingModule } from '@angular/fire/messaging'; +import { COMMON_CONFIG } from './test-config'; + +describe('AngularFireMessaging', () => { + let app: FirebaseApp; + let afm: AngularFireMessaging; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG), + AngularFireMessagingModule + ] + }); + inject([FirebaseApp, AngularFireMessaging], (app_: FirebaseApp, _afm: AngularFireMessaging) => { + app = app_; + afm = _afm; + })(); + }); + + afterEach(done => { + app.delete(); + done(); + }); + + it('should be exist', () => { + expect(afm instanceof AngularFireMessaging).toBe(true); + }); + + it('should have the FCM instance', () => { + expect(afm.messaging).toBeDefined(); + }); + +}); + +const FIREBASE_APP_NAME_TOO = (Math.random() + 1).toString(36).substring(7); + +describe('AngularFireMessaging with different app', () => { + let app: FirebaseApp; + let afm: AngularFireMessaging; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + AngularFireModule.initializeApp(COMMON_CONFIG), + AngularFireMessagingModule + ], + providers: [ + { provide: FirebaseNameOrConfigToken, useValue: FIREBASE_APP_NAME_TOO }, + { provide: FirebaseOptionsToken, useValue: COMMON_CONFIG } + ] + }); + inject([FirebaseApp, AngularFireMessaging], (app_: FirebaseApp, _afm: AngularFireMessaging) => { + app = app_; + afm = _afm; + })(); + }); + + afterEach(done => { + app.delete(); + done(); + }); + + describe('', () => { + + it('should be an AngularFireMessaging type', () => { + expect(afm instanceof AngularFireMessaging).toEqual(true); + }); + + it('should have the FCM instance', () => { + expect(afm.messaging).toBeDefined(); + }); + + }); + +}); diff --git a/src/root.spec.js b/src/root.spec.js index 2819c3a0e..7863e7671 100644 --- a/src/root.spec.js +++ b/src/root.spec.js @@ -2,6 +2,7 @@ export * from './packages-dist/angularfire2.spec'; export * from './packages-dist/auth/auth.spec'; export * from './packages-dist/auth-guard/auth-guard.spec'; +export * from './packages-dist/analtyics/analtyics.spec'; export * from './packages-dist/firestore/firestore.spec'; export * from './packages-dist/firestore/document/document.spec'; export * from './packages-dist/firestore/collection/collection.spec'; @@ -16,7 +17,8 @@ export * from './packages-dist/database/list/state-changes.spec'; export * from './packages-dist/database/list/audit-trail.spec'; export * from './packages-dist/storage/storage.spec'; export * from './packages-dist/performance/performance.spec'; -//export * from './packages-dist/messaging/messaging.spec'; +export * from './packages-dist/remote-config/remote-config.spec'; +export * from './packages-dist/messaging/messaging.spec'; // // Since this a deprecated API, we run on it on manual tests only // // It needs a network connection to run which makes it flaky on Travis From 9731e3ce8392d7ce7ceeb396aa2417a13e26ff56 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Fri, 8 Nov 2019 16:44:31 -0800 Subject: [PATCH 07/37] Spelling is good. --- src/messaging/{messaing.spec.ts => messaging.spec.ts} | 0 src/root.spec.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename src/messaging/{messaing.spec.ts => messaging.spec.ts} (100%) diff --git a/src/messaging/messaing.spec.ts b/src/messaging/messaging.spec.ts similarity index 100% rename from src/messaging/messaing.spec.ts rename to src/messaging/messaging.spec.ts diff --git a/src/root.spec.js b/src/root.spec.js index 7863e7671..3ec5f5649 100644 --- a/src/root.spec.js +++ b/src/root.spec.js @@ -2,7 +2,7 @@ export * from './packages-dist/angularfire2.spec'; export * from './packages-dist/auth/auth.spec'; export * from './packages-dist/auth-guard/auth-guard.spec'; -export * from './packages-dist/analtyics/analtyics.spec'; +export * from './packages-dist/analytics/analytics.spec'; export * from './packages-dist/firestore/firestore.spec'; export * from './packages-dist/firestore/document/document.spec'; export * from './packages-dist/firestore/collection/collection.spec'; From dcdf8bcf3a52f42dfcbe20d63d8eb00aead2460a Mon Sep 17 00:00:00 2001 From: James Daniels Date: Sat, 9 Nov 2019 13:14:28 -0800 Subject: [PATCH 08/37] Have to include UMDs in the karma.conf --- karma.conf.js | 3 +++ src/messaging/tsconfig-test.json | 3 ++- src/remote-config/tsconfig-test.json | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/karma.conf.js b/karma.conf.js index 5ab48f0da..1eb9c4980 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -34,10 +34,13 @@ module.exports = function(config) { 'node_modules/firebase/firebase-storage.js', 'dist/packages-dist/bundles/core.umd.{js,map}', 'dist/packages-dist/bundles/auth.umd.{js,map}', + 'dist/packages-dist/bundles/analytics.umd.{js,map}', 'dist/packages-dist/bundles/auth-guard.umd.{js,map}', 'dist/packages-dist/bundles/database.umd.{js,map}', 'dist/packages-dist/bundles/firestore.umd.{js,map}', 'dist/packages-dist/bundles/functions.umd.{js,map}', + 'dist/packages-dist/bundles/messaging.umd.{js,map}', + 'dist/packages-dist/bundles/remote-config.umd.{js,map}', 'dist/packages-dist/bundles/storage.umd.{js,map}', 'dist/packages-dist/bundles/performance.umd.{js,map}', 'dist/packages-dist/bundles/database-deprecated.umd.{js,map}', diff --git a/src/messaging/tsconfig-test.json b/src/messaging/tsconfig-test.json index f6ca2b3c2..3b5da1a2b 100644 --- a/src/messaging/tsconfig-test.json +++ b/src/messaging/tsconfig-test.json @@ -3,7 +3,8 @@ "compilerOptions": { "baseUrl": ".", "paths": { - "@angular/fire": ["../../dist/packages-dist"] + "@angular/fire": ["../../dist/packages-dist"], + "@angular/fire/messaging": ["../../dist/packages-dist/messaging"] } }, "files": [ diff --git a/src/remote-config/tsconfig-test.json b/src/remote-config/tsconfig-test.json index 29d4b4f44..20810b081 100644 --- a/src/remote-config/tsconfig-test.json +++ b/src/remote-config/tsconfig-test.json @@ -4,7 +4,7 @@ "baseUrl": ".", "paths": { "@angular/fire": ["../../dist/packages-dist"], - "@angular/fire/auth": ["../../dist/packages-dist/auth"] + "@angular/fire/reomte-config": ["../../dist/packages-dist/remote-config"] } }, "files": [ From 6f71d4639e7d36d29889fcfb62d805e08740fa06 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Mon, 11 Nov 2019 15:08:53 -0800 Subject: [PATCH 09/37] Just importing performance is destablizing the app --- src/performance/performance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/performance/performance.ts b/src/performance/performance.ts index 2c8e8ab4b..1794600c6 100644 --- a/src/performance/performance.ts +++ b/src/performance/performance.ts @@ -31,7 +31,7 @@ export class AngularFirePerformance { ) { // @ts-ignore zapping in the UMD in the build script - const requirePerformance = from(import('firebase/performance')); + const requirePerformance = from(zone.runOutsideAngular(() => import('firebase/performance'))); this.performance = requirePerformance.pipe( // SEMVER while < 6 need to type, drop next major From d7d52c8aad9f8730dc716b49e3145c2cd6eb0f42 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 12 Nov 2019 11:48:51 -0800 Subject: [PATCH 10/37] Adding the zone arg to the app factory --- src/analytics/analytics.ts | 4 ++-- src/remote-config/remote-config.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index 1e9665a7a..112f96c6a 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -37,7 +37,7 @@ export class AngularFireAnalytics { ) { // @ts-ignore zapping in the UMD in the build script const requireAnalytics = from(import('firebase/analytics')); - const app = _firebaseAppFactory(options, nameOrConfig); + const app = _firebaseAppFactory(options, zone, nameOrConfig); this.analytics = requireAnalytics.pipe( map(() => app.analytics()), @@ -63,7 +63,7 @@ export class AngularFireAnalytics { } if (automaticallySetCurrentScreen !== false) { // TODO when is screen_name undefined? - analytics.setCurrentScreen(screen_name || url, { global: outlet == "primary" }) + analytics.setCurrentScreen(screen_name || url, { global: outlet == "primary" }); } }), runOutsideAngular(zone) diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index f7dd98e2c..d3855ef14 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -43,7 +43,7 @@ export class AngularFireRemoteConfig { this.remoteConfig = requireRemoteConfig.pipe( map(rc => rc.registerRemoteConfig(firebase)), - map(() => _firebaseAppFactory(options, nameOrConfig)), + map(() => _firebaseAppFactory(options, zone, nameOrConfig)), map(app => app.remoteConfig()), tap(rc => { if (settings) { rc.settings = settings } From eb4bc00f223107660706b3711b47dfbf65b456a4 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Wed, 13 Nov 2019 15:09:47 -0800 Subject: [PATCH 11/37] First pass on the new RC API and the start of the AngularFireLazy effort * Add all, numbers, strings, and booleans Observables to AngularFireRemoteConfig * Proxy all of firebase.remoteConfig() in AngularFireRemoteConfig dealing with lazy loading of the SDK * Same effort with AngularFireAnalytics --- src/analytics/analytics.ts | 34 +++++++--- src/core/angularfire2.ts | 33 ++++++++++ src/remote-config/remote-config.ts | 99 +++++++++++++++++++++--------- 3 files changed, 128 insertions(+), 38 deletions(-) diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index 112f96c6a..77d8c0431 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -1,9 +1,9 @@ import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; import { Observable, from } from 'rxjs'; -import { map, tap, filter, withLatestFrom } from 'rxjs/operators'; -import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular } from '@angular/fire'; +import { map, tap, filter, withLatestFrom, shareReplay } from 'rxjs/operators'; import { Router, NavigationEnd, ActivationEnd } from '@angular/router'; -import { FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire'; +import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular, _lazySDKProxy, FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire'; +import { analytics, app } from 'firebase'; export const AUTOMATICALLY_SET_CURRENT_SCREEN = new InjectionToken('angularfire2.analytics.setCurrentScreen'); export const AUTOMATICALLY_LOG_SCREEN_VIEWS = new InjectionToken('angularfire2.analytics.logScreenViews'); @@ -15,13 +15,28 @@ export const APP_NAME = new InjectionToken('angularfire2.analytics.appNa export const DEFAULT_APP_VERSION = '?'; export const DEFAULT_APP_NAME = 'Angular App'; +// SEMVER: once we move to Typescript 3.6 use `PromiseProxy` +type AnalyticsProxy = { + // TODO can we pull the richer types from the Firebase SDK .d.ts? ReturnType is infering + // I could even do this in a manual build-step + logEvent(eventName: string, eventParams?: {[key: string]: any}, options?: analytics.AnalyticsCallOptions): Promise, + setCurrentScreen(screenName: string, options?: analytics.AnalyticsCallOptions): Promise, + setUserId(id: string, options?: analytics.AnalyticsCallOptions): Promise, + setUserProperties(properties: analytics.CustomParams, options?: analytics.AnalyticsCallOptions): Promise, + setAnalyticsCollectionEnabled(enabled: boolean): Promise, + app: Promise +}; + +export interface AngularFireAnalytics extends AnalyticsProxy {}; + @Injectable() export class AngularFireAnalytics { /** * Firebase Analytics instance */ - public readonly analytics: Observable; + private readonly analytics$: Observable; + private get analytics() { return this.analytics$.toPromise(); } constructor( @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, @@ -39,12 +54,13 @@ export class AngularFireAnalytics { const requireAnalytics = from(import('firebase/analytics')); const app = _firebaseAppFactory(options, zone, nameOrConfig); - this.analytics = requireAnalytics.pipe( + this.analytics$ = requireAnalytics.pipe( map(() => app.analytics()), tap(analytics => { if (analyticsCollectionEnabled === false) { analytics.setAnalyticsCollectionEnabled(false) } }), - runOutsideAngular(zone) + runOutsideAngular(zone), + shareReplay(1) ); if (router && (automaticallySetCurrentScreen !== false || automaticallyLogScreenViews !== false)) { @@ -53,7 +69,7 @@ export class AngularFireAnalytics { const activationEndEvents = router.events.pipe(filter(e => e instanceof ActivationEnd)); const navigationEndEvents = router.events.pipe(filter(e => e instanceof NavigationEnd)); navigationEndEvents.pipe( - withLatestFrom(activationEndEvents, this.analytics), + withLatestFrom(activationEndEvents, this.analytics$), tap(([navigationEnd, activationEnd, analytics]) => { const url = navigationEnd.url; const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || undefined; @@ -73,12 +89,14 @@ export class AngularFireAnalytics { // TODO do something other than just check auth presence, what if it's lazy loaded? if (app.auth && automaticallyTrackUserIdentifier !== false) { new Observable(app.auth().onAuthStateChanged.bind(app.auth())).pipe( - withLatestFrom(this.analytics), + withLatestFrom(this.analytics$), tap(([user, analytics]) => analytics.setUserId(user ? user.uid : null!, { global: true })), runOutsideAngular(zone) ).subscribe() } + return _lazySDKProxy(this, this.analytics, zone); + } } diff --git a/src/core/angularfire2.ts b/src/core/angularfire2.ts index e95292bfd..dab20fac5 100644 --- a/src/core/angularfire2.ts +++ b/src/core/angularfire2.ts @@ -66,3 +66,36 @@ export const runInZone = (zone: NgZone) => (obs$: Observable): Observable< ); }); } + +//SEMVER: once we move to TypeScript 3.6, we can use these to build lazy interfaces +/* + type FunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T]; + type PromiseReturningFunctionPropertyNames = { [K in FunctionPropertyNames]: ReturnType extends Promise ? K : never }[FunctionPropertyNames]; + type NonPromiseReturningFunctionPropertyNames = { [K in FunctionPropertyNames]: ReturnType extends Promise ? never : K }[FunctionPropertyNames]; + type NonFunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]; + + export type PromiseProxy = { [K in NonFunctionPropertyNames]: Promise } & + { [K in NonPromiseReturningFunctionPropertyNames]: (...args: Parameters) => Promise> } & + { [K in PromiseReturningFunctionPropertyNames ]: (...args: Parameters) => ReturnType }; +*/ + +export const _lazySDKProxy = (klass: any, promise: Promise, zone: NgZone) => new Proxy(klass, { + get: (_, name) => zone.runOutsideAngular(() => + klass[name] || new Proxy(() => + promise.then(mod => { + const ret = mod[name]; + // TODO move to proper type guards + if (typeof ret == 'function') { + return ret.bind(mod); + } else if (ret && ret.then) { + return ret.then((res:any) => zone.run(() => res)); + } else { + return zone.run(() => ret); + } + }), { + get: (self, name) => self()[name], + // TODO handle callbacks + apply: (self, _, args) => self().then(it => it(...args)) + }) + ) +}); \ No newline at end of file diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index d3855ef14..ab92c33bf 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -1,7 +1,7 @@ import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; import { Observable, from, concat } from 'rxjs'; -import { map, switchMap, tap, take } from 'rxjs/operators'; -import { FirebaseAppConfig, FirebaseOptions, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; +import { map, switchMap, tap, shareReplay, distinctUntilChanged } from 'rxjs/operators'; +import { FirebaseAppConfig, FirebaseOptions, _lazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; import { remoteConfig } from 'firebase/app'; // @ts-ignore @@ -14,19 +14,38 @@ export const DEFAULT_CONFIG = new InjectionToken('angularfire2.re import { FirebaseRemoteConfig, _firebaseAppFactory, runOutsideAngular } from '@angular/fire'; -@Injectable() -export class AngularFireRemoteConfig { +// SEMVER: once we move to Typescript 3.6 use `PromiseProxy` rather than hardcoding +type RemoteConfigProxy = { + activate: () => Promise; + ensureInitialized: () => Promise; + fetch: () => Promise; + fetchAndActivate: () => Promise; + getAll: () => Promise<{[key:string]: remoteConfig.Value}>; + getBoolean: (key:string) => Promise; + getNumber: (key:string) => Promise; + getString: (key:string) => Promise; + getValue: (key:string) => Promise; + setLogLevel: (logLevel: remoteConfig.LogLevel) => Promise; + settings: Promise; + defaultConfig: Promise<{ + [key: string]: string | number | boolean; + }>; + fetchTimeMillis: Promise; + lastFetchStatus: Promise; +}; - /** - * Firebase RemoteConfig instance - */ - public readonly remoteConfig: Observable; +export interface AngularFireRemoteConfig extends RemoteConfigProxy {}; - public readonly freshConfiguration: Observable<{[key:string]: remoteConfig.Value}>; +@Injectable() +export class AngularFireRemoteConfig { - public readonly configuration: Observable<{[key:string]: remoteConfig.Value}>; + private readonly remoteConfig$: Observable; + private get remoteConfig() { return this.remoteConfig$.toPromise(); } - public readonly activate: Observable<{[key:string]: remoteConfig.Value}>; + readonly all: Observable<{[key:string]: remoteConfig.Value}>; + readonly numbers: Observable<{[key:string]: number}> & {[key:string]: Observable}; + readonly booleans: Observable<{[key:string]: boolean}> & {[key:string]: Observable}; + readonly strings: Observable<{[key:string]: string}> & {[key:string]: Observable}; constructor( @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, @@ -41,7 +60,7 @@ export class AngularFireRemoteConfig { // @ts-ignore zapping in the UMD in the build script const requireRemoteConfig = from(import('@firebase/remote-config')); - this.remoteConfig = requireRemoteConfig.pipe( + this.remoteConfig$ = requireRemoteConfig.pipe( map(rc => rc.registerRemoteConfig(firebase)), map(() => _firebaseAppFactory(options, zone, nameOrConfig)), map(app => app.remoteConfig()), @@ -49,30 +68,50 @@ export class AngularFireRemoteConfig { if (settings) { rc.settings = settings } if (defaultConfig) { rc.defaultConfig = defaultConfig } }), - runOutsideAngular(zone) - ); - - this.activate = this.remoteConfig.pipe( - switchMap(rc => rc.activate().then(() => rc)), - tap(rc => rc.fetch()), - map(rc => rc.getAll()), - runOutsideAngular(zone), - take(1) - ) - - this.freshConfiguration = this.remoteConfig.pipe( - switchMap(rc => rc.fetchAndActivate().then(() => rc.getAll())), runOutsideAngular(zone), - take(1) - ) + shareReplay(1) + ); - this.configuration = this.remoteConfig.pipe( + this.all = this.remoteConfig$.pipe( switchMap(rc => concat( rc.activate().then(() => rc.getAll()), rc.fetchAndActivate().then(() => rc.getAll()) )), - runOutsideAngular(zone) - ) + runOutsideAngular(zone), + // TODO startWith(rehydrate(deafultConfig)), + shareReplay(1) + // TODO distinctUntilChanged(compareFn) + ); + + const allAs = (type: 'String'|'Boolean'|'Number') => this.all.pipe( + map(all => Object.keys(all).reduce((c, k) => { + c[k] = all[k][`as${type}`](); + return c; + }, {})) + ) as any; + + this.strings = new Proxy(allAs('String'), { + get: (self, name:string) => self[name] || this.all.pipe( + map(rc => rc[name].asString()), + distinctUntilChanged() + ) + }); + + this.booleans = new Proxy(allAs('Boolean'), { + get: (self, name:string) => self[name] || this.all.pipe( + map(rc => rc[name].asBoolean()), + distinctUntilChanged() + ) + }); + + this.numbers = new Proxy(allAs('Number'), { + get: (self, name:string) => self[name] || this.all.pipe( + map(rc => rc[name].asNumber()), + distinctUntilChanged() + ) + }); + + return _lazySDKProxy(this, this.remoteConfig, zone); } } From 89344cc1cdb2c8c0b77f5a2d7fddae3206f29a93 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 14 Nov 2019 14:40:10 -0800 Subject: [PATCH 12/37] Update src/remote-config/tsconfig-test.json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Andi Pätzold <4947671+andipaetzold@users.noreply.github.com> --- src/remote-config/tsconfig-test.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/remote-config/tsconfig-test.json b/src/remote-config/tsconfig-test.json index 20810b081..8516466dd 100644 --- a/src/remote-config/tsconfig-test.json +++ b/src/remote-config/tsconfig-test.json @@ -4,7 +4,7 @@ "baseUrl": ".", "paths": { "@angular/fire": ["../../dist/packages-dist"], - "@angular/fire/reomte-config": ["../../dist/packages-dist/remote-config"] + "@angular/fire/remote-config": ["../../dist/packages-dist/remote-config"] } }, "files": [ From 075afe66d4c75c336ca3f378b401909858c9ef24 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 19 Nov 2019 18:40:54 -0800 Subject: [PATCH 13/37] Reworking things a bit --- src/analytics/analytics.module.ts | 15 ++-- src/analytics/analytics.service.ts | 100 ++++++++++++++++++++++ src/analytics/analytics.ts | 77 +++-------------- src/analytics/public_api.ts | 1 + src/core/angularfire2.ts | 4 +- src/remote-config/remote-config.module.ts | 5 +- src/remote-config/remote-config.ts | 95 +++++++++++++------- 7 files changed, 188 insertions(+), 109 deletions(-) create mode 100644 src/analytics/analytics.service.ts diff --git a/src/analytics/analytics.module.ts b/src/analytics/analytics.module.ts index 31371ef46..977bb72de 100644 --- a/src/analytics/analytics.module.ts +++ b/src/analytics/analytics.module.ts @@ -1,11 +1,12 @@ -import { NgModule } from '@angular/core'; +import { NgModule, Optional } from '@angular/core'; +import { UserTrackingService, ScreenTrackingService } from './analytics.service'; import { AngularFireAnalytics } from './analytics'; -@NgModule({ - providers: [ AngularFireAnalytics ] -}) +@NgModule() export class AngularFireAnalyticsModule { - constructor(_: AngularFireAnalytics) { - // DI inject Analytics here for the automatic data collection - } + constructor( + analytics: AngularFireAnalytics, + @Optional() screenTracking: ScreenTrackingService, + @Optional() userTracking: UserTrackingService + ) { } } diff --git a/src/analytics/analytics.service.ts b/src/analytics/analytics.service.ts new file mode 100644 index 000000000..eed57d684 --- /dev/null +++ b/src/analytics/analytics.service.ts @@ -0,0 +1,100 @@ +import { Injectable, Inject, Optional, NgZone, OnDestroy, InjectionToken } from '@angular/core'; +import { Subscription, from, Observable, empty } from 'rxjs'; +import { filter, withLatestFrom, switchMap, map, tap } from 'rxjs/operators'; +import { Router, NavigationEnd, ActivationEnd } from '@angular/router'; +import { runOutsideAngular, _lazySDKProxy, _firebaseAppFactory } from '@angular/fire'; +import { AngularFireAnalytics } from './analytics'; +import { User } from 'firebase/app'; + +export const AUTOMATICALLY_SET_CURRENT_SCREEN = new InjectionToken('angularfire2.analytics.setCurrentScreen'); +export const AUTOMATICALLY_LOG_SCREEN_VIEWS = new InjectionToken('angularfire2.analytics.logScreenViews'); +export const APP_VERSION = new InjectionToken('angularfire2.analytics.appVersion'); +export const APP_NAME = new InjectionToken('angularfire2.analytics.appName'); + +const DEFAULT_APP_VERSION = '?'; +const DEFAULT_APP_NAME = 'Angular App'; + +@Injectable({ + providedIn: 'root' +}) +export class ScreenTrackingService implements OnDestroy { + + private disposable: Subscription|undefined; + + constructor( + private analytics: AngularFireAnalytics, + private router:Router, + @Optional() @Inject(AUTOMATICALLY_SET_CURRENT_SCREEN) private automaticallySetCurrentScreen:boolean|null, + @Optional() @Inject(AUTOMATICALLY_LOG_SCREEN_VIEWS) private automaticallyLogScreenViews:boolean|null, + @Optional() @Inject(APP_VERSION) private providedAppVersion:string|null, + @Optional() @Inject(APP_NAME) private providedAppName:string|null, + private zone: NgZone + ) { + if (this.automaticallySetCurrentScreen !== false || this.automaticallyLogScreenViews !== false) { + const app_name = this.providedAppName || DEFAULT_APP_NAME; + const app_version = this.providedAppVersion || DEFAULT_APP_VERSION; + const activationEndEvents = this.router.events.pipe(filter(e => e instanceof ActivationEnd)); + const navigationEndEvents = this.router.events.pipe(filter(e => e instanceof NavigationEnd)); + this.disposable = navigationEndEvents.pipe( + withLatestFrom(activationEndEvents), + switchMap(([navigationEnd, activationEnd]) => { + const url = navigationEnd.url; + const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || url; + const outlet = activationEnd.snapshot.outlet; + const component = activationEnd.snapshot.component; + const ret = new Array>(); + if (this.automaticallyLogScreenViews !== false) { + if (component) { + const screen_class = component.hasOwnProperty('name') && (component as any).name || component.toString(); + ret.push(this.analytics.logEvent("screen_view", { app_name, screen_class, app_version, screen_name, outlet, url })); + } else if (activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.loadChildren) { + ret.push((activationEnd.snapshot.routeConfig.loadChildren as any)().then((child:any) => { + const screen_class = child.name; + console.log("logEvent", "screen_view", { app_name, screen_class, app_version, screen_name, outlet, url }); + return this.analytics.logEvent("screen_view", { app_name, screen_class, app_version, screen_name, outlet, url }); + })); + } else { + ret.push(this.analytics.logEvent("screen_view", { app_name, app_version, screen_name, outlet, url })); + } + } + if (this.automaticallySetCurrentScreen !== false) { + ret.push(this.analytics.setCurrentScreen(screen_name || url, { global: outlet == "primary" })); + } + return Promise.all(ret); + }), + runOutsideAngular(this.zone) + ).subscribe(); + } + } + + ngOnDestroy() { + if (this.disposable) { this.disposable.unsubscribe(); } + } + +} + +@Injectable({ + providedIn: 'root' +}) +export class UserTrackingService implements OnDestroy { + + private disposable: Subscription|undefined; + + // TODO a user properties injector + constructor( + analytics: AngularFireAnalytics, + zone: NgZone + ) { + this.disposable = from(analytics.app).pipe( + // TODO can I hook into auth being loaded... + map(app => app.auth()), + switchMap(auth => auth ? new Observable(auth.onAuthStateChanged.bind(auth)) : empty()), + switchMap(user => analytics.setUserId(user ? user.uid : null!, { global: true })), + runOutsideAngular(zone) + ).subscribe(); + } + + ngOnDestroy() { + if (this.disposable) { this.disposable.unsubscribe(); } + } +} \ No newline at end of file diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index 77d8c0431..e501abe1d 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -1,19 +1,10 @@ import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; -import { Observable, from } from 'rxjs'; -import { map, tap, filter, withLatestFrom, shareReplay } from 'rxjs/operators'; -import { Router, NavigationEnd, ActivationEnd } from '@angular/router'; +import { of } from 'rxjs'; +import { map, tap, shareReplay, switchMap } from 'rxjs/operators'; import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular, _lazySDKProxy, FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire'; import { analytics, app } from 'firebase'; -export const AUTOMATICALLY_SET_CURRENT_SCREEN = new InjectionToken('angularfire2.analytics.setCurrentScreen'); -export const AUTOMATICALLY_LOG_SCREEN_VIEWS = new InjectionToken('angularfire2.analytics.logScreenViews'); export const ANALYTICS_COLLECTION_ENABLED = new InjectionToken('angularfire2.analytics.analyticsCollectionEnabled'); -export const AUTOMATICALLY_TRACK_USER_IDENTIFIER = new InjectionToken('angularfire2.analytics.trackUserIdentifier'); -export const APP_VERSION = new InjectionToken('angularfire2.analytics.appVersion'); -export const APP_NAME = new InjectionToken('angularfire2.analytics.appName'); - -export const DEFAULT_APP_VERSION = '?'; -export const DEFAULT_APP_NAME = 'Angular App'; // SEMVER: once we move to Typescript 3.6 use `PromiseProxy` type AnalyticsProxy = { @@ -29,33 +20,22 @@ type AnalyticsProxy = { export interface AngularFireAnalytics extends AnalyticsProxy {}; -@Injectable() +@Injectable({ + providedIn: "root" +}) export class AngularFireAnalytics { - /** - * Firebase Analytics instance - */ - private readonly analytics$: Observable; - private get analytics() { return this.analytics$.toPromise(); } - constructor( @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, - @Optional() router:Router, - @Optional() @Inject(AUTOMATICALLY_SET_CURRENT_SCREEN) automaticallySetCurrentScreen:boolean|null, - @Optional() @Inject(AUTOMATICALLY_LOG_SCREEN_VIEWS) automaticallyLogScreenViews:boolean|null, @Optional() @Inject(ANALYTICS_COLLECTION_ENABLED) analyticsCollectionEnabled:boolean|null, - @Optional() @Inject(AUTOMATICALLY_TRACK_USER_IDENTIFIER) automaticallyTrackUserIdentifier:boolean|null, - @Optional() @Inject(APP_VERSION) providedAppVersion:string|null, - @Optional() @Inject(APP_NAME) providedAppName:string|null, zone: NgZone ) { - // @ts-ignore zapping in the UMD in the build script - const requireAnalytics = from(import('firebase/analytics')); - const app = _firebaseAppFactory(options, zone, nameOrConfig); - - this.analytics$ = requireAnalytics.pipe( - map(() => app.analytics()), + const analytics = of(undefined).pipe( + // @ts-ignore zapping in the UMD in the build script + switchMap(() => zone.runOutsideAngular(() => import('firebase/analytics'))), + map(() => _firebaseAppFactory(options, zone, nameOrConfig)), + map(app => app.analytics()), tap(analytics => { if (analyticsCollectionEnabled === false) { analytics.setAnalyticsCollectionEnabled(false) } }), @@ -63,40 +43,7 @@ export class AngularFireAnalytics { shareReplay(1) ); - if (router && (automaticallySetCurrentScreen !== false || automaticallyLogScreenViews !== false)) { - const app_name = providedAppName || DEFAULT_APP_NAME; - const app_version = providedAppVersion || DEFAULT_APP_VERSION; - const activationEndEvents = router.events.pipe(filter(e => e instanceof ActivationEnd)); - const navigationEndEvents = router.events.pipe(filter(e => e instanceof NavigationEnd)); - navigationEndEvents.pipe( - withLatestFrom(activationEndEvents, this.analytics$), - tap(([navigationEnd, activationEnd, analytics]) => { - const url = navigationEnd.url; - const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || undefined; - const outlet = activationEnd.snapshot.outlet; - if (automaticallyLogScreenViews !== false) { - analytics.logEvent("screen_view", { app_name, app_version, screen_name, outlet, url }); - } - if (automaticallySetCurrentScreen !== false) { - // TODO when is screen_name undefined? - analytics.setCurrentScreen(screen_name || url, { global: outlet == "primary" }); - } - }), - runOutsideAngular(zone) - ).subscribe(); - } - - // TODO do something other than just check auth presence, what if it's lazy loaded? - if (app.auth && automaticallyTrackUserIdentifier !== false) { - new Observable(app.auth().onAuthStateChanged.bind(app.auth())).pipe( - withLatestFrom(this.analytics$), - tap(([user, analytics]) => analytics.setUserId(user ? user.uid : null!, { global: true })), - runOutsideAngular(zone) - ).subscribe() - } - - return _lazySDKProxy(this, this.analytics, zone); - + return _lazySDKProxy(this, analytics, zone); } -} +} \ No newline at end of file diff --git a/src/analytics/public_api.ts b/src/analytics/public_api.ts index aee785b0e..8ce1376e6 100644 --- a/src/analytics/public_api.ts +++ b/src/analytics/public_api.ts @@ -1,2 +1,3 @@ export * from './analytics'; export * from './analytics.module'; +export * from './analytics.service'; diff --git a/src/core/angularfire2.ts b/src/core/angularfire2.ts index dab20fac5..cbc91af34 100644 --- a/src/core/angularfire2.ts +++ b/src/core/angularfire2.ts @@ -79,10 +79,10 @@ export const runInZone = (zone: NgZone) => (obs$: Observable): Observable< { [K in PromiseReturningFunctionPropertyNames ]: (...args: Parameters) => ReturnType }; */ -export const _lazySDKProxy = (klass: any, promise: Promise, zone: NgZone) => new Proxy(klass, { +export const _lazySDKProxy = (klass: any, observable: Observable, zone: NgZone) => new Proxy(klass, { get: (_, name) => zone.runOutsideAngular(() => klass[name] || new Proxy(() => - promise.then(mod => { + observable.toPromise().then(mod => { const ret = mod[name]; // TODO move to proper type guards if (typeof ret == 'function') { diff --git a/src/remote-config/remote-config.module.ts b/src/remote-config/remote-config.module.ts index fd0fe7ac5..46a3075c8 100644 --- a/src/remote-config/remote-config.module.ts +++ b/src/remote-config/remote-config.module.ts @@ -1,7 +1,4 @@ import { NgModule } from '@angular/core'; -import { AngularFireRemoteConfig } from './remote-config'; -@NgModule({ - providers: [ AngularFireRemoteConfig ] -}) +@NgModule() export class AngularFireRemoteConfigModule { } diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index ab92c33bf..bca2393f5 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -1,25 +1,23 @@ import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; -import { Observable, from, concat } from 'rxjs'; +import { Observable, concat, of, empty } from 'rxjs'; import { map, switchMap, tap, shareReplay, distinctUntilChanged } from 'rxjs/operators'; import { FirebaseAppConfig, FirebaseOptions, _lazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; import { remoteConfig } from 'firebase/app'; -// @ts-ignore -import firebase from 'firebase/app'; - export interface DefaultConfig {[key:string]: string|number|boolean}; export const REMOTE_CONFIG_SETTINGS = new InjectionToken('angularfire2.remoteConfig.settings'); export const DEFAULT_CONFIG = new InjectionToken('angularfire2.remoteConfig.defaultConfig'); import { FirebaseRemoteConfig, _firebaseAppFactory, runOutsideAngular } from '@angular/fire'; +import { AngularFireRemoteConfigModule } from './remote-config.module'; // SEMVER: once we move to Typescript 3.6 use `PromiseProxy` rather than hardcoding type RemoteConfigProxy = { - activate: () => Promise; + activate: () => Promise; ensureInitialized: () => Promise; fetch: () => Promise; - fetchAndActivate: () => Promise; + fetchAndActivate: () => Promise; getAll: () => Promise<{[key:string]: remoteConfig.Value}>; getBoolean: (key:string) => Promise; getNumber: (key:string) => Promise; @@ -36,13 +34,24 @@ type RemoteConfigProxy = { export interface AngularFireRemoteConfig extends RemoteConfigProxy {}; -@Injectable() +// TODO export as implements Partial<...> so minor doesn't break us +export class Value implements remoteConfig.Value { + asBoolean() { return ['1', 'true', 't', 'y', 'yes', 'on'].indexOf(this._value.toLowerCase()) > -1 } + asString() { return this._value } + asNumber() { return Number(this._value) || 0 } + getSource() { return this._source; } + constructor(private _source: remoteConfig.ValueSource, private _value: string) { } +} + +@Injectable({ + providedIn: AngularFireRemoteConfigModule +}) export class AngularFireRemoteConfig { - private readonly remoteConfig$: Observable; - private get remoteConfig() { return this.remoteConfig$.toPromise(); } + private default$: Observable<{[key:string]: remoteConfig.Value}>; - readonly all: Observable<{[key:string]: remoteConfig.Value}>; + readonly changes: Observable<{}>; + readonly all: Observable<{[key:string]: remoteConfig.Value}> & {[key:string]: Observable}; readonly numbers: Observable<{[key:string]: number}> & {[key:string]: Observable}; readonly booleans: Observable<{[key:string]: boolean}> & {[key:string]: Observable}; readonly strings: Observable<{[key:string]: string}> & {[key:string]: Observable}; @@ -54,33 +63,57 @@ export class AngularFireRemoteConfig { @Optional() @Inject(DEFAULT_CONFIG) defaultConfig:DefaultConfig|null, private zone: NgZone ) { - // import('firebase/remote-config') isn't working for some reason... - // it might have something to do with https://github.com/firebase/firebase-js-sdk/pull/2229 - // import from @firebase/remote-config so I can manually register on the Firebase instance - // @ts-ignore zapping in the UMD in the build script - const requireRemoteConfig = from(import('@firebase/remote-config')); - - this.remoteConfig$ = requireRemoteConfig.pipe( - map(rc => rc.registerRemoteConfig(firebase)), + + const remoteConfig = of(undefined).pipe( + // @ts-ignore zapping in the UMD in the build script + switchMap(() => import('firebase/remote-config')), map(() => _firebaseAppFactory(options, zone, nameOrConfig)), map(app => app.remoteConfig()), tap(rc => { if (settings) { rc.settings = settings } if (defaultConfig) { rc.defaultConfig = defaultConfig } + this.default$ = empty(); // once the SDK is loaded, we don't need our defaults anylonger }), runOutsideAngular(zone), shareReplay(1) ); - this.all = this.remoteConfig$.pipe( - switchMap(rc => concat( - rc.activate().then(() => rc.getAll()), - rc.fetchAndActivate().then(() => rc.getAll()) - )), - runOutsideAngular(zone), - // TODO startWith(rehydrate(deafultConfig)), - shareReplay(1) - // TODO distinctUntilChanged(compareFn) + const defaultToStartWith = Object.keys(defaultConfig || {}).reduce((c, k) => { + c[k] = new Value("default", defaultConfig![k].toString()); + return c; + }, {}); + + const mapRemoteConfig = (rc: {[key:string]: Value | remoteConfig.Value}) => { + return Object.keys(rc).reduce((c, key, index) => { + const value = rc[key]; + c[index] = { key, value }; + return c; + }, new Array<{}>(rc.length)); + } + + const proxy: AngularFireRemoteConfig = _lazySDKProxy(this, remoteConfig, zone); + + this.default$ = of(defaultToStartWith); + + const existing = of(undefined).pipe( + switchMap(() => proxy.activate()), + switchMap(() => proxy.getAll()) + ); + + let fresh = of(undefined).pipe( + switchMap(() => proxy.fetchAndActivate()), + switchMap(() => proxy.getAll()) + ); + + this.all = new Proxy(concat(this.default$, existing, fresh), { + get: (self, name:string) => self[name] || self.pipe( + map(rc => rc[name] ? rc[name] : undefined), + distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b)) + ) + }) as any; + + this.changes = this.all.pipe( + switchMap(all => of(...mapRemoteConfig(all))) ); const allAs = (type: 'String'|'Boolean'|'Number') => this.all.pipe( @@ -92,26 +125,26 @@ export class AngularFireRemoteConfig { this.strings = new Proxy(allAs('String'), { get: (self, name:string) => self[name] || this.all.pipe( - map(rc => rc[name].asString()), + map(rc => rc[name] ? rc[name].asString() : undefined), distinctUntilChanged() ) }); this.booleans = new Proxy(allAs('Boolean'), { get: (self, name:string) => self[name] || this.all.pipe( - map(rc => rc[name].asBoolean()), + map(rc => rc[name] ? rc[name].asBoolean() : false), distinctUntilChanged() ) }); this.numbers = new Proxy(allAs('Number'), { get: (self, name:string) => self[name] || this.all.pipe( - map(rc => rc[name].asNumber()), + map(rc => rc[name] ? rc[name].asNumber() : 0), distinctUntilChanged() ) }); - return _lazySDKProxy(this, this.remoteConfig, zone); + return proxy; } } From b8b351ada6dff5d01dfd0ad0dda9f2338f3a6d5a Mon Sep 17 00:00:00 2001 From: James Daniels Date: Wed, 20 Nov 2019 00:08:14 -0800 Subject: [PATCH 14/37] Router as optional, drop this/private from screen tracking --- src/analytics/analytics.service.ts | 40 ++++++++++++++++-------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/analytics/analytics.service.ts b/src/analytics/analytics.service.ts index eed57d684..78a826ce3 100644 --- a/src/analytics/analytics.service.ts +++ b/src/analytics/analytics.service.ts @@ -22,19 +22,21 @@ export class ScreenTrackingService implements OnDestroy { private disposable: Subscription|undefined; constructor( - private analytics: AngularFireAnalytics, - private router:Router, - @Optional() @Inject(AUTOMATICALLY_SET_CURRENT_SCREEN) private automaticallySetCurrentScreen:boolean|null, - @Optional() @Inject(AUTOMATICALLY_LOG_SCREEN_VIEWS) private automaticallyLogScreenViews:boolean|null, - @Optional() @Inject(APP_VERSION) private providedAppVersion:string|null, - @Optional() @Inject(APP_NAME) private providedAppName:string|null, - private zone: NgZone + analytics: AngularFireAnalytics, + @Optional() router:Router, + @Optional() @Inject(AUTOMATICALLY_SET_CURRENT_SCREEN) automaticallySetCurrentScreen:boolean|null, + @Optional() @Inject(AUTOMATICALLY_LOG_SCREEN_VIEWS) automaticallyLogScreenViews:boolean|null, + @Optional() @Inject(APP_VERSION) providedAppVersion:string|null, + @Optional() @Inject(APP_NAME) providedAppName:string|null, + zone: NgZone ) { - if (this.automaticallySetCurrentScreen !== false || this.automaticallyLogScreenViews !== false) { - const app_name = this.providedAppName || DEFAULT_APP_NAME; - const app_version = this.providedAppVersion || DEFAULT_APP_VERSION; - const activationEndEvents = this.router.events.pipe(filter(e => e instanceof ActivationEnd)); - const navigationEndEvents = this.router.events.pipe(filter(e => e instanceof NavigationEnd)); + if (!router) { + // TODO warning about Router + } else if (automaticallySetCurrentScreen !== false || automaticallyLogScreenViews !== false) { + const app_name = providedAppName || DEFAULT_APP_NAME; + const app_version = providedAppVersion || DEFAULT_APP_VERSION; + const activationEndEvents = router.events.pipe(filter(e => e instanceof ActivationEnd)); + const navigationEndEvents = router.events.pipe(filter(e => e instanceof NavigationEnd)); this.disposable = navigationEndEvents.pipe( withLatestFrom(activationEndEvents), switchMap(([navigationEnd, activationEnd]) => { @@ -43,26 +45,26 @@ export class ScreenTrackingService implements OnDestroy { const outlet = activationEnd.snapshot.outlet; const component = activationEnd.snapshot.component; const ret = new Array>(); - if (this.automaticallyLogScreenViews !== false) { + if (automaticallyLogScreenViews !== false) { if (component) { const screen_class = component.hasOwnProperty('name') && (component as any).name || component.toString(); - ret.push(this.analytics.logEvent("screen_view", { app_name, screen_class, app_version, screen_name, outlet, url })); + ret.push(analytics.logEvent("screen_view", { app_name, screen_class, app_version, screen_name, outlet, url })); } else if (activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.loadChildren) { ret.push((activationEnd.snapshot.routeConfig.loadChildren as any)().then((child:any) => { const screen_class = child.name; console.log("logEvent", "screen_view", { app_name, screen_class, app_version, screen_name, outlet, url }); - return this.analytics.logEvent("screen_view", { app_name, screen_class, app_version, screen_name, outlet, url }); + return analytics.logEvent("screen_view", { app_name, screen_class, app_version, screen_name, outlet, url }); })); } else { - ret.push(this.analytics.logEvent("screen_view", { app_name, app_version, screen_name, outlet, url })); + ret.push(analytics.logEvent("screen_view", { app_name, app_version, screen_name, outlet, url })); } } - if (this.automaticallySetCurrentScreen !== false) { - ret.push(this.analytics.setCurrentScreen(screen_name || url, { global: outlet == "primary" })); + if (automaticallySetCurrentScreen !== false) { + ret.push(analytics.setCurrentScreen(screen_name || url, { global: outlet == "primary" })); } return Promise.all(ret); }), - runOutsideAngular(this.zone) + runOutsideAngular(zone) ).subscribe(); } } From 1e39052a5ce22d8e5a3d2b91b85c3732d2d62c4f Mon Sep 17 00:00:00 2001 From: James Daniels Date: Wed, 20 Nov 2019 15:40:58 -0800 Subject: [PATCH 15/37] Minor changes to RC --- src/remote-config/remote-config.ts | 40 ++++++++++++++++++------------ 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index bca2393f5..d2612add1 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -40,7 +40,14 @@ export class Value implements remoteConfig.Value { asString() { return this._value } asNumber() { return Number(this._value) || 0 } getSource() { return this._source; } - constructor(private _source: remoteConfig.ValueSource, private _value: string) { } + constructor(public _source: remoteConfig.ValueSource, public _value: string) { } +} + +// SEMVER use ConstructorParameters when we can support Typescript 3.6 +export class KeyedValue extends Value { + constructor(private key: string, source: remoteConfig.ValueSource, value: string) { + super(source, value); + } } @Injectable({ @@ -50,8 +57,8 @@ export class AngularFireRemoteConfig { private default$: Observable<{[key:string]: remoteConfig.Value}>; - readonly changes: Observable<{}>; - readonly all: Observable<{[key:string]: remoteConfig.Value}> & {[key:string]: Observable}; + readonly changes: Observable<{key:string} & remoteConfig.Value>; + readonly values: Observable<{[key:string]: remoteConfig.Value}> & {[key:string]: Observable}; readonly numbers: Observable<{[key:string]: number}> & {[key:string]: Observable}; readonly booleans: Observable<{[key:string]: boolean}> & {[key:string]: Observable}; readonly strings: Observable<{[key:string]: string}> & {[key:string]: Observable}; @@ -81,14 +88,15 @@ export class AngularFireRemoteConfig { const defaultToStartWith = Object.keys(defaultConfig || {}).reduce((c, k) => { c[k] = new Value("default", defaultConfig![k].toString()); return c; - }, {}); + }, {} as {[key:string]: remoteConfig.Value}); const mapRemoteConfig = (rc: {[key:string]: Value | remoteConfig.Value}) => { - return Object.keys(rc).reduce((c, key, index) => { + const keys = Object.keys(rc); + return keys.reduce((c, key, index) => { const value = rc[key]; - c[index] = { key, value }; + c[index] = new KeyedValue(key, value.getSource(), value.asString()); return c; - }, new Array<{}>(rc.length)); + }, new Array(keys.length)); } const proxy: AngularFireRemoteConfig = _lazySDKProxy(this, remoteConfig, zone); @@ -105,40 +113,40 @@ export class AngularFireRemoteConfig { switchMap(() => proxy.getAll()) ); - this.all = new Proxy(concat(this.default$, existing, fresh), { + this.values = new Proxy(concat(this.default$, existing, fresh), { get: (self, name:string) => self[name] || self.pipe( map(rc => rc[name] ? rc[name] : undefined), distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b)) ) - }) as any; + }) as any; // TODO figure out the types here - this.changes = this.all.pipe( + this.changes = this.values.pipe( switchMap(all => of(...mapRemoteConfig(all))) - ); + ) as any; // TODO figure out the types here - const allAs = (type: 'String'|'Boolean'|'Number') => this.all.pipe( + const allAs = (type: 'String'|'Boolean'|'Number') => this.values.pipe( map(all => Object.keys(all).reduce((c, k) => { c[k] = all[k][`as${type}`](); return c; }, {})) - ) as any; + ) as any; // TODO figure out the types here this.strings = new Proxy(allAs('String'), { - get: (self, name:string) => self[name] || this.all.pipe( + get: (self, name:string) => self[name] || this.values.pipe( map(rc => rc[name] ? rc[name].asString() : undefined), distinctUntilChanged() ) }); this.booleans = new Proxy(allAs('Boolean'), { - get: (self, name:string) => self[name] || this.all.pipe( + get: (self, name:string) => self[name] || this.values.pipe( map(rc => rc[name] ? rc[name].asBoolean() : false), distinctUntilChanged() ) }); this.numbers = new Proxy(allAs('Number'), { - get: (self, name:string) => self[name] || this.all.pipe( + get: (self, name:string) => self[name] || this.values.pipe( map(rc => rc[name] ? rc[name].asNumber() : 0), distinctUntilChanged() ) From 768c21bc4b88ba83328c77a7f85bbc6759eb61fa Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 21 Nov 2019 14:37:01 -0800 Subject: [PATCH 16/37] It's firebase_screen_class --- src/analytics/analytics.service.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/analytics/analytics.service.ts b/src/analytics/analytics.service.ts index 78a826ce3..8540a765e 100644 --- a/src/analytics/analytics.service.ts +++ b/src/analytics/analytics.service.ts @@ -47,13 +47,12 @@ export class ScreenTrackingService implements OnDestroy { const ret = new Array>(); if (automaticallyLogScreenViews !== false) { if (component) { - const screen_class = component.hasOwnProperty('name') && (component as any).name || component.toString(); - ret.push(analytics.logEvent("screen_view", { app_name, screen_class, app_version, screen_name, outlet, url })); + const firebase_screen_class = component.hasOwnProperty('name') && (component as any).name || component.toString(); + ret.push(analytics.logEvent("screen_view", { app_name, firebase_screen_class, app_version, screen_name, outlet, url })); } else if (activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.loadChildren) { ret.push((activationEnd.snapshot.routeConfig.loadChildren as any)().then((child:any) => { - const screen_class = child.name; - console.log("logEvent", "screen_view", { app_name, screen_class, app_version, screen_name, outlet, url }); - return analytics.logEvent("screen_view", { app_name, screen_class, app_version, screen_name, outlet, url }); + const firebase_screen_class = child.name; + return analytics.logEvent("screen_view", { app_name, firebase_screen_class, app_version, screen_name, outlet, url }); })); } else { ret.push(analytics.logEvent("screen_view", { app_name, app_version, screen_name, outlet, url })); From 60c0cad0e8b15e614383bc57d7bd4e754e32d543 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 21 Nov 2019 17:27:43 -0800 Subject: [PATCH 17/37] Reworking analytics --- src/analytics/analytics.service.ts | 126 +++++++++++++++++++---------- 1 file changed, 83 insertions(+), 43 deletions(-) diff --git a/src/analytics/analytics.service.ts b/src/analytics/analytics.service.ts index 8540a765e..24ad54799 100644 --- a/src/analytics/analytics.service.ts +++ b/src/analytics/analytics.service.ts @@ -1,19 +1,27 @@ import { Injectable, Inject, Optional, NgZone, OnDestroy, InjectionToken } from '@angular/core'; -import { Subscription, from, Observable, empty } from 'rxjs'; -import { filter, withLatestFrom, switchMap, map, tap } from 'rxjs/operators'; +import { Subscription, from, Observable, empty, of } from 'rxjs'; +import { filter, withLatestFrom, switchMap, map, tap, pairwise, startWith, groupBy, mergeMap } from 'rxjs/operators'; import { Router, NavigationEnd, ActivationEnd } from '@angular/router'; import { runOutsideAngular, _lazySDKProxy, _firebaseAppFactory } from '@angular/fire'; import { AngularFireAnalytics } from './analytics'; import { User } from 'firebase/app'; -export const AUTOMATICALLY_SET_CURRENT_SCREEN = new InjectionToken('angularfire2.analytics.setCurrentScreen'); -export const AUTOMATICALLY_LOG_SCREEN_VIEWS = new InjectionToken('angularfire2.analytics.logScreenViews'); export const APP_VERSION = new InjectionToken('angularfire2.analytics.appVersion'); export const APP_NAME = new InjectionToken('angularfire2.analytics.appName'); const DEFAULT_APP_VERSION = '?'; const DEFAULT_APP_NAME = 'Angular App'; +type AngularFireAnalyticsEventParams = { + app_name: string; + firebase_screen_class: string | undefined; + firebase_screen: string; + app_version: string; + screen_name: string; + outlet: string; + url: string; +}; + @Injectable({ providedIn: 'root' }) @@ -24,48 +32,62 @@ export class ScreenTrackingService implements OnDestroy { constructor( analytics: AngularFireAnalytics, @Optional() router:Router, - @Optional() @Inject(AUTOMATICALLY_SET_CURRENT_SCREEN) automaticallySetCurrentScreen:boolean|null, - @Optional() @Inject(AUTOMATICALLY_LOG_SCREEN_VIEWS) automaticallyLogScreenViews:boolean|null, @Optional() @Inject(APP_VERSION) providedAppVersion:string|null, @Optional() @Inject(APP_NAME) providedAppName:string|null, zone: NgZone ) { - if (!router) { - // TODO warning about Router - } else if (automaticallySetCurrentScreen !== false || automaticallyLogScreenViews !== false) { - const app_name = providedAppName || DEFAULT_APP_NAME; - const app_version = providedAppVersion || DEFAULT_APP_VERSION; - const activationEndEvents = router.events.pipe(filter(e => e instanceof ActivationEnd)); - const navigationEndEvents = router.events.pipe(filter(e => e instanceof NavigationEnd)); - this.disposable = navigationEndEvents.pipe( - withLatestFrom(activationEndEvents), - switchMap(([navigationEnd, activationEnd]) => { - const url = navigationEnd.url; - const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || url; - const outlet = activationEnd.snapshot.outlet; - const component = activationEnd.snapshot.component; - const ret = new Array>(); - if (automaticallyLogScreenViews !== false) { - if (component) { - const firebase_screen_class = component.hasOwnProperty('name') && (component as any).name || component.toString(); - ret.push(analytics.logEvent("screen_view", { app_name, firebase_screen_class, app_version, screen_name, outlet, url })); - } else if (activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.loadChildren) { - ret.push((activationEnd.snapshot.routeConfig.loadChildren as any)().then((child:any) => { - const firebase_screen_class = child.name; - return analytics.logEvent("screen_view", { app_name, firebase_screen_class, app_version, screen_name, outlet, url }); - })); - } else { - ret.push(analytics.logEvent("screen_view", { app_name, app_version, screen_name, outlet, url })); - } - } - if (automaticallySetCurrentScreen !== false) { - ret.push(analytics.setCurrentScreen(screen_name || url, { global: outlet == "primary" })); - } - return Promise.all(ret); - }), - runOutsideAngular(zone) - ).subscribe(); - } + if (!router) { return this } + const app_name = providedAppName || DEFAULT_APP_NAME; + const app_version = providedAppVersion || DEFAULT_APP_VERSION; + const activationEndEvents = router.events.pipe(filter(e => e instanceof ActivationEnd)); + const navigationEndEvents = router.events.pipe(filter(e => e instanceof NavigationEnd)); + this.disposable = navigationEndEvents.pipe( + withLatestFrom(activationEndEvents), + switchMap(([navigationEnd, activationEnd]) => { + const url = navigationEnd.url; + const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || url; + const params: AngularFireAnalyticsEventParams = { + app_name, app_version, screen_name, url, + firebase_screen_class: undefined, + firebase_screen: screen_name, + outlet: activationEnd.snapshot.outlet + }; + const component = activationEnd.snapshot.component; + const routeConfig = activationEnd.snapshot.routeConfig; + const loadChildren = routeConfig && routeConfig.loadChildren; + if (component) { + return of({...params, firebase_screen_class: nameOrToString(component) }); + } else if (typeof loadChildren === "string") { + // TODO is this an older lazy loading style parse + return of({...params, firebase_screen_class: loadChildren }); + } else if (loadChildren) { + // TODO look into the return types here + return from(loadChildren).pipe(map(child => ({...params, firebase_screen_class: nameOrToString(child) }))); + } else { + // TODO figure out what forms of router events I might be missing + return of(params); + } + }), + tap(params => { + // TODO perhaps I can be smarter about this, bubble events up to the nearest outlet? + if (params.outlet == "primary") { + // TODO do I need to add gtag config for firebase_screen, firebase_screen_class, firebase_screen_id? + // also shouldn't these be computed in the setCurrentScreen function? prior too? + analytics.setCurrentScreen(params.screen_name, { global: true }) + } + }), + map(params => ({ firebase_screen_id: getScreenId(params), ...params})), + groupBy(params => params.outlet), + mergeMap(group => group.pipe(startWith(undefined), pairwise())), + map(([prior, current]) => prior ? { + firebase_previous_class: prior.firebase_screen_class, + firebase_previous_screen: prior.firebase_screen, + firebase_previous_id: prior.firebase_screen_id, + ...current + } : current), + switchMap(params => analytics.logEvent('screen_view', params)), + runOutsideAngular(zone) + ).subscribe(); } ngOnDestroy() { @@ -98,4 +120,22 @@ export class UserTrackingService implements OnDestroy { ngOnDestroy() { if (this.disposable) { this.disposable.unsubscribe(); } } -} \ No newline at end of file +} + +let nextScreenId = Math.floor(Math.random() * 2**64) - 2**63; + +const screenIds: {[key:string]: number} = {}; + +const getScreenId = (params:AngularFireAnalyticsEventParams) => { + const name = params.screen_name; + const existingScreenId = screenIds[name]; + if (existingScreenId) { + return existingScreenId; + } else { + const screenId = nextScreenId++; + screenIds[name] = screenId; + return screenId; + } +} + +const nameOrToString = (it:any): string => it.name || it.toString(); \ No newline at end of file From 9b2e920969dd592c363f8e776cc64dc9544e8a47 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 21 Nov 2019 17:39:56 -0800 Subject: [PATCH 18/37] current! --- src/analytics/analytics.service.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/analytics/analytics.service.ts b/src/analytics/analytics.service.ts index 24ad54799..a106e1678 100644 --- a/src/analytics/analytics.service.ts +++ b/src/analytics/analytics.service.ts @@ -73,6 +73,7 @@ export class ScreenTrackingService implements OnDestroy { if (params.outlet == "primary") { // TODO do I need to add gtag config for firebase_screen, firebase_screen_class, firebase_screen_id? // also shouldn't these be computed in the setCurrentScreen function? prior too? + // do we want to be logging screen name or class? analytics.setCurrentScreen(params.screen_name, { global: true }) } }), @@ -83,9 +84,9 @@ export class ScreenTrackingService implements OnDestroy { firebase_previous_class: prior.firebase_screen_class, firebase_previous_screen: prior.firebase_screen, firebase_previous_id: prior.firebase_screen_id, - ...current - } : current), - switchMap(params => analytics.logEvent('screen_view', params)), + ...current! + } : current!), + tap(params => analytics.logEvent('screen_view', params)), runOutsideAngular(zone) ).subscribe(); } @@ -127,7 +128,7 @@ let nextScreenId = Math.floor(Math.random() * 2**64) - 2**63; const screenIds: {[key:string]: number} = {}; const getScreenId = (params:AngularFireAnalyticsEventParams) => { - const name = params.screen_name; + const name = params.firebase_screen_class || params.screen_name; const existingScreenId = screenIds[name]; if (existingScreenId) { return existingScreenId; From 916e069d20d5b2cf07cbf6da29fb178da15bb31f Mon Sep 17 00:00:00 2001 From: James Daniels Date: Thu, 21 Nov 2019 20:37:52 -0800 Subject: [PATCH 19/37] Use _loadedConfig if available and scope screen_id on outlet --- src/analytics/analytics.service.ts | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/analytics/analytics.service.ts b/src/analytics/analytics.service.ts index a106e1678..31fbc6afe 100644 --- a/src/analytics/analytics.service.ts +++ b/src/analytics/analytics.service.ts @@ -54,15 +54,18 @@ export class ScreenTrackingService implements OnDestroy { }; const component = activationEnd.snapshot.component; const routeConfig = activationEnd.snapshot.routeConfig; + const loadedConfig = routeConfig && (routeConfig as any)._loadedConfig; const loadChildren = routeConfig && routeConfig.loadChildren; if (component) { return of({...params, firebase_screen_class: nameOrToString(component) }); + } else if (loadedConfig && loadedConfig.module && loadedConfig.module._moduleType) { + return of({...params, firebase_screen_class: nameOrToString(loadedConfig.module._moduleType)}); } else if (typeof loadChildren === "string") { // TODO is this an older lazy loading style parse return of({...params, firebase_screen_class: loadChildren }); } else if (loadChildren) { - // TODO look into the return types here - return from(loadChildren).pipe(map(child => ({...params, firebase_screen_class: nameOrToString(child) }))); + // TODO look into the other return types here + return from(loadChildren() as Promise).pipe(map(child => ({...params, firebase_screen_class: nameOrToString(child) }))); } else { // TODO figure out what forms of router events I might be missing return of(params); @@ -77,7 +80,7 @@ export class ScreenTrackingService implements OnDestroy { analytics.setCurrentScreen(params.screen_name, { global: true }) } }), - map(params => ({ firebase_screen_id: getScreenId(params), ...params})), + map(params => ({ firebase_screen_id: nextScreenId(params), ...params})), groupBy(params => params.outlet), mergeMap(group => group.pipe(startWith(undefined), pairwise())), map(([prior, current]) => prior ? { @@ -123,19 +126,19 @@ export class UserTrackingService implements OnDestroy { } } -let nextScreenId = Math.floor(Math.random() * 2**64) - 2**63; +// firebase_screen_id is an INT64 but use INT32 cause javascript +const randomInt32 = () => Math.floor(Math.random() * (2**32 - 1)) - 2**31; -const screenIds: {[key:string]: number} = {}; +const currentScreenIds: {[key:string]: number} = {}; -const getScreenId = (params:AngularFireAnalyticsEventParams) => { - const name = params.firebase_screen_class || params.screen_name; - const existingScreenId = screenIds[name]; - if (existingScreenId) { - return existingScreenId; +const nextScreenId = (params:AngularFireAnalyticsEventParams) => { + const scope = params.outlet; + if (currentScreenIds.hasOwnProperty(scope)) { + return ++currentScreenIds[scope]; } else { - const screenId = nextScreenId++; - screenIds[name] = screenId; - return screenId; + const ret = randomInt32(); + currentScreenIds[scope] = ret; + return ret; } } From 5955925e305f1647e54cac939ea70248b7ead66b Mon Sep 17 00:00:00 2001 From: James Daniels Date: Fri, 22 Nov 2019 10:56:42 -0800 Subject: [PATCH 20/37] Fixing the types to handle older Firebase SDKs --- src/analytics/analytics.ts | 3 ++- src/core/firebase.app.module.ts | 12 ++++++++---- src/remote-config/remote-config.ts | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index e501abe1d..b1ae85eda 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -35,7 +35,8 @@ export class AngularFireAnalytics { // @ts-ignore zapping in the UMD in the build script switchMap(() => zone.runOutsideAngular(() => import('firebase/analytics'))), map(() => _firebaseAppFactory(options, zone, nameOrConfig)), - map(app => app.analytics()), + // SEMVER no need to cast once we drop older Firebase + map(app => app.analytics()), tap(analytics => { if (analyticsCollectionEnabled === false) { analytics.setAnalyticsCollectionEnabled(false) } }), diff --git a/src/core/firebase.app.module.ts b/src/core/firebase.app.module.ts index 55216d838..f0269e71c 100644 --- a/src/core/firebase.app.module.ts +++ b/src/core/firebase.app.module.ts @@ -1,5 +1,5 @@ import { InjectionToken, NgModule, Optional, NgZone } from '@angular/core'; -import { auth, database, firestore, functions, messaging, storage, analytics, remoteConfig } from 'firebase/app'; +import { auth, database, messaging, storage, firestore, functions } from 'firebase/app'; // @ts-ignore (https://github.com/firebase/firebase-js-sdk/pull/1206) import firebase from 'firebase/app'; // once fixed can pull in as "default as firebase" above @@ -15,12 +15,16 @@ export const FIREBASE_APP_NAME = FirebaseNameOrConfigToken; export type FirebaseDatabase = database.Database; export type FirebaseAuth = auth.Auth; -export type FirebaseAnalytics = analytics.Analytics; +// SEMVER analytics.Analytics; +export type FirebaseAnalytics = any; export type FirebaseMessaging = messaging.Messaging; +// SEMVER performance.Performance +export type FirebasePerformance = any; export type FirebaseStorage = storage.Storage; export type FirebaseFirestore = firestore.Firestore; export type FirebaseFunctions = functions.Functions; -export type FirebaseRemoteConfig = remoteConfig.RemoteConfig; +// SEMVER remoteConfig.RemoteConfig; +export type FirebaseRemoteConfig = any; // Have to implement as we need to return a class from the provider, we should consider exporting // this in the firebase/app types as this is our highest risk of breaks @@ -31,7 +35,7 @@ export class FirebaseApp { auth: () => FirebaseAuth; database: (databaseURL?: string) => FirebaseDatabase; messaging: () => FirebaseMessaging; - performance: () => any; // SEMVER: once >= 6 import performance.Performance + performance: () => FirebasePerformance; storage: (storageBucket?: string) => FirebaseStorage; delete: () => Promise; firestore: () => FirebaseFirestore; diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index d2612add1..b9d8927fb 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -75,7 +75,8 @@ export class AngularFireRemoteConfig { // @ts-ignore zapping in the UMD in the build script switchMap(() => import('firebase/remote-config')), map(() => _firebaseAppFactory(options, zone, nameOrConfig)), - map(app => app.remoteConfig()), + // SEMVER no need to cast once we drop older Firebase + map(app => app.remoteConfig()), tap(rc => { if (settings) { rc.settings = settings } if (defaultConfig) { rc.defaultConfig = defaultConfig } From 659165e3f04f4753a42de0ffe82388a4fd771e90 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Fri, 22 Nov 2019 11:07:16 -0800 Subject: [PATCH 21/37] SEMVER notes on the DI tokens --- src/core/angularfire2.ts | 1 + src/core/firebase.app.module.ts | 1 + src/firestore/firestore.ts | 2 ++ src/storage/storage.ts | 2 +- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/angularfire2.ts b/src/core/angularfire2.ts index cbc91af34..021474fbd 100644 --- a/src/core/angularfire2.ts +++ b/src/core/angularfire2.ts @@ -3,6 +3,7 @@ import { isPlatformServer } from '@angular/common'; import { Observable, Subscription, queueScheduler as queue } from 'rxjs'; // Put in database.ts when we drop database-depreciated +// SEMVER drop RealtimeDatabaseURL in favor of DATABASE_URL in next major export const RealtimeDatabaseURL = new InjectionToken('angularfire2.realtimeDatabaseURL'); export const DATABASE_URL = RealtimeDatabaseURL; diff --git a/src/core/firebase.app.module.ts b/src/core/firebase.app.module.ts index f0269e71c..c10e2c7e9 100644 --- a/src/core/firebase.app.module.ts +++ b/src/core/firebase.app.module.ts @@ -7,6 +7,7 @@ import firebase from 'firebase/app'; // once fixed can pull in as "default as fi export type FirebaseOptions = {[key:string]: any}; export type FirebaseAppConfig = {[key:string]: any}; +// SEMVER drop FirebaseOptionsToken and FirebaseNameOrConfigToken in favor of FIREBASE_OPTIONS and FIREBASE_APP_NAME in next major export const FirebaseOptionsToken = new InjectionToken('angularfire2.app.options'); export const FirebaseNameOrConfigToken = new InjectionToken('angularfire2.app.nameOrConfig'); diff --git a/src/firestore/firestore.ts b/src/firestore/firestore.ts index 4c3d4fbf6..c42fe46a5 100644 --- a/src/firestore/firestore.ts +++ b/src/firestore/firestore.ts @@ -17,6 +17,7 @@ import firebase from 'firebase/app'; // SEMVER: have to import here while we target ng 6, as the version of typescript doesn't allow dynamic import of types import { firestore } from 'firebase/app'; +// SEMVER drop EnablePersistenceToken, PersistenceSettingsToken, and FirestoreSettingsToken in favor of new export names /** * The value of this token determines whether or not the firestore will have persistance enabled */ @@ -28,6 +29,7 @@ export const ENABLE_PERSISTENCE = EnablePersistenceToken; export const PERSISTENCE_SETTINGS = PersistenceSettingsToken export const FIRESTORE_SETTINGS = FirestoreSettingsToken; +// SEMVER kill this in the next major // timestampsInSnapshots was depreciated in 5.8.0 const major = parseInt(firebase.SDK_VERSION.split('.')[0]); const minor = parseInt(firebase.SDK_VERSION.split('.')[1]); diff --git a/src/storage/storage.ts b/src/storage/storage.ts index edc1b17fb..0c1d27b7d 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -6,8 +6,8 @@ import { FirebaseStorage, FirebaseOptions, FirebaseAppConfig, FirebaseZoneSchedu import { UploadMetadata } from './interfaces'; +// SEMVER drop StorageBucket in favor of STORAGE_BUCKET export const StorageBucket = new InjectionToken('angularfire2.storageBucket'); - export const STORAGE_BUCKET = StorageBucket; /** From 62d90b919100f4086c27ce05230e42a47a3c6c05 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Fri, 22 Nov 2019 11:47:16 -0800 Subject: [PATCH 22/37] Starting on the docs --- README.md | 18 ++++++++++++++-- docs/analytics/getting-started.md | 27 ++++++++++++++++++++++++ docs/remote-config/getting-started.md | 30 +++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 docs/analytics/getting-started.md create mode 100644 docs/remote-config/getting-started.md diff --git a/README.md b/README.md index 40f872a4f..f5e98a672 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,12 @@ export class MyApp { - [Installation & Setup](docs/install-and-setup.md) +### **NEW:** Monitor usage of your application in production + +> `AngularFireAnalytics` provides a convient method of interacting with Google Analytics in your Angular application. The provided `ScreenTrackingService` and `UserTrackingService` automatically log events when you're using the Angular Router or Firebase Authentication respectively. [Learn more about Google Analytics](https://firebase.google.com/docs/analytics). + +- [Getting started with Google Analytics](docs/analytics/getting-started.md) + ### Interacting with your database(s) Firebase offers two cloud-based, client-accessible database solutions that support realtime data syncing. [Learn about the differences between them in the Firebase Documentation](https://firebase.google.com/docs/firestore/rtdb-vs-firestore). @@ -94,11 +100,19 @@ Firebase offers two cloud-based, client-accessible database solutions that suppo - [Getting started with Cloud Storage](docs/storage/storage.md) -### Send push notifications +### Receive push notifications - [Getting started with Firebase Messaging](docs/messaging/messaging.md) -### Monitor your application performance in production +### **NEW:** Change behavior and appearance of your application without deploying + +> Firebase Remote Config is a cloud service that lets you change the behavior and appearance of your app without requiring users to download an app update. [Learn more about Remote Config](https://firebase.google.com/docs/remote-config). + +- [Getting started with Remote Config](docs/remote-config/getting-started.md) + +### **NEW:** Monitor your application performance in production + +> Firebase Performance Monitoring is a service that helps you to gain insight into the performance characteristics of your iOS, Android, and web apps. [Learn more about Performance Monitor](https://firebase.google.com/docs/perf-mon). - [Getting started with Performance Monitoring](docs/performance/getting-started.md) diff --git a/docs/analytics/getting-started.md b/docs/analytics/getting-started.md new file mode 100644 index 000000000..2cea4c8d5 --- /dev/null +++ b/docs/analytics/getting-started.md @@ -0,0 +1,27 @@ +# Getting started with Google Analytics + +### Something, something + +TBD + +### Putting it all together + +```ts +@NgModule({ + imports: [ + AngularFireModule.initializeApp(environment.firebase), + AngularFireAnalyticsModule + ], + providers: [ + ScreenTrackingService, + UserTrackingService + ] +}) +export class AppModule { } +``` + +```ts +constructor(analytics: AngularFireAnalytics) { + analytics.logEvent('custom_event', { ... }); +} +``` \ No newline at end of file diff --git a/docs/remote-config/getting-started.md b/docs/remote-config/getting-started.md new file mode 100644 index 000000000..633cefe66 --- /dev/null +++ b/docs/remote-config/getting-started.md @@ -0,0 +1,30 @@ +# Getting started with Remote Config + +### Something, something + +TBD + +### Putting it all together + +```ts +@NgModule({ + imports: [ + AngularFireModule.initializeApp(environment.firebase), + AngularFireRemoteConfigModule + ], + providers: [ + { provide: DEFAULT_CONFIG, useValue: { enableAwesome: true } }, + { + provide: REMOTE_CONFIG_SETTINGS, + useFactory: () => isDevMode ? { minimumFetchIntervalMillis: 1 } : {} + } + ] +}) +export class AppModule { } +``` + +```ts +constructor(remoteConfig: AngularFireRemoteConfig) { + remoteConfig.changes.subscribe(changes => …); +} +``` \ No newline at end of file From 67e1b5561f906645dd51768bc6edec0680013509 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Mon, 9 Dec 2019 13:45:12 -0800 Subject: [PATCH 23/37] Monitoring... --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f5e98a672..bb36416e2 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Firebase offers two cloud-based, client-accessible database solutions that suppo ### **NEW:** Monitor your application performance in production -> Firebase Performance Monitoring is a service that helps you to gain insight into the performance characteristics of your iOS, Android, and web apps. [Learn more about Performance Monitor](https://firebase.google.com/docs/perf-mon). +> Firebase Performance Monitoring is a service that helps you to gain insight into the performance characteristics of your iOS, Android, and web apps. [Learn more about Performance Monitoring](https://firebase.google.com/docs/perf-mon). - [Getting started with Performance Monitoring](docs/performance/getting-started.md) From 91778ff414a82cc0a527b52fdfe90c0fba75c87b Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 10 Dec 2019 18:22:27 -0800 Subject: [PATCH 24/37] More work on analytics --- src/analytics/analytics.module.ts | 5 +- src/analytics/analytics.service.ts | 129 +++++++++++++++-------------- src/analytics/analytics.ts | 33 ++++++-- src/core/angularfire2.ts | 2 +- src/remote-config/remote-config.ts | 4 +- 5 files changed, 103 insertions(+), 70 deletions(-) diff --git a/src/analytics/analytics.module.ts b/src/analytics/analytics.module.ts index 977bb72de..5956bbee4 100644 --- a/src/analytics/analytics.module.ts +++ b/src/analytics/analytics.module.ts @@ -2,10 +2,11 @@ import { NgModule, Optional } from '@angular/core'; import { UserTrackingService, ScreenTrackingService } from './analytics.service'; import { AngularFireAnalytics } from './analytics'; -@NgModule() +@NgModule({ + providers: [ AngularFireAnalytics ] +}) export class AngularFireAnalyticsModule { constructor( - analytics: AngularFireAnalytics, @Optional() screenTracking: ScreenTrackingService, @Optional() userTracking: UserTrackingService ) { } diff --git a/src/analytics/analytics.service.ts b/src/analytics/analytics.service.ts index 31fbc6afe..85e80a7cd 100644 --- a/src/analytics/analytics.service.ts +++ b/src/analytics/analytics.service.ts @@ -1,30 +1,36 @@ -import { Injectable, Inject, Optional, NgZone, OnDestroy, InjectionToken } from '@angular/core'; +import { Injectable, Optional, NgZone, OnDestroy } from '@angular/core'; import { Subscription, from, Observable, empty, of } from 'rxjs'; import { filter, withLatestFrom, switchMap, map, tap, pairwise, startWith, groupBy, mergeMap } from 'rxjs/operators'; import { Router, NavigationEnd, ActivationEnd } from '@angular/router'; -import { runOutsideAngular, _lazySDKProxy, _firebaseAppFactory } from '@angular/fire'; +import { runOutsideAngular } from '@angular/fire'; import { AngularFireAnalytics } from './analytics'; import { User } from 'firebase/app'; +import { Title } from '@angular/platform-browser'; -export const APP_VERSION = new InjectionToken('angularfire2.analytics.appVersion'); -export const APP_NAME = new InjectionToken('angularfire2.analytics.appName'); +// Gold seems to take page_title and screen_path but the v2 protocol doesn't seem +// to allow any class name, obviously v2 was designed for the basic web. I'm still +// sending firebase_screen_class (largely for BQ compatability) but the Firebase Console +// doesn't appear to be consuming the event properties. +// FWIW I'm seeing notes that firebase_* is depreciated in favor of ga_* in GMS... so IDK +const SCREEN_NAME_KEY = 'screen_name'; +const PAGE_PATH_KEY = 'page_path'; +const EVENT_ORIGIN_KEY = 'event_origin'; +const FIREBASE_SCREEN_NAME_KEY = 'firebase_screen'; +const SCREEN_CLASS_KEY = 'firebase_screen_class'; +const OUTLET_KEY = 'outlet'; +const PAGE_TITLE_KEY = 'page_title'; +const PREVIOUS_SCREEN_CLASS_KEY = 'firebase_previous_class'; +const PREVIOUS_SCREEN_INSTANCE_ID_KEY = 'firebase_previous_id'; +const PREVIOUS_SCREEN_NAME_KEY = 'firebase_previous_screen'; +const SCREEN_INSTANCE_ID_KEY = 'firebase_screen_id'; -const DEFAULT_APP_VERSION = '?'; -const DEFAULT_APP_NAME = 'Angular App'; +const SCREEN_VIEW_EVENT = 'screen_view'; +const EVENT_ORIGIN_AUTO = 'auto'; +const DEFAULT_SCREEN_CLASS = '???'; +const NG_PRIMARY_OUTLET = 'primary'; +const SCREEN_INSTANCE_DELIMITER = '#'; -type AngularFireAnalyticsEventParams = { - app_name: string; - firebase_screen_class: string | undefined; - firebase_screen: string; - app_version: string; - screen_name: string; - outlet: string; - url: string; -}; - -@Injectable({ - providedIn: 'root' -}) +@Injectable() export class ScreenTrackingService implements OnDestroy { private disposable: Subscription|undefined; @@ -32,64 +38,65 @@ export class ScreenTrackingService implements OnDestroy { constructor( analytics: AngularFireAnalytics, @Optional() router:Router, - @Optional() @Inject(APP_VERSION) providedAppVersion:string|null, - @Optional() @Inject(APP_NAME) providedAppName:string|null, + @Optional() title:Title, zone: NgZone ) { if (!router) { return this } - const app_name = providedAppName || DEFAULT_APP_NAME; - const app_version = providedAppVersion || DEFAULT_APP_VERSION; const activationEndEvents = router.events.pipe(filter(e => e instanceof ActivationEnd)); const navigationEndEvents = router.events.pipe(filter(e => e instanceof NavigationEnd)); this.disposable = navigationEndEvents.pipe( withLatestFrom(activationEndEvents), switchMap(([navigationEnd, activationEnd]) => { - const url = navigationEnd.url; - const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || url; - const params: AngularFireAnalyticsEventParams = { - app_name, app_version, screen_name, url, - firebase_screen_class: undefined, - firebase_screen: screen_name, - outlet: activationEnd.snapshot.outlet + // SEMVER: start using optional chains and nullish coalescing once we support newer typescript + const page_path = navigationEnd.url; + const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || page_path; + const params = { + [SCREEN_NAME_KEY]: screen_name, + [PAGE_PATH_KEY]: page_path, + [EVENT_ORIGIN_KEY]: EVENT_ORIGIN_AUTO, + [FIREBASE_SCREEN_NAME_KEY]: screen_name, + [OUTLET_KEY]: activationEnd.snapshot.outlet }; + if (title) { params[PAGE_TITLE_KEY] = title.getTitle() } const component = activationEnd.snapshot.component; const routeConfig = activationEnd.snapshot.routeConfig; + // TODO maybe not lean on _loadedConfig... const loadedConfig = routeConfig && (routeConfig as any)._loadedConfig; const loadChildren = routeConfig && routeConfig.loadChildren; if (component) { - return of({...params, firebase_screen_class: nameOrToString(component) }); + return of({...params, [SCREEN_CLASS_KEY]: nameOrToString(component) }); } else if (loadedConfig && loadedConfig.module && loadedConfig.module._moduleType) { - return of({...params, firebase_screen_class: nameOrToString(loadedConfig.module._moduleType)}); + return of({...params, [SCREEN_CLASS_KEY]: nameOrToString(loadedConfig.module._moduleType)}); } else if (typeof loadChildren === "string") { - // TODO is this an older lazy loading style parse - return of({...params, firebase_screen_class: loadChildren }); + // TODO is the an older lazy loading style? parse, if so + return of({...params, [SCREEN_CLASS_KEY]: loadChildren }); } else if (loadChildren) { // TODO look into the other return types here - return from(loadChildren() as Promise).pipe(map(child => ({...params, firebase_screen_class: nameOrToString(child) }))); + return from(loadChildren() as Promise).pipe(map(child => ({...params, [SCREEN_CLASS_KEY]: nameOrToString(child) }))); } else { // TODO figure out what forms of router events I might be missing - return of(params); + return of({...params, [SCREEN_CLASS_KEY]: DEFAULT_SCREEN_CLASS}); } }), tap(params => { // TODO perhaps I can be smarter about this, bubble events up to the nearest outlet? - if (params.outlet == "primary") { - // TODO do I need to add gtag config for firebase_screen, firebase_screen_class, firebase_screen_id? - // also shouldn't these be computed in the setCurrentScreen function? prior too? - // do we want to be logging screen name or class? - analytics.setCurrentScreen(params.screen_name, { global: true }) + if (params[OUTLET_KEY] == NG_PRIMARY_OUTLET) { + // TODO do we want to track the firebase_ attributes? + analytics.setCurrentScreen(params.screen_name); + analytics.updateConfig({ [PAGE_PATH_KEY]: params[PAGE_PATH_KEY] }); + if (title) { analytics.updateConfig({ [PAGE_TITLE_KEY]: params[PAGE_TITLE_KEY] }) } } }), - map(params => ({ firebase_screen_id: nextScreenId(params), ...params})), - groupBy(params => params.outlet), + map(params => ({ [SCREEN_INSTANCE_ID_KEY]: getScreenInstanceID(params), ...params })), + groupBy(params => params[OUTLET_KEY]), mergeMap(group => group.pipe(startWith(undefined), pairwise())), map(([prior, current]) => prior ? { - firebase_previous_class: prior.firebase_screen_class, - firebase_previous_screen: prior.firebase_screen, - firebase_previous_id: prior.firebase_screen_id, + [PREVIOUS_SCREEN_CLASS_KEY]: prior[SCREEN_CLASS_KEY], + [PREVIOUS_SCREEN_NAME_KEY]: prior[SCREEN_NAME_KEY], + [PREVIOUS_SCREEN_INSTANCE_ID_KEY]: prior[SCREEN_INSTANCE_ID_KEY], ...current! } : current!), - tap(params => analytics.logEvent('screen_view', params)), + tap(params => analytics.logEvent(SCREEN_VIEW_EVENT, params)), runOutsideAngular(zone) ).subscribe(); } @@ -100,9 +107,7 @@ export class ScreenTrackingService implements OnDestroy { } -@Injectable({ - providedIn: 'root' -}) +@Injectable() export class UserTrackingService implements OnDestroy { private disposable: Subscription|undefined; @@ -116,7 +121,7 @@ export class UserTrackingService implements OnDestroy { // TODO can I hook into auth being loaded... map(app => app.auth()), switchMap(auth => auth ? new Observable(auth.onAuthStateChanged.bind(auth)) : empty()), - switchMap(user => analytics.setUserId(user ? user.uid : null!, { global: true })), + switchMap(user => analytics.setUserId(user ? user.uid : null!)), runOutsideAngular(zone) ).subscribe(); } @@ -126,18 +131,22 @@ export class UserTrackingService implements OnDestroy { } } -// firebase_screen_id is an INT64 but use INT32 cause javascript -const randomInt32 = () => Math.floor(Math.random() * (2**32 - 1)) - 2**31; +// this is an INT64 in iOS/Android but use INT32 cause javascript +let nextScreenInstanceID = Math.floor(Math.random() * (2**32 - 1)) - 2**31; -const currentScreenIds: {[key:string]: number} = {}; +const knownScreenInstanceIDs: {[key:string]: number} = {}; -const nextScreenId = (params:AngularFireAnalyticsEventParams) => { - const scope = params.outlet; - if (currentScreenIds.hasOwnProperty(scope)) { - return ++currentScreenIds[scope]; +const getScreenInstanceID = (params:{[key:string]: any}) => { + // unique the screen class against the outlet name + const screenInstanceKey = [ + params[SCREEN_CLASS_KEY], + params[OUTLET_KEY] + ].join(SCREEN_INSTANCE_DELIMITER); + if (knownScreenInstanceIDs.hasOwnProperty(screenInstanceKey)) { + return knownScreenInstanceIDs[screenInstanceKey]; } else { - const ret = randomInt32(); - currentScreenIds[scope] = ret; + const ret = nextScreenInstanceID++; + knownScreenInstanceIDs[screenInstanceKey] = ret; return ret; } } diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index b1ae85eda..f77604cb8 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -1,11 +1,24 @@ import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; import { of } from 'rxjs'; import { map, tap, shareReplay, switchMap } from 'rxjs/operators'; -import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular, _lazySDKProxy, FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire'; +import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular, ɵlazySDKProxy, FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire'; import { analytics, app } from 'firebase'; export const ANALYTICS_COLLECTION_ENABLED = new InjectionToken('angularfire2.analytics.analyticsCollectionEnabled'); +export const APP_VERSION = new InjectionToken('angularfire2.analytics.appVersion'); +export const APP_NAME = new InjectionToken('angularfire2.analytics.appName'); +export const DEBUG_MODE = new InjectionToken('angularfire2.analytics.debugMode'); + +const APP_NAME_KEY = 'app_name'; +const APP_VERSION_KEY = 'app_version'; +const DEBUG_MODE_KEY = 'debug_mode'; +const ANALYTICS_ID_FIELD = 'measurementId'; +const GTAG_CONFIG_COMMAND = 'config'; + +// TODO can we get this from js sdk? +const GTAG_FUNCTION = 'gtag'; + // SEMVER: once we move to Typescript 3.6 use `PromiseProxy` type AnalyticsProxy = { // TODO can we pull the richer types from the Firebase SDK .d.ts? ReturnType is infering @@ -20,15 +33,18 @@ type AnalyticsProxy = { export interface AngularFireAnalytics extends AnalyticsProxy {}; -@Injectable({ - providedIn: "root" -}) +@Injectable() export class AngularFireAnalytics { + public updateConfig: (options: {[key:string]: any}) => void; + constructor( @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, @Optional() @Inject(ANALYTICS_COLLECTION_ENABLED) analyticsCollectionEnabled:boolean|null, + @Optional() @Inject(APP_VERSION) providedAppVersion:string|null, + @Optional() @Inject(APP_NAME) providedAppName:string|null, + @Optional() @Inject(DEBUG_MODE) debugModeEnabled:boolean|null, zone: NgZone ) { const analytics = of(undefined).pipe( @@ -39,12 +55,19 @@ export class AngularFireAnalytics { map(app => app.analytics()), tap(analytics => { if (analyticsCollectionEnabled === false) { analytics.setAnalyticsCollectionEnabled(false) } + if (providedAppName) { this.updateConfig({ [APP_NAME_KEY]: providedAppName }) } + if (providedAppVersion) { this.updateConfig({ [APP_VERSION_KEY]: providedAppVersion }) } + if (debugModeEnabled) { this.updateConfig({ [DEBUG_MODE_KEY]: 1 }) } }), runOutsideAngular(zone), shareReplay(1) ); - return _lazySDKProxy(this, analytics, zone); + this.updateConfig = (config: {[key:string]: any}) => analytics.toPromise().then(() => + window[GTAG_FUNCTION](GTAG_CONFIG_COMMAND, options[ANALYTICS_ID_FIELD], { ...config, update: true }) + ); + + return ɵlazySDKProxy(this, analytics, zone); } } \ No newline at end of file diff --git a/src/core/angularfire2.ts b/src/core/angularfire2.ts index 021474fbd..1bfafeb56 100644 --- a/src/core/angularfire2.ts +++ b/src/core/angularfire2.ts @@ -80,7 +80,7 @@ export const runInZone = (zone: NgZone) => (obs$: Observable): Observable< { [K in PromiseReturningFunctionPropertyNames ]: (...args: Parameters) => ReturnType }; */ -export const _lazySDKProxy = (klass: any, observable: Observable, zone: NgZone) => new Proxy(klass, { +export const ɵlazySDKProxy = (klass: any, observable: Observable, zone: NgZone) => new Proxy(klass, { get: (_, name) => zone.runOutsideAngular(() => klass[name] || new Proxy(() => observable.toPromise().then(mod => { diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index b9d8927fb..f6a94d3fc 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -1,7 +1,7 @@ import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; import { Observable, concat, of, empty } from 'rxjs'; import { map, switchMap, tap, shareReplay, distinctUntilChanged } from 'rxjs/operators'; -import { FirebaseAppConfig, FirebaseOptions, _lazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; +import { FirebaseAppConfig, FirebaseOptions, ɵlazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; import { remoteConfig } from 'firebase/app'; export interface DefaultConfig {[key:string]: string|number|boolean}; @@ -100,7 +100,7 @@ export class AngularFireRemoteConfig { }, new Array(keys.length)); } - const proxy: AngularFireRemoteConfig = _lazySDKProxy(this, remoteConfig, zone); + const proxy: AngularFireRemoteConfig = ɵlazySDKProxy(this, remoteConfig, zone); this.default$ = of(defaultToStartWith); From 460170c997297eba73440a6cf71c2959717ee5bd Mon Sep 17 00:00:00 2001 From: James Daniels Date: Wed, 11 Dec 2019 16:15:19 -0800 Subject: [PATCH 25/37] more expirimentation --- src/analytics/analytics.service.ts | 26 ++++++++++++++---- src/analytics/analytics.ts | 44 +++++++++++++++++++++--------- 2 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/analytics/analytics.service.ts b/src/analytics/analytics.service.ts index 85e80a7cd..71bc1d974 100644 --- a/src/analytics/analytics.service.ts +++ b/src/analytics/analytics.service.ts @@ -16,7 +16,7 @@ const SCREEN_NAME_KEY = 'screen_name'; const PAGE_PATH_KEY = 'page_path'; const EVENT_ORIGIN_KEY = 'event_origin'; const FIREBASE_SCREEN_NAME_KEY = 'firebase_screen'; -const SCREEN_CLASS_KEY = 'firebase_screen_class'; +const FIREBASE_SCREEN_CLASS_KEY = 'firebase_screen_class'; const OUTLET_KEY = 'outlet'; const PAGE_TITLE_KEY = 'page_title'; const PREVIOUS_SCREEN_CLASS_KEY = 'firebase_previous_class'; @@ -24,6 +24,11 @@ const PREVIOUS_SCREEN_INSTANCE_ID_KEY = 'firebase_previous_id'; const PREVIOUS_SCREEN_NAME_KEY = 'firebase_previous_screen'; const SCREEN_INSTANCE_ID_KEY = 'firebase_screen_id'; +// Do I need these? +const SCREEN_CLASS_KEY = 'screen_class'; +const GA_SCREEN_CLASS_KEY = 'ga_screen_class'; +const GA_SCREEN_NAME_KEY = 'ga_screen'; + const SCREEN_VIEW_EVENT = 'screen_view'; const EVENT_ORIGIN_AUTO = 'auto'; const DEFAULT_SCREEN_CLASS = '???'; @@ -54,7 +59,9 @@ export class ScreenTrackingService implements OnDestroy { [SCREEN_NAME_KEY]: screen_name, [PAGE_PATH_KEY]: page_path, [EVENT_ORIGIN_KEY]: EVENT_ORIGIN_AUTO, - [FIREBASE_SCREEN_NAME_KEY]: screen_name, + // TODO remove unneeded, just testing here + [FIREBASE_SCREEN_NAME_KEY]: `${screen_name} (firebase)`, + [GA_SCREEN_NAME_KEY]: `${screen_name} (ga)`, [OUTLET_KEY]: activationEnd.snapshot.outlet }; if (title) { params[PAGE_TITLE_KEY] = title.getTitle() } @@ -78,16 +85,25 @@ export class ScreenTrackingService implements OnDestroy { return of({...params, [SCREEN_CLASS_KEY]: DEFAULT_SCREEN_CLASS}); } }), + map(params => ({ + // TODO remove unneeded, just testing here + [GA_SCREEN_CLASS_KEY]: `${params[SCREEN_CLASS_KEY]} (ga)`, + [FIREBASE_SCREEN_CLASS_KEY]: `${params[SCREEN_CLASS_KEY]} (firebase)`, + [SCREEN_INSTANCE_ID_KEY]: getScreenInstanceID(params), + ...params + })), tap(params => { // TODO perhaps I can be smarter about this, bubble events up to the nearest outlet? if (params[OUTLET_KEY] == NG_PRIMARY_OUTLET) { // TODO do we want to track the firebase_ attributes? - analytics.setCurrentScreen(params.screen_name); - analytics.updateConfig({ [PAGE_PATH_KEY]: params[PAGE_PATH_KEY] }); + analytics.setCurrentScreen(params[SCREEN_NAME_KEY]); + analytics.updateConfig({ + [PAGE_PATH_KEY]: params[PAGE_PATH_KEY], + [SCREEN_CLASS_KEY]: params[SCREEN_CLASS_KEY] + }); if (title) { analytics.updateConfig({ [PAGE_TITLE_KEY]: params[PAGE_TITLE_KEY] }) } } }), - map(params => ({ [SCREEN_INSTANCE_ID_KEY]: getScreenInstanceID(params), ...params })), groupBy(params => params[OUTLET_KEY]), mergeMap(group => group.pipe(startWith(undefined), pairwise())), map(([prior, current]) => prior ? { diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index f77604cb8..21d3b4cf8 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -1,5 +1,6 @@ -import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; +import { Injectable, Inject, Optional, NgZone, InjectionToken, PLATFORM_ID } from '@angular/core'; import { of } from 'rxjs'; +import { isPlatformBrowser } from '@angular/common'; import { map, tap, shareReplay, switchMap } from 'rxjs/operators'; import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular, ɵlazySDKProxy, FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire'; import { analytics, app } from 'firebase'; @@ -15,9 +16,8 @@ const APP_VERSION_KEY = 'app_version'; const DEBUG_MODE_KEY = 'debug_mode'; const ANALYTICS_ID_FIELD = 'measurementId'; const GTAG_CONFIG_COMMAND = 'config'; - -// TODO can we get this from js sdk? -const GTAG_FUNCTION = 'gtag'; +const GTAG_FUNCTION_NAME = 'gtag'; +const DATA_LAYER_NAME = 'dataLayer'; // SEMVER: once we move to Typescript 3.6 use `PromiseProxy` type AnalyticsProxy = { @@ -36,17 +36,42 @@ export interface AngularFireAnalytics extends AnalyticsProxy {}; @Injectable() export class AngularFireAnalytics { - public updateConfig: (options: {[key:string]: any}) => void; + private gtag: (...args: any[]) => {}; + private analyticsInitialized: Promise; + + async updateConfig(config: {[key:string]: any}) { + await this.analyticsInitialized; + this.gtag(GTAG_CONFIG_COMMAND, this.options[ANALYTICS_ID_FIELD], { ...config, update: true }); + }; constructor( - @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, + @Inject(FIREBASE_OPTIONS) private options:FirebaseOptions, @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, @Optional() @Inject(ANALYTICS_COLLECTION_ENABLED) analyticsCollectionEnabled:boolean|null, @Optional() @Inject(APP_VERSION) providedAppVersion:string|null, @Optional() @Inject(APP_NAME) providedAppName:string|null, @Optional() @Inject(DEBUG_MODE) debugModeEnabled:boolean|null, + @Inject(PLATFORM_ID) platformId, zone: NgZone ) { + + if (isPlatformBrowser(platformId)) { + window[DATA_LAYER_NAME] = window[DATA_LAYER_NAME] || []; + this.gtag = window[GTAG_FUNCTION_NAME] || function() { window[DATA_LAYER_NAME].push(arguments) } + this.analyticsInitialized = new Promise(resolve => { + window[GTAG_FUNCTION_NAME] = (...args: any[]) => { + if (args[0] == 'js') { resolve() } + this.gtag(...args); + } + }); + } else { + this.analyticsInitialized = Promise.reject(); + } + + if (providedAppName) { this.updateConfig({ [APP_NAME_KEY]: providedAppName }) } + if (providedAppVersion) { this.updateConfig({ [APP_VERSION_KEY]: providedAppVersion }) } + if (debugModeEnabled) { this.updateConfig({ [DEBUG_MODE_KEY]: 1 }) } + const analytics = of(undefined).pipe( // @ts-ignore zapping in the UMD in the build script switchMap(() => zone.runOutsideAngular(() => import('firebase/analytics'))), @@ -55,18 +80,11 @@ export class AngularFireAnalytics { map(app => app.analytics()), tap(analytics => { if (analyticsCollectionEnabled === false) { analytics.setAnalyticsCollectionEnabled(false) } - if (providedAppName) { this.updateConfig({ [APP_NAME_KEY]: providedAppName }) } - if (providedAppVersion) { this.updateConfig({ [APP_VERSION_KEY]: providedAppVersion }) } - if (debugModeEnabled) { this.updateConfig({ [DEBUG_MODE_KEY]: 1 }) } }), runOutsideAngular(zone), shareReplay(1) ); - this.updateConfig = (config: {[key:string]: any}) => analytics.toPromise().then(() => - window[GTAG_FUNCTION](GTAG_CONFIG_COMMAND, options[ANALYTICS_ID_FIELD], { ...config, update: true }) - ); - return ɵlazySDKProxy(this, analytics, zone); } From 3c1ad1f349d0d1eb25d38e387f4f1fcfb9081f98 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Sat, 14 Dec 2019 01:46:47 -0800 Subject: [PATCH 26/37] Flushing out RC more and fixing SSR issues --- src/analytics/analytics.service.ts | 182 +++++++++++----------- src/analytics/analytics.ts | 27 ++-- src/core/angularfire2.ts | 20 ++- src/remote-config/remote-config.module.ts | 5 +- src/remote-config/remote-config.ts | 146 ++++++++++------- tools/build.js | 6 +- 6 files changed, 218 insertions(+), 168 deletions(-) diff --git a/src/analytics/analytics.service.ts b/src/analytics/analytics.service.ts index 71bc1d974..50680273f 100644 --- a/src/analytics/analytics.service.ts +++ b/src/analytics/analytics.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Optional, NgZone, OnDestroy } from '@angular/core'; +import { Injectable, Optional, NgZone, OnDestroy, ComponentFactoryResolver, Inject, PLATFORM_ID } from '@angular/core'; import { Subscription, from, Observable, empty, of } from 'rxjs'; import { filter, withLatestFrom, switchMap, map, tap, pairwise, startWith, groupBy, mergeMap } from 'rxjs/operators'; import { Router, NavigationEnd, ActivationEnd } from '@angular/router'; @@ -6,28 +6,20 @@ import { runOutsideAngular } from '@angular/fire'; import { AngularFireAnalytics } from './analytics'; import { User } from 'firebase/app'; import { Title } from '@angular/platform-browser'; +import { isPlatformBrowser } from '@angular/common'; -// Gold seems to take page_title and screen_path but the v2 protocol doesn't seem -// to allow any class name, obviously v2 was designed for the basic web. I'm still -// sending firebase_screen_class (largely for BQ compatability) but the Firebase Console -// doesn't appear to be consuming the event properties. -// FWIW I'm seeing notes that firebase_* is depreciated in favor of ga_* in GMS... so IDK -const SCREEN_NAME_KEY = 'screen_name'; -const PAGE_PATH_KEY = 'page_path'; -const EVENT_ORIGIN_KEY = 'event_origin'; -const FIREBASE_SCREEN_NAME_KEY = 'firebase_screen'; +const FIREBASE_EVENT_ORIGIN_KEY = 'firebase_event_origin'; +const FIREBASE_PREVIOUS_SCREEN_CLASS_KEY = 'firebase_previous_class'; +const FIREBASE_PREVIOUS_SCREEN_INSTANCE_ID_KEY = 'firebase_previous_id'; +const FIREBASE_PREVIOUS_SCREEN_NAME_KEY = 'firebase_previous_screen'; const FIREBASE_SCREEN_CLASS_KEY = 'firebase_screen_class'; +const FIREBASE_SCREEN_INSTANCE_ID_KEY = 'firebase_screen_id'; +const FIREBASE_SCREEN_NAME_KEY = 'firebase_screen'; const OUTLET_KEY = 'outlet'; +const PAGE_PATH_KEY = 'page_path'; const PAGE_TITLE_KEY = 'page_title'; -const PREVIOUS_SCREEN_CLASS_KEY = 'firebase_previous_class'; -const PREVIOUS_SCREEN_INSTANCE_ID_KEY = 'firebase_previous_id'; -const PREVIOUS_SCREEN_NAME_KEY = 'firebase_previous_screen'; -const SCREEN_INSTANCE_ID_KEY = 'firebase_screen_id'; - -// Do I need these? const SCREEN_CLASS_KEY = 'screen_class'; -const GA_SCREEN_CLASS_KEY = 'ga_screen_class'; -const GA_SCREEN_NAME_KEY = 'ga_screen'; +const SCREEN_NAME_KEY = 'screen_name'; const SCREEN_VIEW_EVENT = 'screen_view'; const EVENT_ORIGIN_AUTO = 'auto'; @@ -44,77 +36,89 @@ export class ScreenTrackingService implements OnDestroy { analytics: AngularFireAnalytics, @Optional() router:Router, @Optional() title:Title, + componentFactoryResolver: ComponentFactoryResolver, + @Inject(PLATFORM_ID) platformId:Object, zone: NgZone ) { - if (!router) { return this } - const activationEndEvents = router.events.pipe(filter(e => e instanceof ActivationEnd)); - const navigationEndEvents = router.events.pipe(filter(e => e instanceof NavigationEnd)); - this.disposable = navigationEndEvents.pipe( - withLatestFrom(activationEndEvents), - switchMap(([navigationEnd, activationEnd]) => { - // SEMVER: start using optional chains and nullish coalescing once we support newer typescript - const page_path = navigationEnd.url; - const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || page_path; - const params = { - [SCREEN_NAME_KEY]: screen_name, - [PAGE_PATH_KEY]: page_path, - [EVENT_ORIGIN_KEY]: EVENT_ORIGIN_AUTO, - // TODO remove unneeded, just testing here - [FIREBASE_SCREEN_NAME_KEY]: `${screen_name} (firebase)`, - [GA_SCREEN_NAME_KEY]: `${screen_name} (ga)`, - [OUTLET_KEY]: activationEnd.snapshot.outlet - }; - if (title) { params[PAGE_TITLE_KEY] = title.getTitle() } - const component = activationEnd.snapshot.component; - const routeConfig = activationEnd.snapshot.routeConfig; - // TODO maybe not lean on _loadedConfig... - const loadedConfig = routeConfig && (routeConfig as any)._loadedConfig; - const loadChildren = routeConfig && routeConfig.loadChildren; - if (component) { - return of({...params, [SCREEN_CLASS_KEY]: nameOrToString(component) }); - } else if (loadedConfig && loadedConfig.module && loadedConfig.module._moduleType) { - return of({...params, [SCREEN_CLASS_KEY]: nameOrToString(loadedConfig.module._moduleType)}); - } else if (typeof loadChildren === "string") { - // TODO is the an older lazy loading style? parse, if so - return of({...params, [SCREEN_CLASS_KEY]: loadChildren }); - } else if (loadChildren) { - // TODO look into the other return types here - return from(loadChildren() as Promise).pipe(map(child => ({...params, [SCREEN_CLASS_KEY]: nameOrToString(child) }))); - } else { - // TODO figure out what forms of router events I might be missing - return of({...params, [SCREEN_CLASS_KEY]: DEFAULT_SCREEN_CLASS}); - } - }), - map(params => ({ - // TODO remove unneeded, just testing here - [GA_SCREEN_CLASS_KEY]: `${params[SCREEN_CLASS_KEY]} (ga)`, - [FIREBASE_SCREEN_CLASS_KEY]: `${params[SCREEN_CLASS_KEY]} (firebase)`, - [SCREEN_INSTANCE_ID_KEY]: getScreenInstanceID(params), - ...params - })), - tap(params => { - // TODO perhaps I can be smarter about this, bubble events up to the nearest outlet? - if (params[OUTLET_KEY] == NG_PRIMARY_OUTLET) { - // TODO do we want to track the firebase_ attributes? - analytics.setCurrentScreen(params[SCREEN_NAME_KEY]); - analytics.updateConfig({ - [PAGE_PATH_KEY]: params[PAGE_PATH_KEY], - [SCREEN_CLASS_KEY]: params[SCREEN_CLASS_KEY] - }); - if (title) { analytics.updateConfig({ [PAGE_TITLE_KEY]: params[PAGE_TITLE_KEY] }) } - } - }), - groupBy(params => params[OUTLET_KEY]), - mergeMap(group => group.pipe(startWith(undefined), pairwise())), - map(([prior, current]) => prior ? { - [PREVIOUS_SCREEN_CLASS_KEY]: prior[SCREEN_CLASS_KEY], - [PREVIOUS_SCREEN_NAME_KEY]: prior[SCREEN_NAME_KEY], - [PREVIOUS_SCREEN_INSTANCE_ID_KEY]: prior[SCREEN_INSTANCE_ID_KEY], - ...current! - } : current!), - tap(params => analytics.logEvent(SCREEN_VIEW_EVENT, params)), - runOutsideAngular(zone) - ).subscribe(); + if (!router || !isPlatformBrowser(platformId)) { return this } + zone.runOutsideAngular(() => { + const activationEndEvents = router.events.pipe(filter(e => e instanceof ActivationEnd)); + const navigationEndEvents = router.events.pipe(filter(e => e instanceof NavigationEnd)); + this.disposable = navigationEndEvents.pipe( + withLatestFrom(activationEndEvents), + switchMap(([navigationEnd, activationEnd]) => { + // SEMVER: start using optional chains and nullish coalescing once we support newer typescript + const page_path = navigationEnd.url; + const screen_name = activationEnd.snapshot.routeConfig && activationEnd.snapshot.routeConfig.path || page_path; + const params = { + [SCREEN_NAME_KEY]: screen_name, + [PAGE_PATH_KEY]: page_path, + [FIREBASE_EVENT_ORIGIN_KEY]: EVENT_ORIGIN_AUTO, + [FIREBASE_SCREEN_NAME_KEY]: screen_name, + [OUTLET_KEY]: activationEnd.snapshot.outlet + }; + if (title) { + params[PAGE_TITLE_KEY] = title.getTitle() + } + const component = activationEnd.snapshot.component; + const routeConfig = activationEnd.snapshot.routeConfig; + const loadChildren = routeConfig && routeConfig.loadChildren; + // TODO figure out how to handle minification + if (typeof loadChildren === "string") { + // SEMVER: this is the older lazy load style "./path#ClassName", drop this when we drop old ng + // TODO is it worth seeing if I can look up the component factory selector from the module name? + // it's lazy so it's not registered with componentFactoryResolver yet... seems a pain for a depreciated style + return of({...params, [SCREEN_CLASS_KEY]: loadChildren.split('#')[1]}); + } else if (typeof component === 'string') { + // TODO figure out when this would this be a string + return of({...params, [SCREEN_CLASS_KEY]: component }); + } else if (component) { + const componentFactory = componentFactoryResolver.resolveComponentFactory(component); + return of({...params, [SCREEN_CLASS_KEY]: componentFactory.selector }); + } else if (loadChildren) { + const loadedChildren = loadChildren(); + var loadedChildren$: Observable; + // TODO clean up this handling... + // can componentFactorymoduleType take an ngmodulefactory or should i pass moduletype? + try { loadedChildren$ = from(zone.runOutsideAngular(() => loadedChildren as any)) } catch(_) { loadedChildren$ = of(loadedChildren as any) } + return loadedChildren$.pipe(map(child => { + const componentFactory = componentFactoryResolver.resolveComponentFactory(child); + return {...params, [SCREEN_CLASS_KEY]: componentFactory.selector }; + })); + } else { + // TODO figure out what forms of router events I might be missing + return of({...params, [SCREEN_CLASS_KEY]: DEFAULT_SCREEN_CLASS}); + } + }), + map(params => ({ + [FIREBASE_SCREEN_CLASS_KEY]: params[SCREEN_CLASS_KEY], + [FIREBASE_SCREEN_INSTANCE_ID_KEY]: getScreenInstanceID(params), + ...params + })), + tap(params => { + // TODO perhaps I can be smarter about this, bubble events up to the nearest outlet? + if (params[OUTLET_KEY] == NG_PRIMARY_OUTLET) { + analytics.setCurrentScreen(params[SCREEN_NAME_KEY]); + analytics.updateConfig({ + [PAGE_PATH_KEY]: params[PAGE_PATH_KEY], + [SCREEN_CLASS_KEY]: params[SCREEN_CLASS_KEY] + }); + if (title) { + analytics.updateConfig({ [PAGE_TITLE_KEY]: params[PAGE_TITLE_KEY] }) + } + } + }), + groupBy(params => params[OUTLET_KEY]), + mergeMap(group => group.pipe(startWith(undefined), pairwise())), + map(([prior, current]) => prior ? { + [FIREBASE_PREVIOUS_SCREEN_CLASS_KEY]: prior[SCREEN_CLASS_KEY], + [FIREBASE_PREVIOUS_SCREEN_NAME_KEY]: prior[SCREEN_NAME_KEY], + [FIREBASE_PREVIOUS_SCREEN_INSTANCE_ID_KEY]: prior[FIREBASE_SCREEN_INSTANCE_ID_KEY], + ...current! + } : current!), + tap(params => zone.runOutsideAngular(() => analytics.logEvent(SCREEN_VIEW_EVENT, params))) + ).subscribe(); + }); } ngOnDestroy() { @@ -165,6 +169,4 @@ const getScreenInstanceID = (params:{[key:string]: any}) => { knownScreenInstanceIDs[screenInstanceKey] = ret; return ret; } -} - -const nameOrToString = (it:any): string => it.name || it.toString(); \ No newline at end of file +} \ No newline at end of file diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index 21d3b4cf8..f604a27f6 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -36,7 +36,7 @@ export interface AngularFireAnalytics extends AnalyticsProxy {}; @Injectable() export class AngularFireAnalytics { - private gtag: (...args: any[]) => {}; + private gtag: (...args: any[]) => void; private analyticsInitialized: Promise; async updateConfig(config: {[key:string]: any}) { @@ -51,22 +51,28 @@ export class AngularFireAnalytics { @Optional() @Inject(APP_VERSION) providedAppVersion:string|null, @Optional() @Inject(APP_NAME) providedAppName:string|null, @Optional() @Inject(DEBUG_MODE) debugModeEnabled:boolean|null, - @Inject(PLATFORM_ID) platformId, + @Inject(PLATFORM_ID) platformId:Object, zone: NgZone ) { - if (isPlatformBrowser(platformId)) { - window[DATA_LAYER_NAME] = window[DATA_LAYER_NAME] || []; - this.gtag = window[GTAG_FUNCTION_NAME] || function() { window[DATA_LAYER_NAME].push(arguments) } - this.analyticsInitialized = new Promise(resolve => { + if (!isPlatformBrowser(platformId)) { + // TODO flush out non-browser support + this.analyticsInitialized = Promise.resolve(); + this.gtag = () => {} + // TODO fix the proxy for the server + return this; + } + + window[DATA_LAYER_NAME] = window[DATA_LAYER_NAME] || []; + this.gtag = window[GTAG_FUNCTION_NAME] || function() { window[DATA_LAYER_NAME].push(arguments) } + this.analyticsInitialized = zone.runOutsideAngular(() => + new Promise(resolve => { window[GTAG_FUNCTION_NAME] = (...args: any[]) => { if (args[0] == 'js') { resolve() } this.gtag(...args); } - }); - } else { - this.analyticsInitialized = Promise.reject(); - } + }) + ); if (providedAppName) { this.updateConfig({ [APP_NAME_KEY]: providedAppName }) } if (providedAppVersion) { this.updateConfig({ [APP_VERSION_KEY]: providedAppVersion }) } @@ -86,6 +92,7 @@ export class AngularFireAnalytics { ); return ɵlazySDKProxy(this, analytics, zone); + } } \ No newline at end of file diff --git a/src/core/angularfire2.ts b/src/core/angularfire2.ts index 1bfafeb56..bd7c27210 100644 --- a/src/core/angularfire2.ts +++ b/src/core/angularfire2.ts @@ -84,14 +84,20 @@ export const ɵlazySDKProxy = (klass: any, observable: Observable, zone: Ng get: (_, name) => zone.runOutsideAngular(() => klass[name] || new Proxy(() => observable.toPromise().then(mod => { - const ret = mod[name]; - // TODO move to proper type guards - if (typeof ret == 'function') { - return ret.bind(mod); - } else if (ret && ret.then) { - return ret.then((res:any) => zone.run(() => res)); + if (mod) { + const ret = mod[name]; + // TODO move to proper type guards + if (typeof ret == 'function') { + return ret.bind(mod); + } else if (ret && ret.then) { + return ret.then((res:any) => zone.run(() => res)); + } else { + return zone.run(() => ret); + } } else { - return zone.run(() => ret); + // the module is not available, SSR maybe? + // TODO dig into this deeper, maybe return a never resolving promise? + return () => {}; } }), { get: (self, name) => self()[name], diff --git a/src/remote-config/remote-config.module.ts b/src/remote-config/remote-config.module.ts index 46a3075c8..0c8392d72 100644 --- a/src/remote-config/remote-config.module.ts +++ b/src/remote-config/remote-config.module.ts @@ -1,4 +1,7 @@ import { NgModule } from '@angular/core'; +import { AngularFireRemoteConfig } from './remote-config'; -@NgModule() +@NgModule({ + providers: [AngularFireRemoteConfig] +}) export class AngularFireRemoteConfigModule { } diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index f6a94d3fc..a5dab66bf 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -1,6 +1,6 @@ -import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; -import { Observable, concat, of, empty } from 'rxjs'; -import { map, switchMap, tap, shareReplay, distinctUntilChanged } from 'rxjs/operators'; +import { Injectable, Inject, Optional, NgZone, InjectionToken, PLATFORM_ID } from '@angular/core'; +import { Observable, concat, of, empty, pipe, OperatorFunction } from 'rxjs'; +import { map, switchMap, tap, shareReplay, distinctUntilChanged, filter, groupBy, mergeMap } from 'rxjs/operators'; import { FirebaseAppConfig, FirebaseOptions, ɵlazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; import { remoteConfig } from 'firebase/app'; @@ -10,7 +10,7 @@ export const REMOTE_CONFIG_SETTINGS = new InjectionToken( export const DEFAULT_CONFIG = new InjectionToken('angularfire2.remoteConfig.defaultConfig'); import { FirebaseRemoteConfig, _firebaseAppFactory, runOutsideAngular } from '@angular/fire'; -import { AngularFireRemoteConfigModule } from './remote-config.module'; +import { isPlatformServer } from '@angular/common'; // SEMVER: once we move to Typescript 3.6 use `PromiseProxy` rather than hardcoding type RemoteConfigProxy = { @@ -44,116 +44,148 @@ export class Value implements remoteConfig.Value { } // SEMVER use ConstructorParameters when we can support Typescript 3.6 -export class KeyedValue extends Value { - constructor(private key: string, source: remoteConfig.ValueSource, value: string) { +export class Parameter extends Value { + constructor(public key: string, public fetchTimeMillis: number, source: remoteConfig.ValueSource, value: string) { super(source, value); } } -@Injectable({ - providedIn: AngularFireRemoteConfigModule -}) -export class AngularFireRemoteConfig { +type Filter = T extends {[key:string]: M} ? + OperatorFunction : + OperatorFunction; + +const filterKey = (attribute: any, test: (param:any) => boolean) => pipe( + map((value:Parameter | Record) => { + const param = value[attribute]; + if (param) { + if (test(param)) { + return value; + } else { + return undefined; + } + } else { + const filtered = Object.keys(value).reduce((c, k) => { + if (test(value[k][attribute])) { + return {...c, [k]: value[k]}; + } else { + return c; + } + }, {}); + return Object.keys(filtered).length > 0 ? filtered : undefined + } + }), + filter(a => !!a) +) as any; // TODO figure out the typing here + +export const filterStatic = (): Filter 'static'}> => filterKey('_source', s => s === 'static'); +export const filterRemote = (): Filter 'remote'}> => filterKey('_source', s => s === 'remote'); +export const filterDefault = (): Filter 'default'}> => filterKey('_source', s => s === 'default'); + +const DEFAULT_INTERVAL = 60 * 60 * 1000; // 1 hour +export const filterFresh = (howRecentInMillis: number = DEFAULT_INTERVAL): OperatorFunction => filterKey('fetchTimeMillis', f => f + howRecentInMillis >= new Date().getTime()); - private default$: Observable<{[key:string]: remoteConfig.Value}>; +@Injectable() +export class AngularFireRemoteConfig { - readonly changes: Observable<{key:string} & remoteConfig.Value>; - readonly values: Observable<{[key:string]: remoteConfig.Value}> & {[key:string]: Observable}; - readonly numbers: Observable<{[key:string]: number}> & {[key:string]: Observable}; - readonly booleans: Observable<{[key:string]: boolean}> & {[key:string]: Observable}; - readonly strings: Observable<{[key:string]: string}> & {[key:string]: Observable}; + readonly changes: Observable; + readonly values: Observable> & Record>; + readonly numbers: Observable> & Record>; + readonly booleans: Observable> & Record>; + readonly strings: Observable> & Record>; constructor( @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, @Optional() @Inject(REMOTE_CONFIG_SETTINGS) settings:remoteConfig.Settings|null, @Optional() @Inject(DEFAULT_CONFIG) defaultConfig:DefaultConfig|null, + @Inject(PLATFORM_ID) platformId:Object, private zone: NgZone ) { + let default$: Observable<{[key:string]: remoteConfig.Value}> = of(Object.keys(defaultConfig || {}).reduce( + (c, k) => ({...c, [k]: new Value("default", defaultConfig![k].toString()) }), {} + )); + + let _remoteConfig: remoteConfig.RemoteConfig|undefined = undefined; + const fetchTimeMillis = () => _remoteConfig && _remoteConfig.fetchTimeMillis || -1; + const remoteConfig = of(undefined).pipe( // @ts-ignore zapping in the UMD in the build script - switchMap(() => import('firebase/remote-config')), + switchMap(() => zone.runOutsideAngular(() => import('firebase/remote-config'))), map(() => _firebaseAppFactory(options, zone, nameOrConfig)), // SEMVER no need to cast once we drop older Firebase map(app => app.remoteConfig()), tap(rc => { if (settings) { rc.settings = settings } if (defaultConfig) { rc.defaultConfig = defaultConfig } - this.default$ = empty(); // once the SDK is loaded, we don't need our defaults anylonger + default$ = empty(); // once the SDK is loaded, we don't need our defaults anylonger + _remoteConfig = rc; // hack, keep the state around for easy injection of fetchTimeMillis }), runOutsideAngular(zone), shareReplay(1) ); - const defaultToStartWith = Object.keys(defaultConfig || {}).reduce((c, k) => { - c[k] = new Value("default", defaultConfig![k].toString()); - return c; - }, {} as {[key:string]: remoteConfig.Value}); - - const mapRemoteConfig = (rc: {[key:string]: Value | remoteConfig.Value}) => { - const keys = Object.keys(rc); - return keys.reduce((c, key, index) => { - const value = rc[key]; - c[index] = new KeyedValue(key, value.getSource(), value.asString()); - return c; - }, new Array(keys.length)); - } - - const proxy: AngularFireRemoteConfig = ɵlazySDKProxy(this, remoteConfig, zone); - - this.default$ = of(defaultToStartWith); - const existing = of(undefined).pipe( - switchMap(() => proxy.activate()), - switchMap(() => proxy.getAll()) + switchMap(() => remoteConfig), + switchMap(rc => rc.activate().then(() => rc.getAll())) ); let fresh = of(undefined).pipe( - switchMap(() => proxy.fetchAndActivate()), - switchMap(() => proxy.getAll()) + switchMap(() => remoteConfig), + switchMap(rc => zone.runOutsideAngular(() => rc.fetchAndActivate().then(() => rc.getAll()))) + ); + + const all = concat(default$, existing, fresh).pipe( + distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), + map(all => Object.keys(all).reduce((c, k) => ({...c, [k]: new Parameter(k, fetchTimeMillis(), all[k].getSource(), all[k].asString())}), {} as Record)), + shareReplay({ bufferSize: 1, refCount: true }) ); - this.values = new Proxy(concat(this.default$, existing, fresh), { - get: (self, name:string) => self[name] || self.pipe( + this.changes = all.pipe( + map(all => Object.values(all)), + switchMap(params => of(...params)), + groupBy(param => param.key), + mergeMap(group => group.pipe( + distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b)) + )) + ); + + this.values = new Proxy(all, { + get: (self, name:string) => self[name] || all.pipe( map(rc => rc[name] ? rc[name] : undefined), distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b)) ) - }) as any; // TODO figure out the types here - - this.changes = this.values.pipe( - switchMap(all => of(...mapRemoteConfig(all))) - ) as any; // TODO figure out the types here + }) as any; // TODO types - const allAs = (type: 'String'|'Boolean'|'Number') => this.values.pipe( - map(all => Object.keys(all).reduce((c, k) => { - c[k] = all[k][`as${type}`](); - return c; - }, {})) - ) as any; // TODO figure out the types here + // TODO change the any, once i figure out how to type the proxies better + const allAs = (type: 'Number'|'Boolean'|'String') => all.pipe( + map(all => Object.values(all).reduce((c, p) => ({...c, [p.key]: p[`as${type}`]()}), {})), + distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b)) + ) as any; this.strings = new Proxy(allAs('String'), { - get: (self, name:string) => self[name] || this.values.pipe( + get: (self, name:string) => self[name] || all.pipe( map(rc => rc[name] ? rc[name].asString() : undefined), distinctUntilChanged() ) }); this.booleans = new Proxy(allAs('Boolean'), { - get: (self, name:string) => self[name] || this.values.pipe( + get: (self, name:string) => self[name] || all.pipe( map(rc => rc[name] ? rc[name].asBoolean() : false), distinctUntilChanged() ) }); this.numbers = new Proxy(allAs('Number'), { - get: (self, name:string) => self[name] || this.values.pipe( + get: (self, name:string) => self[name] || all.pipe( map(rc => rc[name] ? rc[name].asNumber() : 0), distinctUntilChanged() ) }); - return proxy; + // TODO fix the proxy for server + return isPlatformServer(platformId) ? this : ɵlazySDKProxy(this, remoteConfig, zone); } } diff --git a/tools/build.js b/tools/build.js index 87237bbc5..59e06259e 100644 --- a/tools/build.js +++ b/tools/build.js @@ -299,10 +299,10 @@ function copySchematicFiles() { } function replaceDynamicImportsForUMD() { - writeFileSync('./dist/packages-dist/bundles/performance.umd.js', readFileSync('./dist/packages-dist/bundles/performance.umd.js', 'utf8').replace("rxjs.from(import('firebase/performance'))", "rxjs.empty()")); + writeFileSync('./dist/packages-dist/bundles/performance.umd.js', readFileSync('./dist/packages-dist/bundles/performance.umd.js', 'utf8').replace("return import('firebase/performance');", "return rxjs.empty();")); writeFileSync('./dist/packages-dist/bundles/messaging.umd.js', readFileSync('./dist/packages-dist/bundles/messaging.umd.js', 'utf8').replace("rxjs.from(import('firebase/messaging'))", "rxjs.empty()")); - writeFileSync('./dist/packages-dist/bundles/remote-config.umd.js', readFileSync('./dist/packages-dist/bundles/remote-config.umd.js', 'utf8').replace("rxjs.from(import('firebase/remote-config'))", "rxjs.empty()")); - writeFileSync('./dist/packages-dist/bundles/analytics.umd.js', readFileSync('./dist/packages-dist/bundles/analytics.umd.js', 'utf8').replace("rxjs.from(import('firebase/analytics'))", "rxjs.empty()")); + writeFileSync('./dist/packages-dist/bundles/remote-config.umd.js', readFileSync('./dist/packages-dist/bundles/remote-config.umd.js', 'utf8').replace("return import('firebase/remote-config');", "return rxjs.empty();")); + writeFileSync('./dist/packages-dist/bundles/analytics.umd.js', readFileSync('./dist/packages-dist/bundles/analytics.umd.js', 'utf8').replace("return import('firebase/analytics');", "return rxjs.empty();")); } function measure(module) { From e2d83c87669152b49abd8e063fce6b707d1b3dc8 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Sun, 15 Dec 2019 17:08:09 -0800 Subject: [PATCH 27/37] New RC API --- src/remote-config/public_api.ts | 2 +- src/remote-config/remote-config.ts | 180 ++++++++++++++--------------- 2 files changed, 85 insertions(+), 97 deletions(-) diff --git a/src/remote-config/public_api.ts b/src/remote-config/public_api.ts index f3239f665..af5b96822 100644 --- a/src/remote-config/public_api.ts +++ b/src/remote-config/public_api.ts @@ -1,2 +1,2 @@ export * from './remote-config'; -export * from './remote-config.module'; +export * from './remote-config.module'; \ No newline at end of file diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index a5dab66bf..ce454bebe 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -1,6 +1,6 @@ import { Injectable, Inject, Optional, NgZone, InjectionToken, PLATFORM_ID } from '@angular/core'; -import { Observable, concat, of, empty, pipe, OperatorFunction } from 'rxjs'; -import { map, switchMap, tap, shareReplay, distinctUntilChanged, filter, groupBy, mergeMap } from 'rxjs/operators'; +import { Observable, concat, of, pipe, OperatorFunction, UnaryFunction } from 'rxjs'; +import { map, switchMap, tap, shareReplay, distinctUntilChanged, filter, groupBy, mergeMap, scan, withLatestFrom, startWith } from 'rxjs/operators'; import { FirebaseAppConfig, FirebaseOptions, ɵlazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; import { remoteConfig } from 'firebase/app'; @@ -50,48 +50,24 @@ export class Parameter extends Value { } } -type Filter = T extends {[key:string]: M} ? - OperatorFunction : - OperatorFunction; - -const filterKey = (attribute: any, test: (param:any) => boolean) => pipe( - map((value:Parameter | Record) => { - const param = value[attribute]; - if (param) { - if (test(param)) { - return value; - } else { - return undefined; - } - } else { - const filtered = Object.keys(value).reduce((c, k) => { - if (test(value[k][attribute])) { - return {...c, [k]: value[k]}; - } else { - return c; - } - }, {}); - return Object.keys(filtered).length > 0 ? filtered : undefined - } - }), - filter(a => !!a) -) as any; // TODO figure out the typing here - -export const filterStatic = (): Filter 'static'}> => filterKey('_source', s => s === 'static'); -export const filterRemote = (): Filter 'remote'}> => filterKey('_source', s => s === 'remote'); -export const filterDefault = (): Filter 'default'}> => filterKey('_source', s => s === 'default'); - -const DEFAULT_INTERVAL = 60 * 60 * 1000; // 1 hour -export const filterFresh = (howRecentInMillis: number = DEFAULT_INTERVAL): OperatorFunction => filterKey('fetchTimeMillis', f => f + howRecentInMillis >= new Date().getTime()); +// If it's a Parameter array, test any, else test the individual Parameter +const filterTest = (fn: (param:Parameter) => boolean) => filter(it => Array.isArray(it) ? it.some(fn) : fn(it)) + +// Allow the user to bypass the default values and wait till they get something from the server, even if it's a cached copy; +// if used in conjuntion with first() it will only fetch RC values from the server if they aren't cached locally +export const filterRemote = () => filterTest(p => p.getSource() === 'remote'); + +// filterFresh allows the developer to effectively set up a maximum cache time +export const filterFresh = (howRecentInMillis: number) => filterTest(p => p.fetchTimeMillis + howRecentInMillis >= new Date().getTime()); @Injectable() export class AngularFireRemoteConfig { - readonly changes: Observable; - readonly values: Observable> & Record>; - readonly numbers: Observable> & Record>; - readonly booleans: Observable> & Record>; - readonly strings: Observable> & Record>; + readonly changes: Observable; + readonly parameters: Observable; + readonly numbers: Observable> & Record>; + readonly booleans: Observable> & Record>; + readonly strings: Observable> & Record>; constructor( @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, @@ -101,15 +77,8 @@ export class AngularFireRemoteConfig { @Inject(PLATFORM_ID) platformId:Object, private zone: NgZone ) { - - let default$: Observable<{[key:string]: remoteConfig.Value}> = of(Object.keys(defaultConfig || {}).reduce( - (c, k) => ({...c, [k]: new Value("default", defaultConfig![k].toString()) }), {} - )); - - let _remoteConfig: remoteConfig.RemoteConfig|undefined = undefined; - const fetchTimeMillis = () => _remoteConfig && _remoteConfig.fetchTimeMillis || -1; - const remoteConfig = of(undefined).pipe( + const remoteConfig$ = of(undefined).pipe( // @ts-ignore zapping in the UMD in the build script switchMap(() => zone.runOutsideAngular(() => import('firebase/remote-config'))), map(() => _firebaseAppFactory(options, zone, nameOrConfig)), @@ -117,75 +86,94 @@ export class AngularFireRemoteConfig { map(app => app.remoteConfig()), tap(rc => { if (settings) { rc.settings = settings } - if (defaultConfig) { rc.defaultConfig = defaultConfig } - default$ = empty(); // once the SDK is loaded, we don't need our defaults anylonger - _remoteConfig = rc; // hack, keep the state around for easy injection of fetchTimeMillis + // FYI we don't load the defaults into remote config, since we have our own implementation + // see the comment on scanToParametersArray }), + startWith(undefined), runOutsideAngular(zone), - shareReplay(1) + shareReplay({ bufferSize: 1, refCount: false }) ); - const existing = of(undefined).pipe( - switchMap(() => remoteConfig), + const loadedRemoteConfig$ = remoteConfig$.pipe( + filter(rc => !!rc) + ); + + let default$: Observable<{[key:string]: remoteConfig.Value}> = of(Object.keys(defaultConfig || {}).reduce( + (c, k) => ({...c, [k]: new Value("default", defaultConfig![k].toString()) }), {} + )); + + const existing$ = loadedRemoteConfig$.pipe( switchMap(rc => rc.activate().then(() => rc.getAll())) ); - let fresh = of(undefined).pipe( - switchMap(() => remoteConfig), + const fresh$ = loadedRemoteConfig$.pipe( switchMap(rc => zone.runOutsideAngular(() => rc.fetchAndActivate().then(() => rc.getAll()))) ); - const all = concat(default$, existing, fresh).pipe( - distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)), - map(all => Object.keys(all).reduce((c, k) => ({...c, [k]: new Parameter(k, fetchTimeMillis(), all[k].getSource(), all[k].asString())}), {} as Record)), + this.parameters = concat(default$, existing$, fresh$).pipe( + scanToParametersArray(remoteConfig$), shareReplay({ bufferSize: 1, refCount: true }) ); - this.changes = all.pipe( - map(all => Object.values(all)), + this.changes = this.parameters.pipe( switchMap(params => of(...params)), groupBy(param => param.key), mergeMap(group => group.pipe( - distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b)) + distinctUntilChanged() )) ); - this.values = new Proxy(all, { - get: (self, name:string) => self[name] || all.pipe( - map(rc => rc[name] ? rc[name] : undefined), - distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b)) - ) - }) as any; // TODO types - - // TODO change the any, once i figure out how to type the proxies better - const allAs = (type: 'Number'|'Boolean'|'String') => all.pipe( - map(all => Object.values(all).reduce((c, p) => ({...c, [p.key]: p[`as${type}`]()}), {})), - distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b)) - ) as any; - - this.strings = new Proxy(allAs('String'), { - get: (self, name:string) => self[name] || all.pipe( - map(rc => rc[name] ? rc[name].asString() : undefined), - distinctUntilChanged() - ) - }); - - this.booleans = new Proxy(allAs('Boolean'), { - get: (self, name:string) => self[name] || all.pipe( - map(rc => rc[name] ? rc[name].asBoolean() : false), - distinctUntilChanged() - ) - }); - - this.numbers = new Proxy(allAs('Number'), { - get: (self, name:string) => self[name] || all.pipe( - map(rc => rc[name] ? rc[name].asNumber() : 0), - distinctUntilChanged() - ) - }); + this.strings = proxyAll(this.parameters, 'asString'); + this.booleans = proxyAll(this.parameters, 'asBoolean'); + this.numbers = proxyAll(this.parameters, 'asNumber'); // TODO fix the proxy for server - return isPlatformServer(platformId) ? this : ɵlazySDKProxy(this, remoteConfig, zone); + return isPlatformServer(platformId) ? this : ɵlazySDKProxy(this, remoteConfig$, zone); } } + +// I ditched loading the defaults into RC and a simple map for scan since we already have our own defaults implementation. +// The idea here being that if they have a default that never loads from the server, they will be able to tell via fetchTimeMillis on the Parameter. +// Also if it doesn't come from the server it won't emit again in .changes, due to the distinctUntilChanged, which we can simplify to === rather than deep comparison +const scanToParametersArray = (remoteConfig: Observable): OperatorFunction, Parameter[]> => pipe( + withLatestFrom(remoteConfig), + scan((existing, [all, rc]) => { + // SEMVER use "new Set" to unique once we're only targeting es6 + // at the scale we expect remote config to be at, we probably won't see a performance hit from this unoptimized uniqueness implementation + // const allKeys = [...new Set([...existing.map(p => p.key), ...Object.keys(all)])]; + const allKeys = [...existing.map(p => p.key), ...Object.keys(all)].filter((v, i, a) => a.indexOf(v) === i); + return allKeys.map(key => { + const updatedValue = all[key]; + return updatedValue ? new Parameter(key, rc ? rc.fetchTimeMillis : -1, updatedValue.getSource(), updatedValue.asString()) + : existing.find(p => p.key === key)! + }); + }, [] as Array) +); + +const PROXY_DEFAULTS = {'asNumber': 0, 'asBoolean': false, 'asString': undefined}; + + +function mapToObject(fn: 'asNumber'): UnaryFunction, Observable>>; +function mapToObject(fn: 'asBoolean'): UnaryFunction, Observable>>; +function mapToObject(fn: 'asString'): UnaryFunction, Observable>>; +function mapToObject(fn: 'asNumber'|'asBoolean'|'asString') { + return pipe( + map((params: Parameter[]) => params.reduce((c, p) => ({...c, [p.key]: p[fn]()}), {} as Record)), + distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b)) + ); +}; + +export const mapAsStrings = () => mapToObject('asString'); +export const mapAsBooleans = () => mapToObject('asBoolean'); +export const mapAsNumbers = () => mapToObject('asNumber'); + +// TODO look into the types here, I don't like the anys +const proxyAll = (observable: Observable, fn: 'asNumber'|'asBoolean'|'asString') => new Proxy( + observable.pipe(mapToObject(fn as any)), { + get: (self, name:string) => self[name] || self.pipe( + map(all => all[name] || PROXY_DEFAULTS[fn]), + distinctUntilChanged() + ) + } +) as any; \ No newline at end of file From 4601932188ad1cb361c9e1957c42da84bde218b8 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Mon, 16 Dec 2019 20:55:16 -0800 Subject: [PATCH 28/37] Mapping to objects and templates, budget pipe --- src/remote-config/remote-config.ts | 90 +++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 26 deletions(-) diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index ce454bebe..6433b1961 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -1,13 +1,13 @@ import { Injectable, Inject, Optional, NgZone, InjectionToken, PLATFORM_ID } from '@angular/core'; -import { Observable, concat, of, pipe, OperatorFunction, UnaryFunction } from 'rxjs'; -import { map, switchMap, tap, shareReplay, distinctUntilChanged, filter, groupBy, mergeMap, scan, withLatestFrom, startWith } from 'rxjs/operators'; +import { Observable, concat, of, pipe, OperatorFunction } from 'rxjs'; +import { map, switchMap, tap, shareReplay, distinctUntilChanged, filter, groupBy, mergeMap, scan, withLatestFrom, startWith, debounceTime } from 'rxjs/operators'; import { FirebaseAppConfig, FirebaseOptions, ɵlazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; import { remoteConfig } from 'firebase/app'; -export interface DefaultConfig {[key:string]: string|number|boolean}; +export interface ConfigTemplate {[key:string]: string|number|boolean}; export const REMOTE_CONFIG_SETTINGS = new InjectionToken('angularfire2.remoteConfig.settings'); -export const DEFAULT_CONFIG = new InjectionToken('angularfire2.remoteConfig.defaultConfig'); +export const DEFAULT_CONFIG = new InjectionToken('angularfire2.remoteConfig.defaultConfig'); import { FirebaseRemoteConfig, _firebaseAppFactory, runOutsideAngular } from '@angular/fire'; import { isPlatformServer } from '@angular/common'; @@ -65,15 +65,15 @@ export class AngularFireRemoteConfig { readonly changes: Observable; readonly parameters: Observable; - readonly numbers: Observable> & Record>; - readonly booleans: Observable> & Record>; - readonly strings: Observable> & Record>; + readonly numbers: Observable<{[key:string]: number}> & {[key:string]: Observable}; + readonly booleans: Observable<{[key:string]: boolean}> & {[key:string]: Observable}; + readonly strings: Observable<{[key:string]: string}> & {[key:string]: Observable}; constructor( @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, @Optional() @Inject(REMOTE_CONFIG_SETTINGS) settings:remoteConfig.Settings|null, - @Optional() @Inject(DEFAULT_CONFIG) defaultConfig:DefaultConfig|null, + @Optional() @Inject(DEFAULT_CONFIG) defaultConfig:ConfigTemplate|null, @Inject(PLATFORM_ID) platformId:Object, private zone: NgZone ) { @@ -123,9 +123,9 @@ export class AngularFireRemoteConfig { )) ); - this.strings = proxyAll(this.parameters, 'asString'); - this.booleans = proxyAll(this.parameters, 'asBoolean'); - this.numbers = proxyAll(this.parameters, 'asNumber'); + this.strings = proxyAll(this.parameters, 'strings'); + this.booleans = proxyAll(this.parameters, 'booleans'); + this.numbers = proxyAll(this.parameters, 'numbers'); // TODO fix the proxy for server return isPlatformServer(platformId) ? this : ɵlazySDKProxy(this, remoteConfig$, zone); @@ -136,7 +136,7 @@ export class AngularFireRemoteConfig { // I ditched loading the defaults into RC and a simple map for scan since we already have our own defaults implementation. // The idea here being that if they have a default that never loads from the server, they will be able to tell via fetchTimeMillis on the Parameter. // Also if it doesn't come from the server it won't emit again in .changes, due to the distinctUntilChanged, which we can simplify to === rather than deep comparison -const scanToParametersArray = (remoteConfig: Observable): OperatorFunction, Parameter[]> => pipe( +const scanToParametersArray = (remoteConfig: Observable): OperatorFunction<{[key:string]: remoteConfig.Value}, Parameter[]> => pipe( withLatestFrom(remoteConfig), scan((existing, [all, rc]) => { // SEMVER use "new Set" to unique once we're only targeting es6 @@ -151,28 +151,66 @@ const scanToParametersArray = (remoteConfig: Observable) ); -const PROXY_DEFAULTS = {'asNumber': 0, 'asBoolean': false, 'asString': undefined}; - +const AS_TO_FN = { 'strings': 'asString', 'numbers': 'asNumber', 'booleans': 'asBoolean' }; +const PROXY_DEFAULTS = { 'numbers': 0, 'booleans': false, 'strings': undefined }; + +export const budget = (interval: number) => (source: Observable) => new Observable(observer => { + let timedOut = false; + // TODO use scheduler task rather than settimeout + const timeout = setTimeout(() => { + observer.complete(); + timedOut = true; + }, interval); + return source.subscribe({ + next(val) { if (!timedOut) { observer.next(val); } }, + error(err) { if (!timedOut) { clearTimeout(timeout); observer.error(err); } }, + complete() { if (!timedOut) { clearTimeout(timeout); observer.complete(); } } + }) + }); + +const typedMethod = (it:any) => { + switch(typeof it) { + case 'string': return 'asString'; + case 'boolean': return 'asBoolean'; + case 'number': return 'asNumber'; + default: return 'asString'; + } +}; -function mapToObject(fn: 'asNumber'): UnaryFunction, Observable>>; -function mapToObject(fn: 'asBoolean'): UnaryFunction, Observable>>; -function mapToObject(fn: 'asString'): UnaryFunction, Observable>>; -function mapToObject(fn: 'asNumber'|'asBoolean'|'asString') { +export function scanToObject(): OperatorFunction; +export function scanToObject(as: 'numbers'): OperatorFunction; +export function scanToObject(as: 'booleans'): OperatorFunction; +export function scanToObject(as: 'strings'): OperatorFunction; +export function scanToObject(template: T): OperatorFunction; +export function scanToObject(as: 'numbers'|'booleans'|'strings'|ConfigTemplate = 'strings') { return pipe( - map((params: Parameter[]) => params.reduce((c, p) => ({...c, [p.key]: p[fn]()}), {} as Record)), + // TODO cleanup + scan((c, p: Parameter) => ({...c, [p.key]: typeof as === 'object' ? p[typedMethod(as[p.key])]() : p[AS_TO_FN[as]]()}), typeof as === 'object' ? as : {} as {[key:string]: number|boolean|string}), + debounceTime(1), + budget(10), distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b)) ); }; -export const mapAsStrings = () => mapToObject('asString'); -export const mapAsBooleans = () => mapToObject('asBoolean'); -export const mapAsNumbers = () => mapToObject('asNumber'); +export function mapToObject(): OperatorFunction; +export function mapToObject(as: 'numbers'): OperatorFunction; +export function mapToObject(as: 'booleans'): OperatorFunction; +export function mapToObject(as: 'strings'): OperatorFunction; +export function mapToObject(template: T): OperatorFunction; +export function mapToObject(as: 'numbers'|'booleans'|'strings'|ConfigTemplate = 'strings') { + return pipe( + // TODO this is getting a little long, cleanup + map((params: Parameter[]) => params.reduce((c, p) => ({...c, [p.key]: typeof as === 'object' ? p[typedMethod(as[p.key])]() : p[AS_TO_FN[as]]()}), typeof as === 'object' ? as : {} as {[key:string]: number|boolean|string})), + distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b)) + ); +}; // TODO look into the types here, I don't like the anys -const proxyAll = (observable: Observable, fn: 'asNumber'|'asBoolean'|'asString') => new Proxy( - observable.pipe(mapToObject(fn as any)), { - get: (self, name:string) => self[name] || self.pipe( - map(all => all[name] || PROXY_DEFAULTS[fn]), +const proxyAll = (observable: Observable, as: 'numbers'|'booleans'|'strings') => new Proxy( + observable.pipe(mapToObject(as as any)), { + get: (self, name:string) => self[name] || observable.pipe( + map(all => all.find(p => p.key === name)), + map(param => param ? param[AS_TO_FN[as]]() : PROXY_DEFAULTS[as]), distinctUntilChanged() ) } From 7fe92ed685e941f3bb4611ede89cd2ad63fecca5 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 17 Dec 2019 09:05:36 -0800 Subject: [PATCH 29/37] More strict types --- src/remote-config/remote-config.ts | 48 +++++++++++++++++++----------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index 6433b1961..9ac678a85 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -1,5 +1,5 @@ import { Injectable, Inject, Optional, NgZone, InjectionToken, PLATFORM_ID } from '@angular/core'; -import { Observable, concat, of, pipe, OperatorFunction } from 'rxjs'; +import { Observable, concat, of, pipe, OperatorFunction, MonoTypeOperatorFunction } from 'rxjs'; import { map, switchMap, tap, shareReplay, distinctUntilChanged, filter, groupBy, mergeMap, scan, withLatestFrom, startWith, debounceTime } from 'rxjs/operators'; import { FirebaseAppConfig, FirebaseOptions, ɵlazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; import { remoteConfig } from 'firebase/app'; @@ -65,9 +65,9 @@ export class AngularFireRemoteConfig { readonly changes: Observable; readonly parameters: Observable; - readonly numbers: Observable<{[key:string]: number}> & {[key:string]: Observable}; - readonly booleans: Observable<{[key:string]: boolean}> & {[key:string]: Observable}; - readonly strings: Observable<{[key:string]: string}> & {[key:string]: Observable}; + readonly numbers: Observable<{[key:string]: number|undefined}> & {[key:string]: Observable}; + readonly booleans: Observable<{[key:string]: boolean|undefined}> & {[key:string]: Observable}; + readonly strings: Observable<{[key:string]: string|undefined}> & {[key:string]: Observable}; constructor( @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, @@ -154,7 +154,7 @@ const scanToParametersArray = (remoteConfig: Observable (source: Observable) => new Observable(observer => { +export const budget = (interval: number): MonoTypeOperatorFunction => (source: Observable) => new Observable(observer => { let timedOut = false; // TODO use scheduler task rather than settimeout const timeout = setTimeout(() => { @@ -177,30 +177,44 @@ const typedMethod = (it:any) => { } }; -export function scanToObject(): OperatorFunction; -export function scanToObject(as: 'numbers'): OperatorFunction; -export function scanToObject(as: 'booleans'): OperatorFunction; -export function scanToObject(as: 'strings'): OperatorFunction; +export function scanToObject(): OperatorFunction; +export function scanToObject(to: 'numbers'): OperatorFunction; +export function scanToObject(to: 'booleans'): OperatorFunction; +export function scanToObject(to: 'strings'): OperatorFunction; export function scanToObject(template: T): OperatorFunction; -export function scanToObject(as: 'numbers'|'booleans'|'strings'|ConfigTemplate = 'strings') { +export function scanToObject(to: 'numbers'|'booleans'|'strings'|T = 'strings') { return pipe( // TODO cleanup - scan((c, p: Parameter) => ({...c, [p.key]: typeof as === 'object' ? p[typedMethod(as[p.key])]() : p[AS_TO_FN[as]]()}), typeof as === 'object' ? as : {} as {[key:string]: number|boolean|string}), + scan( + (c, p: Parameter) => ({...c, [p.key]: typeof to === 'object' ? + p[typedMethod(to[p.key])]() : + p[AS_TO_FN[to]]() }), + typeof to === 'object' ? + to as T & {[key:string]: string|undefined}: + {} as {[key:string]: number|boolean|string} + ), debounceTime(1), budget(10), distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b)) ); }; -export function mapToObject(): OperatorFunction; -export function mapToObject(as: 'numbers'): OperatorFunction; -export function mapToObject(as: 'booleans'): OperatorFunction; -export function mapToObject(as: 'strings'): OperatorFunction; +export function mapToObject(): OperatorFunction; +export function mapToObject(to: 'numbers'): OperatorFunction; +export function mapToObject(to: 'booleans'): OperatorFunction; +export function mapToObject(to: 'strings'): OperatorFunction; export function mapToObject(template: T): OperatorFunction; -export function mapToObject(as: 'numbers'|'booleans'|'strings'|ConfigTemplate = 'strings') { +export function mapToObject(to: 'numbers'|'booleans'|'strings'|T = 'strings') { return pipe( // TODO this is getting a little long, cleanup - map((params: Parameter[]) => params.reduce((c, p) => ({...c, [p.key]: typeof as === 'object' ? p[typedMethod(as[p.key])]() : p[AS_TO_FN[as]]()}), typeof as === 'object' ? as : {} as {[key:string]: number|boolean|string})), + map((params: Parameter[]) => params.reduce( + (c, p) => ({...c, [p.key]: typeof to === 'object' ? + p[typedMethod(to[p.key])]() : + p[AS_TO_FN[to]]() }), + typeof to === 'object' ? + to as T & {[key:string]: string|undefined} : + {} as {[key:string]: number|boolean|string} + )), distinctUntilChanged((a,b) => JSON.stringify(a) === JSON.stringify(b)) ); }; From d47dc3f8bfb093a6cd885226a1b4a79f4dc62120 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Fri, 20 Dec 2019 21:09:44 -0800 Subject: [PATCH 30/37] Fix proxy in Node, get component name for analytics in both JIT and AOT --- src/analytics/analytics.service.ts | 50 ++++++++++++++++++------- src/analytics/analytics.spec.ts | 9 ++--- src/analytics/analytics.ts | 31 +++++++-------- src/core/angularfire2.ts | 33 ++++++++-------- src/remote-config/remote-config.spec.ts | 4 +- 5 files changed, 73 insertions(+), 54 deletions(-) diff --git a/src/analytics/analytics.service.ts b/src/analytics/analytics.service.ts index 50680273f..d31c4f078 100644 --- a/src/analytics/analytics.service.ts +++ b/src/analytics/analytics.service.ts @@ -1,9 +1,9 @@ -import { Injectable, Optional, NgZone, OnDestroy, ComponentFactoryResolver, Inject, PLATFORM_ID } from '@angular/core'; +import { Injectable, Optional, NgZone, OnDestroy, ComponentFactoryResolver, Inject, PLATFORM_ID, Injector, NgModuleFactory } from '@angular/core'; import { Subscription, from, Observable, empty, of } from 'rxjs'; import { filter, withLatestFrom, switchMap, map, tap, pairwise, startWith, groupBy, mergeMap } from 'rxjs/operators'; -import { Router, NavigationEnd, ActivationEnd } from '@angular/router'; +import { Router, NavigationEnd, ActivationEnd, ROUTES } from '@angular/router'; import { runOutsideAngular } from '@angular/fire'; -import { AngularFireAnalytics } from './analytics'; +import { AngularFireAnalytics, DEBUG_MODE } from './analytics'; import { User } from 'firebase/app'; import { Title } from '@angular/platform-browser'; import { isPlatformBrowser } from '@angular/common'; @@ -27,6 +27,8 @@ const DEFAULT_SCREEN_CLASS = '???'; const NG_PRIMARY_OUTLET = 'primary'; const SCREEN_INSTANCE_DELIMITER = '#'; +const ANNOTATIONS = '__annotations__'; + @Injectable() export class ScreenTrackingService implements OnDestroy { @@ -38,7 +40,9 @@ export class ScreenTrackingService implements OnDestroy { @Optional() title:Title, componentFactoryResolver: ComponentFactoryResolver, @Inject(PLATFORM_ID) platformId:Object, - zone: NgZone + @Optional() @Inject(DEBUG_MODE) debugModeEnabled:boolean|null, + zone: NgZone, + injector: Injector ) { if (!router || !isPlatformBrowser(platformId)) { return this } zone.runOutsideAngular(() => { @@ -70,23 +74,40 @@ export class ScreenTrackingService implements OnDestroy { // it's lazy so it's not registered with componentFactoryResolver yet... seems a pain for a depreciated style return of({...params, [SCREEN_CLASS_KEY]: loadChildren.split('#')[1]}); } else if (typeof component === 'string') { - // TODO figure out when this would this be a string return of({...params, [SCREEN_CLASS_KEY]: component }); } else if (component) { const componentFactory = componentFactoryResolver.resolveComponentFactory(component); return of({...params, [SCREEN_CLASS_KEY]: componentFactory.selector }); } else if (loadChildren) { const loadedChildren = loadChildren(); - var loadedChildren$: Observable; - // TODO clean up this handling... - // can componentFactorymoduleType take an ngmodulefactory or should i pass moduletype? - try { loadedChildren$ = from(zone.runOutsideAngular(() => loadedChildren as any)) } catch(_) { loadedChildren$ = of(loadedChildren as any) } - return loadedChildren$.pipe(map(child => { - const componentFactory = componentFactoryResolver.resolveComponentFactory(child); - return {...params, [SCREEN_CLASS_KEY]: componentFactory.selector }; - })); + var loadedChildren$: Observable = (loadedChildren instanceof Observable) ? loadedChildren : from(Promise.resolve(loadedChildren)); + return loadedChildren$.pipe( + map(lazyModule => { + if (lazyModule instanceof NgModuleFactory) { + // AOT create an injector + const moduleRef = lazyModule.create(injector); + // INVESTIGATE is this the right way to get at the matching route? + const routes = moduleRef.injector.get(ROUTES); + const component = routes[0][0].component; // should i just be grabbing 0-0 here? + try { + const componentFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(component!); + return {...params, [SCREEN_CLASS_KEY]: componentFactory.selector}; + } catch(_) { + return {...params, [SCREEN_CLASS_KEY]: DEFAULT_SCREEN_CLASS}; + } + } else { + // JIT look at the annotations + // INVESTIGATE are there public APIs for this stuff? + const declarations = [].concat.apply([], (lazyModule[ANNOTATIONS] || []).map((f:any) => f.declarations)); + const selectors = [].concat.apply([], declarations.map((c:any) => (c[ANNOTATIONS] || []).map((f:any) => f.selector))); + // should I just be grabbing the selector like this or should i match against the route component? + // const routerModule = lazyModule.ngInjectorDef.imports.find(i => !!i.ngModule); + // const route = routerModule.providers[0].find(p => p.provide == ROUTES).useValue[0]; + return {...params, [SCREEN_CLASS_KEY]: selectors[0] || DEFAULT_SCREEN_CLASS}; + } + }) + ); } else { - // TODO figure out what forms of router events I might be missing return of({...params, [SCREEN_CLASS_KEY]: DEFAULT_SCREEN_CLASS}); } }), @@ -116,6 +137,7 @@ export class ScreenTrackingService implements OnDestroy { [FIREBASE_PREVIOUS_SCREEN_INSTANCE_ID_KEY]: prior[FIREBASE_SCREEN_INSTANCE_ID_KEY], ...current! } : current!), + tap(params => debugModeEnabled && console.info(SCREEN_VIEW_EVENT, params)), tap(params => zone.runOutsideAngular(() => analytics.logEvent(SCREEN_VIEW_EVENT, params))) ).subscribe(); }); diff --git a/src/analytics/analytics.spec.ts b/src/analytics/analytics.spec.ts index da486fe61..00dc5c2c5 100644 --- a/src/analytics/analytics.spec.ts +++ b/src/analytics/analytics.spec.ts @@ -1,7 +1,7 @@ import { ReflectiveInjector, Provider } from '@angular/core'; import { TestBed, inject } from '@angular/core/testing'; import { FirebaseApp, FirebaseOptionsToken, AngularFireModule, FirebaseNameOrConfigToken } from '@angular/fire'; -import { AngularFireAnalytics, AngularFireAnalyticsModule, AUTOMATICALLY_SET_CURRENT_SCREEN, AUTOMATICALLY_LOG_SCREEN_VIEWS, ANALYTICS_COLLECTION_ENABLED, AUTOMATICALLY_TRACK_USER_IDENTIFIER, APP_VERSION, APP_NAME } from '@angular/fire/analytics'; +import { AngularFireAnalytics, AngularFireAnalyticsModule, ANALYTICS_COLLECTION_ENABLED, APP_VERSION, APP_NAME } from '@angular/fire/analytics'; import { COMMON_CONFIG } from './test-config'; @@ -32,7 +32,7 @@ describe('AngularFireAnalytics', () => { }); it('should have the Firebase Functions instance', () => { - expect(analytics.analytics).toBeDefined(); + expect(analytics.app).toBeDefined(); }); }); @@ -52,10 +52,7 @@ describe('AngularFireAnalytics with different app', () => { providers: [ { provide: FirebaseNameOrConfigToken, useValue: FIREBASE_APP_NAME_TOO }, { provide: FirebaseOptionsToken, useValue: COMMON_CONFIG }, - { provide: AUTOMATICALLY_SET_CURRENT_SCREEN, useValue: true } - { provide: AUTOMATICALLY_LOG_SCREEN_VIEWS, useValue: true }, { provide: ANALYTICS_COLLECTION_ENABLED, useValue: true }, - { provide: AUTOMATICALLY_TRACK_USER_IDENTIFIER, useValue: true }, { provide: APP_VERSION, useValue: '0.0' }, { provide: APP_NAME, useValue: 'Test App!' } ] @@ -78,7 +75,7 @@ describe('AngularFireAnalytics with different app', () => { }); it('should have the Firebase Functions instance', () => { - expect(analytics.analytics).toBeDefined(); + expect(analytics.app).toBeDefined(); }); }); diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index f604a27f6..6bb97ae86 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -55,24 +55,25 @@ export class AngularFireAnalytics { zone: NgZone ) { - if (!isPlatformBrowser(platformId)) { - // TODO flush out non-browser support + if (isPlatformBrowser(platformId)) { + + window[DATA_LAYER_NAME] = window[DATA_LAYER_NAME] || []; + this.gtag = window[GTAG_FUNCTION_NAME] || function() { window[DATA_LAYER_NAME].push(arguments) } + this.analyticsInitialized = zone.runOutsideAngular(() => + new Promise(resolve => { + window[GTAG_FUNCTION_NAME] = (...args: any[]) => { + if (args[0] == 'js') { resolve() } + this.gtag(...args); + } + }) + ); + + } else { + this.analyticsInitialized = Promise.resolve(); this.gtag = () => {} - // TODO fix the proxy for the server - return this; - } - window[DATA_LAYER_NAME] = window[DATA_LAYER_NAME] || []; - this.gtag = window[GTAG_FUNCTION_NAME] || function() { window[DATA_LAYER_NAME].push(arguments) } - this.analyticsInitialized = zone.runOutsideAngular(() => - new Promise(resolve => { - window[GTAG_FUNCTION_NAME] = (...args: any[]) => { - if (args[0] == 'js') { resolve() } - this.gtag(...args); - } - }) - ); + } if (providedAppName) { this.updateConfig({ [APP_NAME_KEY]: providedAppName }) } if (providedAppVersion) { this.updateConfig({ [APP_VERSION_KEY]: providedAppVersion }) } diff --git a/src/core/angularfire2.ts b/src/core/angularfire2.ts index bd7c27210..1b7241b88 100644 --- a/src/core/angularfire2.ts +++ b/src/core/angularfire2.ts @@ -84,25 +84,24 @@ export const ɵlazySDKProxy = (klass: any, observable: Observable, zone: Ng get: (_, name) => zone.runOutsideAngular(() => klass[name] || new Proxy(() => observable.toPromise().then(mod => { - if (mod) { - const ret = mod[name]; - // TODO move to proper type guards - if (typeof ret == 'function') { - return ret.bind(mod); - } else if (ret && ret.then) { - return ret.then((res:any) => zone.run(() => res)); - } else { - return zone.run(() => ret); - } + const ret = mod && mod[name]; + // TODO move to proper type guards + if (typeof ret == 'function') { + return ret.bind(mod); + } else if (ret && ret.then) { + return ret.then((res:any) => zone.run(() => res)); } else { - // the module is not available, SSR maybe? - // TODO dig into this deeper, maybe return a never resolving promise? - return () => {}; + return zone.run(() => ret); } }), { - get: (self, name) => self()[name], - // TODO handle callbacks - apply: (self, _, args) => self().then(it => it(...args)) - }) + get: (self, name) => { + const ret = self(); + // TODO proxy so we can have apply? + return ret && ret[name] || (() => {}); + }, + // TODO handle callbacks as transparently as I can + apply: (self, _, args) => self().then(it => it && it(...args)) + } + ) ) }); \ No newline at end of file diff --git a/src/remote-config/remote-config.spec.ts b/src/remote-config/remote-config.spec.ts index a7023d5a7..dd59d7a98 100644 --- a/src/remote-config/remote-config.spec.ts +++ b/src/remote-config/remote-config.spec.ts @@ -31,7 +31,7 @@ describe('AngularFireRemoteConfig', () => { }); it('should have the Firebase Functions instance', () => { - expect(rc.remoteConfig).toBeDefined(); + expect(rc.getValue).toBeDefined(); }); }); @@ -73,7 +73,7 @@ describe('AngularFireRemoteConfig with different app', () => { }); it('should have the Firebase Functions instance', () => { - expect(rc.remoteConfig).toBeDefined(); + expect(rc.getValue).toBeDefined(); }); }); From 05c840bb5d6a8f0c3f82b279eaa40242d0a2f4ff Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 7 Jan 2020 10:23:16 -0800 Subject: [PATCH 31/37] Cleaning things up, beyond docs ready for release --- src/analytics/analytics.service.ts | 28 +++++++++++-------- src/analytics/analytics.ts | 2 +- src/core/angularfire2.ts | 45 +++++++++++++++++++----------- src/remote-config/remote-config.ts | 4 +-- 4 files changed, 48 insertions(+), 31 deletions(-) diff --git a/src/analytics/analytics.service.ts b/src/analytics/analytics.service.ts index d31c4f078..9f37da3fe 100644 --- a/src/analytics/analytics.service.ts +++ b/src/analytics/analytics.service.ts @@ -1,12 +1,12 @@ import { Injectable, Optional, NgZone, OnDestroy, ComponentFactoryResolver, Inject, PLATFORM_ID, Injector, NgModuleFactory } from '@angular/core'; -import { Subscription, from, Observable, empty, of } from 'rxjs'; +import { Subscription, from, Observable, of } from 'rxjs'; import { filter, withLatestFrom, switchMap, map, tap, pairwise, startWith, groupBy, mergeMap } from 'rxjs/operators'; import { Router, NavigationEnd, ActivationEnd, ROUTES } from '@angular/router'; import { runOutsideAngular } from '@angular/fire'; import { AngularFireAnalytics, DEBUG_MODE } from './analytics'; import { User } from 'firebase/app'; import { Title } from '@angular/platform-browser'; -import { isPlatformBrowser } from '@angular/common'; +import { isPlatformBrowser, isPlatformServer } from '@angular/common'; const FIREBASE_EVENT_ORIGIN_KEY = 'firebase_event_origin'; const FIREBASE_PREVIOUS_SCREEN_CLASS_KEY = 'firebase_previous_class'; @@ -101,7 +101,7 @@ export class ScreenTrackingService implements OnDestroy { const declarations = [].concat.apply([], (lazyModule[ANNOTATIONS] || []).map((f:any) => f.declarations)); const selectors = [].concat.apply([], declarations.map((c:any) => (c[ANNOTATIONS] || []).map((f:any) => f.selector))); // should I just be grabbing the selector like this or should i match against the route component? - // const routerModule = lazyModule.ngInjectorDef.imports.find(i => !!i.ngModule); + // const routerModule = lazyModule.ngInjectorDef.imports.find(i => i.ngModule && ....); // const route = routerModule.providers[0].find(p => p.provide == ROUTES).useValue[0]; return {...params, [SCREEN_CLASS_KEY]: selectors[0] || DEFAULT_SCREEN_CLASS}; } @@ -157,15 +157,21 @@ export class UserTrackingService implements OnDestroy { // TODO a user properties injector constructor( analytics: AngularFireAnalytics, - zone: NgZone + zone: NgZone, + @Inject(PLATFORM_ID) platformId:Object ) { - this.disposable = from(analytics.app).pipe( - // TODO can I hook into auth being loaded... - map(app => app.auth()), - switchMap(auth => auth ? new Observable(auth.onAuthStateChanged.bind(auth)) : empty()), - switchMap(user => analytics.setUserId(user ? user.uid : null!)), - runOutsideAngular(zone) - ).subscribe(); + if (!isPlatformServer(platformId)) { + zone.runOutsideAngular(() => { + // @ts-ignore zap the import in the UMD + this.disposable = from(import('firebase/auth')).pipe( + switchMap(() => analytics.app), + map(app => app.auth()), + switchMap(auth => new Observable(auth.onAuthStateChanged.bind(auth))), + switchMap(user => analytics.setUserId(user ? user.uid : null!)), + runOutsideAngular(zone) + ).subscribe(); + }); + } } ngOnDestroy() { diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index 6bb97ae86..3e7053fa5 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -89,7 +89,7 @@ export class AngularFireAnalytics { if (analyticsCollectionEnabled === false) { analytics.setAnalyticsCollectionEnabled(false) } }), runOutsideAngular(zone), - shareReplay(1) + shareReplay({ bufferSize: 1, refCount: false }), ); return ɵlazySDKProxy(this, analytics, zone); diff --git a/src/core/angularfire2.ts b/src/core/angularfire2.ts index 1b7241b88..e3992375e 100644 --- a/src/core/angularfire2.ts +++ b/src/core/angularfire2.ts @@ -80,10 +80,24 @@ export const runInZone = (zone: NgZone) => (obs$: Observable): Observable< { [K in PromiseReturningFunctionPropertyNames ]: (...args: Parameters) => ReturnType }; */ -export const ɵlazySDKProxy = (klass: any, observable: Observable, zone: NgZone) => new Proxy(klass, { - get: (_, name) => zone.runOutsideAngular(() => - klass[name] || new Proxy(() => - observable.toPromise().then(mod => { +// DEBUG quick debugger function for inline logging that typescript doesn't complain about +// wrote it for debugging the ɵlazySDKProxy, commenting out for now; should consider exposing a +// verbose mode for AngularFire in a future release that uses something like this in multiple places +// usage: () => log('something') || returnValue +// const log = (...args: any[]): false => { console.log(...args); return false } + +// The problem here are things like ngOnDestroy are missing, then triggering the service +// rather than dig too far; I'm capturing these as I go. +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) => { + return new Proxy(klass, { + get: (_, name:string) => zone.runOutsideAngular(() => { + if (klass[name]) { return klass[name] } + if (noopFunctions.includes(name)) { return () => {} } + let promise = observable.toPromise().then(mod => { const ret = mod && mod[name]; // TODO move to proper type guards if (typeof ret == 'function') { @@ -93,15 +107,14 @@ export const ɵlazySDKProxy = (klass: any, observable: Observable, zone: Ng } else { return zone.run(() => ret); } - }), { - get: (self, name) => { - const ret = self(); - // TODO proxy so we can have apply? - return ret && ret[name] || (() => {}); - }, - // TODO handle callbacks as transparently as I can - apply: (self, _, args) => self().then(it => it && it(...args)) - } - ) - ) -}); \ No newline at end of file + }); + // recurse the proxy + return new Proxy(() => undefined, { + get: (_, name) => promise[name], + // TODO handle callbacks as transparently as I can + apply: (self, _, args) => promise.then(it => it && it(...args)) + } + ) + }) + }) +}; \ No newline at end of file diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index 9ac678a85..72878c932 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -10,7 +10,6 @@ export const REMOTE_CONFIG_SETTINGS = new InjectionToken( export const DEFAULT_CONFIG = new InjectionToken('angularfire2.remoteConfig.defaultConfig'); import { FirebaseRemoteConfig, _firebaseAppFactory, runOutsideAngular } from '@angular/fire'; -import { isPlatformServer } from '@angular/common'; // SEMVER: once we move to Typescript 3.6 use `PromiseProxy` rather than hardcoding type RemoteConfigProxy = { @@ -127,8 +126,7 @@ export class AngularFireRemoteConfig { this.booleans = proxyAll(this.parameters, 'booleans'); this.numbers = proxyAll(this.parameters, 'numbers'); - // TODO fix the proxy for server - return isPlatformServer(platformId) ? this : ɵlazySDKProxy(this, remoteConfig$, zone); + return ɵlazySDKProxy(this, loadedRemoteConfig$, zone); } } From b46b382cc44d867f21a29de0799d9000cb905075 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 7 Jan 2020 14:14:14 -0800 Subject: [PATCH 32/37] Docs and cleanup --- README.md | 2 +- docs/analytics/getting-started.md | 102 ++++++++++++++++++++++-- docs/remote-config/getting-started.md | 108 ++++++++++++++++++++++++-- src/analytics/analytics.module.ts | 6 +- src/analytics/analytics.ts | 10 ++- src/remote-config/remote-config.ts | 32 +++++--- 6 files changed, 227 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index bb36416e2..6eefa101d 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ Firebase offers two cloud-based, client-accessible database solutions that suppo - [Getting started with Firebase Messaging](docs/messaging/messaging.md) -### **NEW:** Change behavior and appearance of your application without deploying +### **BETA:** Change behavior and appearance of your application without deploying > Firebase Remote Config is a cloud service that lets you change the behavior and appearance of your app without requiring users to download an app update. [Learn more about Remote Config](https://firebase.google.com/docs/remote-config). diff --git a/docs/analytics/getting-started.md b/docs/analytics/getting-started.md index 2cea4c8d5..f0dd0e36e 100644 --- a/docs/analytics/getting-started.md +++ b/docs/analytics/getting-started.md @@ -1,27 +1,113 @@ # Getting started with Google Analytics -### Something, something +`AngularFireAnalytics` dynamic imports the `firebase/analytics` library and provides a promisified version of the [Firebase Analytics SDK (`firebase.analytics.Analytics`)](https://firebase.google.com/docs/reference/js/firebase.analytics.Analytics.html). -TBD +### Usage: -### Putting it all together +```ts +import { AngularFireAnalyticsModule } from '@angular/fire/analytics'; + +@NgModule({ + imports: [ + AngularFireModule.initializeApp(environment.firebase), + AngularFireAnalyticsModule + ] +}) +export class AppModule { } +``` + +`AngularFireAnalyticsModule` will dyanamically import and configure `firebase/analytics`. A `page_view` event will automatically be logged (see `CONFIG` below if you wish to disable this behavior.) + +In your component you can then dependency inject `AngularFireAnalytics` and make calls against the SDK: + +```ts +import { AngularFireAnalytics } from '@angular/fire/analytics'; + +constructor(analytics: AngularFireAnalytics) { + analytics.logEvent('custom_event', { ... }); +} +``` + +## Tracking Screen Views + +You can log [`screen_view` events](https://firebase.google.com/docs/reference/js/firebase.analytics.Analytics.html#parameters_10) yourself of course, but AngularFire provides the `ScreenTrackingService` which automatically integrates with the Angular Router to provide Firebase with screen view tracking. You simply can integrate like so: ```ts +import { AngularFireAnalyticsModule, ScreenTrackingService } from '@angular/fire/analytics'; + +@NgModule({ + imports: [ + AngularFireModule.initializeApp(environment.firebase), + AngularFireAnalyticsModule + ], + providers: [ + ScreenTrackingService + ] +}) +export class AppModule { } +``` + +`AngularFireAnalyticsModule` will initialize `ScreenTrackingService` if it is provided. + +## Tracking User Identifiers + +To enrich your Analytics data you can track the currently signed in user by setting [`setuserid`](https://firebase.google.com/docs/reference/js/firebase.analytics.Analytics.html#setuserid) and [`setUserProperties`](https://firebase.google.com/docs/reference/js/firebase.analytics.Analytics.html#set-user-properties). AngularFire provides a `UserTrackingService` which will dynamically import `firebase/auth`, monitor for changes in the logged in user, and call `setuserid` for you automatically. + + +```ts +import { AngularFireAnalyticsModule, UserTrackingService } from '@angular/fire/analytics'; + @NgModule({ imports: [ AngularFireModule.initializeApp(environment.firebase), AngularFireAnalyticsModule ], providers: [ - ScreenTrackingService, UserTrackingService ] }) export class AppModule { } ``` +`AngularFireAnalyticsModule` will initialize `UserTrackingService` if it is provided. + +## Configuration with Dependency Injection + +### Configure Google Analtyics with `CONFIG` + +Using the `CONFIG` DI Token (*default: {}*) will allow you to configure Google Analytics. E.g, you could skip sending the initial `page_view` event, anonymize IP addresses, and disallow ads personalization signals for all events like so: + ```ts -constructor(analytics: AngularFireAnalytics) { - analytics.logEvent('custom_event', { ... }); -} -``` \ No newline at end of file +import { AngularFireAnalyticsModule, CONFIG } from '@angular/fire/analytics'; + +@NgModule({ + imports: [ + AngularFireModule.initializeApp(environment.firebase), + AngularFireAnalyticsModule + ], + providers: [ + { provide: CONFIG, useValue: { + send_page_view: false, + allow_ad_personalization_signals: false, + anonymize_ip: true + } } + ] +}) +export class AppModule { } +``` + +See the gtag.js documentation to learn of the different configuration options at your disposal. + +### Use DebugView `DEBUG_MODE` + +To use [DebugView in Analtyics](https://console.firebase.google.com/project/_/analytics/debugview) set `DEBUG_MODE` to `true` (*default: false*). + +### Track deployments with `APP_NAME` and `APP_VERSION` + +If you provide `APP_NAME` and `APP_VERSION` (*default: undefined*) you will be able to [track version adoption](https://console.firebase.google.com/project/_/analytics/latestrelease) of your PWA. + +### Disable analytics collection via `COLLECTION_ENABLED` + +If you set `COLLECTION_ENABLED` (*default: true*) to `false` then analytics collection will be disabled for this app on this device. To opt back in to analytics collection you could then call `setAnalyticsCollectionEnabled(true)`. + +Putting these APIs to use with cookies would allow you to create a flexible analytics collection scheme that would respect your user's desire for privacy. \ No newline at end of file diff --git a/docs/remote-config/getting-started.md b/docs/remote-config/getting-started.md index 633cefe66..23dfdfd9c 100644 --- a/docs/remote-config/getting-started.md +++ b/docs/remote-config/getting-started.md @@ -1,10 +1,63 @@ -# Getting started with Remote Config +

Getting started with Remote Config β

-### Something, something +`AngularFireRemoteConfig` dynamically imports the `firebase/remote-config` library on demand, provides convenience observables, pipes, and a promisified version of the [Firebase Remote Config SDK (`firebase.remoteConfig.RemoteConfig`)](https://firebase.google.com/docs/reference/js/firebase.remoteconfig.RemoteConfig). -TBD +### API: -### Putting it all together +```ts +interface ConfigTemplate {[key:string]: string|number|boolean} + +type Parameter extends remoteConfig.Value { + key: string, + fetchTimeMillis: number +} + +class AngularFireRemoteConfig { + changes: Observable; + parameters: Observable; + numbers: Observable<{[key:string]: number|undefined}> & {[key:string]: Observable}; + booleans: Observable<{[key:string]: boolean|undefined}> & {[key:string]: Observable}; + strings: Observable<{[key:string]: string|undefined}> & {[key:string]: Observable}; + + // from firebase.remoteConfig() proxy: + activate: () => Promise; + ensureInitialized: () => Promise; + fetch: () => Promise; + fetchAndActivate: () => Promise; + getAll: () => Promise<{[key:string]: remoteConfig.Value}>; + getBoolean: (key:string) => Promise; + getNumber: (key:string) => Promise; + getString: (key:string) => Promise; + getValue: (key:string) => Promise; + setLogLevel: (logLevel: remoteConfig.LogLevel) => Promise; + settings: Promise; + defaultConfig: Promise<{[key: string]: string | number | boolean}>; + fetchTimeMillis: Promise; + lastFetchStatus: Promise; +} + +// Pipes for working with .changes and .parameters +filterRemote: () => MonoTypeOperatorFunction +filterFresh: (interval: number) => MonoTypeOperatorFunction +budget: (interval: number) => MonoTypeOperatorFunction + +// scanToObject is for use with .changes +scanToObject: () => OperatorFunction +// mapToObject is the same behavior are scanToObject but for use with .parameters, +mapToObject: () => OperatorFunction +``` + +## Configuration with Dependency Injection + +### Configure Remote Config with `SETTINGS` + +Using the `SETTINGS` DI Token (*default: {}*) will allow you to [configure Firebase Remote Config](https://firebase.google.com/docs/reference/js/firebase.remoteconfig.Settings.html). + +### Configure default values with `DEFAULTS` + +Providing `DEFAULTS ({[key: string]: string | number | boolean})` has `AngularFireRemoteConfig` emit the provided defaults first, which allows you to count on Remote Config when the user is offline or in environments that the Remote Config service does not handle (i.e, Server Side Rendering). + +## Putting it all together: ```ts @NgModule({ @@ -16,15 +69,54 @@ TBD { provide: DEFAULT_CONFIG, useValue: { enableAwesome: true } }, { provide: REMOTE_CONFIG_SETTINGS, - useFactory: () => isDevMode ? { minimumFetchIntervalMillis: 1 } : {} + useFactory: () => isDevMode() ? { minimumFetchIntervalMillis: 10_000 } : {} } ] }) export class AppModule { } -``` -```ts +... + constructor(remoteConfig: AngularFireRemoteConfig) { - remoteConfig.changes.subscribe(changes => …); + remoteConfig.changes.pipe( + filterFresh(172_800_000), // ensure we have values from at least 48 hours ago + first(), + // scanToObject when used this way is similar to defaults + // but most importantly smart-casts remote config values and adds type safety + scanToObject({ + enableAwesome: true, + titleBackgroundColor: 'blue', + titleFontSize: 12 + }) + ).subscribe(…); + + // all remote config values cast as strings + remoteConfig.strings.subscribe(...) + remoteConfig.booleans.subscribe(...); // as booleans + remoteConfig.numbers.subscribe(...); // as numbers + + // convenience for observing a single string + remoteConfig.strings.titleBackgroundColor.subscribe(...); + remoteConfig.booleans.enableAwesome.subscribe(...); // boolean + remoteConfig.numbers.titleBackgroundColor.subscribe(...); // number + + // however those may emit more than once as the remote config cache fires and gets fresh values from the server + // you can filter it out of .changes for more control: + remoteConfig.changes.pipe( + filter(param => param.key === 'titleBackgroundColor'), + map(param => param.asString()) + // budget at most 800ms and return the freshest value possible in that time + // our budget pipe is similar to timeout but won't error or abort the pending server fetch (it won't emit it, if the deadline is exceeded, but it will have been fetched so can use the freshest values on next subscription) + budget(800), + last() + ).subscribe(...) + + // just like .changes, but scanned as into an array + remoteConfig.parameters.subscribe(all => ...); + + // or make promisified firebase().remoteConfig() calls direct off AngularFireRemoteConfig + // using our proxy + remoteConfig.getAll().then(all => ...); + remoteConfig.lastFetchStatus.then(status => ...); } ``` \ No newline at end of file diff --git a/src/analytics/analytics.module.ts b/src/analytics/analytics.module.ts index 5956bbee4..a1e470a6f 100644 --- a/src/analytics/analytics.module.ts +++ b/src/analytics/analytics.module.ts @@ -7,7 +7,11 @@ import { AngularFireAnalytics } from './analytics'; }) export class AngularFireAnalyticsModule { constructor( + analytics: AngularFireAnalytics, @Optional() screenTracking: ScreenTrackingService, @Optional() userTracking: UserTrackingService - ) { } + ) { + // calling anything on analytics will eagerly load the SDK + analytics.app; + } } diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index 3e7053fa5..ac42be224 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -5,11 +5,13 @@ import { map, tap, shareReplay, switchMap } from 'rxjs/operators'; import { FirebaseAppConfig, FirebaseOptions, runOutsideAngular, ɵlazySDKProxy, FirebaseAnalytics, FIREBASE_OPTIONS, FIREBASE_APP_NAME, _firebaseAppFactory } from '@angular/fire'; import { analytics, app } from 'firebase'; -export const ANALYTICS_COLLECTION_ENABLED = new InjectionToken('angularfire2.analytics.analyticsCollectionEnabled'); +export interface Config {[key:string]: any}; +export const COLLECTION_ENABLED = new InjectionToken('angularfire2.analytics.analyticsCollectionEnabled'); export const APP_VERSION = new InjectionToken('angularfire2.analytics.appVersion'); export const APP_NAME = new InjectionToken('angularfire2.analytics.appName'); export const DEBUG_MODE = new InjectionToken('angularfire2.analytics.debugMode'); +export const CONFIG = new InjectionToken('angularfire2.analytics.config'); const APP_NAME_KEY = 'app_name'; const APP_VERSION_KEY = 'app_version'; @@ -39,7 +41,7 @@ export class AngularFireAnalytics { private gtag: (...args: any[]) => void; private analyticsInitialized: Promise; - async updateConfig(config: {[key:string]: any}) { + async updateConfig(config: Config) { await this.analyticsInitialized; this.gtag(GTAG_CONFIG_COMMAND, this.options[ANALYTICS_ID_FIELD], { ...config, update: true }); }; @@ -47,10 +49,11 @@ export class AngularFireAnalytics { constructor( @Inject(FIREBASE_OPTIONS) private options:FirebaseOptions, @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, - @Optional() @Inject(ANALYTICS_COLLECTION_ENABLED) analyticsCollectionEnabled:boolean|null, + @Optional() @Inject(COLLECTION_ENABLED) analyticsCollectionEnabled:boolean|null, @Optional() @Inject(APP_VERSION) providedAppVersion:string|null, @Optional() @Inject(APP_NAME) providedAppName:string|null, @Optional() @Inject(DEBUG_MODE) debugModeEnabled:boolean|null, + @Optional() @Inject(CONFIG) providedConfig:Config|null, @Inject(PLATFORM_ID) platformId:Object, zone: NgZone ) { @@ -75,6 +78,7 @@ export class AngularFireAnalytics { } + if (providedConfig) { this.updateConfig(providedConfig) } if (providedAppName) { this.updateConfig({ [APP_NAME_KEY]: providedAppName }) } if (providedAppVersion) { this.updateConfig({ [APP_VERSION_KEY]: providedAppVersion }) } if (debugModeEnabled) { this.updateConfig({ [DEBUG_MODE_KEY]: 1 }) } diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index 72878c932..648e19c28 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -1,4 +1,4 @@ -import { Injectable, Inject, Optional, NgZone, InjectionToken, PLATFORM_ID } from '@angular/core'; +import { Injectable, Inject, Optional, NgZone, InjectionToken } from '@angular/core'; import { Observable, concat, of, pipe, OperatorFunction, MonoTypeOperatorFunction } from 'rxjs'; import { map, switchMap, tap, shareReplay, distinctUntilChanged, filter, groupBy, mergeMap, scan, withLatestFrom, startWith, debounceTime } from 'rxjs/operators'; import { FirebaseAppConfig, FirebaseOptions, ɵlazySDKProxy, FIREBASE_OPTIONS, FIREBASE_APP_NAME } from '@angular/fire'; @@ -6,8 +6,8 @@ import { remoteConfig } from 'firebase/app'; export interface ConfigTemplate {[key:string]: string|number|boolean}; -export const REMOTE_CONFIG_SETTINGS = new InjectionToken('angularfire2.remoteConfig.settings'); -export const DEFAULT_CONFIG = new InjectionToken('angularfire2.remoteConfig.defaultConfig'); +export const SETTINGS = new InjectionToken('angularfire2.remoteConfig.settings'); +export const DEFAULTS = new InjectionToken('angularfire2.remoteConfig.defaultConfig'); import { FirebaseRemoteConfig, _firebaseAppFactory, runOutsideAngular } from '@angular/fire'; @@ -71,9 +71,8 @@ export class AngularFireRemoteConfig { constructor( @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, - @Optional() @Inject(REMOTE_CONFIG_SETTINGS) settings:remoteConfig.Settings|null, - @Optional() @Inject(DEFAULT_CONFIG) defaultConfig:ConfigTemplate|null, - @Inject(PLATFORM_ID) platformId:Object, + @Optional() @Inject(SETTINGS) settings:remoteConfig.Settings|null, + @Optional() @Inject(DEFAULTS) defaultConfig:ConfigTemplate|null, private zone: NgZone ) { @@ -85,8 +84,7 @@ export class AngularFireRemoteConfig { map(app => app.remoteConfig()), tap(rc => { if (settings) { rc.settings = settings } - // FYI we don't load the defaults into remote config, since we have our own implementation - // see the comment on scanToParametersArray + if (defaultConfig) { rc.defaultConfig = defaultConfig } }), startWith(undefined), runOutsideAngular(zone), @@ -101,12 +99,22 @@ export class AngularFireRemoteConfig { (c, k) => ({...c, [k]: new Value("default", defaultConfig![k].toString()) }), {} )); + // we should filter out the defaults we provided to RC, since we have our own implementation + // that gives us a -1 for fetchTimeMillis (so filterFresh can filter them out) + const filterOutDefaults = map<{[key: string]: remoteConfig.Value}, {[key: string]: remoteConfig.Value}>(all => + Object.keys(all) + .filter(key => all[key].getSource() != 'default') + .reduce((acc, key) => ({...acc, [key]: all[key]}), {}) + ); + const existing$ = loadedRemoteConfig$.pipe( - switchMap(rc => rc.activate().then(() => rc.getAll())) + switchMap(rc => rc.activate().then(() => rc.getAll())), + filterOutDefaults ); const fresh$ = loadedRemoteConfig$.pipe( - switchMap(rc => zone.runOutsideAngular(() => rc.fetchAndActivate().then(() => rc.getAll()))) + switchMap(rc => zone.runOutsideAngular(() => rc.fetchAndActivate().then(() => rc.getAll()))), + filterOutDefaults ); this.parameters = concat(default$, existing$, fresh$).pipe( @@ -150,7 +158,7 @@ const scanToParametersArray = (remoteConfig: Observable(interval: number): MonoTypeOperatorFunction => (source: Observable) => new Observable(observer => { let timedOut = false; @@ -222,7 +230,7 @@ const proxyAll = (observable: Observable, as: 'numbers'|'booleans'| observable.pipe(mapToObject(as as any)), { get: (self, name:string) => self[name] || observable.pipe( map(all => all.find(p => p.key === name)), - map(param => param ? param[AS_TO_FN[as]]() : PROXY_DEFAULTS[as]), + map(param => param ? param[AS_TO_FN[as]]() : STATIC_VALUES[as]), distinctUntilChanged() ) } From ff65db8584b0c6f92c890ed63cb8462911b0c645 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 7 Jan 2020 14:14:45 -0800 Subject: [PATCH 33/37] Fixing analytics spec --- src/analytics/analytics.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/analytics/analytics.spec.ts b/src/analytics/analytics.spec.ts index 00dc5c2c5..d72115469 100644 --- a/src/analytics/analytics.spec.ts +++ b/src/analytics/analytics.spec.ts @@ -1,7 +1,7 @@ import { ReflectiveInjector, Provider } from '@angular/core'; import { TestBed, inject } from '@angular/core/testing'; import { FirebaseApp, FirebaseOptionsToken, AngularFireModule, FirebaseNameOrConfigToken } from '@angular/fire'; -import { AngularFireAnalytics, AngularFireAnalyticsModule, ANALYTICS_COLLECTION_ENABLED, APP_VERSION, APP_NAME } from '@angular/fire/analytics'; +import { AngularFireAnalytics, AngularFireAnalyticsModule, COLLECTION_ENABLED, APP_VERSION, APP_NAME } from '@angular/fire/analytics'; import { COMMON_CONFIG } from './test-config'; @@ -52,7 +52,7 @@ describe('AngularFireAnalytics with different app', () => { providers: [ { provide: FirebaseNameOrConfigToken, useValue: FIREBASE_APP_NAME_TOO }, { provide: FirebaseOptionsToken, useValue: COMMON_CONFIG }, - { provide: ANALYTICS_COLLECTION_ENABLED, useValue: true }, + { provide: COLLECTION_ENABLED, useValue: true }, { provide: APP_VERSION, useValue: '0.0' }, { provide: APP_NAME, useValue: 'Test App!' } ] From baaeccfdbe61b63ec80c229adc3948bf1f7ba1ac Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 7 Jan 2020 14:26:00 -0800 Subject: [PATCH 34/37] Adding more API to the docs --- docs/analytics/getting-started.md | 24 +++++++++++++++++++++++- docs/remote-config/getting-started.md | 6 ++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/docs/analytics/getting-started.md b/docs/analytics/getting-started.md index f0dd0e36e..8db7a2f9d 100644 --- a/docs/analytics/getting-started.md +++ b/docs/analytics/getting-started.md @@ -1,6 +1,28 @@ # Getting started with Google Analytics -`AngularFireAnalytics` dynamic imports the `firebase/analytics` library and provides a promisified version of the [Firebase Analytics SDK (`firebase.analytics.Analytics`)](https://firebase.google.com/docs/reference/js/firebase.analytics.Analytics.html). +`AngularFireAnalytics` dynamically imports the `firebase/analytics` library and provides a promisified version of the [Firebase Analytics SDK (`firebase.analytics.Analytics`)](https://firebase.google.com/docs/reference/js/firebase.analytics.Analytics.html). + +### API: + +```ts +class AngularFireAnalytics { + updateConfig(options: {[key:string]: any}): Promise; + + // from firebase.analytics() proxy: + logEvent(eventName: string, eventParams?: {[key: string]: any}, options?: analytics.AnalyticsCallOptions): Promise; + setCurrentScreen(screenName: string, options?: analytics.AnalyticsCallOptions): Promise; + setUserId(id: string, options?: analytics.AnalyticsCallOptions): Promise; + setUserProperties(properties: analytics.CustomParams, options?: analytics.AnalyticsCallOptions): Promise; + setAnalyticsCollectionEnabled(enabled: boolean): Promise; + app: Promise; +} + +COLLECTION_ENABLED = InjectionToken; +APP_VERSION = InjectionToken; +APP_NAME = InjectionToken; +DEBUG_MODE = InjectionToken; +CONFIG = InjectionToken; +``` ### Usage: diff --git a/docs/remote-config/getting-started.md b/docs/remote-config/getting-started.md index 23dfdfd9c..3d108db7a 100644 --- a/docs/remote-config/getting-started.md +++ b/docs/remote-config/getting-started.md @@ -5,6 +5,8 @@ ### API: ```ts +class AngularFireRemoteConfigModule { } + interface ConfigTemplate {[key:string]: string|number|boolean} type Parameter extends remoteConfig.Value { @@ -43,8 +45,12 @@ budget: (interval: number) => MonoTypeOperatorFunction // scanToObject is for use with .changes scanToObject: () => OperatorFunction + // mapToObject is the same behavior are scanToObject but for use with .parameters, mapToObject: () => OperatorFunction + +SETTINGS = InjectionToken; +DEFAULTS = InjectionToken; ``` ## Configuration with Dependency Injection From 04a3bb15517417944eb772d1ba2386200b23087e Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 7 Jan 2020 14:35:15 -0800 Subject: [PATCH 35/37] Further simplifications to the DI tokens --- src/database-deprecated/database.ts | 2 +- src/database/database.ts | 2 +- src/firestore/firestore.ts | 4 ++-- src/functions/functions.ts | 12 ++++++++---- src/performance/performance.ts | 2 ++ src/storage/storage.ts | 6 +++--- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/database-deprecated/database.ts b/src/database-deprecated/database.ts index 6ed7d1734..291784e15 100644 --- a/src/database-deprecated/database.ts +++ b/src/database-deprecated/database.ts @@ -39,4 +39,4 @@ export class AngularFireDatabase { } -export { DATABASE_URL }; \ No newline at end of file +export { DATABASE_URL, DATABASE_URL as URL }; \ No newline at end of file diff --git a/src/database/database.ts b/src/database/database.ts index 2889f4fea..e9afb569f 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -58,4 +58,4 @@ export { SnapshotAction } from './interfaces'; -export { RealtimeDatabaseURL, DATABASE_URL }; \ No newline at end of file +export { RealtimeDatabaseURL, DATABASE_URL, DATABASE_URL as URL }; \ No newline at end of file diff --git a/src/firestore/firestore.ts b/src/firestore/firestore.ts index c42fe46a5..e9fea2416 100644 --- a/src/firestore/firestore.ts +++ b/src/firestore/firestore.ts @@ -27,7 +27,7 @@ export const FirestoreSettingsToken = new InjectionToken('angularfire2 export const ENABLE_PERSISTENCE = EnablePersistenceToken; export const PERSISTENCE_SETTINGS = PersistenceSettingsToken -export const FIRESTORE_SETTINGS = FirestoreSettingsToken; +export const SETTINGS = FirestoreSettingsToken; // SEMVER kill this in the next major // timestampsInSnapshots was depreciated in 5.8.0 @@ -124,7 +124,7 @@ export class AngularFirestore { @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, @Optional() @Inject(ENABLE_PERSISTENCE) shouldEnablePersistence: boolean|null, - @Optional() @Inject(FIRESTORE_SETTINGS) settings: Settings|null, + @Optional() @Inject(SETTINGS) settings: Settings|null, @Inject(PLATFORM_ID) platformId: Object, zone: NgZone, @Optional() @Inject(PERSISTENCE_SETTINGS) persistenceSettings: PersistenceSettings|null, diff --git a/src/functions/functions.ts b/src/functions/functions.ts index a90d6f70f..a619f71d8 100644 --- a/src/functions/functions.ts +++ b/src/functions/functions.ts @@ -4,10 +4,14 @@ import { map } from 'rxjs/operators'; import { FirebaseOptions, FirebaseAppConfig, FIREBASE_APP_NAME } from '@angular/fire'; import { FirebaseFunctions, FIREBASE_OPTIONS, _firebaseAppFactory, FirebaseZoneScheduler } from '@angular/fire'; -// SEMVER: @ v6 remove FunctionsRegionToken in favor of FUNCTIONS_REGION +// SEMVER: @ v6 remove FunctionsRegionToken and FUNCTIONS_REGION in favor of REGION export const FunctionsRegionToken = new InjectionToken('angularfire2.functions.region'); -export const FUNCTIONS_ORIGIN = new InjectionToken('angularfire2.functions.origin'); export const FUNCTIONS_REGION = FunctionsRegionToken; +// SEMVER: @ v6 remove FUNCTIONS_ORIGIN in favor of ORIGIN +export const FUNCTIONS_ORIGIN = new InjectionToken('angularfire2.functions.origin'); + +export const ORIGIN = FUNCTIONS_ORIGIN; +export const REGION = FunctionsRegionToken; @Injectable() export class AngularFireFunctions { @@ -24,8 +28,8 @@ export class AngularFireFunctions { @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, @Inject(PLATFORM_ID) platformId: Object, zone: NgZone, - @Optional() @Inject(FUNCTIONS_REGION) region:string|null, - @Optional() @Inject(FUNCTIONS_ORIGIN) origin:string|null + @Optional() @Inject(REGION) region:string|null, + @Optional() @Inject(ORIGIN) origin:string|null ) { this.scheduler = new FirebaseZoneScheduler(zone, platformId); diff --git a/src/performance/performance.ts b/src/performance/performance.ts index 1794600c6..c21f31d79 100644 --- a/src/performance/performance.ts +++ b/src/performance/performance.ts @@ -4,6 +4,7 @@ import { first, tap, map, shareReplay, switchMap } from 'rxjs/operators'; import { performance } from 'firebase/app'; import { FirebaseApp } 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'); export const INSTRUMENTATION_ENABLED = new InjectionToken('angularfire2.performance.instrumentationEnabled'); export const DATA_COLLECTION_ENABLED = new InjectionToken('angularfire2.performance.dataCollectionEnabled'); @@ -46,6 +47,7 @@ export class AngularFirePerformance { if (automaticallyTraceCoreNgMetrics != false) { // TODO determine more built in metrics + // this leaks... appRef.isStable.pipe( first(it => it), this.traceUntilComplete('isStable') diff --git a/src/storage/storage.ts b/src/storage/storage.ts index 0c1d27b7d..10191a5ae 100644 --- a/src/storage/storage.ts +++ b/src/storage/storage.ts @@ -6,9 +6,9 @@ import { FirebaseStorage, FirebaseOptions, FirebaseAppConfig, FirebaseZoneSchedu import { UploadMetadata } from './interfaces'; -// SEMVER drop StorageBucket in favor of STORAGE_BUCKET +// SEMVER drop StorageBucket in favor of BUCKET export const StorageBucket = new InjectionToken('angularfire2.storageBucket'); -export const STORAGE_BUCKET = StorageBucket; +export const BUCKET = StorageBucket; /** * AngularFireStorage Service @@ -25,7 +25,7 @@ export class AngularFireStorage { constructor( @Inject(FIREBASE_OPTIONS) options:FirebaseOptions, @Optional() @Inject(FIREBASE_APP_NAME) nameOrConfig:string|FirebaseAppConfig|null|undefined, - @Optional() @Inject(STORAGE_BUCKET) storageBucket:string|null, + @Optional() @Inject(BUCKET) storageBucket:string|null, @Inject(PLATFORM_ID) platformId: Object, zone: NgZone ) { From c37fcb79df5ee50f202259b3ab3c2eb56b5d3f94 Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 7 Jan 2020 14:42:28 -0800 Subject: [PATCH 36/37] Add Analytics and RemoteConfig to install-and-setup --- docs/install-and-setup.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/install-and-setup.md b/docs/install-and-setup.md index e97d1e023..28027b6aa 100644 --- a/docs/install-and-setup.md +++ b/docs/install-and-setup.md @@ -96,10 +96,12 @@ export class AppModule {} After adding the AngularFireModule you also need to add modules for the individual @NgModules that your application needs. + - `AngularFireAnalytics` - `AngularFireAuthModule` - `AngularFireDatabaseModule` - `AngularFireFunctionsModule` - `AngularFirestoreModule` + - `AngularFireRemoteConfigModule` - `AngularFireStorageModule` - `AngularFireMessagingModule` @@ -112,6 +114,7 @@ import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { AngularFireModule } from '@angular/fire'; +import { AngularFireAnalyticsModule } from '@angular/fire/analytics'; import { AngularFirestoreModule } from '@angular/fire/firestore'; import { AngularFireStorageModule } from '@angular/fire/storage'; import { AngularFireAuthModule } from '@angular/fire/auth'; @@ -121,6 +124,7 @@ import { environment } from '../environments/environment'; imports: [ BrowserModule, AngularFireModule.initializeApp(environment.firebase, 'my-app-name'), // imports firebase/app needed for everything + AngularFireAnalyticsModule, // dynamically imports firebase/analytics AngularFirestoreModule, // imports firebase/firestore, only needed for database features AngularFireAuthModule, // imports firebase/auth, only needed for auth features, AngularFireStorageModule // imports firebase/storage only needed for storage features From 6f114c60a55c8128163e6ac3f5279f6b6d484b5f Mon Sep 17 00:00:00 2001 From: James Daniels Date: Tue, 7 Jan 2020 14:57:14 -0800 Subject: [PATCH 37/37] Our RC Value implements a Partial, so minors dont break --- tools/build.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/build.js b/tools/build.js index 59e06259e..9a43e3b32 100644 --- a/tools/build.js +++ b/tools/build.js @@ -303,6 +303,8 @@ function replaceDynamicImportsForUMD() { writeFileSync('./dist/packages-dist/bundles/messaging.umd.js', readFileSync('./dist/packages-dist/bundles/messaging.umd.js', 'utf8').replace("rxjs.from(import('firebase/messaging'))", "rxjs.empty()")); writeFileSync('./dist/packages-dist/bundles/remote-config.umd.js', readFileSync('./dist/packages-dist/bundles/remote-config.umd.js', 'utf8').replace("return import('firebase/remote-config');", "return rxjs.empty();")); writeFileSync('./dist/packages-dist/bundles/analytics.umd.js', readFileSync('./dist/packages-dist/bundles/analytics.umd.js', 'utf8').replace("return import('firebase/analytics');", "return rxjs.empty();")); + // TODO move into own step + writeFileSync('./dist/packages-dist/remote-config/remote-config.d.ts', readFileSync('./dist/packages-dist/remote-config/remote-config.d.ts', 'utf8').replace("implements remoteConfig.Value", "implements Partial")); } function measure(module) {