├── src ├── app │ ├── app.html │ ├── main.ts │ ├── app.scss │ ├── app.component.ts │ └── app.module.ts ├── pages │ ├── home │ │ ├── home.scss │ │ ├── home.html │ │ ├── home.module.ts │ │ └── home.ts │ ├── login │ │ ├── login.scss │ │ ├── login.ts │ │ └── login.html │ ├── users │ │ ├── users.scss │ │ ├── users.module.ts │ │ ├── users.ts │ │ └── users.html │ ├── user-posts │ │ ├── user-posts.scss │ │ ├── user-posts.html │ │ ├── user-posts.module.ts │ │ └── user-posts.ts │ ├── profile-edit │ │ ├── profile-edit.scss │ │ ├── profile-edit.module.ts │ │ ├── profile-edit.html │ │ └── profile-edit.ts │ ├── posts-create │ │ ├── posts-create.scss │ │ ├── posts-create.module.ts │ │ ├── posts-create.html │ │ └── posts-create.ts │ ├── profile │ │ ├── profile.scss │ │ ├── profile.module.ts │ │ ├── profile.ts │ │ └── profile.html │ └── tabs │ │ ├── tabs.html │ │ └── tabs.ts ├── components │ ├── post-feed │ │ ├── post-feed.scss │ │ ├── post-feed.html │ │ └── post-feed.ts │ ├── fcm-topic │ │ ├── fcm-topic.scss │ │ ├── fcm-topic.html │ │ └── fcm-topic.ts │ ├── fcm-handler │ │ ├── fcm-handler.scss │ │ ├── fcm-handler.html │ │ └── fcm-handler.ts │ ├── user-logout │ │ ├── user-logout.scss │ │ ├── user-logout.html │ │ └── user-logout.ts │ ├── image-upload │ │ ├── image-upload.scss │ │ ├── image-upload.html │ │ └── image-upload.ts │ ├── facebook-login │ │ ├── facebook-login.scss │ │ ├── facebook-login.html │ │ └── facebook-login.ts │ ├── anonymous-login │ │ ├── anonymous-login.scss │ │ ├── anonymous-login.html │ │ └── anonymous-login.ts │ ├── user-relationship │ │ ├── user-relationship.scss │ │ ├── user-relationship.html │ │ └── user-relationship.ts │ ├── heart-button │ │ ├── heart-button.scss │ │ ├── heart-button.html │ │ └── heart-button.ts │ └── components.module.ts ├── assets │ ├── imgs │ │ └── logo.png │ └── icon │ │ └── favicon.ico ├── manifest.json ├── service-worker.js ├── providers │ ├── remote-config │ │ └── remote-config.ts │ ├── analytics │ │ └── analytics.ts │ ├── database │ │ └── database.ts │ ├── fcm │ │ └── fcm.ts │ └── auth │ │ └── auth.ts ├── index.html └── theme │ └── variables.scss ├── resources ├── icon.png.md5 ├── splash.png.md5 ├── icon.png ├── splash.png ├── ios │ ├── icon │ │ ├── icon.png │ │ ├── icon-40.png │ │ ├── icon-50.png │ │ ├── icon-60.png │ │ ├── icon-72.png │ │ ├── icon-76.png │ │ ├── icon@2x.png │ │ ├── icon-1024.png │ │ ├── icon-40@2x.png │ │ ├── icon-40@3x.png │ │ ├── icon-50@2x.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-72@2x.png │ │ ├── icon-76@2x.png │ │ ├── icon-small.png │ │ ├── icon-83.5@2x.png │ │ ├── icon-small@2x.png │ │ └── icon-small@3x.png │ └── splash │ │ ├── Default-667h.png │ │ ├── Default-736h.png │ │ ├── Default~iphone.png │ │ ├── Default@2x~iphone.png │ │ ├── Default-Portrait~ipad.png │ │ ├── Default-568h@2x~iphone.png │ │ ├── Default-Landscape-736h.png │ │ ├── Default-Landscape~ipad.png │ │ ├── Default-Portrait@2x~ipad.png │ │ ├── Default-Landscape@2x~ipad.png │ │ ├── Default-Landscape@~ipadpro.png │ │ ├── Default-Portrait@~ipadpro.png │ │ └── Default@2x~universal~anyany.png ├── android │ ├── icon │ │ ├── drawable-hdpi-icon.png │ │ ├── drawable-ldpi-icon.png │ │ ├── drawable-mdpi-icon.png │ │ ├── drawable-xhdpi-icon.png │ │ ├── drawable-xxhdpi-icon.png │ │ └── drawable-xxxhdpi-icon.png │ └── splash │ │ ├── drawable-land-hdpi-screen.png │ │ ├── drawable-land-ldpi-screen.png │ │ ├── drawable-land-mdpi-screen.png │ │ ├── drawable-port-hdpi-screen.png │ │ ├── drawable-port-ldpi-screen.png │ │ ├── drawable-port-mdpi-screen.png │ │ ├── drawable-land-xhdpi-screen.png │ │ ├── drawable-land-xxhdpi-screen.png │ │ ├── drawable-port-xhdpi-screen.png │ │ ├── drawable-port-xxhdpi-screen.png │ │ ├── drawable-land-xxxhdpi-screen.png │ │ └── drawable-port-xxxhdpi-screen.png └── README.md ├── .firebaserc ├── ionic.config.json ├── tslint.json ├── functions ├── lib │ ├── index.js.map │ ├── index.js │ ├── notifications.js.map │ ├── notifications.js │ ├── aggregation.js.map │ └── aggregation.js ├── tsconfig.json ├── src │ ├── index.ts │ ├── notifications.ts │ └── aggregation.ts ├── package.json └── tslint.json ├── firebase.json ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── README.md ├── package.json └── config.xml /src/app/app.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/pages/home/home.scss: -------------------------------------------------------------------------------- 1 | page-home { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /resources/icon.png.md5: -------------------------------------------------------------------------------- 1 | edd9e2f9d2e0649b2234c4139fb22878 -------------------------------------------------------------------------------- /resources/splash.png.md5: -------------------------------------------------------------------------------- 1 | deb265becb3b85487a856480bb38e4ab -------------------------------------------------------------------------------- /src/pages/login/login.scss: -------------------------------------------------------------------------------- 1 | page-login { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/users/users.scss: -------------------------------------------------------------------------------- 1 | page-users { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/components/post-feed/post-feed.scss: -------------------------------------------------------------------------------- 1 | post-feed { 2 | } 3 | -------------------------------------------------------------------------------- /src/components/fcm-topic/fcm-topic.scss: -------------------------------------------------------------------------------- 1 | fcm-topic { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/components/fcm-handler/fcm-handler.scss: -------------------------------------------------------------------------------- 1 | fcm-handler { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/components/user-logout/user-logout.scss: -------------------------------------------------------------------------------- 1 | user-logout { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/user-posts/user-posts.scss: -------------------------------------------------------------------------------- 1 | page-user-posts { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/components/image-upload/image-upload.scss: -------------------------------------------------------------------------------- 1 | image-upload { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/pages/profile-edit/profile-edit.scss: -------------------------------------------------------------------------------- 1 | page-profile-edit { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/components/facebook-login/facebook-login.scss: -------------------------------------------------------------------------------- 1 | facebook-login { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /src/components/anonymous-login/anonymous-login.scss: -------------------------------------------------------------------------------- 1 | anonymous-login { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "firebasics-f9f42" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/user-relationship/user-relationship.scss: -------------------------------------------------------------------------------- 1 | user-relationship { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/icon.png -------------------------------------------------------------------------------- /resources/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/splash.png -------------------------------------------------------------------------------- /src/assets/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/src/assets/imgs/logo.png -------------------------------------------------------------------------------- /resources/ios/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon.png -------------------------------------------------------------------------------- /src/assets/icon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/src/assets/icon/favicon.ico -------------------------------------------------------------------------------- /resources/ios/icon/icon-40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-40.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-50.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-60.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-72.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-76.png -------------------------------------------------------------------------------- /resources/ios/icon/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon@2x.png -------------------------------------------------------------------------------- /src/components/user-logout/user-logout.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /resources/ios/icon/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-1024.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-40@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-40@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-50@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-60@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-60@3x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-72@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-76@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-small.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-83.5@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-small@2x.png -------------------------------------------------------------------------------- /resources/ios/icon/icon-small@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/icon/icon-small@3x.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-667h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/splash/Default-667h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/splash/Default-736h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/splash/Default~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/splash/Default@2x~iphone.png -------------------------------------------------------------------------------- /src/components/fcm-handler/fcm-handler.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{text}} 4 |
5 | -------------------------------------------------------------------------------- /src/components/image-upload/image-upload.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resources/android/icon/drawable-hdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/icon/drawable-hdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-ldpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/icon/drawable-ldpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-mdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/icon/drawable-mdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/icon/drawable-xhdpi-icon.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/splash/Default-Portrait~ipad.png -------------------------------------------------------------------------------- /src/components/heart-button/heart-button.scss: -------------------------------------------------------------------------------- 1 | heart-button { 2 | ion-icon { 3 | color: firebrick; 4 | font-size: 2em; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/posts-create/posts-create.scss: -------------------------------------------------------------------------------- 1 | page-posts-create { 2 | input { 3 | min-height: 15vh; 4 | font-size: 1.5em; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/icon/drawable-xxhdpi-icon.png -------------------------------------------------------------------------------- /resources/android/icon/drawable-xxxhdpi-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/icon/drawable-xxxhdpi-icon.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-568h@2x~iphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/splash/Default-568h@2x~iphone.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape-736h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/splash/Default-Landscape-736h.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/splash/Default-Landscape~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/splash/Default-Portrait@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape@2x~ipad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/splash/Default-Landscape@2x~ipad.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Landscape@~ipadpro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/splash/Default-Landscape@~ipadpro.png -------------------------------------------------------------------------------- /resources/ios/splash/Default-Portrait@~ipadpro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/splash/Default-Portrait@~ipadpro.png -------------------------------------------------------------------------------- /src/pages/profile/profile.scss: -------------------------------------------------------------------------------- 1 | page-profile { 2 | img { 3 | border-radius: 50%; 4 | width: 25vw; 5 | max-width: 128px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/splash/drawable-land-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/splash/drawable-land-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/splash/drawable-land-mdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-hdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/splash/drawable-port-hdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-ldpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/splash/drawable-port-ldpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-mdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/splash/drawable-port-mdpi-screen.png -------------------------------------------------------------------------------- /resources/ios/splash/Default@2x~universal~anyany.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/ios/splash/Default@2x~universal~anyany.png -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebaseStarter", 3 | "app_id": "70c69565", 4 | "type": "ionic-angular", 5 | "integrations": { 6 | "cordova": {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/splash/drawable-land-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/splash/drawable-land-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/splash/drawable-port-xhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/splash/drawable-port-xxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-land-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/splash/drawable-land-xxxhdpi-screen.png -------------------------------------------------------------------------------- /resources/android/splash/drawable-port-xxxhdpi-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codediodeio/ionic-firestarter/HEAD/resources/android/splash/drawable-port-xxxhdpi-screen.png -------------------------------------------------------------------------------- /src/components/facebook-login/facebook-login.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/app/main.ts: -------------------------------------------------------------------------------- 1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 2 | 3 | import { AppModule } from './app.module'; 4 | 5 | platformBrowserDynamic().bootstrapModule(AppModule); 6 | -------------------------------------------------------------------------------- /src/components/anonymous-login/anonymous-login.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-duplicate-variable": true, 4 | "no-unused-variable": [ 5 | true 6 | ] 7 | }, 8 | "rulesDirectory": [ 9 | "node_modules/tslint-eslint-rules/dist/rules" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/home/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Home - Recent Posts 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/pages/user-posts/user-posts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Posts by {{ displayName }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /functions/lib/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAAA,qCAAqC;AACrC,0CAA0C;AAE1C,wBAAwB;AACX,QAAA,mBAAmB,GAAG,GAAG,CAAC,oBAAoB,CAAC;AAC/C,QAAA,eAAe,GAAO,GAAG,CAAC,eAAe,CAAC;AAEvD,yBAAyB;AAEZ,QAAA,oBAAoB,GAAG,MAAM,CAAC,cAAc,CAAC"} -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["es6"], 4 | "module": "commonjs", 5 | "noImplicitReturns": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "target": "es6" 9 | }, 10 | "compileOnSave": true, 11 | "include": [ 12 | "src" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Ionic", 3 | "short_name": "Ionic", 4 | "start_url": "index.html", 5 | "display": "standalone", 6 | "icons": [{ 7 | "src": "assets/imgs/logo.png", 8 | "sizes": "512x512", 9 | "type": "image/png" 10 | }], 11 | "background_color": "#4e8ef7", 12 | "theme_color": "#4e8ef7" 13 | } -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as agg from './aggregation'; 2 | import * as notify from './notifications'; 3 | 4 | // Aggregation Functions 5 | export const updateFollowerCount = agg.updateFollowerCounts; 6 | export const updatePostCount = agg.updatePostCount; 7 | 8 | // Notification Functions 9 | 10 | export const unicornNotifications = notify.newUnicornPost; -------------------------------------------------------------------------------- /src/pages/tabs/tabs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/heart-button/heart-button.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | {{ heartCount }} 17 | 18 | -------------------------------------------------------------------------------- /src/pages/profile-edit/profile-edit.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { ProfileEditPage } from './profile-edit'; 4 | 5 | @NgModule({ 6 | declarations: [ 7 | ProfileEditPage, 8 | ], 9 | imports: [ 10 | IonicPageModule.forChild(ProfileEditPage), 11 | ], 12 | }) 13 | export class ProfileEditPageModule {} 14 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": { 3 | "predeploy": "npm --prefix functions run build", 4 | "source": "functions" 5 | }, 6 | "hosting": { 7 | "public": "www", 8 | "ignore": [ 9 | "firebase.json", 10 | "**/.*", 11 | "**/node_modules/**" 12 | ], 13 | "rewrites": [ 14 | { 15 | "source": "**", 16 | "destination": "/index.html" 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent coding styles between different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | 10 | # We recommend you to keep these unchanged 11 | end_of_line = lf 12 | charset = utf-8 13 | trim_trailing_whitespace = true 14 | insert_final_newline = true 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/pages/login/login.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NavController, NavParams } from 'ionic-angular'; 3 | 4 | @Component({ 5 | selector: 'page-login', 6 | templateUrl: 'login.html', 7 | }) 8 | export class LoginPage { 9 | 10 | constructor(public navCtrl: NavController, public navParams: NavParams) { 11 | } 12 | 13 | ionViewDidLoad() { 14 | console.log('ionViewDidLoad LoginPage'); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /functions/lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const agg = require("./aggregation"); 4 | const notify = require("./notifications"); 5 | // Aggregation Functions 6 | exports.updateFollowerCount = agg.updateFollowerCounts; 7 | exports.updatePostCount = agg.updatePostCount; 8 | // Notification Functions 9 | exports.unicornNotifications = notify.newUnicornPost; 10 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /src/pages/home/home.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { HomePage } from './home'; 4 | import { ComponentsModule } from '../../components/components.module'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | HomePage, 9 | ], 10 | imports: [ 11 | IonicPageModule.forChild(HomePage), 12 | ComponentsModule 13 | ], 14 | }) 15 | export class HomePageModule {} 16 | -------------------------------------------------------------------------------- /resources/README.md: -------------------------------------------------------------------------------- 1 | These are Cordova resources. You can replace icon.png and splash.png and run 2 | `ionic cordova resources` to generate custom icons and splash screens for your 3 | app. See `ionic cordova resources --help` for details. 4 | 5 | Cordova reference documentation: 6 | 7 | - Icons: https://cordova.apache.org/docs/en/latest/config_ref/images.html 8 | - Splash Screens: https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-splashscreen/ 9 | -------------------------------------------------------------------------------- /src/pages/users/users.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { UsersPage } from './users'; 4 | import { ComponentsModule } from '../../components/components.module'; 5 | 6 | 7 | @NgModule({ 8 | declarations: [ 9 | UsersPage, 10 | ], 11 | imports: [ 12 | IonicPageModule.forChild(UsersPage), 13 | ComponentsModule 14 | ], 15 | }) 16 | export class UsersPageModule {} 17 | -------------------------------------------------------------------------------- /src/pages/profile/profile.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { ProfilePage } from './profile'; 4 | import { ComponentsModule } from '../../components/components.module'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | ProfilePage, 9 | ], 10 | imports: [ 11 | IonicPageModule.forChild(ProfilePage), 12 | ComponentsModule 13 | ], 14 | }) 15 | export class ProfilePageModule {} 16 | -------------------------------------------------------------------------------- /src/pages/user-posts/user-posts.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { UserPostsPage } from './user-posts'; 4 | import { ComponentsModule } from '../../components/components.module'; 5 | 6 | @NgModule({ 7 | declarations: [ 8 | UserPostsPage, 9 | ], 10 | imports: [ 11 | IonicPageModule.forChild(UserPostsPage), 12 | ComponentsModule 13 | ], 14 | }) 15 | export class UserPostsPageModule {} 16 | -------------------------------------------------------------------------------- /src/pages/posts-create/posts-create.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { IonicPageModule } from 'ionic-angular'; 3 | import { PostsCreatePage } from './posts-create'; 4 | import { ComponentsModule } from '../../components/components.module'; 5 | 6 | 7 | @NgModule({ 8 | declarations: [ 9 | PostsCreatePage, 10 | ], 11 | imports: [ 12 | IonicPageModule.forChild(PostsCreatePage), 13 | ComponentsModule 14 | ], 15 | }) 16 | export class PostsCreatePageModule {} 17 | -------------------------------------------------------------------------------- /src/pages/tabs/tabs.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AuthProvider } from '../../providers/auth/auth'; 3 | 4 | @Component({ 5 | templateUrl: 'tabs.html' 6 | }) 7 | export class TabsPage { 8 | 9 | tab1Root = 'HomePage'; 10 | tab2Root = 'PostsCreatePage'; 11 | tab3Root = 'UsersPage'; 12 | tab4Root = 'ProfilePage'; 13 | 14 | constructor(public auth: AuthProvider) { 15 | 16 | } 17 | 18 | ionViewCanEnter() { 19 | return this.auth.isLoggedIn(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/post-feed/post-feed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ post.content }} 8 | 9 |
10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 |
-------------------------------------------------------------------------------- /src/pages/login/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ionic Firestarter 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Welcome to Ionic Firestarter!

13 | 14 | Thanks for installing me :) I am a starter app designed for developers building mobile apps with Ionic and Firebase. 15 | Login and explore Facebook Auth, Firestore, Push Notifications, Analytics, and more! 16 | 17 |
18 | 19 | 20 | 21 |
22 | -------------------------------------------------------------------------------- /src/components/user-relationship/user-relationship.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 14 | 15 | 16 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/fcm-topic/fcm-topic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | You must be on a native device to subscribe to topic notifications 22 | -------------------------------------------------------------------------------- /src/components/user-logout/user-logout.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AuthProvider } from '../../providers/auth/auth'; 3 | import { NavController } from 'ionic-angular'; 4 | import { LoginPage } from '../../pages/login/login'; 5 | 6 | @Component({ 7 | selector: 'user-logout', 8 | templateUrl: 'user-logout.html' 9 | }) 10 | export class UserLogoutComponent { 11 | 12 | constructor( 13 | public auth: AuthProvider, 14 | public navCtrl: NavController 15 | ) {} 16 | 17 | async logout() { 18 | await this.auth.logout(); 19 | await this.navCtrl.setRoot(LoginPage) 20 | } 21 | 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/profile-edit/profile-edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Edit your Profile 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Display Name 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": false, 5 | "emitDecoratorMetadata": true, 6 | "experimentalDecorators": true, 7 | "lib": [ 8 | "dom", 9 | "es2015" 10 | ], 11 | "module": "es2015", 12 | "moduleResolution": "node", 13 | "sourceMap": true, 14 | "target": "es5" 15 | }, 16 | "include": [ 17 | "src/**/*.ts" 18 | ], 19 | "exclude": [ 20 | "node_modules", 21 | "src/**/*.spec.ts", 22 | "src/**/__tests__/*.ts" 23 | ], 24 | "compileOnSave": false, 25 | "atom": { 26 | "rewriteTsconfig": false 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/facebook-login/facebook-login.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AuthProvider } from '../../providers/auth/auth'; 3 | import { NavController } from 'ionic-angular'; 4 | import { TabsPage } from '../../pages/tabs/tabs'; 5 | 6 | @Component({ 7 | selector: 'facebook-login', 8 | templateUrl: 'facebook-login.html' 9 | }) 10 | 11 | export class FacebookLoginComponent { 12 | 13 | constructor( 14 | public auth: AuthProvider, 15 | public navCtrl: NavController 16 | ) {} 17 | 18 | async login() { 19 | 20 | await this.auth.facebookLogin(); 21 | 22 | await this.navCtrl.setRoot(TabsPage) 23 | 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/components/fcm-topic/fcm-topic.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { FcmProvider } from '../../providers/fcm/fcm'; 3 | import { Platform } from 'ionic-angular'; 4 | 5 | 6 | @Component({ 7 | selector: 'fcm-topic', 8 | templateUrl: 'fcm-topic.html' 9 | }) 10 | export class FcmTopicComponent { 11 | 12 | @Input() user: any; 13 | @Input() topic: string; 14 | 15 | constructor(public fcm: FcmProvider, private platform: Platform) { } 16 | 17 | get isSubscribed() { 18 | return this.user.topics && this.user.topics[this.topic]; 19 | } 20 | 21 | get isSupportedPlatform() { 22 | return this.platform.is('cordova'); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/components/heart-button/heart-button.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { DatabaseProvider } from '../../providers/database/database'; 3 | 4 | 5 | @Component({ 6 | selector: 'heart-button', 7 | templateUrl: 'heart-button.html' 8 | }) 9 | export class HeartButtonComponent { 10 | 11 | @Input() userId: string; 12 | @Input() post: any; 13 | 14 | constructor(public db: DatabaseProvider) {} 15 | 16 | 17 | get heartCount(): number { 18 | return this.post.hearts ? Object.keys(this.post.hearts).length : 0 19 | } 20 | 21 | get isHearted(): boolean { 22 | return !!(this.post.hearts && this.post.hearts[this.userId]) 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "build": "./node_modules/.bin/tslint -p tslint.json && ./node_modules/.bin/tsc", 5 | "serve": "npm run build && firebase serve --only functions", 6 | "shell": "npm run build && firebase experimental:functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "main": "lib/index.js", 12 | "dependencies": { 13 | "firebase-admin": "~5.4.2", 14 | "firebase-functions": "^0.7.1" 15 | }, 16 | "devDependencies": { 17 | "tslint": "^5.8.0", 18 | "typescript": "^2.5.3" 19 | }, 20 | "private": true 21 | } 22 | -------------------------------------------------------------------------------- /functions/lib/notifications.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../src/notifications.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,gDAAgD;AAChD,wCAAwC;AAG3B,QAAA,cAAc,GAAG,SAAS,CAAC,SAAS;KAE5C,QAAQ,CAAC,gBAAgB,CAAC;KAC1B,QAAQ,CAAC,CAAM,KAAK,EAAC,EAAE;IAEpB,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IAE/B,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IAErE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;QACb,MAAM,CAAC,IAAI,CAAC;IAChB,CAAC;IAED,uBAAuB;IACvB,MAAM,OAAO,GAAG;QACZ,YAAY,EAAE;YACV,KAAK,EAAE,yBAAyB;YAChC,IAAI,EAAE,+BAA+B;YACrC,IAAI,EAAE,uBAAuB;SAChC;KACJ,CAAA;IAED,MAAM,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC,WAAW,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;AAEjE,CAAC,CAAA,CAAC,CAAC"} -------------------------------------------------------------------------------- /src/pages/user-posts/user-posts.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IonicPage, NavController, NavParams } from 'ionic-angular'; 3 | // import { DatabaseProvider, Post } from '../../providers/database/database'; 4 | 5 | @IonicPage() 6 | @Component({ 7 | selector: 'page-user-posts', 8 | templateUrl: 'user-posts.html', 9 | }) 10 | export class UserPostsPage { 11 | 12 | constructor( 13 | public navCtrl: NavController, 14 | public navParams: NavParams 15 | ) {} 16 | 17 | ionViewDidLoad() { 18 | } 19 | 20 | get userId() { 21 | return this.navParams.get('userId'); 22 | } 23 | 24 | get displayName() { 25 | return this.navParams.get('displayName'); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | NOTES.md 5 | src/env.ts 6 | google-services.json 7 | GoogleService-Info.plist 8 | my-release-key.jks 9 | *.apk 10 | *.p12 11 | 12 | 13 | *~ 14 | *.sw[mnpcod] 15 | *.log 16 | *.tmp 17 | *.tmp.* 18 | log.txt 19 | *.sublime-project 20 | *.sublime-workspace 21 | .vscode/ 22 | npm-debug.log* 23 | 24 | .idea/ 25 | .sourcemaps/ 26 | .sass-cache/ 27 | .tmp/ 28 | .versions/ 29 | coverage/ 30 | dist/ 31 | node_modules/ 32 | tmp/ 33 | temp/ 34 | hooks/ 35 | platforms/ 36 | plugins/ 37 | plugins/android.json 38 | plugins/ios.json 39 | www/ 40 | $RECYCLE.BIN/ 41 | 42 | .DS_Store 43 | Thumbs.db 44 | UserInterfaceState.xcuserstate 45 | -------------------------------------------------------------------------------- /src/app/app.scss: -------------------------------------------------------------------------------- 1 | // http://ionicframework.com/docs/theming/ 2 | 3 | 4 | // App Global Sass 5 | // -------------------------------------------------- 6 | // Put style rules here that you want to apply globally. These 7 | // styles are for the entire app and not just one component. 8 | // Additionally, this file can be also used as an entry point 9 | // to import other Sass files to be included in the output CSS. 10 | // 11 | // Shared Sass variables, which can be used to adjust Ionic's 12 | // default Sass variables, belong in "theme/variables.scss". 13 | // 14 | // To declare rules for a specific mode, create a child rule 15 | // for the .md, .ios, or .wp mode classes. The mode class is 16 | // automatically applied to the element in the app. 17 | -------------------------------------------------------------------------------- /src/components/user-relationship/user-relationship.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { DatabaseProvider } from '../../providers/database/database'; 3 | 4 | @Component({ 5 | selector: 'user-relationship', 6 | templateUrl: 'user-relationship.html' 7 | }) 8 | export class UserRelationshipComponent { 9 | 10 | @Input() currentUserId; // logged in user 11 | @Input() followId; // user to be followed/unfollowed 12 | 13 | isOwner: boolean; 14 | isFollowing: any; 15 | 16 | constructor(public db: DatabaseProvider) { } 17 | 18 | ngOnInit() { 19 | this.isOwner = this.currentUserId === this.followId; 20 | 21 | this.isFollowing = this.db.isFollowing(this.currentUserId, this.followId); 22 | } 23 | 24 | 25 | } 26 | -------------------------------------------------------------------------------- /functions/src/notifications.ts: -------------------------------------------------------------------------------- 1 | import * as functions from "firebase-functions"; 2 | import * as admin from "firebase-admin"; 3 | 4 | export const newUnicornPost = functions.firestore 5 | 6 | .document("posts/{postId}") 7 | .onCreate(async event => { 8 | const post = event.data.data(); 9 | 10 | const isUnicorn = post.content.toLowerCase().indexOf("unicorn") >= 0; 11 | 12 | if (!isUnicorn) { 13 | return null; 14 | } 15 | 16 | // Notification content 17 | const payload = { 18 | notification: { 19 | title: "New Post about Unicorns", 20 | body: `Read the latest unicorn post!`, 21 | icon: "https://goo.gl/Fz9nrQ" 22 | } 23 | }; 24 | 25 | return admin.messaging().sendToTopic("unicorns", payload); 26 | }); 27 | -------------------------------------------------------------------------------- /src/pages/users/users.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IonicPage, NavController, NavParams } from 'ionic-angular'; 3 | import { DatabaseProvider } from '../../providers/database/database'; 4 | import { AuthProvider } from '../../providers/auth/auth'; 5 | 6 | 7 | @IonicPage() 8 | @Component({ 9 | selector: 'page-users', 10 | templateUrl: 'users.html', 11 | }) 12 | export class UsersPage { 13 | 14 | users: any; 15 | 16 | constructor( 17 | public navCtrl: NavController, 18 | public navParams: NavParams, 19 | public auth: AuthProvider, 20 | public db: DatabaseProvider, 21 | ) {} 22 | 23 | ionViewDidLoad() { 24 | this.users = this.db.getUsers(); 25 | } 26 | 27 | trackByFn(index, user) { 28 | return user.uid; // or item.id 29 | } 30 | 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/users/users.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Users 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |

{{ user.displayName }}

19 | 20 | 23 | 24 | 25 |
26 |
27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Check out https://googlechromelabs.github.io/sw-toolbox/ for 3 | * more info on how to use sw-toolbox to custom configure your service worker. 4 | */ 5 | 6 | 7 | 'use strict'; 8 | importScripts('./build/sw-toolbox.js'); 9 | 10 | self.toolbox.options.cache = { 11 | name: 'ionic-cache' 12 | }; 13 | 14 | // pre-cache our key assets 15 | self.toolbox.precache( 16 | [ 17 | './build/main.js', 18 | './build/vendor.js', 19 | './build/main.css', 20 | './build/polyfills.js', 21 | 'index.html', 22 | 'manifest.json' 23 | ] 24 | ); 25 | 26 | // dynamically cache any other local assets 27 | self.toolbox.router.any('/*', self.toolbox.fastest); 28 | 29 | // for any other requests go to the network, cache, 30 | // and then only use that cached resource if your user goes offline 31 | self.toolbox.router.default = self.toolbox.networkFirst; 32 | -------------------------------------------------------------------------------- /src/components/anonymous-login/anonymous-login.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { AuthProvider } from '../../providers/auth/auth'; 3 | import { NavController } from 'ionic-angular'; 4 | import { LoadingController } from 'ionic-angular'; 5 | import { TabsPage } from '../../pages/tabs/tabs'; 6 | 7 | @Component({ 8 | selector: 'anonymous-login', 9 | templateUrl: 'anonymous-login.html' 10 | }) 11 | export class AnonymousLoginComponent { 12 | constructor( 13 | public auth: AuthProvider, 14 | public navCtrl: NavController, 15 | public loadingCtrl: LoadingController 16 | ) {} 17 | 18 | 19 | async login() { 20 | const loader = this.loadingCtrl.create({ 21 | content: "Logging in anonymously..." 22 | }); 23 | 24 | loader.present() 25 | 26 | await this.auth.anonymousLogin(); 27 | 28 | loader.dismiss() 29 | await this.navCtrl.setRoot(TabsPage) 30 | 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/components/post-feed/post-feed.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input } from '@angular/core'; 2 | import { DatabaseProvider } from '../../providers/database/database'; 3 | import { AuthProvider } from '../../providers/auth/auth'; 4 | import { Observable } from 'rxjs/Observable'; 5 | import { map } from 'rxjs/operators'; 6 | 7 | @Component({ 8 | selector: 'post-feed', 9 | templateUrl: 'post-feed.html' 10 | }) 11 | export class PostFeedComponent implements OnInit { 12 | 13 | @Input() userId: string; 14 | 15 | posts: Observable; 16 | 17 | constructor(private db: DatabaseProvider, public auth: AuthProvider) { } 18 | 19 | ngOnInit() { 20 | 21 | this.posts = this.db.getRecentPosts().snapshotChanges().pipe( 22 | map(arr => arr.map(doc => { 23 | return { id: doc.payload.doc.id, ...doc.payload.doc.data() } 24 | })) 25 | ) 26 | } 27 | 28 | trackByFn(index, post) { 29 | return post.id; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/profile-edit/profile-edit.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IonicPage, NavController, NavParams } from 'ionic-angular'; 3 | import { AuthProvider } from '../../providers/auth/auth'; 4 | import { AngularFirestore } from 'angularfire2/firestore'; 5 | 6 | @IonicPage() 7 | @Component({ 8 | selector: 'page-profile-edit', 9 | templateUrl: 'profile-edit.html', 10 | }) 11 | export class ProfileEditPage { 12 | 13 | newName: string; 14 | 15 | constructor( 16 | public navCtrl: NavController, 17 | public navParams: NavParams, 18 | public auth: AuthProvider, 19 | public afs: AngularFirestore 20 | ) {} 21 | 22 | 23 | ionViewDidLoad() { 24 | console.log('ionViewDidLoad ProfileEditPage'); 25 | } 26 | 27 | updateProfile(user) { 28 | console.log(this.newName, user) 29 | const ref = this.afs.collection('users').doc(user.uid) 30 | 31 | return ref.update({ displayName: this.newName }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/pages/posts-create/posts-create.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Create a New Post 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |

Upload an image from your device and add some content to the post

15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /src/pages/home/home.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IonicPage, NavController, NavParams, Platform } from 'ionic-angular'; 3 | import { AdMobFree, AdMobFreeInterstitialConfig } from '@ionic-native/admob-free'; 4 | 5 | @IonicPage() 6 | @Component({ 7 | selector: 'page-home', 8 | templateUrl: 'home.html', 9 | }) 10 | export class HomePage { 11 | 12 | constructor( 13 | public navCtrl: NavController, 14 | public navParams: NavParams, 15 | public adMob: AdMobFree, 16 | public platform: Platform 17 | ) { 18 | } 19 | 20 | ionViewDidLoad() { 21 | console.log('ionViewDidLoad HomePage'); 22 | if (this.platform.is('cordova')) { 23 | const ad: AdMobFreeInterstitialConfig = { 24 | id: 'ca-app-pub-6881663307118312/4719939210', 25 | isTesting: false, 26 | autoShow: false 27 | } 28 | this.adMob.interstitial.config(ad); 29 | 30 | // 1 in 4 chance to see an ad 31 | if (new Date().getTime() % 4 === 0) { 32 | this.adMob.interstitial.prepare() 33 | } 34 | 35 | } 36 | 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/components/fcm-handler/fcm-handler.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from "@angular/core"; 2 | import { FcmProvider } from "../../providers/fcm/fcm"; 3 | import { ToastController } from "ionic-angular"; 4 | import { tap } from "rxjs/operators"; 5 | 6 | @Component({ 7 | selector: "fcm-handler", 8 | templateUrl: "fcm-handler.html" 9 | }) 10 | export class FcmHandlerComponent implements OnInit { 11 | constructor(private fcm: FcmProvider, private toastCtrl: ToastController) {} 12 | 13 | ngOnInit() { 14 | // Get the initial token 15 | this.fcm.getToken(); 16 | 17 | // Update on refresh 18 | this.fcm.monitorTokenRefresh().subscribe(); 19 | 20 | // Listen to incoming messages 21 | this.fcm 22 | .listenToNotifications() 23 | .pipe( 24 | tap(msg => { 25 | // show a toast 26 | if (msg) { 27 | const toast = this.toastCtrl.create({ 28 | message: msg.body, 29 | duration: 3000 30 | }); 31 | toast.present(); 32 | } 33 | }) 34 | ) 35 | .subscribe(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/profile/profile.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IonicPage, NavController, NavParams } from 'ionic-angular'; 3 | import { AuthProvider } from '../../providers/auth/auth'; 4 | import { RemoteConfigProvider } from '../../providers/remote-config/remote-config'; 5 | 6 | 7 | @IonicPage() 8 | @Component({ 9 | selector: 'page-profile', 10 | templateUrl: 'profile.html', 11 | }) 12 | export class ProfilePage { 13 | 14 | bannerText: Promise; 15 | 16 | constructor( 17 | public navCtrl: NavController, 18 | public navParams: NavParams, 19 | public auth: AuthProvider, 20 | public remoteConfig: RemoteConfigProvider 21 | ) {} 22 | 23 | ionViewCanEnter() { 24 | return this.auth.isLoggedIn(); 25 | } 26 | 27 | 28 | ionViewDidLoad() { 29 | console.log('ionViewDidLoad ProfilePage'); 30 | this.bannerText = this.remoteConfig.getValue('profile_banner') 31 | } 32 | 33 | openEditPage() { 34 | this.navCtrl.push('ProfileEditPage') 35 | } 36 | 37 | openPostFeed(user) { 38 | this.navCtrl.push('UserPostsPage', { 39 | userId: user.uid, 40 | name: user.displayName 41 | }) 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/pages/posts-create/posts-create.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { IonicPage, NavController, NavParams } from 'ionic-angular'; 3 | import { DatabaseProvider, Post } from '../../providers/database/database'; 4 | import { AuthProvider } from '../../providers/auth/auth'; 5 | import { AnalyticsProvider } from '../../providers/analytics/analytics'; 6 | 7 | @IonicPage() 8 | @Component({ 9 | selector: 'page-posts-create', 10 | templateUrl: 'posts-create.html', 11 | }) 12 | export class PostsCreatePage { 13 | 14 | post: Partial = {}; 15 | 16 | constructor( 17 | public navCtrl: NavController, 18 | public navParams: NavParams, 19 | public db: DatabaseProvider, 20 | public auth: AuthProvider, 21 | public analytics: AnalyticsProvider 22 | ) {} 23 | 24 | ionViewDidLoad() { 25 | console.log('ionViewDidLoad PostsCreatePage'); 26 | } 27 | 28 | async create(user) { 29 | await this.db.createPost(user.uid, this.post as Post) 30 | await this.analytics.logEvent('create_post', {}) 31 | this.post = {} 32 | await this.navCtrl.setRoot('HomePage') 33 | } 34 | 35 | updateURL(e) { 36 | this.post.img = e; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Platform } from 'ionic-angular'; 3 | import { StatusBar } from '@ionic-native/status-bar'; 4 | import { SplashScreen } from '@ionic-native/splash-screen'; 5 | 6 | import { TabsPage } from '../pages/tabs/tabs'; 7 | import { LoginPage } from '../pages/login/login'; 8 | 9 | import { AuthProvider } from '../providers/auth/auth'; 10 | import { AnalyticsProvider } from '../providers/analytics/analytics'; 11 | import { RemoteConfigProvider } from '../providers/remote-config/remote-config'; 12 | 13 | @Component({ 14 | templateUrl: 'app.html' 15 | }) 16 | export class MyApp { 17 | rootPage:any = null; 18 | 19 | constructor( 20 | platform: Platform, 21 | statusBar: StatusBar, 22 | splashScreen: SplashScreen, 23 | auth: AuthProvider, 24 | analytics: AnalyticsProvider, 25 | remoteConfig: RemoteConfigProvider 26 | ) { 27 | 28 | platform.ready().then(() => { 29 | 30 | auth.getCurrentUser() 31 | .then(user => { 32 | 33 | if (user) { 34 | this.rootPage = TabsPage 35 | } else { 36 | this.rootPage = LoginPage 37 | } 38 | 39 | statusBar.styleDefault(); 40 | splashScreen.hide(); 41 | 42 | remoteConfig.initialize() 43 | 44 | }) 45 | 46 | }); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/pages/profile/profile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Profile 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{ bannerText | async }}
12 | Value from via Remote Config 13 |
14 | 15 | 16 | 17 | 18 |

{{ user.displayName }}

19 | 20 | 23 | 24 |
25 | 26 | 27 | 28 | 29 |

{{ user.postCount || 0 }}

30 |

Posts

31 |
32 | 33 |

{{ user.followingCount || 0 }}

34 |

Following

35 |
36 | 37 |

{{ user.followerCount || 0 }}

38 |

Followers

39 |
40 |
41 |
42 | 43 |
44 | 45 | 46 | 47 |
48 | 49 |

Get Notifications about Unicorns

50 | 51 | 52 | 53 |
54 |
55 | -------------------------------------------------------------------------------- /functions/lib/notifications.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | Object.defineProperty(exports, "__esModule", { value: true }); 11 | const functions = require("firebase-functions"); 12 | const admin = require("firebase-admin"); 13 | exports.newUnicornPost = functions.firestore 14 | .document('posts/{postId}') 15 | .onCreate((event) => __awaiter(this, void 0, void 0, function* () { 16 | const post = event.data.data(); 17 | const isUnicorn = post.content.toLowerCase().indexOf("unicorn") >= 0; 18 | if (!isUnicorn) { 19 | return null; 20 | } 21 | // Notification content 22 | const payload = { 23 | notification: { 24 | title: 'New Post about Unicorns', 25 | body: `Read the latest unicorn post!`, 26 | icon: 'https://goo.gl/Fz9nrQ' 27 | } 28 | }; 29 | return admin.messaging().sendToTopic("unicorns", payload); 30 | })); 31 | //# sourceMappingURL=notifications.js.map -------------------------------------------------------------------------------- /src/providers/remote-config/remote-config.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Platform } from 'ionic-angular'; 3 | import { Firebase } from '@ionic-native/firebase'; 4 | 5 | @Injectable() 6 | export class RemoteConfigProvider { 7 | private defaults = { 8 | profile_banner: 'Default Hello' 9 | }; 10 | 11 | constructor(public firebaseNative: Firebase, public platform: Platform) {} 12 | 13 | initialize() { 14 | if (this.platform.is('cordova')) { 15 | const win = (window as any); 16 | 17 | win.FirebasePlugin.fetch( 18 | 600, 19 | () => { 20 | console.log('fetched remote config '); 21 | win.FirebasePlugin.activateFetched( 22 | () => { 23 | console.log('activated remote config'); 24 | }, 25 | error => { 26 | console.error('error initializing remote config', error); 27 | } 28 | ); 29 | }, 30 | error => { 31 | console.error('error fetching remote config', error); 32 | } 33 | ); 34 | } 35 | } 36 | 37 | async getValue(key: string) { 38 | 39 | if (this.platform.is('cordova')) { 40 | const win = window as any; 41 | 42 | const remoteVal = await new Promise((resolve, reject) => { 43 | win.FirebasePlugin.getValue( 44 | key, 45 | value => { 46 | console.log('value', value); 47 | resolve(value); 48 | }, 49 | error => { 50 | console.log('error getting value' + error); 51 | reject(error); 52 | } 53 | ); 54 | }); 55 | 56 | return remoteVal || this.defaults[key]; 57 | } else { 58 | // PWA Implementation 59 | return this.defaults[key] 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ionic App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/components/components.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { IonicModule } from 'ionic-angular'; 4 | import { CommonModule } from '@angular/common'; 5 | 6 | import { FacebookLoginComponent } from './facebook-login/facebook-login'; 7 | import { AnonymousLoginComponent } from './anonymous-login/anonymous-login'; 8 | import { UserLogoutComponent } from './user-logout/user-logout'; 9 | import { PostFeedComponent } from './post-feed/post-feed'; 10 | import { HeartButtonComponent } from './heart-button/heart-button'; 11 | import { UserRelationshipComponent } from './user-relationship/user-relationship'; 12 | import { ImageUploadComponent, UploadModal } from './image-upload/image-upload'; 13 | import { FcmHandlerComponent } from './fcm-handler/fcm-handler'; 14 | import { FcmTopicComponent } from './fcm-topic/fcm-topic'; 15 | 16 | 17 | @NgModule({ 18 | declarations: [ 19 | FacebookLoginComponent, 20 | FacebookLoginComponent, 21 | AnonymousLoginComponent, 22 | UserLogoutComponent, 23 | PostFeedComponent, 24 | HeartButtonComponent, 25 | UserRelationshipComponent, 26 | ImageUploadComponent, 27 | UploadModal, 28 | FcmHandlerComponent, 29 | FcmTopicComponent, 30 | 31 | ], 32 | imports: [ 33 | CommonModule, 34 | IonicModule 35 | ], 36 | exports: [ 37 | FacebookLoginComponent, 38 | FacebookLoginComponent, 39 | AnonymousLoginComponent, 40 | UserLogoutComponent, 41 | PostFeedComponent, 42 | HeartButtonComponent, 43 | UserRelationshipComponent, 44 | ImageUploadComponent, 45 | UploadModal, 46 | FcmHandlerComponent, 47 | FcmTopicComponent, 48 | 49 | ], 50 | entryComponents: [ 51 | UploadModal 52 | ] 53 | }) 54 | export class ComponentsModule { } 55 | -------------------------------------------------------------------------------- /src/providers/analytics/analytics.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { App, Platform } from 'ionic-angular'; 3 | import { Firebase } from '@ionic-native/firebase'; 4 | import { AuthProvider } from '../auth/auth'; 5 | 6 | @Injectable() 7 | export class AnalyticsProvider { 8 | constructor( 9 | app: App, 10 | auth: AuthProvider, 11 | private platform: Platform, 12 | private firebaseNative: Firebase, 13 | ) { 14 | 15 | if (platform.is('cordova')) { 16 | // Tracks the current page name 17 | app.viewDidEnter.subscribe(view => { 18 | firebaseNative.setScreenName(view.name); 19 | firebaseNative.logEvent('page_view', { page: view.name }); 20 | }); 21 | auth.user.subscribe(user => { 22 | const uid = user ? user.uid : 'guest'; 23 | firebaseNative.setUserId(uid); 24 | }); 25 | 26 | 27 | } 28 | } 29 | 30 | logEvent(event: string, data?: Object) { 31 | if (this.platform.is('cordova')) { 32 | console.log('analytics log...', event); 33 | return this.firebaseNative.logEvent(event, data); 34 | } 35 | } 36 | 37 | //// REMOTE CONFIG //// 38 | 39 | // getRc(){ 40 | // (window).FirebasePlugin.fetch(600, result => { 41 | // // activate the fetched remote config 42 | // console.log(JSON.stringify(result)); //Always "OK" 43 | // (window).FirebasePlugin.activateFetched( 44 | // // Android seems to return error always, so we want to cath both 45 | // result => { 46 | // console.log(JSON.stringify(result)); //either true or false 47 | // (window).FirebasePlugin.getValue("value", result => { 48 | // console.log(result) 49 | // }, reason => { 50 | // console.warn(`Error ${reason}`); 51 | // }); 52 | // }, reason => { 53 | // console.warn(reason); 54 | // } 55 | // ) 56 | // }); 57 | // } 58 | 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /functions/lib/aggregation.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"aggregation.js","sourceRoot":"","sources":["../src/aggregation.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,gDAAgD;AAEhD,wCAAwC;AACxC,KAAK,CAAC,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,CAAC;AAEpC,QAAA,oBAAoB,GAAG,SAAS,CAAC,SAAS;KAElD,QAAQ,CAAC,gCAAgC,CAAC;KAC1C,OAAO,CAAC,KAAK,CAAC,EAAE;IAEb,8CAA8C;IAC9C,uEAAuE;IACvE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;QAC3C,MAAM,CAAC,IAAI,CAAC;IAChB,CAAC;IAED,kEAAkE;IAClE,MAAM,WAAW,GAAG,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAE9C,mBAAmB;IACnB,MAAM,GAAG,GAAG,KAAK,CAAC,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAEnD,MAAM,UAAU,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;IACzB,MAAM,UAAU,GAAG,GAAG,CAAC,CAAC,CAAC,CAAA;IAEzB,mCAAmC;IACnC,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAE5B,MAAM,WAAW,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IAC1D,MAAM,WAAW,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAA;IAE1D,+BAA+B;IAC/B,MAAM,CAAC,EAAE,CAAC,cAAc,CAAC,CAAM,CAAC,EAAC,EAAE;QAE/B,6BAA6B;QAC7B,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QACzC,MAAM,QAAQ,GAAG,MAAM,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QAEzC,qBAAqB;QACrB,MAAM,cAAc,GAAG;YACnB,cAAc,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,cAAc,IAAI,CAAC,CAAC,GAAG,WAAW;SACtE,CAAA;QACD,MAAM,cAAc,GAAG;YACnB,aAAa,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,aAAa,IAAI,CAAC,CAAC,GAAG,WAAW;SACpE,CAAA;QAED,kBAAkB;QAClB,MAAM,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,cAAc,EAAE,EAAE,KAAK,EAAG,IAAI,EAAE,CAAC,CAAA;QAC1D,MAAM,CAAC,CAAC,GAAG,CAAC,WAAW,EAAE,cAAc,EAAE,EAAE,KAAK,EAAG,IAAI,EAAE,CAAC,CAAA;QAE1D,MAAM,CAAC,CAAC,CAAA;IACZ,CAAC,CAAA,CAAC,CAAA;AAEN,CAAC,CAAC,CAAA;AAGO,QAAA,eAAe,GAAG,SAAS,CAAC,SAAS;KAE7C,QAAQ,CAAC,gBAAgB,CAAC;KAC1B,QAAQ,CAAC,CAAM,KAAK,EAAC,EAAE;IAEpB,sBAAsB;IACtB,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,CAAC;IAExC,qBAAqB;IACrB,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAC5B,MAAM,OAAO,GAAG,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAElD,oBAAoB;IACpB,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,CAAA;IAGhC,sCAAsC;IACtC,MAAM,SAAS,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,SAAS,IAAI,CAAC,CAAC,GAAG,CAAC,CAAA;IAElD,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,CAAA;AAE5C,CAAC,CAAA,CAAC,CAAA"} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ionic Firebase Starter 2 | 3 | [![Slack](https://firebasestorage.googleapis.com/v0/b/firestarter-96e46.appspot.com/o/assets%2Fslack-badge.svg?alt=media&token=3e68acef-3e00-4925-9710-e11cee5923e4)](https://goo.gl/qF8Q5r) 4 | 5 | An Ionic + Firebase starter app targeted at native deployment to iOS and Android platforms (and PWA). 6 | 7 | ## Features 8 | 9 | This app is demonstrates variety of features that can be deployed cross-platform. In most cases, you can drop a provider/component into an existing app painlessly. 10 | 11 | ### Auth 12 | 13 | - Anonymous and Facebook User Auth 14 | - Customizable User Profile 15 | 16 | ### Firestore 17 | 18 | - Basic CRUD Demo 19 | - Heart/Liking System 20 | - User Follow/Unfollow System 21 | - Automatic Data Aggregation with Cloud Functions 22 | 23 | ### Storage 24 | 25 | - Native Camera Capture 26 | - Firebase Storage Uploads 27 | 28 | ### Push Notifications 29 | 30 | - Multi-device Token Management 31 | - Topic based Notifications 32 | - Automatic Notifications with Cloud Functions 33 | 34 | ### Firebase iOS Android Platform Features 35 | 36 | - Collect Custom User Analytics 37 | - Create a Dymanic User Experience with Remote Config 38 | - Increase Conversions with Predictions 39 | - Serve Ads with Admob 40 | 41 | ## Install Steps 42 | 43 | If only targeting the web, you can skip steps 2 and 3 below. 44 | 45 | ### Ionic App 46 | 47 | 0. `git clone` this repo, cd into it, and run `npm install` 48 | 1. Add your Firebase web config to the `app.module` 49 | 2. Save `google-services.json` and `GoogleService-Info.plist` from Firebase to the project root. 50 | 3. Run `ionic cordova emulate android -l -c` or (ios) to 51 | 52 | ### Cloud Functions Deployment 53 | 54 | Cloud functions handle backend tasks, such as push notifications and data aggregation. 55 | 56 | 0. `cd functions` 57 | 1. `npm install` 58 | 2. `firebase deploy --only functions` 59 | 60 | Building a native app is hard... Watch the videos [Ionic Native + Firebase](https://projects.angularfirebase.com/p/ionic-native-with-firebase) or get in touch on Slack. 61 | 62 | 63 | ## License 64 | 65 | You must enroll in the [Ionic Native + Firebase](https://projects.angularfirebase.com/p/ionic-native-with-firebase) for an unrestricted commercial license to the source code. 66 | 67 | 68 | -------------------------------------------------------------------------------- /functions/src/aggregation.ts: -------------------------------------------------------------------------------- 1 | import * as functions from "firebase-functions"; 2 | 3 | import * as admin from "firebase-admin"; 4 | admin.initializeApp(functions.config().firebase); 5 | 6 | export const updateFollowerCounts = functions.firestore 7 | 8 | .document("relationships/{relationshipId}") 9 | .onWrite(event => { 10 | // Check if previous and current data exists, 11 | // If true, it's an update and we don't want to change the follow count 12 | if (event.data.exists && event.data.previous) { 13 | return null; 14 | } 15 | 16 | // Otherwise, we want to +1 for a new doc, or -1 for a deleted doc 17 | const countChange = event.data.exists ? 1 : -1; 18 | 19 | // Get the user ids 20 | const ids = event.params.relationshipId.split("_"); 21 | 22 | const followerId = ids[0]; 23 | const followedId = ids[1]; 24 | 25 | // Reference the document locations 26 | const db = admin.firestore(); 27 | 28 | const followerRef = db.collection("users").doc(followerId); 29 | const followedRef = db.collection("users").doc(followedId); 30 | 31 | // Return a transaction promise 32 | return db.runTransaction(async t => { 33 | // Fetch the data from the DB 34 | const follower = await t.get(followerRef); 35 | const followed = await t.get(followedRef); 36 | 37 | // format the counts 38 | const followerUpdate = { 39 | followingCount: (follower.data().followingCount || 0) + countChange 40 | }; 41 | const followedUpdate = { 42 | followerCount: (followed.data().followerCount || 0) + countChange 43 | }; 44 | 45 | // run the updates 46 | await t.set(followerRef, followerUpdate, { merge: true }); 47 | await t.set(followedRef, followedUpdate, { merge: true }); 48 | 49 | return t; 50 | }); 51 | }); 52 | 53 | export const updatePostCount = functions.firestore 54 | 55 | .document("posts/{postId}") 56 | .onCreate(async event => { 57 | // Get the post UserID 58 | const userId = event.data.data().userId; 59 | 60 | // Reference user doc 61 | const db = admin.firestore(); 62 | const userRef = db.collection("users").doc(userId); 63 | 64 | // Get the user data 65 | const user = await userRef.get(); 66 | 67 | // Update the count and run the update 68 | const postCount = (user.data().postCount || 0) + 1; 69 | 70 | return userRef.update({ postCount }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule, ErrorHandler } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular'; 4 | import { MyApp } from './app.component'; 5 | 6 | import { TabsPage } from '../pages/tabs/tabs'; 7 | import { LoginPage } from '../pages/login/login'; 8 | 9 | import { StatusBar } from '@ionic-native/status-bar'; 10 | import { SplashScreen } from '@ionic-native/splash-screen'; 11 | import { AuthProvider } from '../providers/auth/auth'; 12 | 13 | import { AngularFireModule } from 'angularfire2'; 14 | import { AngularFirestoreModule } from 'angularfire2/firestore'; 15 | import { AngularFireStorageModule } from 'angularfire2/storage'; 16 | import { AngularFireAuthModule } from 'angularfire2/auth'; 17 | 18 | /// DELETE this line 19 | import { firebaseConfig } from '../env'; 20 | 21 | /// ADD your firebase web credentials in the object below 22 | 23 | // const firebaseConfig = { 24 | // } 25 | 26 | import { Firebase } from '@ionic-native/firebase'; 27 | import { Facebook } from '@ionic-native/facebook'; 28 | import { Camera } from '@ionic-native/camera'; 29 | import { AdMobFree } from '@ionic-native/admob-free'; 30 | 31 | import { ComponentsModule } from '../components/components.module' 32 | import { DatabaseProvider } from '../providers/database/database'; 33 | import { FcmProvider } from '../providers/fcm/fcm'; 34 | import { AnalyticsProvider } from '../providers/analytics/analytics'; 35 | import { RemoteConfigProvider } from '../providers/remote-config/remote-config'; 36 | 37 | 38 | @NgModule({ 39 | declarations: [ 40 | MyApp, 41 | TabsPage, 42 | LoginPage, 43 | ], 44 | imports: [ 45 | BrowserModule, 46 | IonicModule.forRoot(MyApp), 47 | AngularFireModule.initializeApp(firebaseConfig), 48 | AngularFirestoreModule, 49 | AngularFireStorageModule, 50 | AngularFireAuthModule, 51 | ComponentsModule 52 | ], 53 | bootstrap: [IonicApp], 54 | entryComponents: [ 55 | MyApp, 56 | TabsPage, 57 | LoginPage 58 | ], 59 | providers: [ 60 | StatusBar, 61 | SplashScreen, 62 | {provide: ErrorHandler, useClass: IonicErrorHandler}, 63 | AuthProvider, 64 | Firebase, 65 | Facebook, 66 | DatabaseProvider, 67 | Camera, 68 | AdMobFree, 69 | FcmProvider, 70 | AnalyticsProvider, 71 | RemoteConfigProvider 72 | ] 73 | }) 74 | export class AppModule {} 75 | -------------------------------------------------------------------------------- /src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | // Ionic Variables and Theming. For more info, please see: 2 | // http://ionicframework.com/docs/theming/ 3 | 4 | // Font path is used to include ionicons, 5 | // roboto, and noto sans fonts 6 | $font-path: "../assets/fonts"; 7 | 8 | 9 | // The app direction is used to include 10 | // rtl styles in your app. For more info, please see: 11 | // http://ionicframework.com/docs/theming/rtl-support/ 12 | $app-direction: ltr; 13 | 14 | 15 | @import "ionic.globals"; 16 | 17 | 18 | // Shared Variables 19 | // -------------------------------------------------- 20 | // To customize the look and feel of this app, you can override 21 | // the Sass variables found in Ionic's source scss files. 22 | // To view all the possible Ionic variables, see: 23 | // http://ionicframework.com/docs/theming/overriding-ionic-variables/ 24 | 25 | 26 | 27 | 28 | // Named Color Variables 29 | // -------------------------------------------------- 30 | // Named colors makes it easy to reuse colors on various components. 31 | // It's highly recommended to change the default colors 32 | // to match your app's branding. Ionic uses a Sass map of 33 | // colors so you can add, rename and remove colors as needed. 34 | // The "primary" color is the only required color in the map. 35 | 36 | $colors: ( 37 | primary: #488aff, 38 | secondary: #32db64, 39 | danger: #f53d3d, 40 | light: #f4f4f4, 41 | dark: #222 42 | ); 43 | 44 | 45 | // App iOS Variables 46 | // -------------------------------------------------- 47 | // iOS only Sass variables can go here 48 | 49 | 50 | 51 | 52 | // App Material Design Variables 53 | // -------------------------------------------------- 54 | // Material Design only Sass variables can go here 55 | 56 | 57 | 58 | 59 | // App Windows Variables 60 | // -------------------------------------------------- 61 | // Windows only Sass variables can go here 62 | 63 | 64 | 65 | 66 | // App Theme 67 | // -------------------------------------------------- 68 | // Ionic apps can have different themes applied, which can 69 | // then be future customized. This import comes last 70 | // so that the above variables are used and Ionic's 71 | // default are overridden. 72 | 73 | @import "ionic.theme.default"; 74 | 75 | 76 | // Ionicons 77 | // -------------------------------------------------- 78 | // The premium icon font for Ionic. For more info, please see: 79 | // http://ionicframework.com/docs/ionicons/ 80 | 81 | @import "ionic.ionicons"; 82 | 83 | 84 | // Fonts 85 | // -------------------------------------------------- 86 | 87 | @import "roboto"; 88 | @import "noto-sans"; 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firebaseStarter", 3 | "version": "0.0.1", 4 | "author": "Ionic Framework", 5 | "homepage": "http://ionicframework.com/", 6 | "private": true, 7 | "scripts": { 8 | "clean": "ionic-app-scripts clean", 9 | "build": "ionic-app-scripts build", 10 | "lint": "ionic-app-scripts lint", 11 | "ionic:build": "ionic-app-scripts build", 12 | "ionic:serve": "ionic-app-scripts serve" 13 | }, 14 | "dependencies": { 15 | "@angular/common": "5.0.3", 16 | "@angular/compiler": "5.0.3", 17 | "@angular/compiler-cli": "5.0.3", 18 | "@angular/core": "5.0.3", 19 | "@angular/forms": "5.0.3", 20 | "@angular/http": "5.0.3", 21 | "@angular/platform-browser": "5.0.3", 22 | "@angular/platform-browser-dynamic": "5.0.3", 23 | "@ionic-native/admob-free": "^4.5.3", 24 | "@ionic-native/camera": "^4.5.3", 25 | "@ionic-native/core": "4.4.0", 26 | "@ionic-native/facebook": "^4.5.3", 27 | "@ionic-native/firebase": "^4.5.3", 28 | "@ionic-native/splash-screen": "4.4.0", 29 | "@ionic-native/status-bar": "4.4.0", 30 | "@ionic/pro": "1.0.20", 31 | "@ionic/storage": "2.1.3", 32 | "and": "0.0.3", 33 | "angularfire2": "^5.0.0-rc.6", 34 | "chance": "^1.0.13", 35 | "cordova-android": "7.0.0", 36 | "cordova-ios": "4.5.4", 37 | "cordova-plugin-camera": "^4.0.2", 38 | "cordova-plugin-device": "^2.0.1", 39 | "cordova-plugin-facebook4": "^1.9.1", 40 | "cordova-plugin-firebase": "git+https://github.com/arnesson/cordova-plugin-firebase.git", 41 | "cordova-plugin-ionic-keyboard": "^2.0.5", 42 | "cordova-plugin-ionic-webview": "^1.1.16", 43 | "cordova-plugin-splashscreen": "^5.0.2", 44 | "cordova-plugin-statusbar": "^2.4.1", 45 | "cordova-plugin-whitelist": "^1.3.3", 46 | "cordova-promise-polyfill": "0.0.2", 47 | "firebase": "^4.11.0", 48 | "ionic-angular": "3.9.2", 49 | "ionicons": "3.0.0", 50 | "lycwed-cordova-plugin-admob-free": "git+https://github.com/RenaudROHLINGER/lycwed-cordova-plugin-admob-free.git", 51 | "rxjs": "5.5.2", 52 | "sw-toolbox": "3.6.0", 53 | "zone.js": "0.8.18" 54 | }, 55 | "devDependencies": { 56 | "@ionic/app-scripts": "3.1.8", 57 | "typescript": "2.4.2" 58 | }, 59 | "description": "An Ionic project", 60 | "cordova": { 61 | "plugins": { 62 | "cordova-plugin-whitelist": {}, 63 | "cordova-plugin-device": {}, 64 | "cordova-plugin-splashscreen": {}, 65 | "cordova-plugin-ionic-webview": {}, 66 | "cordova-plugin-ionic-keyboard": {}, 67 | "cordova-plugin-facebook4": { 68 | "APP_ID": "1647541315328951", 69 | "APP_NAME": "Ionic Firestarter" 70 | }, 71 | "cordova-plugin-statusbar": {}, 72 | "cordova-plugin-firebase": {}, 73 | "lycwed-cordova-plugin-admob-free": {}, 74 | "cordova-plugin-camera": { 75 | "CAMERA_USAGE_DESCRIPTION": "App would like to access the camera", 76 | "PHOTOLIBRARY_USAGE_DESCRIPTION": "App would like to access the photo library" 77 | } 78 | }, 79 | "platforms": [ 80 | "android", 81 | "ios" 82 | ] 83 | } 84 | } -------------------------------------------------------------------------------- /src/providers/database/database.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import * as firebase from 'firebase/app'; 3 | import { 4 | AngularFirestore, 5 | AngularFirestoreCollection 6 | } from 'angularfire2/firestore'; 7 | 8 | 9 | export interface Post { 10 | userId: string; 11 | createdAt: Date; 12 | image: string; 13 | content: string; 14 | likeCount: number; 15 | [key: string]: any; 16 | } 17 | 18 | @Injectable() 19 | export class DatabaseProvider { 20 | private postsRef: AngularFirestoreCollection; 21 | 22 | constructor(private afs: AngularFirestore) { 23 | this.postsRef = this.afs.collection('posts'); 24 | } 25 | 26 | getRecentPosts() { 27 | return this.afs.collection('posts', ref => 28 | ref.orderBy('createdAt', 'desc').limit(10) 29 | ); 30 | } 31 | 32 | getUserPosts(userId: string) { 33 | return this.afs.collection('posts', ref => 34 | ref 35 | .orderBy('createdAt', 'desc') 36 | .where('userId', '==', userId) 37 | .limit(10) 38 | ); 39 | } 40 | 41 | createPost(userId: string, data: Post) { 42 | const createdAt = firebase.firestore.FieldValue.serverTimestamp(); 43 | const doc = { userId, createdAt, ...data }; 44 | return this.postsRef.add(doc); 45 | } 46 | 47 | deletePost(id: string) { 48 | return this.postsRef.doc(id).delete(); 49 | } 50 | 51 | //// HEARTS //// 52 | 53 | createHeart(userId: string, post: Post) { 54 | const hearts = post.hearts || {}; 55 | hearts[userId] = true; 56 | 57 | return this.afs.doc(`posts/${post.id}`).update({ hearts }); 58 | } 59 | 60 | removeHeart(userId: string, post: Post) { 61 | const hearts = post.hearts; 62 | delete post.hearts[userId]; 63 | 64 | return this.afs.doc(`posts/${post.id}`).update({ hearts }); 65 | } 66 | 67 | //// RELATIONSHIPS //// 68 | 69 | getUsers() { 70 | return this.afs.collection('users', ref => ref.limit(10)).valueChanges(); 71 | } 72 | 73 | follow(followerId: string, followedId: string) { 74 | const docId = this.concatIds(followerId, followedId); 75 | const createdAt = firebase.firestore.FieldValue.serverTimestamp(); 76 | 77 | const data = { 78 | followerId, 79 | followedId, 80 | createdAt 81 | }; 82 | 83 | return this.afs 84 | .collection('relationships') 85 | .doc(docId) 86 | .set(data); 87 | } 88 | 89 | unfollow(followerId: string, followedId: string) { 90 | const docId = this.concatIds(followerId, followedId); 91 | 92 | return this.afs 93 | .collection('relationships') 94 | .doc(docId) 95 | .delete(); 96 | } 97 | 98 | isFollowing(followerId: string, followedId: string) { 99 | const docId = this.concatIds(followerId, followedId); 100 | 101 | return this.afs 102 | .collection('relationships') 103 | .doc(docId) 104 | .valueChanges(); 105 | } 106 | 107 | // Helper to format the docId for relationships 108 | private concatIds(a: string, b: string) { 109 | return `${a}_${b}`; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /functions/lib/aggregation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | Object.defineProperty(exports, "__esModule", { value: true }); 11 | const functions = require("firebase-functions"); 12 | const admin = require("firebase-admin"); 13 | admin.initializeApp(functions.config().firebase); 14 | exports.updateFollowerCounts = functions.firestore 15 | .document('relationships/{relationshipId}') 16 | .onWrite(event => { 17 | // Check if previous and current data exists, 18 | // If true, it's an update and we don't want to change the follow count 19 | if (event.data.exists && event.data.previous) { 20 | return null; 21 | } 22 | // Otherwise, we want to +1 for a new doc, or -1 for a deleted doc 23 | const countChange = event.data.exists ? 1 : -1; 24 | // Get the user ids 25 | const ids = event.params.relationshipId.split('_'); 26 | const followerId = ids[0]; 27 | const followedId = ids[1]; 28 | // Reference the document locations 29 | const db = admin.firestore(); 30 | const followerRef = db.collection('users').doc(followerId); 31 | const followedRef = db.collection('users').doc(followedId); 32 | // Return a transaction promise 33 | return db.runTransaction((t) => __awaiter(this, void 0, void 0, function* () { 34 | // Fetch the data from the DB 35 | const follower = yield t.get(followerRef); 36 | const followed = yield t.get(followedRef); 37 | // format the counts 38 | const followerUpdate = { 39 | followingCount: (follower.data().followingCount || 0) + countChange 40 | }; 41 | const followedUpdate = { 42 | followerCount: (followed.data().followerCount || 0) + countChange 43 | }; 44 | // run the updates 45 | yield t.set(followerRef, followerUpdate, { merge: true }); 46 | yield t.set(followedRef, followedUpdate, { merge: true }); 47 | return t; 48 | })); 49 | }); 50 | exports.updatePostCount = functions.firestore 51 | .document('posts/{postId}') 52 | .onCreate((event) => __awaiter(this, void 0, void 0, function* () { 53 | // Get the post UserID 54 | const userId = event.data.data().userId; 55 | // Reference user doc 56 | const db = admin.firestore(); 57 | const userRef = db.collection('users').doc(userId); 58 | // Get the user data 59 | const user = yield userRef.get(); 60 | // Update the count and run the update 61 | const postCount = (user.data().postCount || 0) + 1; 62 | return userRef.update({ postCount }); 63 | })); 64 | //# sourceMappingURL=aggregation.js.map -------------------------------------------------------------------------------- /src/providers/fcm/fcm.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { Firebase } from "@ionic-native/firebase"; 3 | import { Platform } from "ionic-angular"; 4 | import { AngularFirestore } from "angularfire2/firestore"; 5 | import { tap } from "rxjs/operators"; 6 | import { of } from "rxjs/observable/of"; 7 | import { AuthProvider } from "../auth/auth"; 8 | 9 | @Injectable() 10 | export class FcmProvider { 11 | constructor( 12 | public firebaseNative: Firebase, 13 | public afs: AngularFirestore, 14 | private platform: Platform, 15 | private auth: AuthProvider 16 | ) {} 17 | 18 | async getToken() { 19 | let token; 20 | 21 | if (this.platform.is("cordova")) { 22 | const status = await this.firebaseNative.hasPermission(); 23 | 24 | if (status.isEnabled) { 25 | console.log("already enabled"); 26 | return; 27 | } 28 | 29 | token = await this.firebaseNative.getToken(); 30 | } 31 | 32 | if (this.platform.is("ios")) { 33 | await this.firebaseNative.grantPermission(); 34 | } 35 | 36 | return this.saveTokenToFirestore(token); 37 | } 38 | 39 | // Saves the token to Firestore when refreshed 40 | monitorTokenRefresh() { 41 | if (this.platform.is("cordova")) { 42 | return this.firebaseNative 43 | .onTokenRefresh() 44 | .pipe(tap(token => this.saveTokenToFirestore(token))); 45 | } else { 46 | // PWA implementation 47 | return of(null); 48 | } 49 | } 50 | 51 | private async saveTokenToFirestore(token) { 52 | if (!token) return; 53 | const devicesRef = this.afs.collection("devices"); 54 | 55 | const user = await this.auth.getCurrentUser(); 56 | 57 | const docData = { 58 | token, 59 | userId: user.uid 60 | }; 61 | 62 | return devicesRef.doc(token).set(docData); 63 | } 64 | 65 | async subscribeTo(topic: string) { 66 | const user = await this.auth.getCurrentUser(); 67 | 68 | if (this.platform.is("cordova")) { 69 | await this.firebaseNative.subscribe(topic); 70 | } 71 | 72 | const topics = { [topic]: true }; 73 | return this.afs 74 | .collection("users") 75 | .doc(user.uid) 76 | .set({ topics }, { merge: true }); 77 | } 78 | 79 | async unsubscribeFrom(topic: string) { 80 | const user = await this.auth.getCurrentUser(); 81 | 82 | if (this.platform.is("cordova")) { 83 | await this.firebaseNative.unsubscribe(topic); 84 | } 85 | 86 | const topics = { [topic]: false }; 87 | return this.afs 88 | .collection("users") 89 | .doc(user.uid) 90 | .set({ topics }, { merge: true }); 91 | } 92 | 93 | // Handle incoming messages 94 | listenToNotifications() { 95 | if (this.platform.is("cordova")) { 96 | return this.firebaseNative.onNotificationOpen(); 97 | } else { 98 | // PWA implementation 99 | return of(null); 100 | } 101 | } 102 | 103 | // Add this to the logout method to end notifications 104 | stopNotifications() { 105 | return this.firebaseNative.unregister(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/components/image-upload/image-upload.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, Output, EventEmitter } from "@angular/core"; 2 | import { filter, tap } from "rxjs/operators"; 3 | 4 | import { ModalController, NavParams, ViewController } from "ionic-angular"; 5 | import { Camera, CameraOptions } from "@ionic-native/camera"; 6 | 7 | import { 8 | AngularFireStorage, 9 | AngularFireUploadTask 10 | } from "angularfire2/storage"; 11 | 12 | @Component({ 13 | selector: "image-upload", 14 | templateUrl: "image-upload.html" 15 | }) 16 | export class ImageUploadComponent { 17 | @Input() userId; 18 | 19 | @Output() uploadFinished = new EventEmitter(); 20 | 21 | task: AngularFireUploadTask; 22 | image: string; 23 | 24 | constructor( 25 | private storage: AngularFireStorage, 26 | private modalCtrl: ModalController, 27 | private camera: Camera 28 | ) {} 29 | 30 | startUpload(file: string) { 31 | const path = `${this.userId}/${new Date().getTime()}`; 32 | 33 | // The main task 34 | this.image = "data:image/jpg;base64," + file; 35 | this.task = this.storage.ref(path).putString(this.image, "data_url"); 36 | 37 | // Define and present the modal component 38 | let uploadModal = this.modalCtrl.create(UploadModal, { task: this.task }); 39 | uploadModal.present(); 40 | 41 | // Listen to the progress, when 100% dismiss the modal 42 | this.task 43 | .percentageChanges() 44 | .pipe( 45 | filter(val => val === 100), 46 | tap(complete => { 47 | uploadModal.dismiss(); 48 | }) 49 | ) 50 | .subscribe(); 51 | 52 | // Listen for the Download URL 53 | this.task 54 | .downloadURL() 55 | .pipe(tap(url => this.uploadFinished.emit(url))) 56 | .subscribe(); 57 | } 58 | 59 | async captureAndUpload() { 60 | const options: CameraOptions = { 61 | quality: 70, 62 | targetWidth: 500, 63 | targetHeight: 500, 64 | destinationType: this.camera.DestinationType.DATA_URL, 65 | encodingType: this.camera.EncodingType.JPEG, 66 | mediaType: this.camera.MediaType.PICTURE, 67 | sourceType: this.camera.PictureSourceType.PHOTOLIBRARY 68 | }; 69 | 70 | const base64 = await this.camera.getPicture(options); 71 | 72 | this.startUpload(base64); 73 | } 74 | } 75 | 76 | @Component({ 77 | template: ` 78 | 79 | 80 | 81 | 82 | Uploading to Firebase... 83 | 84 | 85 | 86 | 87 | 88 |

Upload is 89 | 90 | {{ progress | async | number }}% complete 91 | 92 |

93 | 94 | 95 |
96 | ` 97 | }) 98 | export class UploadModal { 99 | task; 100 | progress; 101 | 102 | constructor(params: NavParams, public viewCtrl: ViewController) { 103 | this.task = params.get("task"); 104 | this.progress = this.task.percentageChanges(); 105 | } 106 | 107 | cancel() { 108 | this.task.cancel(); 109 | this.viewCtrl.dismiss(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/providers/auth/auth.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | import { Platform } from "ionic-angular"; 3 | import { AngularFireAuth } from "angularfire2/auth"; 4 | import { 5 | AngularFirestore, 6 | AngularFirestoreDocument 7 | } from "angularfire2/firestore"; 8 | import * as firebase from "firebase/app"; 9 | 10 | import { Observable } from "rxjs/Observable"; 11 | import { switchMap, first } from "rxjs/operators"; 12 | 13 | import { Facebook } from "@ionic-native/facebook"; 14 | 15 | import * as Chance from 'chance'; 16 | 17 | @Injectable() 18 | export class AuthProvider { 19 | user: Observable; 20 | 21 | constructor( 22 | private afAuth: AngularFireAuth, 23 | private afs: AngularFirestore, 24 | private facebook: Facebook, 25 | private platform: Platform 26 | ) { 27 | this.user = this.afAuth.authState.pipe( 28 | switchMap(user => { 29 | if (user) { 30 | return this.afs.doc(`users/${user.uid}`).valueChanges(); 31 | } else { 32 | return Observable.of(null); 33 | } 34 | }) 35 | ); 36 | } 37 | 38 | //// FACEBOOK //// 39 | 40 | async facebookLogin() { 41 | if (this.platform.is("cordova")) { 42 | return await this.nativeFacebookLogin(); 43 | } else { 44 | return await this.webFacebookLogin(); 45 | } 46 | } 47 | 48 | async nativeFacebookLogin(): Promise { 49 | try { 50 | const response = await this.facebook.login(["email", "public_profile"]); 51 | const facebookCredential = firebase.auth.FacebookAuthProvider.credential( 52 | response.authResponse.accessToken 53 | ); 54 | 55 | const firebaseUser = await firebase 56 | .auth() 57 | .signInWithCredential(facebookCredential); 58 | 59 | return await this.updateUserData(firebaseUser); 60 | } catch (err) { 61 | console.log(err); 62 | } 63 | } 64 | 65 | async webFacebookLogin(): Promise { 66 | try { 67 | const provider = new firebase.auth.FacebookAuthProvider(); 68 | const credential = await this.afAuth.auth.signInWithPopup(provider); 69 | 70 | return await this.updateUserData(credential.user); 71 | } catch (err) { 72 | console.log(err); 73 | } 74 | } 75 | 76 | // Save custom user data in Firestore 77 | private updateUserData(user: any) { 78 | const userRef: AngularFirestoreDocument = this.afs.doc( 79 | `users/${user.uid}` 80 | ); 81 | 82 | const data = { 83 | uid: user.uid, 84 | email: user.email || null, 85 | displayName: user.displayName || new Chance().name({ prefix: true }), 86 | photoURL: user.photoURL || "https://goo.gl/7kz9qG" 87 | }; 88 | return userRef.set(data, { merge: true }); 89 | } 90 | 91 | //// ANONYMOUS //// 92 | 93 | async anonymousLogin(): Promise { 94 | const user = await this.afAuth.auth.signInAnonymously(); 95 | await this.updateUserData(user); 96 | } 97 | 98 | //// HELPERS //// 99 | 100 | async logout(): Promise { 101 | return this.afAuth.auth.signOut(); 102 | } 103 | 104 | // Current user as a Promise. Useful for one-off operations. 105 | async getCurrentUser(): Promise { 106 | return this.user.pipe(first()).toPromise(); 107 | } 108 | 109 | // Current user as boolean Promise. Used in router guards 110 | async isLoggedIn(): Promise { 111 | const user = await this.getCurrentUser(); 112 | return !!user; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /functions/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | // -- Strict errors -- 4 | // These lint rules are likely always a good idea. 5 | 6 | // Force function overloads to be declared together. This ensures readers understand APIs. 7 | "adjacent-overload-signatures": true, 8 | 9 | // Do not allow the subtle/obscure comma operator. 10 | "ban-comma-operator": true, 11 | 12 | // Do not allow internal modules or namespaces . These are deprecated in favor of ES6 modules. 13 | "no-namespace": true, 14 | 15 | // Do not allow parameters to be reassigned. To avoid bugs, developers should instead assign new values to new vars. 16 | "no-parameter-reassignment": true, 17 | 18 | // Force the use of ES6-style imports instead of /// imports. 19 | "no-reference": true, 20 | 21 | // Do not allow type assertions that do nothing. This is a big warning that the developer may not understand the 22 | // code currently being edited (they may be incorrectly handling a different type case that does not exist). 23 | "no-unnecessary-type-assertion": true, 24 | 25 | // Disallow nonsensical label usage. 26 | "label-position": true, 27 | 28 | // Disallows the (often typo) syntax if (var1 = var2). Replace with if (var2) { var1 = var2 }. 29 | "no-conditional-assignment": true, 30 | 31 | // Disallows constructors for primitive types (e.g. new Number('123'), though Number('123') is still allowed). 32 | "no-construct": true, 33 | 34 | // Do not allow super() to be called twice in a constructor. 35 | "no-duplicate-super": true, 36 | 37 | // Do not allow the same case to appear more than once in a switch block. 38 | "no-duplicate-switch-case": true, 39 | 40 | // Do not allow a variable to be declared more than once in the same block. Consider function parameters in this 41 | // rule. 42 | "no-duplicate-variable": [true, "check-parameters"], 43 | 44 | // Disallows a variable definition in an inner scope from shadowing a variable in an outer scope. Developers should 45 | // instead use a separate variable name. 46 | "no-shadowed-variable": true, 47 | 48 | // Empty blocks are almost never needed. Allow the one general exception: empty catch blocks. 49 | "no-empty": [true, "allow-empty-catch"], 50 | 51 | // Functions must either be handled directly (e.g. with a catch() handler) or returned to another function. 52 | // This is a major source of errors in Cloud Functions and the team strongly recommends leaving this rule on. 53 | "no-floating-promises": true, 54 | 55 | // Do not allow any imports for modules that are not in package.json. These will almost certainly fail when 56 | // deployed. 57 | "no-implicit-dependencies": true, 58 | 59 | // The 'this' keyword can only be used inside of classes. 60 | "no-invalid-this": true, 61 | 62 | // Do not allow strings to be thrown because they will not include stack traces. Throw Errors instead. 63 | "no-string-throw": true, 64 | 65 | // Disallow control flow statements, such as return, continue, break, and throw in finally blocks. 66 | "no-unsafe-finally": true, 67 | 68 | // Do not allow variables to be used before they are declared. 69 | "no-use-before-declare": true, 70 | 71 | // Expressions must always return a value. Avoids common errors like const myValue = functionReturningVoid(); 72 | "no-void-expression": [true, "ignore-arrow-function-shorthand"], 73 | 74 | // Makes sure result of typeof is compared to correct string values. 75 | "typeof-compare": true, 76 | 77 | // Disallow duplicate imports in the same file. 78 | "no-duplicate-imports": true, 79 | 80 | 81 | // -- Strong Warnings -- 82 | // These rules should almost never be needed, but may be included due to legacy code. 83 | // They are left as a warning to avoid frustration with blocked deploys when the developer 84 | // understand the warning and wants to deploy anyway. 85 | 86 | // Warn when an empty interface is defined. These are generally not useful. 87 | "no-empty-interface": {"severity": "warning"}, 88 | 89 | // Warn when an import will have side effects. 90 | "no-import-side-effect": {"severity": "warning"}, 91 | 92 | // Warn when variables are defined with var. Var has subtle meaning that can lead to bugs. Strongly prefer const for 93 | // most values and let for values that will change. 94 | "no-var-keyword": {"severity": "warning"}, 95 | 96 | // Prefer === and !== over == and !=. The latter operators support overloads that are often accidental. 97 | "triple-equals": {"severity": "warning"}, 98 | 99 | // Warn when using deprecated APIs. 100 | "deprecation": {"severity": "warning"}, 101 | 102 | // -- Light Warnigns -- 103 | // These rules are intended to help developers use better style. Simpler code has fewer bugs. These would be "info" 104 | // if TSLint supported such a level. 105 | 106 | // prefer for( ... of ... ) to an index loop when the index is only used to fetch an object from an array. 107 | // (Even better: check out utils like .map if transforming an array!) 108 | "prefer-for-of": {"severity": "warning"}, 109 | 110 | // Warns if function overloads could be unified into a single function with optional or rest parameters. 111 | "unified-signatures": {"severity": "warning"}, 112 | 113 | // Warns if code has an import or variable that is unused. 114 | "no-unused-variable": {"severity": "warning"}, 115 | 116 | // Prefer const for values that will not change. This better documents code. 117 | "prefer-const": {"severity": "warning"}, 118 | 119 | // Multi-line object liiterals and function calls should have a trailing comma. This helps avoid merge conflicts. 120 | "trailing-comma": {"severity": "warning"} 121 | }, 122 | 123 | "defaultSeverity": "error" 124 | } 125 | -------------------------------------------------------------------------------- /config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ionic Firestarter 4 | Ionic Firebase Starter App 5 | Jeff Delaney 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | --------------------------------------------------------------------------------