Diff to HTML by rtfpessoa

Files changed (67) hide show
  1. .firebaserc +22 -0
  2. CHANGELOG.md +1 -0
  3. DIFF.md +51 -0
  4. FIREBASE.md +53 -1
  5. angular.json +2 -27
  6. capacitor.config.ts +4 -5
  7. firebase.json +85 -40
  8. package.json +28 -25
  9. resources/README.md +1 -8
  10. resources/psd/default/background.psd +0 -0
  11. resources/psd/default/icon.psd +0 -0
  12. resources/psd/default/splash.psd +0 -0
  13. resources/psd/pwa/icon.psd +0 -0
  14. resources/psd/pwa/splash.psd +0 -0
  15. resources/web/HOW-TO.md +13 -13
  16. resources/web/icon/favicon.ico +0 -0
  17. src/app/app.component.ts +3 -2
  18. src/app/components/countdown-timer/countdown-timer.component.ts +6 -5
  19. src/app/deals/details/deals-details.module.ts +4 -1
  20. src/app/deals/details/deals-details.page.html +4 -4
  21. src/app/deals/details/deals-details.page.ts +14 -5
  22. src/app/deals/details/styles/deals-details.page.scss +3 -3
  23. src/app/deals/listing/deals-listing.page.ts +6 -3
  24. src/app/fashion/details/fashion-details.module.ts +5 -1
  25. src/app/fashion/details/fashion-details.page.html +4 -4
  26. src/app/fashion/details/fashion-details.page.ts +36 -26
  27. src/app/fashion/details/styles/fashion-details.page.scss +2 -2
  28. src/app/fashion/listing/fashion-listing.page.ts +6 -3
  29. src/app/firebase/auth/firebase-auth-definitions.ts +6 -0
  30. src/app/firebase/auth/firebase-auth.module.ts +21 -5
  31. src/app/firebase/auth/firebase-auth.service.ts +443 -108
  32. src/app/firebase/auth/profile/firebase-profile.module.ts +7 -5
  33. src/app/firebase/auth/profile/firebase-profile.page.ts +32 -19
  34. src/app/firebase/auth/sign-in/firebase-sign-in.module.ts +10 -8
  35. src/app/firebase/auth/sign-in/firebase-sign-in.page.ts +102 -127
  36. src/app/firebase/auth/sign-up/firebase-sign-up.module.ts +17 -1
  37. src/app/firebase/auth/sign-up/firebase-sign-up.page.html +1 -1
  38. src/app/firebase/auth/sign-up/firebase-sign-up.page.ts +103 -114
  39. src/app/firebase/crud/firebase-crud.module.ts +6 -4
  40. src/app/firebase/crud/firebase-crud.service.ts +159 -92
  41. src/app/firebase/crud/listing/firebase-listing.page.ts +11 -9
  42. src/app/firebase/crud/user/select-image/select-user-image.modal.ts +5 -4
  43. src/app/firebase/crud/user/update/firebase-update-user.modal.ts +1 -1
  44. src/app/food/details/food-details.page.ts +6 -3
  45. src/app/food/listing/food-listing.page.ts +6 -3
  46. src/app/getting-started/getting-started.module.ts +4 -1
  47. src/app/getting-started/getting-started.page.html +8 -8
  48. src/app/getting-started/getting-started.page.ts +22 -23
  49. src/app/getting-started/styles/getting-started.page.scss +6 -6
  50. src/app/notifications/notifications.page.ts +6 -3
  51. src/app/pipes/time-ago.pipe.ts +2 -2
  52. src/app/real-estate/details/real-estate-details.page.ts +6 -3
  53. src/app/real-estate/listing/real-estate-listing.page.ts +6 -3
  54. src/app/showcase/app-shell/data-store-combined/data-store-combined.page.ts +2 -1
  55. src/app/showcase/route-resolvers-ux/progressive-shell-resolvers/progressive-shell-resolvers.page.ts +6 -3
  56. src/app/travel/details/travel-details.page.ts +6 -3
  57. src/app/travel/listing/travel-listing.page.ts +6 -3
  58. src/app/user/friends/user-friends.page.ts +9 -6
  59. src/app/user/profile/user-profile.page.ts +9 -6
  60. src/app/video-playlist/video-playlist.page.ts +10 -7
  61. src/app/walkthrough/styles/walkthrough.page.scss +5 -5
  62. src/app/walkthrough/walkthrough.module.ts +4 -1
  63. src/app/walkthrough/walkthrough.page.html +14 -14
  64. src/app/walkthrough/walkthrough.page.ts +46 -28
  65. src/assets/icon/favicon.ico +0 -0
  66. src/global.scss +5 -0
  67. src/manifest.webmanifest +2 -2
.firebaserc CHANGED
@@ -3,5 +3,27 @@
3
  "dev": "dev-ion4fullpwa",
4
  "prod": "ion4fullpwa",
5
  "pro": "pro-ion4fullpwa"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  }
7
  }
3
  "dev": "dev-ion4fullpwa",
4
  "prod": "ion4fullpwa",
5
  "pro": "pro-ion4fullpwa"
6
+ },
7
+ "targets": {
8
+ "dev-ion4fullpwa": {
9
+ "hosting": {
10
+ "ionic-5": [
11
+ "dev-ion4fullpwa"
12
+ ],
13
+ "ionic-6": [
14
+ "dev-ionic-6-full-app"
15
+ ]
16
+ }
17
+ },
18
+ "pro-ion4fullpwa": {
19
+ "hosting": {
20
+ "12-2021-release": [
21
+ "pro-ion4fullpwa"
22
+ ],
23
+ "06-2022-release": [
24
+ "pro-ionic-6-full-app"
25
+ ]
26
+ }
27
+ }
28
  }
29
  }
CHANGELOG.md ADDED
@@ -0,0 +1 @@
 
1
+ Access the changelog in https://ionic-5-full-starter-app-docs.ionicthemes.com/changelog
DIFF.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Generate diff file
2
+ *PRO vs ELITE (**no /android or /ios configs**)*
3
+ ```bash
4
+ git diff pro-version..elite-version --diff-filter=ADCMRT ':!package-lock.json' ':!dist' ':!.gradle/*' ':!android' ':!ios' ':!*.diff' ':!*.png' ':!*.svg' > diff/pro-vs-elite_changelog.diff
5
+ ```
6
+
7
+ *PRO vs ELITE (**android config**)*
8
+ ```bash
9
+ git diff pro-version..elite-version --diff-filter=ADCMRT -- android/ > diff/pro-vs-elite_android-changelog.diff
10
+ ```
11
+
12
+ *PRO vs ELITE (**ios config**)*
13
+ ```bash
14
+ git diff pro-version..elite-version --diff-filter=ADCMRT -- ios/ > diff/pro-vs-elite_ios-changelog.diff
15
+ ```
16
+
17
+ *Last PRO update (**12-2021**) vs Current PRO update (**06-2022**)*
18
+ ```bash
19
+ git diff 12-2021_pro-update..pro-version --diff-filter=ADCMRT ':!package-lock.json' ':!dist' ':!.gradle/*' ':!android' ':!ios' ':!*.diff' ':!*.png' ':!*.svg' > diff/last-pro-update-vs-current-pro_changelog.diff
20
+ ```
21
+
22
+ *Last PRO update (**12-2021**) vs Current PRO update (**06-2022**) (**android config**)*
23
+ ```bash
24
+ git diff 12-2021_pro-update..pro-version --diff-filter=ADCMRT -- android/ > diff/last-pro-update-vs-current-pro_android-changelog.diff
25
+ ```
26
+
27
+ *Last PRO update (**12-2021**) vs Current PRO update (**06-2022**) (**ios config**)*
28
+ ```bash
29
+ git diff 12-2021_pro-update..pro-version --diff-filter=ADCMRT -- ios/ > diff/last-pro-update-vs-current-pro_ios-changelog.diff
30
+ ```
31
+
32
+ ## Generate visual diff HTML file
33
+ *To generate a static HTML file with the diff*
34
+ ```bash
35
+ diff2html --style side --file visual_diff.html --input file -- changelog.diff
36
+ ```
37
+
38
+ *To open the visual diff on the browser*
39
+ ```bash
40
+ diff2html --style side --input file -- diff/pro-vs-elite_changelog.diff
41
+
42
+ diff2html --style side --input file -- diff/pro-vs-elite_android-changelog.diff
43
+
44
+ diff2html --style side --input file -- diff/pro-vs-elite_ios-changelog.diff
45
+
46
+ diff2html --style side --input file -- diff/last-pro-update-vs-current-pro_changelog.diff
47
+
48
+ diff2html --style side --input file -- diff/last-pro-update-vs-current-pro_android-changelog.diff
49
+
50
+ diff2html --style side --input file -- diff/last-pro-update-vs-current-pro_ios-changelog.diff
51
+ ```
FIREBASE.md CHANGED
@@ -29,10 +29,62 @@ If you don't see these alias (dev, prod), you should create them
29
  `firebase use dev`
30
 
31
  You can also use the `-P` flag to specify an alias like this:
32
- `firebase deploy -P dev`
 
 
33
 
34
  This will deploy to the `dev` alias/environment
35
 
36
  ## Serve and test your Firebase project locally
37
  For more info see: https://firebase.google.com/docs/hosting/deploying
38
  `firebase serve --only hosting`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  `firebase use dev`
30
 
31
  You can also use the `-P` flag to specify an alias like this:
32
+ ``` bash
33
+ firebase deploy --only hosting -P dev
34
+ ```
35
 
36
  This will deploy to the `dev` alias/environment
37
 
38
  ## Serve and test your Firebase project locally
39
  For more info see: https://firebase.google.com/docs/hosting/deploying
40
  `firebase serve --only hosting`
41
+
42
+ ---
43
+
44
+ # [Advanced uses](https://firebase.google.com/docs/cli/targets#deploy-target-commands)
45
+
46
+ ## [Create target](https://firebase.google.com/docs/cli/targets#set-up-deploy-target-hosting)
47
+ - TARGET_NAME = ionic-5
48
+ - RESOURCE_IDENTIFIER (the SITE_ID) = dev-ion4fullpwa
49
+ ``` bash
50
+ firebase target:apply hosting ionic-5 dev-ion4fullpwa -P dev
51
+ ```
52
+
53
+ - TARGET_NAME = ionic-6
54
+ - RESOURCE_IDENTIFIER (the SITE_ID) = dev-ionic-6-full-app
55
+ ``` bash
56
+ firebase target:apply hosting ionic-6 dev-ionic-6-full-app -P dev
57
+ ```
58
+
59
+ - TARGET_NAME = 12-2021-release
60
+ - RESOURCE_IDENTIFIER (the SITE_ID) = pro-ion4fullpwa
61
+ ``` bash
62
+ firebase target:apply hosting 12-2021-release pro-ion4fullpwa -P pro
63
+ ```
64
+
65
+ - TARGET_NAME = 06-2022-release
66
+ - RESOURCE_IDENTIFIER (the SITE_ID) = pro-ionic-6-full-app
67
+ ``` bash
68
+ firebase target:apply hosting 06-2022-release pro-ionic-6-full-app -P pro
69
+ ```
70
+
71
+
72
+ ## [Configure your `firebase.json` file to use deploy targets](https://firebase.google.com/docs/cli/targets#configure_your_firebasejson_file_to_use_deploy_targets)
73
+ Don't forget to configure this before deploying to the new target
74
+
75
+
76
+ ## Deploy to specific target (i.e.: different site)
77
+ ``` bash
78
+ firebase deploy --only hosting:ionic-6 -P dev
79
+ ```
80
+
81
+ ``` bash
82
+ firebase deploy --only hosting:06-2022-release -P pro
83
+ ```
84
+
85
+
86
+ ## Create channel
87
+ - CHANNEL_ID = dev
88
+ ``` bash
89
+ firebase hosting:channel:deploy dev --only ionic-6 -P dev
90
+ ```
angular.json CHANGED
@@ -111,8 +111,7 @@
111
  "production": {
112
  "browserTarget": "app:build:production"
113
  },
114
- "ci": {
115
- }
116
  }
117
  },
118
  "extract-i18n": {
@@ -130,30 +129,6 @@
130
  ]
131
  }
132
  },
133
- "ionic-cordova-build": {
134
- "builder": "@ionic/angular-toolkit:cordova-build",
135
- "options": {
136
- "browserTarget": "app:build"
137
- },
138
- "configurations": {
139
- "production": {
140
- "browserTarget": "app:build:production"
141
- }
142
- }
143
- },
144
- "ionic-cordova-serve": {
145
- "builder": "@ionic/angular-toolkit:cordova-serve",
146
- "options": {
147
- "cordovaBuildTarget": "app:ionic-cordova-build",
148
- "devServerTarget": "app:serve"
149
- },
150
- "configurations": {
151
- "production": {
152
- "cordovaBuildTarget": "app:ionic-cordova-build:production",
153
- "devServerTarget": "app:serve:production"
154
- }
155
- }
156
- },
157
  "server": {
158
  "builder": "@angular-devkit/build-angular:server",
159
  "options": {
@@ -221,4 +196,4 @@
221
  "styleext": "scss"
222
  }
223
  }
224
- }
111
  "production": {
112
  "browserTarget": "app:build:production"
113
  },
114
+ "ci": {}
 
115
  }
116
  },
117
  "extract-i18n": {
129
  ]
130
  }
131
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  "server": {
133
  "builder": "@angular-devkit/build-angular:server",
134
  "options": {
196
  "styleext": "scss"
197
  }
198
  }
199
+ }
capacitor.config.ts CHANGED
@@ -2,22 +2,21 @@ import { CapacitorConfig } from '@capacitor/cli';
2
 
3
  const config: CapacitorConfig = {
4
  appId: 'com.ionicthemes.ionic5fullapp',
5
- appName: 'Ionic5FullApp',
6
  webDir: 'dist/app/browser',
7
  bundledWebRuntime: false,
8
  plugins: {
9
  SplashScreen: {
10
  launchAutoHide: false,
11
  },
12
- CapacitorFirebaseAuth: {
 
13
  providers: [
14
  "google.com",
15
  "twitter.com",
16
  "facebook.com",
17
  "apple.com"
18
- ],
19
- languageCode: "en",
20
- nativeAuth: false
21
  }
22
  }
23
  };
2
 
3
  const config: CapacitorConfig = {
4
  appId: 'com.ionicthemes.ionic5fullapp',
5
+ appName: 'Ionic6FullAppPro',
6
  webDir: 'dist/app/browser',
7
  bundledWebRuntime: false,
8
  plugins: {
9
  SplashScreen: {
10
  launchAutoHide: false,
11
  },
12
+ FirebaseAuthentication: {
13
+ skipNativeAuth: false,
14
  providers: [
15
  "google.com",
16
  "twitter.com",
17
  "facebook.com",
18
  "apple.com"
19
+ ]
 
 
20
  }
21
  }
22
  };
firebase.json CHANGED
@@ -1,43 +1,88 @@
1
  {
2
- "hosting": {
3
- "public": "dist/app/browser",
4
- "ignore": [
5
- "firebase.json",
6
- "**/.*",
7
- "**/node_modules/**"
8
- ],
9
- "rewrites": [ {
10
- "source": "**",
11
- "destination": "/index.html"
12
- } ],
13
- "headers": [
14
- {
15
  "source": "**",
16
- "headers": [
17
- {
18
- "key": "Cache-Control",
19
- "value": "no-cache, no-store, must-revalidate"
20
- }
21
- ]
22
- },
23
- {
24
- "source": "**/*.@(jpg|jpeg|gif|png|svg|webp|js|css|eot|otf|ttf|ttc|woff|font.css)",
25
- "headers": [
26
- {
27
- "key": "Cache-Control",
28
- "value": "no-cache"
29
- }
30
- ]
31
- },
32
- {
33
- "source": "ngsw-worker.js",
34
- "headers": [
35
- {
36
- "key": "Cache-Control",
37
- "value": "no-cache"
38
- }
39
- ]
40
- }
41
- ]
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  }
1
  {
2
+ "hosting": [
3
+ {
4
+ "target": "06-2022-release",
5
+ "public": "dist/app/browser",
6
+ "ignore": [
7
+ "firebase.json",
8
+ "**/.*",
9
+ "**/node_modules/**"
10
+ ],
11
+ "rewrites": [ {
 
 
 
12
  "source": "**",
13
+ "destination": "/index.html"
14
+ } ],
15
+ "headers": [
16
+ {
17
+ "source": "**",
18
+ "headers": [
19
+ {
20
+ "key": "Cache-Control",
21
+ "value": "no-cache, no-store, must-revalidate"
22
+ }
23
+ ]
24
+ },
25
+ {
26
+ "source": "**/*.@(jpg|jpeg|gif|png|svg|webp|js|css|eot|otf|ttf|ttc|woff|font.css)",
27
+ "headers": [
28
+ {
29
+ "key": "Cache-Control",
30
+ "value": "no-cache"
31
+ }
32
+ ]
33
+ },
34
+ {
35
+ "source": "ngsw-worker.js",
36
+ "headers": [
37
+ {
38
+ "key": "Cache-Control",
39
+ "value": "no-cache"
40
+ }
41
+ ]
42
+ }
43
+ ]
44
+ },
45
+ {
46
+ "target": "12-2021-release",
47
+ "public": "dist/app/browser",
48
+ "ignore": [
49
+ "firebase.json",
50
+ "**/.*",
51
+ "**/node_modules/**"
52
+ ],
53
+ "rewrites": [ {
54
+ "source": "**",
55
+ "destination": "/index.html"
56
+ } ],
57
+ "headers": [
58
+ {
59
+ "source": "**",
60
+ "headers": [
61
+ {
62
+ "key": "Cache-Control",
63
+ "value": "no-cache, no-store, must-revalidate"
64
+ }
65
+ ]
66
+ },
67
+ {
68
+ "source": "**/*.@(jpg|jpeg|gif|png|svg|webp|js|css|eot|otf|ttf|ttc|woff|font.css)",
69
+ "headers": [
70
+ {
71
+ "key": "Cache-Control",
72
+ "value": "no-cache"
73
+ }
74
+ ]
75
+ },
76
+ {
77
+ "source": "ngsw-worker.js",
78
+ "headers": [
79
+ {
80
+ "key": "Cache-Control",
81
+ "value": "no-cache"
82
+ }
83
+ ]
84
+ }
85
+ ]
86
+ }
87
+ ]
88
  }
package.json CHANGED
@@ -1,7 +1,7 @@
1
  {
2
  "name": "IonicFullApp-PRO",
3
  "description": "The most advanced and complete Mobile & PWA Ionic starter app template",
4
- "version": "4.0.0",
5
  "author": "IonicThemes",
6
  "contributors": [
7
  "Dayana <dayana@ionicthemes.com>",
@@ -24,41 +24,41 @@
24
  "@angular/animations": "^13.1.1",
25
  "@angular/common": "^13.1.1",
26
  "@angular/core": "^13.1.1",
27
- "@angular/fire": "^6.1.5",
28
  "@angular/forms": "^13.1.1",
29
  "@angular/platform-browser": "^13.1.1",
30
  "@angular/platform-browser-dynamic": "^13.1.1",
31
  "@angular/platform-server": "^13.1.1",
32
  "@angular/router": "^13.1.1",
33
  "@angular/service-worker": "^13.1.1",
34
- "@capacitor/android": "^3.3.3",
35
- "@capacitor/app": "^1.0.7",
36
- "@capacitor/cli": "^3.3.3",
37
- "@capacitor/core": "^3.3.3",
38
- "@capacitor/geolocation": "^1.3.0",
39
- "@capacitor/haptics": "^1.1.3",
40
- "@capacitor/ios": "^3.3.3",
41
- "@capacitor/keyboard": "^1.2.0",
42
- "@capacitor/share": "^1.0.7",
43
- "@capacitor/splash-screen": "^1.2.0",
44
- "@capacitor/status-bar": "^1.0.6",
45
- "@ionic/angular": "^6.0.1",
46
- "@ionic/angular-server": "^6.0.1",
47
  "@nguniversal/express-engine": "^13.0.1",
48
  "@ngx-translate/core": "^14.0.0",
49
  "@ngx-translate/http-loader": "^7.0.0",
50
  "@videogular/ngx-videogular": "^5.0.1",
51
  "angular-pipes": "^10.0.0",
52
- "capacitor-firebase-auth": "^3.0.0",
53
  "core-js": "^3.20.0",
54
  "date-fns": "^2.27.0",
55
- "dayjs": "^1.10.7",
56
  "express": "^4.17.2",
57
- "firebase": "^8.6.8",
58
- "google-libphonenumber": "^3.2.25",
59
  "jetifier": "^2.0.0",
60
  "mobile-detect": "^1.4.5",
61
  "rxjs": "^7.4.0",
 
62
  "tslib": "^2.0.0",
63
  "zone.js": "~0.11.4"
64
  },
@@ -76,18 +76,21 @@
76
  "@angular/compiler": "^13.1.1",
77
  "@angular/compiler-cli": "^13.1.1",
78
  "@angular/language-service": "~13.1.1",
79
- "@commitlint/cli": "^15.0.0",
80
- "@commitlint/config-angular": "^15.0.0",
81
- "@ionic/angular-toolkit": "^5.0.3",
 
 
82
  "@nguniversal/builders": "^13.0.1",
83
  "@types/core-js": "^2.5.5",
84
  "@types/express": "^4.17.13",
85
  "@types/googlemaps": "^3.39.2",
86
  "@types/node": "^17.0.1",
87
- "@typescript-eslint/eslint-plugin": "^5.7.0",
88
- "@typescript-eslint/parser": "^5.7.0",
89
  "@webcomponents/webcomponentsjs": "^2.6.0",
90
- "eslint": "^8.5.0",
 
91
  "husky": "^4.3.8",
92
  "ts-node": "^10.4.0",
93
  "typescript": "~4.5.4"
1
  {
2
  "name": "IonicFullApp-PRO",
3
  "description": "The most advanced and complete Mobile & PWA Ionic starter app template",
4
+ "version": "5.0.0",
5
  "author": "IonicThemes",
6
  "contributors": [
7
  "Dayana <dayana@ionicthemes.com>",
24
  "@angular/animations": "^13.1.1",
25
  "@angular/common": "^13.1.1",
26
  "@angular/core": "^13.1.1",
27
+ "@angular/fire": "^7.3.0",
28
  "@angular/forms": "^13.1.1",
29
  "@angular/platform-browser": "^13.1.1",
30
  "@angular/platform-browser-dynamic": "^13.1.1",
31
  "@angular/platform-server": "^13.1.1",
32
  "@angular/router": "^13.1.1",
33
  "@angular/service-worker": "^13.1.1",
34
+ "@capacitor-firebase/authentication": "^0.3.1",
35
+ "@capacitor/android": "^3.5.1",
36
+ "@capacitor/app": "^1.1.1",
37
+ "@capacitor/core": "^3.5.1",
38
+ "@capacitor/geolocation": "^1.3.1",
39
+ "@capacitor/haptics": "^1.1.4",
40
+ "@capacitor/ios": "^3.5.1",
41
+ "@capacitor/keyboard": "^1.2.2",
42
+ "@capacitor/share": "^1.1.2",
43
+ "@capacitor/splash-screen": "^1.2.2",
44
+ "@capacitor/status-bar": "^1.0.8",
45
+ "@ionic/angular": "^6.1.8",
46
+ "@ionic/angular-server": "^6.1.8",
47
  "@nguniversal/express-engine": "^13.0.1",
48
  "@ngx-translate/core": "^14.0.0",
49
  "@ngx-translate/http-loader": "^7.0.0",
50
  "@videogular/ngx-videogular": "^5.0.1",
51
  "angular-pipes": "^10.0.0",
 
52
  "core-js": "^3.20.0",
53
  "date-fns": "^2.27.0",
54
+ "dayjs": "^1.11.2",
55
  "express": "^4.17.2",
56
+ "firebase": "^9.8.2",
57
+ "google-libphonenumber": "^3.2.27",
58
  "jetifier": "^2.0.0",
59
  "mobile-detect": "^1.4.5",
60
  "rxjs": "^7.4.0",
61
+ "swiper": "^8.2.2",
62
  "tslib": "^2.0.0",
63
  "zone.js": "~0.11.4"
64
  },
76
  "@angular/compiler": "^13.1.1",
77
  "@angular/compiler-cli": "^13.1.1",
78
  "@angular/language-service": "~13.1.1",
79
+ "@capacitor/cli": "^3.5.1",
80
+ "@commitlint/cli": "^17.0.2",
81
+ "@commitlint/config-angular": "^17.0.0",
82
+ "@ionic/angular-toolkit": "^6.1.0",
83
+ "@ionic/cli": "6.19.1",
84
  "@nguniversal/builders": "^13.0.1",
85
  "@types/core-js": "^2.5.5",
86
  "@types/express": "^4.17.13",
87
  "@types/googlemaps": "^3.39.2",
88
  "@types/node": "^17.0.1",
89
+ "@typescript-eslint/eslint-plugin": "^5.27.0",
90
+ "@typescript-eslint/parser": "^5.27.0",
91
  "@webcomponents/webcomponentsjs": "^2.6.0",
92
+ "cordova-res": "0.15.4",
93
+ "eslint": "^8.16.0",
94
  "husky": "^4.3.8",
95
  "ts-node": "^10.4.0",
96
  "typescript": "~4.5.4"
resources/README.md CHANGED
@@ -1,8 +1 @@
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/
1
+ We use [Ionic VSCode plugin](https://marketplace.visualstudio.com/items?itemName=ionic.ionic) to generate assets
 
 
 
 
 
 
 
resources/psd/default/background.psd CHANGED
Binary file
resources/psd/default/icon.psd CHANGED
Binary file
resources/psd/default/splash.psd CHANGED
Binary file
resources/psd/pwa/icon.psd CHANGED
Binary file
resources/psd/pwa/splash.psd CHANGED
Binary file
resources/web/HOW-TO.md CHANGED
@@ -7,18 +7,7 @@ First install ImageMagick
7
  brew install imagemagick
8
  ```
9
 
10
- ### Then run this command
11
- ```
12
- convert icon.png -thumbnail 128x128 -alpha on -background none -flatten favicon-128.png
13
- convert favicon-128.png -define icon:auto-resize:128,64,48,32,24,16 favicon-128.ico
14
-
15
- convert icon.png -thumbnail 64x64 -alpha on -background none -flatten favicon-64.png
16
- convert favicon-64.png -define icon:auto-resize:64,48,32,24,16 favicon-64.ico
17
- ```
18
-
19
- Between favicon-128.ico, and favicon-64.ico choose the onw that fit your image size budget for the favicon and renameit to favicon.ico
20
-
21
- # Generate the other icons
22
  ```
23
  convert icon.png -thumbnail 16x16 -alpha on -background none -flatten icon/favicon-16x16.png
24
  convert icon.png -thumbnail 24x24 -alpha on -background none -flatten icon/favicon-24x24.png
@@ -40,6 +29,17 @@ convert icon.png -thumbnail 512x512 -alpha on -background none -flatten icon/ico
40
  convert icon.png -thumbnail 1024x1024 -alpha on -background none -flatten icon/icon-1024x1024.png
41
  ```
42
 
 
 
 
 
 
 
 
 
 
 
 
43
  # Generate splash screens
44
  Useful tips on how to crop properly from (here)[https://askubuntu.com/a/762841/338320] and (here)[http://www.fmwconcepts.com/imagemagick/aspectcrop/index.php]
45
  ```
@@ -63,4 +63,4 @@ convert splash/splash-1536x2048.png -resize 1536x2048 -alpha on -background none
63
 
64
  ./aspectcrop -a 2048:2732 splash.png splash/splash-2048x2732.png
65
  convert splash/splash-2048x2732.png -resize 2048x2732 -alpha on -background none -flatten -gravity center -extent 2048x2732 splash/splash-2048x2732.png
66
- ```
7
  brew install imagemagick
8
  ```
9
 
10
+ # Generate icons
 
 
 
 
 
 
 
 
 
 
 
11
  ```
12
  convert icon.png -thumbnail 16x16 -alpha on -background none -flatten icon/favicon-16x16.png
13
  convert icon.png -thumbnail 24x24 -alpha on -background none -flatten icon/favicon-24x24.png
29
  convert icon.png -thumbnail 1024x1024 -alpha on -background none -flatten icon/icon-1024x1024.png
30
  ```
31
 
32
+ ### Then run this command
33
+ ```
34
+ convert icon/favicon-128x128.png -define icon:auto-resize:128,64,48,32,24,16 icon/favicon.ico
35
+
36
+ convert icon/favicon-64x64.png -define icon:auto-resize:64,48,32,24,16 icon/favicon-64.ico
37
+ ```
38
+
39
+ > Between favicon-128.ico, and favicon-64.ico choose the one that fit your image size budget for the favicon and rename it to favicon.ico
40
+
41
+ > Finally, copy all the icons to the `src/assets/icon/` folder
42
+
43
  # Generate splash screens
44
  Useful tips on how to crop properly from (here)[https://askubuntu.com/a/762841/338320] and (here)[http://www.fmwconcepts.com/imagemagick/aspectcrop/index.php]
45
  ```
63
 
64
  ./aspectcrop -a 2048:2732 splash.png splash/splash-2048x2732.png
65
  convert splash/splash-2048x2732.png -resize 2048x2732 -alpha on -background none -flatten -gravity center -extent 2048x2732 splash/splash-2048x2732.png
66
+ ```
resources/web/icon/favicon.ico CHANGED
Binary file
src/app/app.component.ts CHANGED
@@ -36,6 +36,7 @@ export class AppComponent {
36
  ionicIcon: 'notifications-outline'
37
  }
38
  ];
 
39
  accountPages = [
40
  {
41
  title: 'Log In',
@@ -78,9 +79,9 @@ export class AppComponent {
78
 
79
  async initializeApp() {
80
  try {
81
- await SplashScreen.hide();
82
  } catch (err) {
83
- console.log('This is normal in a browser', err);
84
  }
85
  }
86
 
36
  ionicIcon: 'notifications-outline'
37
  }
38
  ];
39
+
40
  accountPages = [
41
  {
42
  title: 'Log In',
79
 
80
  async initializeApp() {
81
  try {
82
+ await SplashScreen.hide();
83
  } catch (err) {
84
+ console.log('This is normal in a browser', err);
85
  }
86
  }
87
 
src/app/components/countdown-timer/countdown-timer.component.ts CHANGED
@@ -93,13 +93,14 @@ export class CountdownTimerComponent implements OnInit, OnDestroy {
93
  ngOnInit(): void {
94
  // I believe if we run this on SSR, it won't ever trigger the change detection and thus the server will be stuck loading
95
  if (isPlatformBrowser(this.platformId)) {
96
- this._updateInterval.pipe(takeUntil(this._unsubscribeSubject)).subscribe(
97
- (val) => {
 
98
  this.updateValues();
99
  },
100
- (error) => console.error(error),
101
- () => console.log('[takeUntil] complete')
102
- );
103
  } else {
104
  this.updateValues();
105
  }
93
  ngOnInit(): void {
94
  // I believe if we run this on SSR, it won't ever trigger the change detection and thus the server will be stuck loading
95
  if (isPlatformBrowser(this.platformId)) {
96
+ this._updateInterval.pipe(takeUntil(this._unsubscribeSubject))
97
+ .subscribe({
98
+ next: (val) => {
99
  this.updateValues();
100
  },
101
+ error: (error) => console.error(error),
102
+ complete: () => console.log('[takeUntil] complete')
103
+ });
104
  } else {
105
  this.updateValues();
106
  }
src/app/deals/details/deals-details.module.ts CHANGED
@@ -4,6 +4,8 @@ import { Routes, RouterModule } from '@angular/router';
4
 
5
  import { IonicModule } from '@ionic/angular';
6
 
 
 
7
  import { ComponentsModule } from '../../components/components.module';
8
  import { PipesModule } from '../../pipes/pipes.module';
9
 
@@ -27,7 +29,8 @@ const routes: Routes = [
27
  IonicModule,
28
  RouterModule.forChild(routes),
29
  ComponentsModule,
30
- PipesModule
 
31
  ],
32
  declarations: [
33
  DealsDetailsPage
4
 
5
  import { IonicModule } from '@ionic/angular';
6
 
7
+ import { SwiperModule } from 'swiper/angular';
8
+
9
  import { ComponentsModule } from '../../components/components.module';
10
  import { PipesModule } from '../../pipes/pipes.module';
11
 
29
  IonicModule,
30
  RouterModule.forChild(routes),
31
  ComponentsModule,
32
+ PipesModule,
33
+ SwiperModule
34
  ],
35
  declarations: [
36
  DealsDetailsPage
src/app/deals/details/deals-details.page.html CHANGED
@@ -15,15 +15,15 @@
15
 
16
  <div class="details-wrapper">
17
  <ion-row class="slider-row">
18
- <ion-slides class="details-slides" pager="true" [options]="slidesOptions">
19
- <ion-slide class="" *ngFor="let image of details?.showcaseImages">
20
  <ion-row class="slide-inner-row">
21
  <app-aspect-ratio [ratio]="{w: 56, h: 40}">
22
  <app-image-shell [src]="image" [alt]="'deals details'" class="showcase-image" animation="spinner"></app-image-shell>
23
  </app-aspect-ratio>
24
  </ion-row>
25
- </ion-slide>
26
- </ion-slides>
27
  </ion-row>
28
  <ion-row class="description-row">
29
  <ion-col class="logo-col" size="6">
15
 
16
  <div class="details-wrapper">
17
  <ion-row class="slider-row">
18
+ <swiper [pagination]="true" [config]="slidesOptions" class="details-slides">
19
+ <ng-template swiperSlide *ngFor="let image of details?.showcaseImages">
20
  <ion-row class="slide-inner-row">
21
  <app-aspect-ratio [ratio]="{w: 56, h: 40}">
22
  <app-image-shell [src]="image" [alt]="'deals details'" class="showcase-image" animation="spinner"></app-image-shell>
23
  </app-aspect-ratio>
24
  </ion-row>
25
+ </ng-template>
26
+ </swiper>
27
  </ion-row>
28
  <ion-row class="description-row">
29
  <ion-col class="logo-col" size="6">
src/app/deals/details/deals-details.page.ts CHANGED
@@ -1,10 +1,16 @@
1
  import { Component, OnInit, HostBinding } from '@angular/core';
2
  import { ActivatedRoute } from '@angular/router';
 
 
 
 
3
  import { Subscription } from 'rxjs';
 
4
 
5
  import { IResolvedRouteData, ResolverHelper } from '../../utils/resolver-helper';
6
  import { DealsDetailsModel } from './deals-details.model';
7
- import { switchMap } from 'rxjs/operators';
 
8
 
9
  @Component({
10
  selector: 'app-deals-details',
@@ -21,7 +27,7 @@ export class DealsDetailsPage implements OnInit {
21
  details: DealsDetailsModel;
22
  slidesOptions: any = {
23
  zoom: {
24
- toggle: false // Disable zooming to prevent weird double tap zomming on slide images
25
  }
26
  };
27
 
@@ -39,9 +45,12 @@ export class DealsDetailsPage implements OnInit {
39
  return ResolverHelper.extractData<DealsDetailsModel>(resolvedRouteData.data, DealsDetailsModel);
40
  })
41
  )
42
- .subscribe((state) => {
43
- this.details = state;
44
- }, (error) => console.log(error));
 
 
 
45
  }
46
 
47
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
1
  import { Component, OnInit, HostBinding } from '@angular/core';
2
  import { ActivatedRoute } from '@angular/router';
3
+ import { IonicSwiper } from "@ionic/angular";
4
+
5
+ import SwiperCore, { Pagination } from "swiper";
6
+
7
  import { Subscription } from 'rxjs';
8
+ import { switchMap } from 'rxjs/operators';
9
 
10
  import { IResolvedRouteData, ResolverHelper } from '../../utils/resolver-helper';
11
  import { DealsDetailsModel } from './deals-details.model';
12
+
13
+ SwiperCore.use([Pagination, IonicSwiper]);
14
 
15
  @Component({
16
  selector: 'app-deals-details',
27
  details: DealsDetailsModel;
28
  slidesOptions: any = {
29
  zoom: {
30
+ toggle: false // Disable zooming to prevent weird double tap zooming on slide images
31
  }
32
  };
33
 
45
  return ResolverHelper.extractData<DealsDetailsModel>(resolvedRouteData.data, DealsDetailsModel);
46
  })
47
  )
48
+ .subscribe({
49
+ next: (state) => {
50
+ this.details = state;
51
+ },
52
+ error: (error) => console.log(error)
53
+ });
54
  }
55
 
56
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
src/app/deals/details/styles/deals-details.page.scss CHANGED
@@ -108,7 +108,7 @@
108
  padding: 0px;
109
  // .swiper-pagination space
110
  padding-bottom: var(--page-swiper-pagination-space);
111
- // As we set ViewEncapsulation.ShadowDom, box-sizing get's resetted to content-box if I don't add this
112
  box-sizing: border-box;
113
  }
114
  }
@@ -268,8 +268,8 @@
268
  }
269
 
270
 
271
- // ISSUE: .swiper-paggination gets rendered dynamically. That prevents styling the elements when using the default Angular ViewEncapsulation.None
272
- // (Angular doesn't add an '_ngcontent' attribute to the .swiper-paggination because it's dynamically rendered)
273
  // FIX: See: https://stackoverflow.com/a/36265072/1116959
274
  :host ::ng-deep {
275
  .details-slides {
108
  padding: 0px;
109
  // .swiper-pagination space
110
  padding-bottom: var(--page-swiper-pagination-space);
111
+ // As we set ViewEncapsulation.ShadowDom, box-sizing gets resetted to content-box if I don't add this
112
  box-sizing: border-box;
113
  }
114
  }
268
  }
269
 
270
 
271
+ // ISSUE: .swiper-pagination gets rendered dynamically. That prevents styling the elements when using the default Angular ViewEncapsulation.None
272
+ // (Angular doesn't add an '_ngcontent' attribute to the .swiper-pagination because it's dynamically rendered)
273
  // FIX: See: https://stackoverflow.com/a/36265072/1116959
274
  :host ::ng-deep {
275
  .details-slides {
src/app/deals/listing/deals-listing.page.ts CHANGED
@@ -35,9 +35,12 @@ export class DealsListingPage implements OnInit {
35
  return ResolverHelper.extractData<DealsListingModel>(resolvedRouteData.data, DealsListingModel);
36
  })
37
  )
38
- .subscribe((state) => {
39
- this.listing = state;
40
- }, (error) => console.log(error));
 
 
 
41
  }
42
 
43
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
35
  return ResolverHelper.extractData<DealsListingModel>(resolvedRouteData.data, DealsListingModel);
36
  })
37
  )
38
+ .subscribe({
39
+ next: (state) => {
40
+ this.listing = state;
41
+ },
42
+ error: (error) => console.log(error)
43
+ });
44
  }
45
 
46
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
src/app/fashion/details/fashion-details.module.ts CHANGED
@@ -3,6 +3,9 @@ import { CommonModule } from '@angular/common';
3
  import { Routes, RouterModule } from '@angular/router';
4
 
5
  import { IonicModule } from '@ionic/angular';
 
 
 
6
  import { ComponentsModule } from '../../components/components.module';
7
 
8
  import { FashionService } from '../fashion.service';
@@ -24,7 +27,8 @@ const routes: Routes = [
24
  CommonModule,
25
  IonicModule,
26
  RouterModule.forChild(routes),
27
- ComponentsModule
 
28
  ],
29
  declarations: [
30
  FashionDetailsPage
3
  import { Routes, RouterModule } from '@angular/router';
4
 
5
  import { IonicModule } from '@ionic/angular';
6
+
7
+ import { SwiperModule } from 'swiper/angular';
8
+
9
  import { ComponentsModule } from '../../components/components.module';
10
 
11
  import { FashionService } from '../fashion.service';
27
  CommonModule,
28
  IonicModule,
29
  RouterModule.forChild(routes),
30
+ ComponentsModule,
31
+ SwiperModule
32
  ],
33
  declarations: [
34
  FashionDetailsPage
src/app/fashion/details/fashion-details.page.html CHANGED
@@ -9,16 +9,16 @@
9
 
10
  <ion-content class="fashion-details-content">
11
  <ion-row class="slider-row">
12
- <ion-slides class="details-slides" pager="true" [options]="slidesOptions">
13
- <ion-slide *ngFor="let image of details?.showcaseImages">
14
  <ion-row class="slide-inner-row">
15
  <app-image-shell [display]="'cover'" animation="spinner" class="showcase-image" [ngClass]="{'centered-image': (image.type === 'square'), 'fill-image': (image.type === 'fill')}" [src]="image.source">
16
  <app-aspect-ratio [ratio]="{w:64, h:50}">
17
  </app-aspect-ratio>
18
  </app-image-shell>
19
  </ion-row>
20
- </ion-slide>
21
- </ion-slides>
22
  </ion-row>
23
  <div class="description-wrapper">
24
  <h3 class="details-name">
9
 
10
  <ion-content class="fashion-details-content">
11
  <ion-row class="slider-row">
12
+ <swiper [pagination]="true" [config]="slidesOptions" class="details-slides">
13
+ <ng-template swiperSlide *ngFor="let image of details?.showcaseImages">
14
  <ion-row class="slide-inner-row">
15
  <app-image-shell [display]="'cover'" animation="spinner" class="showcase-image" [ngClass]="{'centered-image': (image.type === 'square'), 'fill-image': (image.type === 'fill')}" [src]="image.source">
16
  <app-aspect-ratio [ratio]="{w:64, h:50}">
17
  </app-aspect-ratio>
18
  </app-image-shell>
19
  </ion-row>
20
+ </ng-template>
21
+ </swiper>
22
  </ion-row>
23
  <div class="description-wrapper">
24
  <h3 class="details-name">
src/app/fashion/details/fashion-details.page.ts CHANGED
@@ -1,11 +1,17 @@
1
  import { Component, OnInit, HostBinding } from '@angular/core';
2
  import { ActivatedRoute } from '@angular/router';
3
  import { AlertController } from '@ionic/angular';
 
 
 
4
  import { Subscription } from 'rxjs';
 
5
 
6
  import { IResolvedRouteData, ResolverHelper } from '../../utils/resolver-helper';
7
  import { FashionDetailsModel } from './fashion-details.model';
8
- import { switchMap } from 'rxjs/operators';
 
 
9
 
10
  @Component({
11
  selector: 'app-fashion-details',
@@ -24,6 +30,7 @@ export class FashionDetailsPage implements OnInit {
24
  details: FashionDetailsModel;
25
  colorVariants = [];
26
  sizeVariants = [];
 
27
  slidesOptions: any = {
28
  zoom: {
29
  toggle: false // Disable zooming to prevent weird double tap zomming on slide images
@@ -48,31 +55,34 @@ export class FashionDetailsPage implements OnInit {
48
  return ResolverHelper.extractData<FashionDetailsModel>(resolvedRouteData.data, FashionDetailsModel);
49
  })
50
  )
51
- .subscribe((state) => {
52
- this.details = state;
53
-
54
- this.colorVariants = this.details.colorVariants
55
- .map(item =>
56
- ({
57
- name: item.name,
58
- type: 'radio',
59
- label: item.name,
60
- value: item.value,
61
- checked: item.default
62
- })
63
- );
64
-
65
- this.sizeVariants = this.details.sizeVariants
66
- .map(item =>
67
- ({
68
- name: item.name,
69
- type: 'radio',
70
- label: item.name,
71
- value: item.value,
72
- checked: item.default
73
- })
74
- );
75
- }, (error) => console.log(error));
 
 
 
76
  }
77
 
78
  async openColorChooser() {
1
  import { Component, OnInit, HostBinding } from '@angular/core';
2
  import { ActivatedRoute } from '@angular/router';
3
  import { AlertController } from '@ionic/angular';
4
+ import { IonicSwiper } from "@ionic/angular";
5
+ import SwiperCore, { Pagination } from "swiper";
6
+
7
  import { Subscription } from 'rxjs';
8
+ import { switchMap } from 'rxjs/operators';
9
 
10
  import { IResolvedRouteData, ResolverHelper } from '../../utils/resolver-helper';
11
  import { FashionDetailsModel } from './fashion-details.model';
12
+
13
+ SwiperCore.use([Pagination, IonicSwiper]);
14
+
15
 
16
  @Component({
17
  selector: 'app-fashion-details',
30
  details: FashionDetailsModel;
31
  colorVariants = [];
32
  sizeVariants = [];
33
+
34
  slidesOptions: any = {
35
  zoom: {
36
  toggle: false // Disable zooming to prevent weird double tap zomming on slide images
55
  return ResolverHelper.extractData<FashionDetailsModel>(resolvedRouteData.data, FashionDetailsModel);
56
  })
57
  )
58
+ .subscribe({
59
+ next: (state) => {
60
+ this.details = state;
61
+
62
+ this.colorVariants = this.details.colorVariants
63
+ .map(item =>
64
+ ({
65
+ name: item.name,
66
+ type: 'radio',
67
+ label: item.name,
68
+ value: item.value,
69
+ checked: item.default
70
+ })
71
+ );
72
+
73
+ this.sizeVariants = this.details.sizeVariants
74
+ .map(item =>
75
+ ({
76
+ name: item.name,
77
+ type: 'radio',
78
+ label: item.name,
79
+ value: item.value,
80
+ checked: item.default
81
+ })
82
+ );
83
+ },
84
+ error: (error) => console.log(error)
85
+ });
86
  }
87
 
88
  async openColorChooser() {
src/app/fashion/details/styles/fashion-details.page.scss CHANGED
@@ -216,8 +216,8 @@
216
  }
217
 
218
 
219
- // ISSUE: .swiper-paggination gets rendered dynamically. That prevents styling the elements when using the default Angular ViewEncapsulation.None
220
- // (Angular doesn't add an '_ngcontent' attribute to the .swiper-paggination because it's dynamically rendered)
221
  // FIX: See: https://stackoverflow.com/a/36265072/1116959
222
  :host ::ng-deep .details-slides {
223
  .swiper-pagination {
216
  }
217
 
218
 
219
+ // ISSUE: .swiper-pagination gets rendered dynamically. That prevents styling the elements when using the default Angular ViewEncapsulation.None
220
+ // (Angular doesn't add an '_ngcontent' attribute to the .swiper-pagination because it's dynamically rendered)
221
  // FIX: See: https://stackoverflow.com/a/36265072/1116959
222
  :host ::ng-deep .details-slides {
223
  .swiper-pagination {
src/app/fashion/listing/fashion-listing.page.ts CHANGED
@@ -34,9 +34,12 @@ export class FashionListingPage implements OnInit {
34
  return ResolverHelper.extractData<FashionListingModel>(resolvedRouteData.data, FashionListingModel);
35
  })
36
  )
37
- .subscribe((state) => {
38
- this.listing = state;
39
- }, (error) => console.log(error));
 
 
 
40
  }
41
 
42
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
34
  return ResolverHelper.extractData<FashionListingModel>(resolvedRouteData.data, FashionListingModel);
35
  })
36
  )
37
+ .subscribe({
38
+ next: (state) => {
39
+ this.listing = state;
40
+ },
41
+ error: (error) => console.log(error)
42
+ });
43
  }
44
 
45
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
src/app/firebase/auth/firebase-auth-definitions.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ export enum SignInProvider {
2
+ apple = 'apple.com',
3
+ facebook = 'facebook.com',
4
+ google = 'google.com',
5
+ twitter = 'twitter.com'
6
+ }
src/app/firebase/auth/firebase-auth.module.ts CHANGED
@@ -2,17 +2,22 @@ import { NgModule } from '@angular/core';
2
  import { CommonModule } from '@angular/common';
3
  import { Routes, RouterModule } from '@angular/router';
4
  import { IonicModule } from '@ionic/angular';
 
 
 
 
 
 
5
  import { ComponentsModule } from '../../components/components.module';
6
- import { AngularFireAuthModule } from '@angular/fire/auth';
7
- import { AngularFireModule } from '@angular/fire';
8
  import { environment } from '../../../environments/environment';
9
  import { FirebaseAuthService } from './firebase-auth.service';
10
 
 
11
  const routes: Routes = [
12
  {
13
  path: '',
14
  children: [
15
- // /firebase/auth redirect
16
  {
17
  path: '',
18
  redirectTo: 'sign-in',
@@ -40,8 +45,19 @@ const routes: Routes = [
40
  IonicModule,
41
  ComponentsModule,
42
  RouterModule.forChild(routes),
43
- AngularFireModule.initializeApp(environment.firebase),
44
- AngularFireAuthModule
 
 
 
 
 
 
 
 
 
 
 
45
  ],
46
  providers: [FirebaseAuthService]
47
  })
2
  import { CommonModule } from '@angular/common';
3
  import { Routes, RouterModule } from '@angular/router';
4
  import { IonicModule } from '@ionic/angular';
5
+
6
+ import { Capacitor } from '@capacitor/core';
7
+
8
+ import { getApp, initializeApp, provideFirebaseApp } from '@angular/fire/app';
9
+ import { provideAuth, getAuth, initializeAuth, indexedDBLocalPersistence } from '@angular/fire/auth';
10
+
11
  import { ComponentsModule } from '../../components/components.module';
 
 
12
  import { environment } from '../../../environments/environment';
13
  import { FirebaseAuthService } from './firebase-auth.service';
14
 
15
+
16
  const routes: Routes = [
17
  {
18
  path: '',
19
  children: [
20
+ // ? /firebase/auth redirect
21
  {
22
  path: '',
23
  redirectTo: 'sign-in',
45
  IonicModule,
46
  ComponentsModule,
47
  RouterModule.forChild(routes),
48
+ // ? Correct way to initialize Firebase using the Capacitor Firebase plugin mixed with the Firebase JS SDK (@angular/fire)
49
+ provideFirebaseApp(() => initializeApp(environment.firebase)),
50
+ provideAuth(() => {
51
+ if (Capacitor.isNativePlatform()) {
52
+ return initializeAuth(getApp(), {
53
+ persistence: indexedDBLocalPersistence
54
+ // persistence: browserLocalPersistence
55
+ // popupRedirectResolver: browserPopupRedirectResolver
56
+ });
57
+ } else {
58
+ return getAuth();
59
+ }
60
+ })
61
  ],
62
  providers: [FirebaseAuthService]
63
  })
src/app/firebase/auth/firebase-auth.service.ts CHANGED
@@ -1,173 +1,508 @@
1
- import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
2
- import { AngularFireAuth } from '@angular/fire/auth';
3
- import { Observable, Subject, from, of } from 'rxjs';
4
- import { DataStore } from '../../shell/data-store';
5
- import { FirebaseProfileModel } from './profile/firebase-profile.model';
6
- import { Platform } from '@ionic/angular';
7
  import { filter, map } from 'rxjs/operators';
8
 
9
- import firebase from 'firebase/app';
10
- import { cfaSignIn, cfaSignOut } from 'capacitor-firebase-auth';
11
- import { isPlatformBrowser } from '@angular/common';
 
 
 
 
 
 
 
 
 
12
 
13
- @Injectable()
14
- export class FirebaseAuthService {
15
 
16
- currentUser: firebase.User;
 
 
 
 
 
17
  profileDataStore: DataStore<FirebaseProfileModel>;
18
- redirectResult: Subject<any> = new Subject<any>();
 
19
 
20
  constructor(
21
- public angularFire: AngularFireAuth,
 
22
  public platform: Platform,
 
 
 
23
  @Inject(PLATFORM_ID) private platformId: object
24
  ) {
25
  if (isPlatformBrowser(this.platformId)) {
26
- this.angularFire.onAuthStateChanged((user) => {
27
- if (user) {
28
- // User is signed in.
29
- this.currentUser = user;
30
- } else {
31
- // No user is signed in.
32
- this.currentUser = null;
33
- }
34
- });
35
 
36
- if (!this.platform.is('capacitor')) {
37
- // when using signInWithRedirect, this listens for the redirect results
38
- this.angularFire.getRedirectResult()
39
- .then((result) => {
40
- // result.credential.accessToken gives you the Provider Access Token. You can use it to access the Provider API.
41
- const user: any = result.user || this.currentUser;
42
-
43
- if (user) {
44
- this.redirectResult.next(user);
45
  }
46
- }, (error) => {
47
- this.redirectResult.next({error: error.code});
48
  });
49
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  }
51
  }
52
 
53
- getRedirectResult(): Observable<any> {
54
- return this.redirectResult.asObservable();
55
  }
56
 
57
- getPhotoURL(signInProviderId: string, photoURL: string): string {
58
- // Default imgs are too small and our app needs a bigger image
59
- switch (signInProviderId) {
60
- case 'facebook.com':
61
- return photoURL + '?height=400';
62
- case 'password':
63
- return 'https://s3-us-west-2.amazonaws.com/ionicthemes/otros/avatar-placeholder.png';
64
- case 'twitter.com':
65
- return photoURL.replace('_normal', '_400x400');
66
- case 'google.com':
67
- return photoURL.split('=')[0];
68
- default:
69
- return photoURL;
70
- }
 
 
 
 
 
 
 
 
71
  }
72
 
73
- signOut(): Observable<any> {
 
 
 
 
74
  if (this.platform.is('capacitor')) {
75
- return cfaSignOut();
 
 
 
 
 
 
 
 
76
  } else {
77
- return from(this.angularFire.signOut());
78
  }
79
  }
80
 
81
- signInWithEmail(email: string, password: string): Promise<firebase.auth.UserCredential> {
82
- return this.angularFire.signInWithEmailAndPassword(email, password);
 
 
83
  }
84
 
85
- signUpWithEmail(email: string, password: string): Promise<firebase.auth.UserCredential> {
86
- return this.angularFire.createUserWithEmailAndPassword(email, password);
 
 
87
  }
88
 
89
- socialSignIn(providerName: string, scopes?: Array<string>): Observable<any> {
90
- if (this.platform.is('capacitor')) {
91
- return cfaSignIn(providerName);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  } else {
93
- const provider = new firebase.auth.OAuthProvider(providerName);
 
 
94
 
95
- if (scopes) {
96
- scopes.forEach(scope => {
97
- provider.addScope(scope);
98
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  }
100
 
101
- if (this.platform.is('desktop')) {
102
- return from(this.angularFire.signInWithPopup(provider));
103
- } else {
104
- // web but not desktop, for example mobile PWA
105
- return from(this.angularFire.signInWithRedirect(provider));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  }
 
 
 
 
 
 
 
107
  }
108
  }
109
 
110
- signInWithFacebook() {
111
- const provider = new firebase.auth.FacebookAuthProvider();
112
  const scopes = ['email'];
113
- return this.socialSignIn(provider.providerId, scopes);
 
 
114
  }
115
 
116
- signInWithGoogle() {
117
- const provider = new firebase.auth.GoogleAuthProvider();
118
  const scopes = ['profile', 'email'];
119
- return this.socialSignIn(provider.providerId, scopes);
 
 
120
  }
121
 
122
- signInWithTwitter() {
123
- const provider = new firebase.auth.TwitterAuthProvider();
124
  const scopes = ['name', 'email'];
125
- return this.socialSignIn(provider.providerId, scopes);
 
 
126
  }
127
 
128
- signInWithApple() {
129
- const provider = new firebase.auth.OAuthProvider('apple.com');
130
  const scopes = ['name', 'email'];
131
- return this.socialSignIn(provider.providerId, scopes);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  }
133
 
134
  public getProfileDataSource(): Observable<FirebaseProfileModel> {
135
- // we need to do this differentiation because there is a bug in
136
- // platform capacitor ios when executing this.angularFire.user
137
- if (this.platform.is('capacitor')) {
138
- return of(this.setUserModelForProfile());
139
- } else {
140
- return this.angularFire.user
141
- .pipe(
142
- filter((user: firebase.User) => user != null),
143
- map((user: firebase.User) => {
144
- return this.setUserModelForProfile();
145
- })
146
- );
147
- }
148
  }
149
 
150
- private setUserModelForProfile(): FirebaseProfileModel {
151
  const userModel = new FirebaseProfileModel();
152
- const provierData = this.currentUser.providerData[0];
153
- const userData: any = provierData;
154
- userModel.image = this.getPhotoURL(provierData.providerId, provierData.photoURL);
155
- userModel.name = userData.displayName || 'What\'s your name?';
156
- userModel.role = 'How would you describe yourself?';
157
- userModel.description = userData.description || 'Anything else you would like to share with the world?';
158
- userModel.phoneNumber = userData.phoneNumber || 'Is there a number where I can reach you?';
159
- userModel.email = userData.email || 'Where can I send you emails?';
160
- userModel.provider = (provierData.providerId !== 'password') ? provierData.providerId : 'Credentials';
 
161
 
162
  return userModel;
163
  }
164
 
165
  public getProfileStore(dataSource: Observable<FirebaseProfileModel>): DataStore<FirebaseProfileModel> {
166
- // Initialize the model specifying that it is a shell model
167
  const shellModel: FirebaseProfileModel = new FirebaseProfileModel();
168
  this.profileDataStore = new DataStore(shellModel);
169
- // Trigger the loading mechanism (with shell) in the dataStore
170
  this.profileDataStore.load(dataSource);
171
  return this.profileDataStore;
172
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  }
1
+ import { Inject, Injectable, NgZone, OnDestroy, PLATFORM_ID } from '@angular/core';
2
+ import { isPlatformBrowser, Location } from '@angular/common';
3
+ import { ActivatedRoute, Router } from '@angular/router';
4
+ import { LoadingController, Platform } from '@ionic/angular';
5
+
6
+ import { Observable, Subject, of } from 'rxjs';
7
  import { filter, map } from 'rxjs/operators';
8
 
9
+ import { AuthProvider, FacebookAuthProvider, GoogleAuthProvider, TwitterAuthProvider, OAuthProvider, OAuthCredential, UserCredential, createUserWithEmailAndPassword, getAuth, getRedirectResult, signInWithCredential, signInWithEmailAndPassword, signInWithPopup, signInWithRedirect, signOut } from '@angular/fire/auth';
10
+
11
+ import type {
12
+ AuthCredential as FirebaseAuthCredential,
13
+ User as FirebaseUser,
14
+ } from '@angular/fire/auth';
15
+
16
+ import { AuthCredential, AuthStateChange, FirebaseAuthentication, SignInResult, User } from '@capacitor-firebase/authentication';
17
+
18
+ import { DataStore } from '../../shell/data-store';
19
+ import { FirebaseProfileModel } from './profile/firebase-profile.model';
20
+ import { SignInProvider } from './firebase-auth-definitions';
21
 
 
 
22
 
23
+ @Injectable({
24
+ providedIn: 'root'
25
+ })
26
+ export class FirebaseAuthService implements OnDestroy {
27
+ currentUser: User;
28
+ authLoader: HTMLIonLoadingElement;
29
  profileDataStore: DataStore<FirebaseProfileModel>;
30
+ redirectResultSubject: Subject<any> = new Subject<any>();
31
+ authStateSubject: Subject<AuthStateChange> = new Subject<AuthStateChange>();
32
 
33
  constructor(
34
+ public router: Router,
35
+ public route: ActivatedRoute,
36
  public platform: Platform,
37
+ private ngZone: NgZone,
38
+ public loadingController: LoadingController,
39
+ public location: Location,
40
  @Inject(PLATFORM_ID) private platformId: object
41
  ) {
42
  if (isPlatformBrowser(this.platformId)) {
43
+ FirebaseAuthentication.removeAllListeners().then(() => {
44
+ FirebaseAuthentication.addListener('authStateChange', (change: AuthStateChange) => {
45
+ this.ngZone.run(() => {
46
+ this.authStateSubject.next(change);
47
+ });
 
 
 
 
48
 
49
+ if (change?.user) {
50
+ // ? User is signed in.
51
+ this.currentUser = change.user;
52
+ } else {
53
+ // ? No user is signed in.
54
+ this.currentUser = null;
 
 
 
55
  }
 
 
56
  });
57
+ });
58
+
59
+ // ? We should only listen for firebase auth redirect results when we have the flag 'auth-redirect' in the query params
60
+ this.route.queryParams.subscribe(params => {
61
+ const authProvider = params['auth-redirect'];
62
+
63
+ if (authProvider) {
64
+ // ? Show a loader while we receive the getRedirectResult notification
65
+ this.presentLoading(authProvider);
66
+
67
+ // ? When using signInWithRedirect, this listens for the redirect results
68
+ const auth = getAuth();
69
+ getRedirectResult(auth)
70
+ .then((result: UserCredential) => {
71
+ // ? result.credential.accessToken gives you the Provider Access Token. You can use it to access the Provider API.
72
+ // const credential = FacebookAuthProvider.credentialFromResult(result);
73
+ // const token = credential.accessToken;
74
+
75
+ let credential: any;
76
+
77
+ if (result && result !== null) {
78
+ switch (result.providerId) {
79
+ case SignInProvider.apple:
80
+ credential = OAuthProvider.credentialFromResult(result);
81
+ break;
82
+ case SignInProvider.facebook:
83
+ credential = FacebookAuthProvider.credentialFromResult(result);
84
+ break;
85
+ case SignInProvider.google:
86
+ credential = GoogleAuthProvider.credentialFromResult(result);
87
+ break;
88
+ case SignInProvider.twitter:
89
+ credential = TwitterAuthProvider.credentialFromResult(result);
90
+ break;
91
+ }
92
+
93
+ const signInResult = this.createSignInResult(result.user, credential);
94
+
95
+ this.dismissLoading();
96
+
97
+ this.redirectResultSubject.next(signInResult);
98
+ } else {
99
+ throw new Error('Could not get user from redirect result');
100
+ }
101
+ }, (reason) => {
102
+ console.log('Promise rejected', reason);
103
+
104
+ // ? Clear redirection loading
105
+ this.clearAuthWithProvidersRedirection();
106
+ }).catch((error) => {
107
+ // ? Clear redirection loading
108
+ this.clearAuthWithProvidersRedirection();
109
+
110
+ // ? Handle Errors here
111
+ // const errorCode = error.code;
112
+ // const errorMessage = error.message;
113
+ // ? The email of the user's account used.
114
+ // const email = error.email;
115
+ // ?AuthCredential type that was used.
116
+ // const credential = FacebookAuthProvider.credentialFromError(error);
117
+
118
+ let errorResult = {error: 'undefined'};
119
+
120
+ if (error && (error.code || error.message)) {
121
+ errorResult = {error: (error.code ? error.code : error.message)};
122
+ }
123
+
124
+ this.redirectResultSubject.next(errorResult);
125
+ });
126
+ }
127
+ });
128
  }
129
  }
130
 
131
+ ngOnDestroy(): void {
132
+ this.dismissLoading();
133
  }
134
 
135
+ public async signOut(): Promise<string> {
136
+ const signOutPromise = new Promise<string>((resolve, reject) => {
137
+ // * 1. Sign out on the native layer
138
+ FirebaseAuthentication.signOut()
139
+ .then((nativeResult) => {
140
+ // * 2. Sign out on the web layer
141
+ const auth = getAuth();
142
+ signOut(auth)
143
+ .then((webResult) => {
144
+ // ? Sign-out successful
145
+ resolve('Successfully sign out from native and web');
146
+ }).catch((webError) => {
147
+ // ? An error happened
148
+ reject(`Web auth sign out error: ${webError}`);
149
+ });
150
+ })
151
+ .catch((nativeError) => {
152
+ reject(`Native auth sign out error: ${nativeError}`);
153
+ });
154
+ });
155
+
156
+ return signOutPromise;
157
  }
158
 
159
+ private async socialSignIn(provider: AuthProvider, scopes?: Array<string>): Promise<SignInResult> {
160
+ this.presentLoading(provider.providerId);
161
+
162
+ let authResult: SignInResult = null;
163
+
164
  if (this.platform.is('capacitor')) {
165
+ authResult = await this.nativeAuth(provider, scopes);
166
+ } else {
167
+ authResult = await this.webAuth(provider);
168
+ }
169
+
170
+ this.dismissLoading();
171
+
172
+ if (authResult !== null) {
173
+ return authResult;
174
  } else {
175
+ return Promise.reject('Could not perform social sign in, authResult is null');
176
  }
177
  }
178
 
179
+ private prepareForAuthWithProvidersRedirection(authProviderId: string): void {
180
+ // ? Before invoking auth provider redirect flow, add a flag to the path.
181
+ // ? The presence of the flag in the path indicates we should wait for the auth redirect to complete
182
+ this.location.replaceState(this.location.path(), 'auth-redirect=' + authProviderId, this.location.getState());
183
  }
184
 
185
+ private clearAuthWithProvidersRedirection(): void {
186
+ // ? Remove auth-redirect param from url
187
+ this.location.replaceState(this.router.url.split('?')[0], '');
188
+ this.dismissLoading();
189
  }
190
 
191
+ private async presentLoading(authProviderId?: string): Promise<void> {
192
+ const authProviderCapitalized = authProviderId[0].toUpperCase() + authProviderId.slice(1);
193
+
194
+ this.loadingController.create({
195
+ message: authProviderId ? 'Signing in with ' + authProviderCapitalized : 'Signing in ...',
196
+ duration: 4000
197
+ }).then((loader) => {
198
+ this.authLoader = loader;
199
+ this.authLoader.present();
200
+ });
201
+ }
202
+
203
+ private async dismissLoading(): Promise<void> {
204
+ if (this.authLoader) {
205
+ await this.authLoader.dismiss();
206
+ }
207
+ }
208
+
209
+ private async webAuth(provider: AuthProvider, scopes?: Array<string>): Promise<SignInResult> {
210
+ // ? Scopes for Firebase JS SDK auth
211
+ if (scopes) {
212
+ let providerWithScopes: any;
213
+
214
+ switch (provider.providerId) {
215
+ case SignInProvider.apple:
216
+ providerWithScopes = (provider as OAuthProvider);
217
+ break;
218
+ case SignInProvider.facebook:
219
+ providerWithScopes = (provider as FacebookAuthProvider);
220
+ break;
221
+ case SignInProvider.google:
222
+ providerWithScopes = (provider as GoogleAuthProvider);
223
+ break;
224
+ case SignInProvider.twitter:
225
+ providerWithScopes = (provider as TwitterAuthProvider);
226
+ break;
227
+ }
228
+
229
+ scopes.forEach(scope => {
230
+ providerWithScopes.addScope(scope);
231
+ });
232
+
233
+ provider = providerWithScopes;
234
+ }
235
+
236
+ const auth = getAuth();
237
+ let webAuthUserCredential: UserCredential = null;
238
+
239
+ if (this.platform.is('desktop')) {
240
+ webAuthUserCredential = await signInWithPopup(auth, provider);
241
  } else {
242
+ // ? Web but not desktop, for example mobile PWA
243
+ this.prepareForAuthWithProvidersRedirection(provider.providerId);
244
+ return signInWithRedirect(auth, provider);
245
 
246
+ // ? If you prefer to use signInWithPopup in every scenario, just un-comment this line
247
+ // webAuthUserCredential = await signInWithPopup(auth, provider);
248
+ }
249
+
250
+ if (webAuthUserCredential && webAuthUserCredential !== null) {
251
+ let webCredential: OAuthCredential = null;
252
+
253
+ switch (provider.providerId) {
254
+ case SignInProvider.apple:
255
+ webCredential = OAuthProvider.credentialFromResult(webAuthUserCredential);
256
+ break;
257
+ case SignInProvider.facebook:
258
+ webCredential = FacebookAuthProvider.credentialFromResult(webAuthUserCredential);
259
+ break;
260
+ case SignInProvider.google:
261
+ webCredential = GoogleAuthProvider.credentialFromResult(webAuthUserCredential);
262
+ break;
263
+ case SignInProvider.twitter:
264
+ webCredential = TwitterAuthProvider.credentialFromResult(webAuthUserCredential);
265
+ break;
266
  }
267
 
268
+ return this.createSignInResult(webAuthUserCredential.user, webCredential);
269
+ } else {
270
+ return Promise.reject('null webAuthUserCredential');
271
+ }
272
+ }
273
+
274
+ private async nativeAuth(provider: AuthProvider, scopes?: Array<string>): Promise<SignInResult> {
275
+ let nativeAuthResult: SignInResult = null;
276
+
277
+ // ? Scopes for Firebase native SDK (iOS and Android)
278
+ // TODO: Scopes for Firebase native SDK auth is a work in progress yet
279
+ // (see: https://github.com/robingenz/capacitor-firebase/issues/32)
280
+
281
+
282
+ // * 1. Sign in on the native layer
283
+ switch (provider.providerId) {
284
+ case SignInProvider.apple:
285
+ nativeAuthResult = await FirebaseAuthentication.signInWithApple();
286
+ break;
287
+ case SignInProvider.facebook:
288
+ nativeAuthResult = await FirebaseAuthentication.signInWithFacebook();
289
+ break;
290
+ case SignInProvider.google:
291
+ nativeAuthResult = await FirebaseAuthentication.signInWithGoogle();
292
+ break;
293
+ case SignInProvider.twitter:
294
+ nativeAuthResult = await FirebaseAuthentication.signInWithTwitter();
295
+ break;
296
+ }
297
+
298
+ // ? Once we have the user authenticated on the native layer, authenticate it in the web layer
299
+ if (nativeAuthResult && nativeAuthResult !== null) {
300
+ const auth = getAuth();
301
+ let nativeCredential: OAuthCredential = null;
302
+
303
+ switch (provider.providerId) {
304
+ case SignInProvider.apple:
305
+ const provider = new OAuthProvider(SignInProvider.apple);
306
+ nativeCredential = provider.credential({
307
+ idToken: nativeAuthResult.credential?.idToken,
308
+ rawNonce: nativeAuthResult.credential?.nonce
309
+ });
310
+ break;
311
+ case SignInProvider.facebook:
312
+ nativeCredential = FacebookAuthProvider.credential(
313
+ nativeAuthResult.credential?.accessToken
314
+ );
315
+ break;
316
+ case SignInProvider.google:
317
+ nativeCredential = GoogleAuthProvider.credential(nativeAuthResult.credential?.idToken, nativeAuthResult.credential?.accessToken);
318
+ break;
319
+ case SignInProvider.twitter:
320
+ try {
321
+ nativeCredential = TwitterAuthProvider.credential(nativeAuthResult.credential?.accessToken, nativeAuthResult.credential?.secret);
322
+ break;
323
+ } catch (error) {
324
+ console.error(error);
325
+ }
326
  }
327
+
328
+ // * 2. Sign in on the web layer using the access token we got from the native sign in
329
+ const webAuthResult = await signInWithCredential(auth, nativeCredential);
330
+
331
+ return this.createSignInResult(webAuthResult.user, nativeCredential);
332
+ } else {
333
+ return Promise.reject('null nativeAuthResult');
334
  }
335
  }
336
 
337
+ public async signInWithFacebook(): Promise<SignInResult> {
338
+ const provider = new FacebookAuthProvider();
339
  const scopes = ['email'];
340
+
341
+ // ? When we use the redirect authentication flow, the code below the socialSignIn() invocation does not get executed as we leave the current page
342
+ return this.socialSignIn(provider, scopes);
343
  }
344
 
345
+ public async signInWithGoogle(): Promise<SignInResult> {
346
+ const provider = new GoogleAuthProvider();
347
  const scopes = ['profile', 'email'];
348
+
349
+ // ? When we use the redirect authentication flow, the code below the socialSignIn() invocation does not get executed as we leave the current page
350
+ return this.socialSignIn(provider, scopes);
351
  }
352
 
353
+ public async signInWithTwitter(): Promise<SignInResult> {
354
+ const provider = new TwitterAuthProvider();
355
  const scopes = ['name', 'email'];
356
+
357
+ // ? When we use the redirect authentication flow, the code below the socialSignIn() invocation does not get executed as we leave the current page
358
+ return this.socialSignIn(provider, scopes);
359
  }
360
 
361
+ public async signInWithApple(): Promise<SignInResult> {
362
+ const provider = new OAuthProvider('apple.com');
363
  const scopes = ['name', 'email'];
364
+
365
+ // ? When we use the redirect authentication flow, the code below the socialSignIn() invocation does not get executed as we leave the current page
366
+ return this.socialSignIn(provider, scopes);
367
+ }
368
+
369
+ public async signInWithEmail(email: string, password: string): Promise<SignInResult> {
370
+ // ? Show a loader while we attempt to perform the login
371
+ this.presentLoading('email');
372
+
373
+ const auth = getAuth();
374
+ const credential = await signInWithEmailAndPassword(auth, email, password);
375
+
376
+ this.dismissLoading();
377
+
378
+ return this.createSignInResultFromUserCredential(credential);
379
+ }
380
+
381
+ public async signUpWithEmail(email: string, password: string): Promise<SignInResult> {
382
+ // ? Show a loader while we attempt to perform the signup
383
+ this.presentLoading('email');
384
+
385
+ const auth = getAuth();
386
+ const credential = await createUserWithEmailAndPassword(auth, email, password);
387
+
388
+ this.dismissLoading();
389
+
390
+ return this.createSignInResultFromUserCredential(credential);
391
+ }
392
+
393
+ public get redirectResult$(): Observable<any> {
394
+ return this.redirectResultSubject.asObservable();
395
+ }
396
+
397
+ public get authState$(): Observable<AuthStateChange> {
398
+ return this.authStateSubject.asObservable();
399
  }
400
 
401
  public getProfileDataSource(): Observable<FirebaseProfileModel> {
402
+ const auth = getAuth();
403
+ return of(auth.currentUser)
404
+ .pipe(
405
+ filter((user: FirebaseUser) => user != null),
406
+ map((user: FirebaseUser) => {
407
+ const userResult = this.createUserResult(user);
408
+ return this.setUserModelForProfile(userResult);
409
+ })
410
+ );
 
 
 
 
411
  }
412
 
413
+ private setUserModelForProfile(userResult?: (User | null)): FirebaseProfileModel {
414
  const userModel = new FirebaseProfileModel();
415
+
416
+ if (userResult) {
417
+ userModel.image = this.getPhotoURL(userResult.providerId, userResult.photoUrl);
418
+ userModel.name = userResult.displayName || 'What\'s your name?';
419
+ userModel.role = 'How would you describe yourself?';
420
+ userModel.description = 'Anything else you would like to share with the world?';
421
+ userModel.phoneNumber = userResult.phoneNumber || 'Is there a number where I can reach you?';
422
+ userModel.email = userResult.email || 'Where can I send you emails?';
423
+ userModel.provider = (userResult.providerId !== 'password') ? userResult.providerId : 'Credentials';
424
+ }
425
 
426
  return userModel;
427
  }
428
 
429
  public getProfileStore(dataSource: Observable<FirebaseProfileModel>): DataStore<FirebaseProfileModel> {
430
+ // ? Initialize the model specifying that it is a shell model
431
  const shellModel: FirebaseProfileModel = new FirebaseProfileModel();
432
  this.profileDataStore = new DataStore(shellModel);
433
+ // ? Trigger the loading mechanism (with shell) in the dataStore
434
  this.profileDataStore.load(dataSource);
435
  return this.profileDataStore;
436
  }
437
+
438
+ private getPhotoURL(signInProviderId: string, photoURL: string): string {
439
+ // ? Default imgs are too small and our app needs a bigger image
440
+ switch (signInProviderId) {
441
+ case SignInProvider.facebook:
442
+ return photoURL + '?height=400';
443
+ case SignInProvider.twitter:
444
+ return photoURL.replace('_normal', '_400x400');
445
+ case SignInProvider.google:
446
+ return photoURL.split('=')[0];
447
+ case 'password':
448
+ return 'https://s3-us-west-2.amazonaws.com/ionicthemes/otros/avatar-placeholder.png';
449
+ default:
450
+ return photoURL;
451
+ }
452
+ }
453
+
454
+ // * Aux methods inspired on the @capacitor-firebase/authentication library
455
+
456
+ // (see: https://github.com/robingenz/capacitor-firebase/blob/a51927ff3acce94cedcd7bfc218952bb106db904/packages/authentication/src/web.ts#L297)
457
+ private createSignInResultFromUserCredential(credential: UserCredential): SignInResult {
458
+ const userResult = this.createUserResult(credential.user);
459
+ const result: SignInResult = {
460
+ user: userResult,
461
+ credential: null,
462
+ };
463
+ return result;
464
+ }
465
+
466
+ private createSignInResult(user: FirebaseUser, credential: FirebaseAuthCredential | null): SignInResult {
467
+ const userResult = this.createUserResult(user);
468
+ const credentialResult = this.createCredentialResult(credential);
469
+ const result: SignInResult = {
470
+ user: userResult,
471
+ credential: credentialResult,
472
+ };
473
+ return result;
474
+ }
475
+
476
+ private createUserResult(user: FirebaseUser | null): User | null {
477
+ if (!user) {
478
+ return null;
479
+ }
480
+ const result: User = {
481
+ displayName: user.displayName,
482
+ email: user.email,
483
+ emailVerified: user.emailVerified,
484
+ isAnonymous: user.isAnonymous,
485
+ phoneNumber: user.phoneNumber,
486
+ photoUrl: user.photoURL,
487
+ providerId: user.providerId,
488
+ tenantId: user.tenantId,
489
+ uid: user.uid,
490
+ };
491
+ return result;
492
+ }
493
+
494
+ private createCredentialResult(credential: FirebaseAuthCredential | null): AuthCredential | null {
495
+ if (!credential) {
496
+ return null;
497
+ }
498
+ const result: AuthCredential = {
499
+ providerId: credential.providerId,
500
+ };
501
+ if (credential instanceof OAuthCredential) {
502
+ result.accessToken = credential.accessToken;
503
+ result.idToken = credential.idToken;
504
+ result.secret = credential.secret;
505
+ }
506
+ return result;
507
+ }
508
  }
src/app/firebase/auth/profile/firebase-profile.module.ts CHANGED
@@ -3,22 +3,24 @@ import { CommonModule } from '@angular/common';
3
  import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4
  import { Routes, RouterModule } from '@angular/router';
5
  import { IonicModule } from '@ionic/angular';
 
 
 
6
  import { ComponentsModule } from '../../../components/components.module';
7
  import { FirebaseProfilePage } from './firebase-profile.page';
8
  import { FirebaseProfileResolver } from './firebase-profile.resolver';
9
- import { AngularFireAuthGuard, redirectUnauthorizedTo } from '@angular/fire/auth-guard';
10
 
11
- const redirectUnauthorizedToLogin = () => redirectUnauthorizedTo(['/firebase/auth/sign-in']);
 
12
 
13
  const routes: Routes = [
14
  {
15
  path: '',
16
  component: FirebaseProfilePage,
17
- canActivate: [AngularFireAuthGuard],
18
- data: { authGuardPipe: redirectUnauthorizedToLogin },
19
  resolve: {
20
  data: FirebaseProfileResolver
21
- }
 
22
  }
23
  ];
24
 
3
  import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4
  import { Routes, RouterModule } from '@angular/router';
5
  import { IonicModule } from '@ionic/angular';
6
+
7
+ import { redirectUnauthorizedTo, canActivate, AuthPipe } from '@angular/fire/auth-guard';
8
+
9
  import { ComponentsModule } from '../../../components/components.module';
10
  import { FirebaseProfilePage } from './firebase-profile.page';
11
  import { FirebaseProfileResolver } from './firebase-profile.resolver';
 
12
 
13
+
14
+ const redirectUnauthorizedToLogin: () => AuthPipe = () => redirectUnauthorizedTo(['/firebase/auth/sign-in']);
15
 
16
  const routes: Routes = [
17
  {
18
  path: '',
19
  component: FirebaseProfilePage,
 
 
20
  resolve: {
21
  data: FirebaseProfileResolver
22
+ },
23
+ ...canActivate(redirectUnauthorizedToLogin)
24
  }
25
  ];
26
 
src/app/firebase/auth/profile/firebase-profile.page.ts CHANGED
@@ -1,11 +1,15 @@
1
  import { Component, OnInit, HostBinding } from '@angular/core';
2
  import { ActivatedRoute, Router } from '@angular/router';
3
- import { FirebaseProfileModel } from './firebase-profile.model';
4
- import { FirebaseAuthService } from '../firebase-auth.service';
5
  import { Subscription } from 'rxjs';
6
- import { IResolvedRouteData, ResolverHelper } from '../../../utils/resolver-helper';
7
  import { switchMap } from 'rxjs/operators';
8
 
 
 
 
 
 
 
9
  @Component({
10
  selector: 'app-firebase-profile',
11
  templateUrl: './firebase-profile.page.html',
@@ -15,7 +19,7 @@ import { switchMap } from 'rxjs/operators';
15
  ]
16
  })
17
  export class FirebaseProfilePage implements OnInit {
18
- // Gather all component subscription in one place. Can be one Subscription or multiple (chained using the Subscription.add() method)
19
  subscriptions: Subscription;
20
  user: FirebaseProfileModel;
21
 
@@ -30,31 +34,40 @@ export class FirebaseProfilePage implements OnInit {
30
  ) {}
31
 
32
  ngOnInit() {
33
-
34
  this.subscriptions = this.route.data
35
  .pipe(
36
- // Extract data for this page
37
  switchMap((resolvedRouteData: IResolvedRouteData<FirebaseProfileModel>) => {
38
  return ResolverHelper.extractData<FirebaseProfileModel>(resolvedRouteData.data, FirebaseProfileModel);
39
  })
40
  )
41
- .subscribe((state) => {
42
- this.user = state;
43
- }, (error) => console.log(error));
 
 
 
44
  }
45
 
46
- signOut() {
47
- this.authService.signOut().subscribe(() => {
48
- // Sign-out successful.
49
- // Replace state as we are no longer authorized to access profile page.
50
- this.router.navigate(['firebase/auth/sign-in'], { replaceUrl: true });
51
- }, (error) => {
52
- console.log('signout error', error);
53
- });
 
 
 
 
 
 
 
54
  }
55
 
56
- // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
57
- // Since ngOnDestroy might not fire when you navigate from the current page, use ionViewWillLeave to cleanup Subscriptions
58
  ionViewWillLeave(): void {
59
  this.subscriptions.unsubscribe();
60
  }
1
  import { Component, OnInit, HostBinding } from '@angular/core';
2
  import { ActivatedRoute, Router } from '@angular/router';
3
+
 
4
  import { Subscription } from 'rxjs';
 
5
  import { switchMap } from 'rxjs/operators';
6
 
7
+ import { IResolvedRouteData, ResolverHelper } from '../../../utils/resolver-helper';
8
+
9
+ import { FirebaseProfileModel } from './firebase-profile.model';
10
+ import { FirebaseAuthService } from '../firebase-auth.service';
11
+
12
+
13
  @Component({
14
  selector: 'app-firebase-profile',
15
  templateUrl: './firebase-profile.page.html',
19
  ]
20
  })
21
  export class FirebaseProfilePage implements OnInit {
22
+ // ? Gather all component subscription in one place. Can be one Subscription or multiple (chained using the Subscription.add() method)
23
  subscriptions: Subscription;
24
  user: FirebaseProfileModel;
25
 
34
  ) {}
35
 
36
  ngOnInit() {
 
37
  this.subscriptions = this.route.data
38
  .pipe(
39
+ // ? Extract data for this page
40
  switchMap((resolvedRouteData: IResolvedRouteData<FirebaseProfileModel>) => {
41
  return ResolverHelper.extractData<FirebaseProfileModel>(resolvedRouteData.data, FirebaseProfileModel);
42
  })
43
  )
44
+ .subscribe({
45
+ next: (state) => {
46
+ this.user = state;
47
+ },
48
+ error: (error) => console.log(error)
49
+ });
50
  }
51
 
52
+ public async signOut(): Promise<void> {
53
+ try {
54
+ // * 1. Sign out on the native layer
55
+ await this.authService.signOut()
56
+ .then((result) => {
57
+ // ? Sign-out successful
58
+ // ? Replace state as we are no longer authorized to access profile page
59
+ this.router.navigate(['firebase/auth/sign-in'], { replaceUrl: true });
60
+ })
61
+ .catch((error) => {
62
+ console.log('userProfile - signOut() - error', error);
63
+ });
64
+ } finally {
65
+ console.log('userProfile - signOut() - finally');
66
+ }
67
  }
68
 
69
+ // ? NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
70
+ // ? Since ngOnDestroy might not fire when you navigate from the current page, use ionViewWillLeave to cleanup Subscriptions
71
  ionViewWillLeave(): void {
72
  this.subscriptions.unsubscribe();
73
  }
src/app/firebase/auth/sign-in/firebase-sign-in.module.ts CHANGED
@@ -2,17 +2,20 @@ import { NgModule } from '@angular/core';
2
  import { CommonModule } from '@angular/common';
3
  import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4
  import { Routes, RouterModule } from '@angular/router';
 
5
  import { IonicModule } from '@ionic/angular';
 
 
 
 
 
6
  import { FirebaseSignInPage } from './firebase-sign-in.page';
7
  import { ComponentsModule } from '../../../components/components.module';
8
- import { AngularFireAuthGuard } from '@angular/fire/auth-guard';
9
 
10
- import { map } from 'rxjs/operators';
11
 
12
- // Firebase guard to redirect logged in users to profile
13
- const redirectLoggedInToProfile = (next) => map(user => {
14
- // when queryParams['auth-redirect'] don't redirect because we want
15
- // the component to handle the redirection
16
  if (user !== null && !next.queryParams['auth-redirect']) {
17
  return ['firebase/auth/profile'];
18
  } else {
@@ -24,8 +27,7 @@ const routes: Routes = [
24
  {
25
  path: '',
26
  component: FirebaseSignInPage,
27
- canActivate: [AngularFireAuthGuard],
28
- data: { authGuardPipe: redirectLoggedInToProfile }
29
  }
30
  ];
31
 
2
  import { CommonModule } from '@angular/common';
3
  import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4
  import { Routes, RouterModule } from '@angular/router';
5
+
6
  import { IonicModule } from '@ionic/angular';
7
+
8
+ import { map } from 'rxjs/operators';
9
+
10
+ import { canActivate, AuthPipeGenerator } from '@angular/fire/auth-guard';
11
+
12
  import { FirebaseSignInPage } from './firebase-sign-in.page';
13
  import { ComponentsModule } from '../../../components/components.module';
 
14
 
 
15
 
16
+ // ? Firebase guard to redirect logged in users to profile
17
+ const redirectLoggedInToProfile: AuthPipeGenerator = (next) => map(user => {
18
+ // ? When queryParams['auth-redirect'] don't redirect because we want the component to handle the redirection
 
19
  if (user !== null && !next.queryParams['auth-redirect']) {
20
  return ['firebase/auth/profile'];
21
  } else {
27
  {
28
  path: '',
29
  component: FirebaseSignInPage,
30
+ ...canActivate(redirectLoggedInToProfile)
 
31
  }
32
  ];
33
 
src/app/firebase/auth/sign-in/firebase-sign-in.page.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { Component, NgZone, OnDestroy } from '@angular/core';
2
- import { Location } from '@angular/common';
3
  import { Validators, FormGroup, FormControl } from '@angular/forms';
4
- import { Router, ActivatedRoute } from '@angular/router';
5
- import { LoadingController } from '@ionic/angular';
 
6
  import { Subscription } from 'rxjs';
7
 
8
  import { HistoryHelperService } from '../../../utils/history-helper.service';
@@ -15,10 +15,9 @@ import { FirebaseAuthService } from '../firebase-auth.service';
15
  './styles/firebase-sign-in.page.scss'
16
  ]
17
  })
18
- export class FirebaseSignInPage implements OnDestroy {
19
  loginForm: FormGroup;
20
  submitError: string;
21
- redirectLoader: HTMLIonLoadingElement;
22
  authRedirectResult: Subscription;
23
 
24
  validation_messages = {
@@ -34,11 +33,8 @@ export class FirebaseSignInPage implements OnDestroy {
34
 
35
  constructor(
36
  public router: Router,
37
- public route: ActivatedRoute,
38
- public authService: FirebaseAuthService,
39
  private ngZone: NgZone,
40
- public loadingController: LoadingController,
41
- public location: Location,
42
  public historyHelper: HistoryHelperService
43
  ) {
44
  this.loginForm = new FormGroup({
@@ -52,155 +48,134 @@ export class FirebaseSignInPage implements OnDestroy {
52
  ]))
53
  });
54
 
55
- // Get firebase authentication redirect result invoken when using signInWithRedirect()
56
- // signInWithRedirect() is only used when client is in web but not desktop
57
- this.authRedirectResult = this.authService.getRedirectResult()
58
  .subscribe(result => {
59
  if (result.error) {
60
  this.manageAuthWithProvidersErrors(result.error);
61
  } else {
62
  this.redirectLoggedUserToProfilePage();
63
  }
64
- });
65
 
66
- // Check if url contains our custom 'auth-redirect' param, then show a loader while we receive the getRedirectResult notification
67
- this.route.queryParams.subscribe(params => {
68
- const authProvider = params['auth-redirect'];
69
- if (authProvider) {
70
- this.presentLoading(authProvider);
 
71
  }
72
  });
73
  }
74
 
75
- ngOnDestroy(): void {
76
- this.dismissLoading();
77
- }
78
-
79
- // Once the auth provider finished the authentication flow, and the auth redirect completes,
80
- // hide the loader and redirect the user to the profile page
81
- redirectLoggedUserToProfilePage() {
82
- this.dismissLoading();
83
- // As we are calling the Angular router navigation inside a subscribe method, the navigation will be triggered outside Angular zone.
84
- // That's why we need to wrap the router navigation call inside an ngZone wrapper
85
- this.ngZone.run(() => {
86
- // Get previous URL from our custom History Helper
87
- // If there's no previous page, then redirect to profile
88
- // const previousUrl = this.historyHelper.previousUrl || 'firebase/auth/profile';
89
- const previousUrl = 'firebase/auth/profile';
90
 
91
- // No need to store in the navigation history the sign-in page with redirect params (it's justa a mandatory mid-step)
92
- // Navigate to profile and replace current url with profile
93
- this.router.navigate([previousUrl], { replaceUrl: true });
94
- });
 
 
 
 
 
 
 
 
 
95
  }
96
 
97
- async presentLoading(authProvider?: string) {
98
- const authProviderCapitalized = authProvider[0].toUpperCase() + authProvider.slice(1);
99
-
100
- this.loadingController.create({
101
- message: authProvider ? 'Signing in with ' + authProviderCapitalized : 'Signin in ...',
102
- duration: 4000
103
- }).then((loader) => {
104
- const currentUrl = this.location.path();
105
- if (currentUrl.includes('auth-redirect')) {
106
- this.redirectLoader = loader;
107
- this.redirectLoader.present();
108
- }
109
- });
110
- }
111
 
112
- async dismissLoading() {
113
- if (this.redirectLoader) {
114
- await this.redirectLoader.dismiss();
 
 
 
 
 
 
 
 
 
115
  }
116
  }
117
 
118
- // Before invoking auth provider redirect flow, present a loading indicator and add a flag to the path.
119
- // The precense of the flag in the path indicates we should wait for the auth redirect to complete.
120
- prepareForAuthWithProvidersRedirection(authProvider: string) {
121
- this.presentLoading(authProvider);
122
 
123
- this.location.replaceState(this.location.path(), 'auth-redirect=' + authProvider, this.location.getState());
 
 
 
 
 
 
 
 
 
 
 
 
124
  }
125
 
126
- manageAuthWithProvidersErrors(errorMessage: string) {
127
- this.submitError = errorMessage;
128
- // remove auth-redirect param from url
129
- this.location.replaceState(this.router.url.split('?')[0], '');
130
- this.dismissLoading();
131
- }
132
 
133
- resetSubmitError() {
134
- this.submitError = null;
 
 
 
 
 
 
 
 
 
135
  }
136
 
137
- signInWithEmail() {
138
  this.resetSubmitError();
139
- this.authService.signInWithEmail(this.loginForm.value['email'], this.loginForm.value['password'])
140
- .then(user => {
141
- // navigate to user profile
142
- this.redirectLoggedUserToProfilePage();
143
- })
144
- .catch(error => {
145
- this.submitError = error.message;
146
- this.dismissLoading();
147
- });
148
- }
149
 
150
- doFacebookLogin(): void {
151
- this.resetSubmitError();
152
- this.prepareForAuthWithProvidersRedirection('facebook');
153
-
154
- this.authService.signInWithFacebook()
155
- .subscribe((result) => {
156
- // This gives you a Facebook Access Token. You can use it to access the Facebook API.
157
- // const token = result.credential.accessToken;
158
- this.redirectLoggedUserToProfilePage();
159
- }, (error) => {
160
- this.manageAuthWithProvidersErrors(error.message);
161
- });
162
  }
163
 
164
- doGoogleLogin(): void {
165
- this.resetSubmitError();
166
- this.prepareForAuthWithProvidersRedirection('google');
167
-
168
- this.authService.signInWithGoogle()
169
- .subscribe((result) => {
170
- // This gives you a Google Access Token. You can use it to access the Google API.
171
- // var token = result.credential.accessToken;
172
- this.redirectLoggedUserToProfilePage();
173
- }, (error) => {
174
- console.log(error);
175
- this.manageAuthWithProvidersErrors(error.message);
 
176
  });
177
  }
178
 
179
- doTwitterLogin(): void {
180
- this.resetSubmitError();
181
- this.prepareForAuthWithProvidersRedirection('twitter');
182
-
183
- this.authService.signInWithTwitter()
184
- .subscribe((result) => {
185
- // This gives you a Twitter Access Token. You can use it to access the Twitter API.
186
- // var token = result.credential.accessToken;
187
- this.redirectLoggedUserToProfilePage();
188
- }, (error) => {
189
- console.log(error);
190
- this.manageAuthWithProvidersErrors(error.message);
191
- });
192
  }
193
 
194
- doAppleLogin(): void {
195
- this.resetSubmitError();
196
- this.prepareForAuthWithProvidersRedirection('apple');
197
-
198
- this.authService.signInWithApple()
199
- .subscribe((result) => {
200
- this.redirectLoggedUserToProfilePage();
201
- }, (error) => {
202
- console.log(error);
203
- this.manageAuthWithProvidersErrors(error.message);
204
- });
205
  }
206
  }
1
+ import { Component, NgZone } from '@angular/core';
 
2
  import { Validators, FormGroup, FormControl } from '@angular/forms';
3
+ import { Router } from '@angular/router';
4
+ import { AuthStateChange, SignInResult } from '@capacitor-firebase/authentication';
5
+
6
  import { Subscription } from 'rxjs';
7
 
8
  import { HistoryHelperService } from '../../../utils/history-helper.service';
15
  './styles/firebase-sign-in.page.scss'
16
  ]
17
  })
18
+ export class FirebaseSignInPage {
19
  loginForm: FormGroup;
20
  submitError: string;
 
21
  authRedirectResult: Subscription;
22
 
23
  validation_messages = {
33
 
34
  constructor(
35
  public router: Router,
36
+ public firebaseAuthService: FirebaseAuthService,
 
37
  private ngZone: NgZone,
 
 
38
  public historyHelper: HistoryHelperService
39
  ) {
40
  this.loginForm = new FormGroup({
48
  ]))
49
  });
50
 
51
+ // ? Get firebase authentication redirect result invoked when using signInWithRedirect()
52
+ // ? signInWithRedirect() is only used when client is in web but not desktop. For example a PWA
53
+ this.authRedirectResult = this.firebaseAuthService.redirectResult$
54
  .subscribe(result => {
55
  if (result.error) {
56
  this.manageAuthWithProvidersErrors(result.error);
57
  } else {
58
  this.redirectLoggedUserToProfilePage();
59
  }
60
+ });
61
 
62
+ this.firebaseAuthService.authState$
63
+ .subscribe((stateChange: AuthStateChange) => {
64
+ if (!stateChange.user) {
65
+ this.manageAuthWithProvidersErrors('No user logged in');
66
+ } else {
67
+ this.redirectLoggedUserToProfilePage();
68
  }
69
  });
70
  }
71
 
72
+ public async doFacebookLogin(): Promise<void> {
73
+ this.resetSubmitError();
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
+ try {
76
+ await this.firebaseAuthService.signInWithFacebook()
77
+ .then((result: SignInResult) => {
78
+ // ? This gives you a Facebook Access Token. You can use it to access the Facebook API.
79
+ // const token = result.credential.accessToken;
80
+ this.redirectLoggedUserToProfilePage();
81
+ })
82
+ .catch((error) => {
83
+ this.manageAuthWithProvidersErrors(error.message);
84
+ });
85
+ } finally {
86
+ // ? Termination code goes here
87
+ }
88
  }
89
 
90
+ public async doGoogleLogin(): Promise<void> {
91
+ this.resetSubmitError();
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
+ try {
94
+ await this.firebaseAuthService.signInWithGoogle()
95
+ .then((result) => {
96
+ // ? This gives you a Google Access Token. You can use it to access the Google API.
97
+ // const token = result.credential.accessToken;
98
+ this.redirectLoggedUserToProfilePage();
99
+ })
100
+ .catch((error) => {
101
+ this.manageAuthWithProvidersErrors(error.message);
102
+ });
103
+ } finally {
104
+ // ? Termination code goes here
105
  }
106
  }
107
 
108
+ public async doTwitterLogin(): Promise<void> {
109
+ this.resetSubmitError();
 
 
110
 
111
+ try {
112
+ await this.firebaseAuthService.signInWithTwitter()
113
+ .then((result) => {
114
+ // ? This gives you a Twitter Access Token. You can use it to access the Twitter API.
115
+ // const token = result.credential.accessToken;
116
+ this.redirectLoggedUserToProfilePage();
117
+ })
118
+ .catch((error) => {
119
+ this.manageAuthWithProvidersErrors(error.message);
120
+ });
121
+ } finally {
122
+ // ? Termination code goes here
123
+ }
124
  }
125
 
126
+ public async doAppleLogin(): Promise<void> {
127
+ this.resetSubmitError();
 
 
 
 
128
 
129
+ try {
130
+ await this.firebaseAuthService.signInWithApple()
131
+ .then((result) => {
132
+ this.redirectLoggedUserToProfilePage();
133
+ })
134
+ .catch((error) => {
135
+ this.manageAuthWithProvidersErrors(error.message);
136
+ });
137
+ } finally {
138
+ // ? Termination code goes here
139
+ }
140
  }
141
 
142
+ public async signInWithEmail(): Promise<void> {
143
  this.resetSubmitError();
 
 
 
 
 
 
 
 
 
 
144
 
145
+ try {
146
+ await this.firebaseAuthService.signInWithEmail(this.loginForm.value['email'], this.loginForm.value['password'])
147
+ .then((result) => {
148
+ this.redirectLoggedUserToProfilePage();
149
+ })
150
+ .catch((error) => {
151
+ this.submitError = error.message;
152
+ });
153
+ } finally {
154
+ // ? Termination code goes here
155
+ }
 
156
  }
157
 
158
+ // ? Once the auth provider finished the authentication flow, and the auth redirect completes, hide the loader and redirect the user to the profile page
159
+ private redirectLoggedUserToProfilePage(): void {
160
+ // As we are calling the Angular router navigation inside a subscribe method, the navigation will be triggered outside Angular zone.
161
+ // That's why we need to wrap the router navigation call inside an ngZone wrapper
162
+ this.ngZone.run(() => {
163
+ // Get previous URL from our custom History Helper
164
+ // If there's no previous page, then redirect to profile
165
+ // const previousUrl = this.historyHelper.previousUrl || 'firebase/auth/profile';
166
+ const previousUrl = 'firebase/auth/profile';
167
+
168
+ // No need to store in the navigation history the sign-in page with redirect params (it's just a a mandatory mid-step)
169
+ // Navigate to profile and replace current url with profile
170
+ this.router.navigate([previousUrl], { replaceUrl: true });
171
  });
172
  }
173
 
174
+ private manageAuthWithProvidersErrors(errorMessage: string): void {
175
+ this.submitError = errorMessage;
 
 
 
 
 
 
 
 
 
 
 
176
  }
177
 
178
+ private resetSubmitError(): void {
179
+ this.submitError = null;
 
 
 
 
 
 
 
 
 
180
  }
181
  }
src/app/firebase/auth/sign-up/firebase-sign-up.module.ts CHANGED
@@ -4,14 +4,30 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
4
  import { Routes, RouterModule } from '@angular/router';
5
 
6
  import { IonicModule } from '@ionic/angular';
 
 
 
 
 
7
  import { FirebaseSignUpPage } from './firebase-sign-up.page';
8
  import { ComponentsModule } from '../../../components/components.module';
9
 
10
 
 
 
 
 
 
 
 
 
 
 
11
  const routes: Routes = [
12
  {
13
  path: '',
14
- component: FirebaseSignUpPage
 
15
  }
16
  ];
17
 
4
  import { Routes, RouterModule } from '@angular/router';
5
 
6
  import { IonicModule } from '@ionic/angular';
7
+
8
+ import { map } from 'rxjs/operators';
9
+
10
+ import { canActivate, AuthPipeGenerator } from '@angular/fire/auth-guard';
11
+
12
  import { FirebaseSignUpPage } from './firebase-sign-up.page';
13
  import { ComponentsModule } from '../../../components/components.module';
14
 
15
 
16
+ // ? Firebase guard to redirect logged in users to profile
17
+ const redirectLoggedInToProfile: AuthPipeGenerator = (next) => map(user => {
18
+ // ? When queryParams['auth-redirect'] don't redirect because we want the component to handle the redirection
19
+ if (user !== null && !next.queryParams['auth-redirect']) {
20
+ return ['firebase/auth/profile'];
21
+ } else {
22
+ return true;
23
+ }
24
+ });
25
+
26
  const routes: Routes = [
27
  {
28
  path: '',
29
+ component: FirebaseSignUpPage,
30
+ ...canActivate(redirectLoggedInToProfile)
31
  }
32
  ];
33
 
src/app/firebase/auth/sign-up/firebase-sign-up.page.html CHANGED
@@ -1,7 +1,7 @@
1
  <ion-header class="ion-no-border">
2
  <ion-toolbar>
3
  <ion-buttons slot="start">
4
- <ion-back-button></ion-back-button>
5
  </ion-buttons>
6
  </ion-toolbar>
7
  </ion-header>
1
  <ion-header class="ion-no-border">
2
  <ion-toolbar>
3
  <ion-buttons slot="start">
4
+ <ion-back-button defaultHref="app/categories"></ion-back-button>
5
  </ion-buttons>
6
  </ion-toolbar>
7
  </ion-header>
src/app/firebase/auth/sign-up/firebase-sign-up.page.ts CHANGED
@@ -1,11 +1,15 @@
1
  import { Component, OnInit, NgZone } from '@angular/core';
2
  import { Validators, FormGroup, FormControl } from '@angular/forms';
3
- import { Location } from '@angular/common';
4
- import { Router, ActivatedRoute } from '@angular/router';
5
- import { MenuController, LoadingController } from '@ionic/angular';
 
 
 
 
 
6
  import { PasswordValidator } from '../../../validators/password.validator';
7
  import { FirebaseAuthService } from '../firebase-auth.service';
8
- import { Subscription } from 'rxjs';
9
 
10
  @Component({
11
  selector: 'app-firebase-sign-up',
@@ -18,7 +22,6 @@ export class FirebaseSignUpPage implements OnInit {
18
  signupForm: FormGroup;
19
  matching_passwords_group: FormGroup;
20
  submitError: string;
21
- redirectLoader: HTMLIonLoadingElement;
22
  authRedirectResult: Subscription;
23
 
24
  validation_messages = {
@@ -39,13 +42,11 @@ export class FirebaseSignUpPage implements OnInit {
39
  };
40
 
41
  constructor(
42
- public router: Router,
43
- public route: ActivatedRoute,
44
  public menu: MenuController,
45
- public authService: FirebaseAuthService,
 
46
  private ngZone: NgZone,
47
- public loadingController: LoadingController,
48
- public location: Location
49
  ) {
50
  this.matching_passwords_group = new FormGroup({
51
  'password': new FormControl('', Validators.compose([
@@ -65,9 +66,9 @@ export class FirebaseSignUpPage implements OnInit {
65
  'matching_passwords': this.matching_passwords_group
66
  });
67
 
68
- // Get firebase authentication redirect result invoken when using signInWithRedirect()
69
- // signInWithRedirect() is only used when client is in web but not desktop
70
- this.authRedirectResult = this.authService.getRedirectResult()
71
  .subscribe(result => {
72
  if (result.error) {
73
  this.manageAuthWithProvidersErrors(result.error);
@@ -76,11 +77,12 @@ export class FirebaseSignUpPage implements OnInit {
76
  }
77
  });
78
 
79
- // Check if url contains our custom 'auth-redirect' param, then show a loader while we receive the getRedirectResult notification
80
- this.route.queryParams.subscribe(params => {
81
- const authProvider = params['auth-redirect'];
82
- if (authProvider) {
83
- this.presentLoading(authProvider);
 
84
  }
85
  });
86
  }
@@ -89,126 +91,113 @@ export class FirebaseSignUpPage implements OnInit {
89
  this.menu.enable(false);
90
  }
91
 
92
- // Once the auth provider finished the authentication flow, and the auth redirect completes,
93
- // hide the loader and redirect the user to the profile page
94
- redirectLoggedUserToProfilePage() {
95
- this.dismissLoading();
96
-
97
- // As we are calling the Angular router navigation inside a subscribe method, the navigation will be triggered outside Angular zone.
98
- // That's why we need to wrap the router navigation call inside an ngZone wrapper
99
- this.ngZone.run(() => {
100
- // Get previous URL from our custom History Helper
101
- // If there's no previous page, then redirect to profile
102
- // const previousUrl = this.historyHelper.previousUrl || 'firebase/auth/profile';
103
- const previousUrl = 'firebase/auth/profile';
104
 
105
- // No need to store in the navigation history the sign-in page with redirect params (it's justa a mandatory mid-step)
106
- // Navigate to profile and replace current url with profile
107
- this.router.navigate([previousUrl], { replaceUrl: true });
108
- });
 
 
 
 
 
 
 
 
 
109
  }
110
 
111
- async presentLoading(authProvider?: string) {
112
- const authProviderCapitalized = authProvider[0].toUpperCase() + authProvider.slice(1);
113
- this.redirectLoader = await this.loadingController.create({
114
- message: authProvider ? 'Signing up with ' + authProviderCapitalized : 'Signin up ...',
115
- duration: 4000
116
- });
117
- await this.redirectLoader.present();
118
- }
119
 
120
- async dismissLoading() {
121
- if (this.redirectLoader) {
122
- await this.redirectLoader.dismiss();
 
 
 
 
 
 
 
 
 
123
  }
124
  }
125
 
126
- resetSubmitError() {
127
- this.submitError = null;
128
- }
129
-
130
- // Before invoking auth provider redirect flow, present a loading indicator and add a flag to the path.
131
- // The precense of the flag in the path indicates we should wait for the auth redirect to complete.
132
- prepareForAuthWithProvidersRedirection(authProvider: string) {
133
- this.presentLoading(authProvider);
134
 
135
- this.location.go(this.location.path(), 'auth-redirect=' + authProvider, this.location.getState());
 
 
 
 
 
 
 
 
 
 
 
 
136
  }
137
 
138
- manageAuthWithProvidersErrors(errorMessage: string) {
139
- this.submitError = errorMessage;
140
- // remove auth-redirect param from url
141
- this.location.replaceState(this.router.url.split('?')[0], '');
142
- this.dismissLoading();
 
 
 
 
 
 
 
 
 
143
  }
144
 
145
- signUpWithEmail(): void {
146
  this.resetSubmitError();
147
- const values = this.signupForm.value;
148
- this.authService.signUpWithEmail(values.email, values.matching_passwords.password)
149
- .then(user => {
150
- // navigate to user profile
151
  this.redirectLoggedUserToProfilePage();
152
  })
153
- .catch(error => {
154
  this.submitError = error.message;
155
  });
 
 
 
156
  }
157
 
158
- doFacebookSignup(): void {
159
- this.resetSubmitError();
160
- this.prepareForAuthWithProvidersRedirection('facebook');
161
-
162
- this.authService.signInWithFacebook()
163
- .subscribe((result) => {
164
- // This gives you a Facebook Access Token. You can use it to access the Facebook API.
165
- // const token = result.credential.accessToken;
166
- this.redirectLoggedUserToProfilePage();
167
- }, (error) => {
168
- this.manageAuthWithProvidersErrors(error.message);
169
- });
170
- }
171
 
172
- doGoogleSignup(): void {
173
- this.resetSubmitError();
174
- this.prepareForAuthWithProvidersRedirection('google');
175
-
176
- this.authService.signInWithGoogle()
177
- .subscribe((result) => {
178
- // This gives you a Google Access Token. You can use it to access the Google API.
179
- // var token = result.credential.accessToken;
180
- this.redirectLoggedUserToProfilePage();
181
- }, (error) => {
182
- console.log(error);
183
- this.manageAuthWithProvidersErrors(error.message);
184
  });
185
  }
186
 
187
- doTwitterSignup(): void {
188
- this.resetSubmitError();
189
- this.prepareForAuthWithProvidersRedirection('twitter');
190
-
191
- this.authService.signInWithTwitter()
192
- .subscribe((result) => {
193
- // This gives you a Twitter Access Token. You can use it to access the Twitter API.
194
- // var token = result.credential.accessToken;
195
- this.redirectLoggedUserToProfilePage();
196
- }, (error) => {
197
- console.log(error);
198
- this.manageAuthWithProvidersErrors(error.message);
199
- });
200
  }
201
 
202
- doAppleSignup(): void {
203
- this.resetSubmitError();
204
- this.prepareForAuthWithProvidersRedirection('apple');
205
-
206
- this.authService.signInWithApple()
207
- .subscribe((result) => {
208
- this.redirectLoggedUserToProfilePage();
209
- }, (error) => {
210
- console.log(error);
211
- this.manageAuthWithProvidersErrors(error.message);
212
- });
213
  }
214
  }
1
  import { Component, OnInit, NgZone } from '@angular/core';
2
  import { Validators, FormGroup, FormControl } from '@angular/forms';
3
+ import { Router } from '@angular/router';
4
+ import { MenuController } from '@ionic/angular';
5
+
6
+ import { AuthStateChange, SignInResult } from '@capacitor-firebase/authentication';
7
+
8
+ import { Subscription } from 'rxjs';
9
+
10
+ import { HistoryHelperService } from '../../../utils/history-helper.service';
11
  import { PasswordValidator } from '../../../validators/password.validator';
12
  import { FirebaseAuthService } from '../firebase-auth.service';
 
13
 
14
  @Component({
15
  selector: 'app-firebase-sign-up',
22
  signupForm: FormGroup;
23
  matching_passwords_group: FormGroup;
24
  submitError: string;
 
25
  authRedirectResult: Subscription;
26
 
27
  validation_messages = {
42
  };
43
 
44
  constructor(
 
 
45
  public menu: MenuController,
46
+ public router: Router,
47
+ public firebaseAuthService: FirebaseAuthService,
48
  private ngZone: NgZone,
49
+ public historyHelper: HistoryHelperService
 
50
  ) {
51
  this.matching_passwords_group = new FormGroup({
52
  'password': new FormControl('', Validators.compose([
66
  'matching_passwords': this.matching_passwords_group
67
  });
68
 
69
+ // ? Get firebase authentication redirect result invoked when using signInWithRedirect()
70
+ // ? signInWithRedirect() is only used when client is in web but not desktop. For example a PWA
71
+ this.authRedirectResult = this.firebaseAuthService.redirectResult$
72
  .subscribe(result => {
73
  if (result.error) {
74
  this.manageAuthWithProvidersErrors(result.error);
77
  }
78
  });
79
 
80
+ this.firebaseAuthService.authState$
81
+ .subscribe((stateChange: AuthStateChange) => {
82
+ if (!stateChange.user) {
83
+ this.manageAuthWithProvidersErrors('No user logged in');
84
+ } else {
85
+ this.redirectLoggedUserToProfilePage();
86
  }
87
  });
88
  }
91
  this.menu.enable(false);
92
  }
93
 
94
+ public async doFacebookSignup(): Promise<void> {
95
+ this.resetSubmitError();
 
 
 
 
 
 
 
 
 
 
96
 
97
+ try {
98
+ await this.firebaseAuthService.signInWithFacebook()
99
+ .then((result: SignInResult) => {
100
+ // ? This gives you a Facebook Access Token. You can use it to access the Facebook API.
101
+ // const token = result.credential.accessToken;
102
+ this.redirectLoggedUserToProfilePage();
103
+ })
104
+ .catch((error) => {
105
+ this.manageAuthWithProvidersErrors(error.message);
106
+ });
107
+ } finally {
108
+ // ? Termination code goes here
109
+ }
110
  }
111
 
112
+ public async doGoogleSignup(): Promise<void> {
113
+ this.resetSubmitError();
 
 
 
 
 
 
114
 
115
+ try {
116
+ await this.firebaseAuthService.signInWithGoogle()
117
+ .then((result) => {
118
+ // ? This gives you a Google Access Token. You can use it to access the Google API.
119
+ // const token = result.credential.accessToken;
120
+ this.redirectLoggedUserToProfilePage();
121
+ })
122
+ .catch((error) => {
123
+ this.manageAuthWithProvidersErrors(error.message);
124
+ });
125
+ } finally {
126
+ // ? Termination code goes here
127
  }
128
  }
129
 
130
+ public async doTwitterSignup(): Promise<void> {
131
+ this.resetSubmitError();
 
 
 
 
 
 
132
 
133
+ try {
134
+ await this.firebaseAuthService.signInWithTwitter()
135
+ .then((result) => {
136
+ // ? This gives you a Twitter Access Token. You can use it to access the Twitter API.
137
+ // const token = result.credential.accessToken;
138
+ this.redirectLoggedUserToProfilePage();
139
+ })
140
+ .catch((error) => {
141
+ this.manageAuthWithProvidersErrors(error.message);
142
+ });
143
+ } finally {
144
+ // ? Termination code goes here
145
+ }
146
  }
147
 
148
+ public async doAppleSignup(): Promise<void> {
149
+ this.resetSubmitError();
150
+
151
+ try {
152
+ await this.firebaseAuthService.signInWithApple()
153
+ .then((result) => {
154
+ this.redirectLoggedUserToProfilePage();
155
+ })
156
+ .catch((error) => {
157
+ this.manageAuthWithProvidersErrors(error.message);
158
+ });
159
+ } finally {
160
+ // ? Termination code goes here
161
+ }
162
  }
163
 
164
+ public async signUpWithEmail(): Promise<void> {
165
  this.resetSubmitError();
166
+
167
+ try {
168
+ await this.firebaseAuthService.signUpWithEmail(this.signupForm.value['email'], this.signupForm.value.matching_passwords.password)
169
+ .then((result) => {
170
  this.redirectLoggedUserToProfilePage();
171
  })
172
+ .catch((error) => {
173
  this.submitError = error.message;
174
  });
175
+ } finally {
176
+ // ? Termination code goes here
177
+ }
178
  }
179
 
180
+ // ? Once the auth provider finished the authentication flow, and the auth redirect completes, hide the loader and redirect the user to the profile page
181
+ private redirectLoggedUserToProfilePage(): void {
182
+ // As we are calling the Angular router navigation inside a subscribe method, the navigation will be triggered outside Angular zone.
183
+ // That's why we need to wrap the router navigation call inside an ngZone wrapper
184
+ this.ngZone.run(() => {
185
+ // Get previous URL from our custom History Helper
186
+ // If there's no previous page, then redirect to profile
187
+ // const previousUrl = this.historyHelper.previousUrl || 'firebase/auth/profile';
188
+ const previousUrl = 'firebase/auth/profile';
 
 
 
 
189
 
190
+ // No need to store in the navigation history the sign-in page with redirect params (it's justa a mandatory mid-step)
191
+ // Navigate to profile and replace current url with profile
192
+ this.router.navigate([previousUrl], { replaceUrl: true });
 
 
 
 
 
 
 
 
 
193
  });
194
  }
195
 
196
+ private manageAuthWithProvidersErrors(errorMessage: string): void {
197
+ this.submitError = errorMessage;
 
 
 
 
 
 
 
 
 
 
 
198
  }
199
 
200
+ private resetSubmitError(): void {
201
+ this.submitError = null;
 
 
 
 
 
 
 
 
 
202
  }
203
  }
src/app/firebase/crud/firebase-crud.module.ts CHANGED
@@ -3,10 +3,12 @@ import { RouterModule, Routes } from '@angular/router';
3
  import { NgModule } from '@angular/core';
4
  import { CommonModule } from '@angular/common';
5
 
6
- import { AngularFireModule } from '@angular/fire';
7
- import { AngularFirestoreModule } from '@angular/fire/firestore';
 
8
  import { environment } from '../../../environments/environment';
9
 
 
10
  const firebaseRoutes: Routes = [
11
  {
12
  path: '',
@@ -34,8 +36,8 @@ const firebaseRoutes: Routes = [
34
  IonicModule,
35
  CommonModule,
36
  RouterModule.forChild(firebaseRoutes),
37
- AngularFireModule.initializeApp(environment.firebase),
38
- AngularFirestoreModule
39
  ],
40
  })
41
  export class FirebaseCrudModule {}
3
  import { NgModule } from '@angular/core';
4
  import { CommonModule } from '@angular/common';
5
 
6
+ import { getFirestore, provideFirestore } from '@angular/fire/firestore';
7
+ import { initializeApp, provideFirebaseApp } from '@angular/fire/app';
8
+
9
  import { environment } from '../../../environments/environment';
10
 
11
+
12
  const firebaseRoutes: Routes = [
13
  {
14
  path: '',
36
  IonicModule,
37
  CommonModule,
38
  RouterModule.forChild(firebaseRoutes),
39
+ provideFirebaseApp(() => initializeApp(environment.firebase)),
40
+ provideFirestore(() => getFirestore())
41
  ],
42
  })
43
  export class FirebaseCrudModule {}
src/app/firebase/crud/firebase-crud.service.ts CHANGED
@@ -1,46 +1,55 @@
1
  import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
2
- import { AngularFirestore, DocumentReference } from '@angular/fire/firestore';
 
 
3
 
4
- import { Observable, of, forkJoin, throwError, combineLatest } from 'rxjs';
5
  import { map, concatMap, first, filter } from 'rxjs/operators';
 
6
  import * as dayjs from 'dayjs';
 
7
  import { DataStore, ShellModel } from '../../shell/data-store';
8
  import { FirebaseListingItemModel } from './../crud/listing/firebase-listing.model';
9
  import { FirebaseCombinedUserModel, FirebaseSkillModel, FirebaseUserModel } from './../crud/user/firebase-user.model';
10
  import { UserImageModel } from './../crud/user/select-image/user-image.model';
11
  import { TransferStateHelper } from '../../utils/transfer-state-helper';
12
- import { isPlatformServer } from '@angular/common';
13
 
14
- @Injectable()
 
 
15
  export class FirebaseCrudService {
16
- // Listing Page
17
  private listingDataStore: DataStore<Array<FirebaseListingItemModel>>;
18
- // User Details Page
19
  private combinedUserDataStore: DataStore<FirebaseCombinedUserModel>;
20
  private relatedUsersDataStore: DataStore<Array<FirebaseListingItemModel>>;
21
- // Select User Image Modal
22
  private avatarsDataStore: DataStore<Array<UserImageModel>>;
23
 
24
  constructor(
25
  @Inject(PLATFORM_ID) private platformId: object,
26
  private transferStateHelper: TransferStateHelper,
27
- private afs: AngularFirestore
28
  ) {}
29
 
30
- /*
31
- Firebase User Listing Page
32
- */
33
  public getListingDataSource(): Observable<Array<FirebaseListingItemModel>> {
34
- const rawDataSource = this.afs.collection<FirebaseListingItemModel>('users').valueChanges({ idField: 'id' })
35
- .pipe(
36
- map(actions => actions.map(user => {
 
 
 
 
 
37
  const age = this.calcUserAge(user.birthdate);
 
38
  return { age, ...user } as FirebaseListingItemModel;
39
- })
40
- )
41
  );
42
 
43
- // This method tapps into the raw data source and stores the resolved data in the TransferState, then when
44
  // transitioning from the server rendered view to the browser, checks if we already loaded the data in the server to prevent
45
  // duplicate http requests.
46
  const cachedDataSource = this.transferStateHelper.checkDataSourceState('firebase-listing-state', rawDataSource);
@@ -63,7 +72,7 @@ export class FirebaseCrudService {
63
  this.listingDataStore = new DataStore(shellModel);
64
 
65
  // If running in the server, then don't add shell to the Data Store
66
- // If we already loaded the Data Source in the server, then don't show a shell when transitioning back to the broswer from the server
67
  if (isPlatformServer(this.platformId) || dataSource['ssr_state']) {
68
  // Trigger loading mechanism with 0 delay (this will prevent the shell to be shown)
69
  this.listingDataStore.load(dataSource, 0);
@@ -76,27 +85,35 @@ export class FirebaseCrudService {
76
  return this.listingDataStore;
77
  }
78
 
79
- // Filter users by age
80
  public searchUsersByAge(lower: number, upper: number): Observable<Array<FirebaseListingItemModel>> {
81
- // we save the dateOfBirth in our DB so we need to calc the min and max dates valid for this query
82
  const minDate = (dayjs(Date.now()).subtract(upper, 'year')).unix();
83
  const maxDate = (dayjs(Date.now()).subtract(lower, 'year')).unix();
84
 
85
- const listingCollection = this.afs.collection<FirebaseListingItemModel>('users', ref =>
86
- ref.orderBy('birthdate').startAt(minDate).endAt(maxDate));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
 
88
- return listingCollection.valueChanges({ idField: 'id' }).pipe(
89
- map(actions => actions.map(user => {
90
- const age = this.calcUserAge(user.birthdate);
91
- return { age, ...user } as FirebaseListingItemModel;
92
- })
93
- ));
94
  }
95
 
96
- /*
97
- Firebase User Details Page
98
- */
99
- // Concat the userData with the details of the userSkills (from the skills collection)
100
  public getCombinedUserDataSource(userId: string): Observable<FirebaseCombinedUserModel> {
101
  const rawDataSource = this.getUser(userId)
102
  .pipe(
@@ -105,8 +122,9 @@ export class FirebaseCrudService {
105
  concatMap(user => {
106
  if (user && user.skills) {
107
  // Map each skill id and get the skill data as an Observable
108
- const userSkillsObservables: Array<Observable<FirebaseSkillModel>> = user.skills.map(skill => {
109
- return this.getSkill(skill).pipe(first());
 
110
  });
111
 
112
  // Combination operator: Take the most recent value from both input sources (of(user) & forkJoin(userSkillsObservables)),
@@ -114,7 +132,8 @@ export class FirebaseCrudService {
114
  return combineLatest([
115
  of(user),
116
  forkJoin(userSkillsObservables)
117
- ]).pipe(
 
118
  map(([userDetails, userSkills]: [FirebaseUserModel, Array<FirebaseSkillModel>]) => {
119
  // Spread operator (see: https://dev.to/napoleon039/how-to-use-the-spread-and-rest-operator-4jbb)
120
  return {
@@ -125,12 +144,12 @@ export class FirebaseCrudService {
125
  );
126
  } else {
127
  // Throw error
128
- return throwError('User does not have any skills.');
129
  }
130
  })
131
  );
132
 
133
- // This method tapps into the raw data source and stores the resolved data in the TransferState, then when
134
  // transitioning from the server rendered view to the browser, checks if we already loaded the data in the server to prevent
135
  // duplicate http requests.
136
  const cachedDataSource = this.transferStateHelper.checkDataSourceState(`firebase-user-${userId}-state`, rawDataSource);
@@ -144,7 +163,7 @@ export class FirebaseCrudService {
144
  this.combinedUserDataStore = new DataStore(shellModel);
145
 
146
  // If running in the server, then don't add shell to the Data Store
147
- // If we already loaded the Data Source in the server, then don't show a shell when transitioning back to the broswer from the server
148
  if (isPlatformServer(this.platformId) || dataSource['ssr_state']) {
149
  // Trigger loading mechanism with 0 delay (this will prevent the shell to be shown)
150
  this.combinedUserDataStore.load(dataSource, 0);
@@ -166,17 +185,17 @@ export class FirebaseCrudService {
166
  if (user && user.skills) {
167
  // Get all users with at least 1 skill in common
168
  const relatedUsersObservable: Observable<Array<FirebaseListingItemModel>> =
169
- this.getUsersWithSameSkill(user.id, user.skills);
170
 
171
  return relatedUsersObservable;
172
  } else {
173
  // Throw error
174
- return throwError('Could not get related user');
175
  }
176
  })
177
  );
178
 
179
- // This method tapps into the raw data source and stores the resolved data in the TransferState, then when
180
  // transitioning from the server rendered view to the browser, checks if we already loaded the data in the server to prevent
181
  // duplicate http requests.
182
  const cachedDataSource = this.transferStateHelper.checkDataSourceState(`firebase-user-${userId}-related-users-state`, rawDataSource);
@@ -194,7 +213,7 @@ export class FirebaseCrudService {
194
  this.relatedUsersDataStore = new DataStore(shellModel);
195
 
196
  // If running in the server, then don't add shell to the Data Store
197
- // If we already loaded the Data Source in the server, then don't show a shell when transitioning back to the broswer from the server
198
  if (isPlatformServer(this.platformId) || dataSource['ssr_state']) {
199
  // Trigger loading mechanism with 0 delay (this will prevent the shell to be shown)
200
  this.relatedUsersDataStore.load(dataSource, 0);
@@ -206,33 +225,39 @@ export class FirebaseCrudService {
206
  return this.relatedUsersDataStore;
207
  }
208
 
209
- /*
210
- Firebase Create User Modal
211
- */
212
- public createUser(userData: FirebaseUserModel): Promise<DocumentReference> {
213
- // remove isShell property so it doesn't get stored in Firebase
214
- const { isShell, ...userDataToSave } = userData;
215
- return this.afs.collection('users').add({...userDataToSave});
216
  }
217
 
218
- /*
219
- Firebase Update User Modal
220
- */
221
- public updateUser(userData: FirebaseUserModel): Promise<void> {
222
- // remove isShell property so it doesn't get stored in Firebase
223
- const { isShell, ...userDataToSave } = userData;
224
- return this.afs.collection('users').doc(userData.id).set({...userDataToSave});
225
  }
226
 
227
- public deleteUser(userKey: string): Promise<void> {
228
- return this.afs.collection('users').doc(userKey).delete();
 
 
229
  }
230
 
231
- /*
232
- Firebase Select User Image Modal
233
- */
234
  public getAvatarsDataSource(): Observable<Array<UserImageModel>> {
235
- return this.afs.collection<UserImageModel>('avatars').valueChanges();
 
 
 
 
 
 
236
  }
237
 
238
  public getAvatarsStore(dataSource: Observable<Array<UserImageModel>>): DataStore<Array<UserImageModel>> {
@@ -251,57 +276,97 @@ export class FirebaseCrudService {
251
  // Trigger the loading mechanism (with shell) in the dataStore
252
  this.avatarsDataStore.load(dataSource);
253
  }
 
254
  return this.avatarsDataStore;
255
  }
256
 
257
- /*
258
- FireStore utility methods
259
- */
260
- // Get list of all available Skills (used in the create and update modals)
 
261
  public getSkills(): Observable<Array<FirebaseSkillModel>> {
262
- return this.afs.collection<FirebaseSkillModel>('skills').valueChanges({ idField: 'id' });
 
 
 
 
 
 
 
263
  }
264
 
265
- // Get data of a specific Skill
266
  private getSkill(skillId: string): Observable<FirebaseSkillModel> {
267
- return this.afs.doc<FirebaseSkillModel>('skills/' + skillId)
268
- .snapshotChanges()
269
- .pipe(
270
- map(a => {
271
- const data = a.payload.data();
272
- const id = a.payload.id;
273
- return { id, ...data } as FirebaseSkillModel;
274
- })
275
  );
276
- }
277
 
 
 
278
 
279
- // Get data of a specific User
280
  private getUser(userId: string): Observable<FirebaseUserModel> {
281
- return this.afs.doc<FirebaseUserModel>('users/' + userId)
282
- .snapshotChanges()
 
 
283
  .pipe(
284
- map(a => {
285
- const userData = a.payload.data();
286
- const id = a.payload.id;
287
- const age = userData ? this.calcUserAge(userData.birthdate) : 0;
288
- return { id, age, ...userData } as FirebaseUserModel;
 
 
 
289
  })
290
  );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  }
292
 
293
- // Get all users who share at least 1 skill of the user's 'skills' list
294
- private getUsersWithSameSkill(userId: string, skills: Array<FirebaseSkillModel>): Observable<Array<FirebaseListingItemModel>> {
295
  // Get the users who have at least 1 skill in common
296
  // Because firestore doesn't have a logical 'OR' operator we need to create multiple queries, one for each skill from the 'skills' list
297
- const queries = skills.map(skill => {
298
- return this.afs.collection('users', ref => ref
299
- .where('skills', 'array-contains', skill.id))
300
- .valueChanges({ idField: 'id' });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  });
302
 
303
  // Combine all these queries
304
- return combineLatest(queries).pipe(
 
305
  map((relatedUsers: FirebaseListingItemModel[][]) => {
306
  // Flatten the array of arrays of FirebaseListingItemModel
307
  const flattenedRelatedUsers = ([] as FirebaseListingItemModel[]).concat(...relatedUsers);
@@ -321,6 +386,8 @@ export class FirebaseCrudService {
321
  return filteredRelatedUsers;
322
  })
323
  );
 
 
324
  }
325
 
326
  private calcUserAge(dateOfBirth: number): number {
1
  import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
2
+ import { isPlatformServer } from '@angular/common';
3
+
4
+ import { Firestore, collection, collectionData, query, CollectionReference, orderBy, startAt, endAt, docData, doc, DocumentReference, where, setDoc, updateDoc, deleteDoc, getDoc, DocumentSnapshot } from '@angular/fire/firestore';
5
 
6
+ import { Observable, of, forkJoin, throwError, combineLatest, from } from 'rxjs';
7
  import { map, concatMap, first, filter } from 'rxjs/operators';
8
+
9
  import * as dayjs from 'dayjs';
10
+
11
  import { DataStore, ShellModel } from '../../shell/data-store';
12
  import { FirebaseListingItemModel } from './../crud/listing/firebase-listing.model';
13
  import { FirebaseCombinedUserModel, FirebaseSkillModel, FirebaseUserModel } from './../crud/user/firebase-user.model';
14
  import { UserImageModel } from './../crud/user/select-image/user-image.model';
15
  import { TransferStateHelper } from '../../utils/transfer-state-helper';
 
16
 
17
+ @Injectable({
18
+ providedIn: 'root'
19
+ })
20
  export class FirebaseCrudService {
21
+ // ? Listing Page
22
  private listingDataStore: DataStore<Array<FirebaseListingItemModel>>;
23
+ // ? User Details Page
24
  private combinedUserDataStore: DataStore<FirebaseCombinedUserModel>;
25
  private relatedUsersDataStore: DataStore<Array<FirebaseListingItemModel>>;
26
+ // ? Select User Image Modal
27
  private avatarsDataStore: DataStore<Array<UserImageModel>>;
28
 
29
  constructor(
30
  @Inject(PLATFORM_ID) private platformId: object,
31
  private transferStateHelper: TransferStateHelper,
32
+ private firestore: Firestore
33
  ) {}
34
 
35
+ // * Firebase User Listing Page
 
 
36
  public getListingDataSource(): Observable<Array<FirebaseListingItemModel>> {
37
+ const rawDataSource: Observable<Array<FirebaseListingItemModel>> = collectionData<FirebaseListingItemModel>(
38
+ query<FirebaseListingItemModel>(
39
+ collection(this.firestore, 'users') as CollectionReference<FirebaseListingItemModel>
40
+ ), { idField: 'id' }
41
+ )
42
+ .pipe(
43
+ map((users: Array<FirebaseListingItemModel>) => {
44
+ return users.map((user: FirebaseListingItemModel) => {
45
  const age = this.calcUserAge(user.birthdate);
46
+
47
  return { age, ...user } as FirebaseListingItemModel;
48
+ });
49
+ })
50
  );
51
 
52
+ // This method taps into the raw data source and stores the resolved data in the TransferState, then when
53
  // transitioning from the server rendered view to the browser, checks if we already loaded the data in the server to prevent
54
  // duplicate http requests.
55
  const cachedDataSource = this.transferStateHelper.checkDataSourceState('firebase-listing-state', rawDataSource);
72
  this.listingDataStore = new DataStore(shellModel);
73
 
74
  // If running in the server, then don't add shell to the Data Store
75
+ // If we already loaded the Data Source in the server, then don't show a shell when transitioning back to the browser from the server
76
  if (isPlatformServer(this.platformId) || dataSource['ssr_state']) {
77
  // Trigger loading mechanism with 0 delay (this will prevent the shell to be shown)
78
  this.listingDataStore.load(dataSource, 0);
85
  return this.listingDataStore;
86
  }
87
 
88
+ // * Filter users by age
89
  public searchUsersByAge(lower: number, upper: number): Observable<Array<FirebaseListingItemModel>> {
90
+ // ? We save the dateOfBirth in our DB so we need to calc the min and max dates valid for this query
91
  const minDate = (dayjs(Date.now()).subtract(upper, 'year')).unix();
92
  const maxDate = (dayjs(Date.now()).subtract(lower, 'year')).unix();
93
 
94
+ const filteredDataSource: Observable<Array<FirebaseListingItemModel>> = collectionData<FirebaseListingItemModel>(
95
+ query<FirebaseListingItemModel>(
96
+ collection(this.firestore, 'users') as CollectionReference<FirebaseListingItemModel>,
97
+ orderBy('birthdate'),
98
+ startAt(minDate),
99
+ endAt(maxDate)
100
+ ), { idField: 'id' }
101
+ )
102
+ .pipe(
103
+ map((users: Array<FirebaseListingItemModel>) => {
104
+ return users.map((user: FirebaseListingItemModel) => {
105
+ const age = this.calcUserAge(user.birthdate);
106
+
107
+ return { age, ...user } as FirebaseListingItemModel;
108
+ });
109
+ })
110
+ );
111
 
112
+ return filteredDataSource;
 
 
 
 
 
113
  }
114
 
115
+ // * Firebase User Details Page
116
+ // ? Concat the userData with the details of the userSkills (from the skills collection)
 
 
117
  public getCombinedUserDataSource(userId: string): Observable<FirebaseCombinedUserModel> {
118
  const rawDataSource = this.getUser(userId)
119
  .pipe(
122
  concatMap(user => {
123
  if (user && user.skills) {
124
  // Map each skill id and get the skill data as an Observable
125
+ const userSkillsObservables: Array<Observable<FirebaseSkillModel>> = user.skills.map((skillId: string) => {
126
+ // ? first() emits the first value of the source Observable, then completes.
127
+ return this.getSkill(skillId).pipe(first());
128
  });
129
 
130
  // Combination operator: Take the most recent value from both input sources (of(user) & forkJoin(userSkillsObservables)),
132
  return combineLatest([
133
  of(user),
134
  forkJoin(userSkillsObservables)
135
+ ])
136
+ .pipe(
137
  map(([userDetails, userSkills]: [FirebaseUserModel, Array<FirebaseSkillModel>]) => {
138
  // Spread operator (see: https://dev.to/napoleon039/how-to-use-the-spread-and-rest-operator-4jbb)
139
  return {
144
  );
145
  } else {
146
  // Throw error
147
+ return throwError(() => new Error('User does not have any skills.'));
148
  }
149
  })
150
  );
151
 
152
+ // This method taps into the raw data source and stores the resolved data in the TransferState, then when
153
  // transitioning from the server rendered view to the browser, checks if we already loaded the data in the server to prevent
154
  // duplicate http requests.
155
  const cachedDataSource = this.transferStateHelper.checkDataSourceState(`firebase-user-${userId}-state`, rawDataSource);
163
  this.combinedUserDataStore = new DataStore(shellModel);
164
 
165
  // If running in the server, then don't add shell to the Data Store
166
+ // If we already loaded the Data Source in the server, then don't show a shell when transitioning back to the browser from the server
167
  if (isPlatformServer(this.platformId) || dataSource['ssr_state']) {
168
  // Trigger loading mechanism with 0 delay (this will prevent the shell to be shown)
169
  this.combinedUserDataStore.load(dataSource, 0);
185
  if (user && user.skills) {
186
  // Get all users with at least 1 skill in common
187
  const relatedUsersObservable: Observable<Array<FirebaseListingItemModel>> =
188
+ this.getUsersWithSameSkills(user.id, user.skills);
189
 
190
  return relatedUsersObservable;
191
  } else {
192
  // Throw error
193
+ return throwError(() => new Error('Could not get related user'));
194
  }
195
  })
196
  );
197
 
198
+ // This method taps into the raw data source and stores the resolved data in the TransferState, then when
199
  // transitioning from the server rendered view to the browser, checks if we already loaded the data in the server to prevent
200
  // duplicate http requests.
201
  const cachedDataSource = this.transferStateHelper.checkDataSourceState(`firebase-user-${userId}-related-users-state`, rawDataSource);
213
  this.relatedUsersDataStore = new DataStore(shellModel);
214
 
215
  // If running in the server, then don't add shell to the Data Store
216
+ // If we already loaded the Data Source in the server, then don't show a shell when transitioning back to the browser from the server
217
  if (isPlatformServer(this.platformId) || dataSource['ssr_state']) {
218
  // Trigger loading mechanism with 0 delay (this will prevent the shell to be shown)
219
  this.relatedUsersDataStore.load(dataSource, 0);
225
  return this.relatedUsersDataStore;
226
  }
227
 
228
+ // * Firebase Create User Modal
229
+ public createUser(user: FirebaseUserModel): Promise<void> {
230
+ // Remove isShell property so it doesn't get stored in Firebase
231
+ const { isShell, ...userDataToSave } = user;
232
+ const userDocumentRef = doc(collection(this.firestore, 'users'));
233
+
234
+ return setDoc(userDocumentRef, {...userDataToSave});
235
  }
236
 
237
+ // * Firebase Update User Modal
238
+ public updateUser(user: FirebaseUserModel): Promise<void> {
239
+ // Remove isShell property so it doesn't get stored in Firebase
240
+ const { isShell, ...userDataToSave } = user;
241
+ const userDocumentRef = doc(this.firestore, 'users', user.id);
242
+
243
+ return updateDoc(userDocumentRef, {...userDataToSave});
244
  }
245
 
246
+ public deleteUser(userId: string): Promise<void> {
247
+ const userDocumentRef = doc(this.firestore, 'users', userId);
248
+
249
+ return deleteDoc(userDocumentRef);
250
  }
251
 
252
+ // * Firebase Select User Image Modal
 
 
253
  public getAvatarsDataSource(): Observable<Array<UserImageModel>> {
254
+ const avatarsDataSource: Observable<Array<UserImageModel>> = collectionData<UserImageModel>(
255
+ query<UserImageModel>(
256
+ collection(this.firestore, 'avatars') as CollectionReference<UserImageModel>
257
+ )
258
+ );
259
+
260
+ return avatarsDataSource;
261
  }
262
 
263
  public getAvatarsStore(dataSource: Observable<Array<UserImageModel>>): DataStore<Array<UserImageModel>> {
276
  // Trigger the loading mechanism (with shell) in the dataStore
277
  this.avatarsDataStore.load(dataSource);
278
  }
279
+
280
  return this.avatarsDataStore;
281
  }
282
 
283
+
284
+ // ! FireStore utility methods
285
+
286
+
287
+ // * Get list of all available Skills (used in the create and update modals)
288
  public getSkills(): Observable<Array<FirebaseSkillModel>> {
289
+ const skillsDataSource: Observable<Array<FirebaseSkillModel>> = collectionData<FirebaseSkillModel>(
290
+ query<FirebaseSkillModel>(
291
+ collection(this.firestore, 'skills') as CollectionReference<FirebaseSkillModel>
292
+ ),
293
+ { idField: 'id' }
294
+ );
295
+
296
+ return skillsDataSource;
297
  }
298
 
299
+ // * Get data of a specific Skill
300
  private getSkill(skillId: string): Observable<FirebaseSkillModel> {
301
+ const skillDataSource: Observable<FirebaseSkillModel> = docData<FirebaseSkillModel>(
302
+ doc(this.firestore, 'skills', skillId) as DocumentReference<FirebaseSkillModel>,
303
+ { idField: 'id' }
 
 
 
 
 
304
  );
 
305
 
306
+ return skillDataSource;
307
+ }
308
 
309
+ // * Get data of a specific User
310
  private getUser(userId: string): Observable<FirebaseUserModel> {
311
+ const userDocumentRef = doc(this.firestore, 'users', userId) as DocumentReference<FirebaseUserModel>;
312
+ const userDocumentSnapshotPromise = getDoc(userDocumentRef);
313
+
314
+ const userDataSource: Observable<FirebaseUserModel> = from(userDocumentSnapshotPromise)
315
  .pipe(
316
+ map((userSnapshot: DocumentSnapshot<FirebaseUserModel>) => {
317
+ if (userSnapshot.exists()) {
318
+ const user: FirebaseUserModel = userSnapshot.data();
319
+ const age = this.calcUserAge(user.birthdate);
320
+ const id = userSnapshot.id;
321
+
322
+ return { id, age, ...user } as FirebaseUserModel;
323
+ }
324
  })
325
  );
326
+
327
+ // ? If you want to listen to document changes use docData() instead
328
+ // const userDataSource: Observable<FirebaseUserModel> = docData<FirebaseUserModel>(
329
+ // doc(this.firestore, 'users', userId) as DocumentReference<FirebaseUserModel>,
330
+ // { idField: 'id' }
331
+ // )
332
+ // .pipe(
333
+ // map((user: FirebaseUserModel) => {
334
+ // const age = this.calcUserAge(user.birthdate);
335
+ // return { age, ...user } as FirebaseUserModel;
336
+ // })
337
+ // );
338
+
339
+ return userDataSource;
340
  }
341
 
342
+ // * Get all users who share at least 1 skill of the user's 'skills' list
343
+ private getUsersWithSameSkills(userId: string, skills: Array<FirebaseSkillModel>): Observable<Array<FirebaseListingItemModel>> {
344
  // Get the users who have at least 1 skill in common
345
  // Because firestore doesn't have a logical 'OR' operator we need to create multiple queries, one for each skill from the 'skills' list
346
+ const rawAggregatedUsersWithSameSkillsDataSource: Array<Observable<Array<FirebaseListingItemModel>>> = skills.map(skill => {
347
+ const usersWithSameSkillDataSource: Observable<Array<FirebaseListingItemModel>> = collectionData<FirebaseListingItemModel>(
348
+ query<FirebaseListingItemModel>(
349
+ collection(this.firestore, 'users') as CollectionReference<FirebaseListingItemModel>,
350
+ where('skills', 'array-contains', skill.id)
351
+ ),
352
+ { idField: 'id' }
353
+ )
354
+ .pipe(
355
+ map((users: Array<FirebaseListingItemModel>) => {
356
+ return users.map((user: FirebaseListingItemModel) => {
357
+ const age = this.calcUserAge(user.birthdate);
358
+
359
+ return { age, ...user } as FirebaseListingItemModel;
360
+ });
361
+ })
362
+ );
363
+
364
+ return usersWithSameSkillDataSource;
365
  });
366
 
367
  // Combine all these queries
368
+ const usersWithSameSkillsDataSource: Observable<Array<FirebaseListingItemModel>> = combineLatest(rawAggregatedUsersWithSameSkillsDataSource)
369
+ .pipe(
370
  map((relatedUsers: FirebaseListingItemModel[][]) => {
371
  // Flatten the array of arrays of FirebaseListingItemModel
372
  const flattenedRelatedUsers = ([] as FirebaseListingItemModel[]).concat(...relatedUsers);
386
  return filteredRelatedUsers;
387
  })
388
  );
389
+
390
+ return usersWithSameSkillsDataSource;
391
  }
392
 
393
  private calcUserAge(dateOfBirth: number): number {
src/app/firebase/crud/listing/firebase-listing.page.ts CHANGED
@@ -58,8 +58,9 @@ export class FirebaseListingPage implements OnInit, OnDestroy {
58
  dual: new FormControl({lower: 1, upper: 100})
59
  });
60
 
61
- this.route.data.subscribe(
62
- (resolvedRouteData) => {
 
63
  this.listingDataStore = resolvedRouteData['data'];
64
 
65
  // We need to avoid having multiple firebase subscriptions open at the same time to avoid memory leaks
@@ -103,16 +104,17 @@ export class FirebaseListingPage implements OnInit, OnDestroy {
103
  this.stateSubscription = merge(
104
  this.listingDataStore.state,
105
  updateSearchObservable
106
- ).subscribe(
107
- (state) => {
 
108
  this.items = state;
109
  },
110
- (error) => console.log(error),
111
- () => console.log('stateSubscription completed')
112
- );
113
  },
114
- (error) => console.log(error)
115
- );
116
  }
117
 
118
  async openFirebaseCreateModal() {
58
  dual: new FormControl({lower: 1, upper: 100})
59
  });
60
 
61
+ this.route.data
62
+ .subscribe({
63
+ next: (resolvedRouteData) => {
64
  this.listingDataStore = resolvedRouteData['data'];
65
 
66
  // We need to avoid having multiple firebase subscriptions open at the same time to avoid memory leaks
104
  this.stateSubscription = merge(
105
  this.listingDataStore.state,
106
  updateSearchObservable
107
+ )
108
+ .subscribe({
109
+ next: (state) => {
110
  this.items = state;
111
  },
112
+ error: (error) => console.log(error),
113
+ complete: () => console.log('stateSubscription completed')
114
+ });
115
  },
116
+ error: (error) => console.log(error)
117
+ });
118
  }
119
 
120
  async openFirebaseCreateModal() {
src/app/firebase/crud/user/select-image/select-user-image.modal.ts CHANGED
@@ -31,12 +31,13 @@ export class SelectUserImageModalComponent implements OnInit {
31
  const dataSource = this.firebaseCrudService.getAvatarsDataSource();
32
  const dataStore = this.firebaseCrudService.getAvatarsStore(dataSource);
33
 
34
- dataStore.state.subscribe(
35
- (state) => {
 
36
  this.avatars = state;
37
  },
38
- (error) => {}
39
- );
40
  }
41
 
42
  dismissModal(avatar?: UserImageModel) {
31
  const dataSource = this.firebaseCrudService.getAvatarsDataSource();
32
  const dataStore = this.firebaseCrudService.getAvatarsStore(dataSource);
33
 
34
+ dataStore.state
35
+ .subscribe({
36
+ next: (state) => {
37
  this.avatars = state;
38
  },
39
+ error: (error) => console.log(error)
40
+ });
41
  }
42
 
43
  dismissModal(avatar?: UserImageModel) {
src/app/firebase/crud/user/update/firebase-update-user.modal.ts CHANGED
@@ -96,7 +96,7 @@ export class FirebaseUpdateUserModalComponent implements OnInit {
96
  }
97
 
98
  dismissModal() {
99
- this.modalController.dismiss(undefined, undefined, this.modalId);
100
  }
101
 
102
  async deleteUser() {
96
  }
97
 
98
  dismissModal() {
99
+ this.modalController.dismiss(undefined, undefined, this.modalId);
100
  }
101
 
102
  async deleteUser() {
src/app/food/details/food-details.page.ts CHANGED
@@ -34,9 +34,12 @@ export class FoodDetailsPage implements OnInit {
34
  return ResolverHelper.extractData<FoodDetailsModel>(resolvedRouteData.data, FoodDetailsModel);
35
  })
36
  )
37
- .subscribe((state) => {
38
- this.details = state;
39
- }, (error) => console.log(error));
 
 
 
40
  }
41
 
42
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
34
  return ResolverHelper.extractData<FoodDetailsModel>(resolvedRouteData.data, FoodDetailsModel);
35
  })
36
  )
37
+ .subscribe({
38
+ next: (state) => {
39
+ this.details = state;
40
+ },
41
+ error: (error) => console.log(error)
42
+ });
43
  }
44
 
45
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
src/app/food/listing/food-listing.page.ts CHANGED
@@ -34,9 +34,12 @@ export class FoodListingPage implements OnInit {
34
  return ResolverHelper.extractData<FoodListingModel>(resolvedRouteData.data, FoodListingModel);
35
  })
36
  )
37
- .subscribe((state) => {
38
- this.listing = state;
39
- }, (error) => console.log(error));
 
 
 
40
  }
41
 
42
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
34
  return ResolverHelper.extractData<FoodListingModel>(resolvedRouteData.data, FoodListingModel);
35
  })
36
  )
37
+ .subscribe({
38
+ next: (state) => {
39
+ this.listing = state;
40
+ },
41
+ error: (error) => console.log(error)
42
+ });
43
  }
44
 
45
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
src/app/getting-started/getting-started.module.ts CHANGED
@@ -5,6 +5,8 @@ import { Routes, RouterModule } from '@angular/router';
5
 
6
  import { IonicModule } from '@ionic/angular';
7
 
 
 
8
  import { ComponentsModule } from '../components/components.module';
9
 
10
  import { GettingStartedPage } from './getting-started.page';
@@ -23,7 +25,8 @@ const routes: Routes = [
23
  ReactiveFormsModule,
24
  IonicModule,
25
  RouterModule.forChild(routes),
26
- ComponentsModule
 
27
  ],
28
  declarations: [GettingStartedPage]
29
  })
5
 
6
  import { IonicModule } from '@ionic/angular';
7
 
8
+ import { SwiperModule } from "swiper/angular";
9
+
10
  import { ComponentsModule } from '../components/components.module';
11
 
12
  import { GettingStartedPage } from './getting-started.page';
25
  ReactiveFormsModule,
26
  IonicModule,
27
  RouterModule.forChild(routes),
28
+ ComponentsModule,
29
+ SwiperModule
30
  ],
31
  declarations: [GettingStartedPage]
32
  })
src/app/getting-started/getting-started.page.html CHANGED
@@ -9,9 +9,9 @@
9
  <ion-content>
10
  <!-- We need the form wrapping the slides so all the inputs in the different slides are part of the same form -->
11
  <form class="getting-started-form" [formGroup]="gettingStartedForm">
12
- <ion-slides class="getting-started-slides" pager="true">
13
- <ion-slide class="browsing-categories-slide question-slide">
14
- <ion-row class="slide-inner-row">
15
  <ion-col class="question-options-col" size="12">
16
  <h2 class="slide-title">What are you browsing for?</h2>
17
  <ion-list class="question-options-list">
@@ -44,9 +44,9 @@
44
  </ion-list>
45
  </ion-col>
46
  </ion-row>
47
- </ion-slide>
48
- <ion-slide class="interests-to-follow-slide question-slide">
49
- <ion-row class="slide-inner-row">
50
  <ion-col class="heading-col">
51
  <h2 class="slide-title">Pick categories to follow</h2>
52
  <p class="slide-subtitle">
@@ -121,7 +121,7 @@
121
  <ion-button class="signup-button" color="secondary" expand="block" [routerLink]="['/auth/signup']">Sign Up</ion-button>
122
  </ion-col>
123
  </ion-row>
124
- </ion-slide>
125
- </ion-slides>
126
  </form>
127
  </ion-content>
9
  <ion-content>
10
  <!-- We need the form wrapping the slides so all the inputs in the different slides are part of the same form -->
11
  <form class="getting-started-form" [formGroup]="gettingStartedForm">
12
+ <swiper class="getting-started-slides" [pagination]="true" (swiper)="swiperInit($event)" (slideChangeTransitionStart)="slideWillChange()">
13
+ <ng-template swiperSlide>
14
+ <ion-row class="browsing-categories-slide question-slide slide-inner-row">
15
  <ion-col class="question-options-col" size="12">
16
  <h2 class="slide-title">What are you browsing for?</h2>
17
  <ion-list class="question-options-list">
44
  </ion-list>
45
  </ion-col>
46
  </ion-row>
47
+ </ng-template>
48
+ <ng-template swiperSlide>
49
+ <ion-row class="slide-inner-row interests-to-follow-slide question-slide">
50
  <ion-col class="heading-col">
51
  <h2 class="slide-title">Pick categories to follow</h2>
52
  <p class="slide-subtitle">
121
  <ion-button class="signup-button" color="secondary" expand="block" [routerLink]="['/auth/signup']">Sign Up</ion-button>
122
  </ion-col>
123
  </ion-row>
124
+ </ng-template>
125
+ </swiper>
126
  </form>
127
  </ion-content>
src/app/getting-started/getting-started.page.ts CHANGED
@@ -1,8 +1,12 @@
1
- import { isPlatformBrowser } from '@angular/common';
2
- import { Component, AfterViewInit, ViewChild, HostBinding, Inject, PLATFORM_ID } from '@angular/core';
3
  import { FormGroup, FormControl } from '@angular/forms';
4
 
5
- import { IonSlides, MenuController } from '@ionic/angular';
 
 
 
 
 
6
 
7
  @Component({
8
  selector: 'app-getting-started',
@@ -13,15 +17,15 @@ import { IonSlides, MenuController } from '@ionic/angular';
13
  './styles/getting-started.responsive.scss'
14
  ]
15
  })
16
- export class GettingStartedPage implements AfterViewInit {
17
- @ViewChild(IonSlides, { static: true }) slides: IonSlides;
18
  @HostBinding('class.last-slide-active') isLastSlide = false;
19
 
 
20
  gettingStartedForm: FormGroup;
21
 
22
  constructor(
23
- @Inject(PLATFORM_ID) private platformId: object,
24
- public menu: MenuController
25
  ) {
26
  this.gettingStartedForm = new FormGroup({
27
  browsingCategory: new FormControl('men'),
@@ -37,29 +41,24 @@ export class GettingStartedPage implements AfterViewInit {
37
  }
38
 
39
  // Disable side menu for this page
40
- ionViewDidEnter(): void {
41
  this.menu.enable(false);
42
  }
43
 
44
  // Restore to default when leaving this page
45
- ionViewDidLeave(): void {
46
  this.menu.enable(true);
47
  }
48
 
49
- ngAfterViewInit(): void {
50
- // Accessing slides in server platform throw errors
51
- if (isPlatformBrowser(this.platformId)) {
52
- // ViewChild is set
53
- this.slides.isEnd().then(isEnd => {
54
- this.isLastSlide = isEnd;
55
- });
56
 
57
- // Subscribe to changes
58
- this.slides.ionSlideWillChange.subscribe(changes => {
59
- this.slides.isEnd().then(isEnd => {
60
- this.isLastSlide = isEnd;
61
- });
62
- });
63
- }
64
  }
65
  }
1
+ import { Component, HostBinding, NgZone } from '@angular/core';
 
2
  import { FormGroup, FormControl } from '@angular/forms';
3
 
4
+ import { MenuController } from '@ionic/angular';
5
+ import { IonicSwiper } from "@ionic/angular";
6
+
7
+ import SwiperCore, { Pagination } from "swiper";
8
+
9
+ SwiperCore.use([Pagination, IonicSwiper]);
10
 
11
  @Component({
12
  selector: 'app-getting-started',
17
  './styles/getting-started.responsive.scss'
18
  ]
19
  })
20
+ export class GettingStartedPage {
 
21
  @HostBinding('class.last-slide-active') isLastSlide = false;
22
 
23
+ swiperRef: SwiperCore;
24
  gettingStartedForm: FormGroup;
25
 
26
  constructor(
27
+ public menu: MenuController,
28
+ private ngZone: NgZone
29
  ) {
30
  this.gettingStartedForm = new FormGroup({
31
  browsingCategory: new FormControl('men'),
41
  }
42
 
43
  // Disable side menu for this page
44
+ public ionViewDidEnter(): void {
45
  this.menu.enable(false);
46
  }
47
 
48
  // Restore to default when leaving this page
49
+ public ionViewDidLeave(): void {
50
  this.menu.enable(true);
51
  }
52
 
53
+ public swiperInit(swiper: SwiperCore): void {
54
+ this.swiperRef = swiper;
55
+ }
 
 
 
 
56
 
57
+ public slideWillChange(): void {
58
+ // ? We need to use ngZone because the change happens outside Angular
59
+ // (see: https://swiperjs.com/angular#swiper-component-events)
60
+ this.ngZone.run(() => {
61
+ this.isLastSlide = this.swiperRef.isEnd;
62
+ });
 
63
  }
64
  }
src/app/getting-started/styles/getting-started.page.scss CHANGED
@@ -78,7 +78,7 @@ ion-content {
78
  }
79
 
80
  .browsing-categories-slide {
81
- .slide-inner-row {
82
  flex-flow: column;
83
  justify-content: space-between;
84
  }
@@ -129,12 +129,12 @@ ion-content {
129
  }
130
 
131
  .interests-to-follow-slide {
132
- .slide-inner-row {
133
  flex-flow: column;
134
  justify-content: space-between;
135
 
136
  // In the last slide .swiper-pagination is hidden
137
- border-width: 0px;
138
  }
139
 
140
  .heading-col {
@@ -214,7 +214,7 @@ ion-content {
214
 
215
  height: 100%;
216
  width: 100%;
217
- // Note: We cannote change the styles of the .checkbox-icon because it's inside the shadow dom.
218
  // An alternative would be to set --width and --height to 0px and add a custom overlay and icon in the <custom-checkbox> html
219
  }
220
 
@@ -234,8 +234,8 @@ ion-content {
234
  }
235
  }
236
 
237
- // ISSUE: .swiper-paggination gets rendered dynamically. That prevents styling the elements when using the default Angular ViewEncapsulation.None
238
- // (Angular doesn't add an '_ngcontent' attribute to the .swiper-paggination because it's dynamically rendered)
239
  // FIX: See: https://stackoverflow.com/a/36265072/1116959
240
  :host ::ng-deep .getting-started-slides {
241
  .swiper-pagination {
78
  }
79
 
80
  .browsing-categories-slide {
81
+ &.slide-inner-row {
82
  flex-flow: column;
83
  justify-content: space-between;
84
  }
129
  }
130
 
131
  .interests-to-follow-slide {
132
+ &.slide-inner-row {
133
  flex-flow: column;
134
  justify-content: space-between;
135
 
136
  // In the last slide .swiper-pagination is hidden
137
+ border-width: 0px !important;
138
  }
139
 
140
  .heading-col {
214
 
215
  height: 100%;
216
  width: 100%;
217
+ // Note: We cannot change the styles of the .checkbox-icon because it's inside the shadow dom.
218
  // An alternative would be to set --width and --height to 0px and add a custom overlay and icon in the <custom-checkbox> html
219
  }
220
 
234
  }
235
  }
236
 
237
+ // ISSUE: .swiper-pagination gets rendered dynamically. That prevents styling the elements when using the default Angular ViewEncapsulation.None
238
+ // (Angular doesn't add an '_ngcontent' attribute to the .swiper-pagination because it's dynamically rendered)
239
  // FIX: See: https://stackoverflow.com/a/36265072/1116959
240
  :host ::ng-deep .getting-started-slides {
241
  .swiper-pagination {
src/app/notifications/notifications.page.ts CHANGED
@@ -27,9 +27,12 @@ export class NotificationsPage implements OnInit {
27
  return resolvedRouteData['data'].source;
28
  })
29
  )
30
- .subscribe((pageData) => {
31
- this.notifications = pageData;
32
- }, (error) => console.log(error));
 
 
 
33
  }
34
 
35
 
27
  return resolvedRouteData['data'].source;
28
  })
29
  )
30
+ .subscribe({
31
+ next: (pageData) => {
32
+ this.notifications = pageData;
33
+ },
34
+ error: (error) => console.log(error)
35
+ });
36
  }
37
 
38
 
src/app/pipes/time-ago.pipe.ts CHANGED
@@ -5,12 +5,12 @@ import * as relativeTime from 'dayjs/plugin/relativeTime';
5
 
6
  @Pipe({ name: 'appTimeAgo' })
7
  export class TimeAgoPipe implements PipeTransform {
8
- transform(value: any): string {
9
  dayjs.extend(relativeTime);
10
  let timeAgo = '';
11
 
12
  if (value) {
13
- const withoutSuffix = (dayjs(value).diff(dayjs(), 'day') < 0) ? false : true;
14
  timeAgo = dayjs().to(dayjs(value), withoutSuffix);
15
  }
16
 
5
 
6
  @Pipe({ name: 'appTimeAgo' })
7
  export class TimeAgoPipe implements PipeTransform {
8
+ transform(value: any, withoutSuffixParam: boolean = false): string {
9
  dayjs.extend(relativeTime);
10
  let timeAgo = '';
11
 
12
  if (value) {
13
+ const withoutSuffix = withoutSuffixParam || ((dayjs(value).diff(dayjs(), 'day') < 0) ? false : true);
14
  timeAgo = dayjs().to(dayjs(value), withoutSuffix);
15
  }
16
 
src/app/real-estate/details/real-estate-details.page.ts CHANGED
@@ -30,8 +30,11 @@ export class RealEstateDetailsPage implements OnInit {
30
  return ResolverHelper.extractData<RealEstateDetailsModel>(resolvedRouteData.data, RealEstateDetailsModel);
31
  })
32
  )
33
- .subscribe((state) => {
34
- this.details = state;
35
- }, (error) => console.log(error));
 
 
 
36
  }
37
  }
30
  return ResolverHelper.extractData<RealEstateDetailsModel>(resolvedRouteData.data, RealEstateDetailsModel);
31
  })
32
  )
33
+ .subscribe({
34
+ next: (state) => {
35
+ this.details = state;
36
+ },
37
+ error: (error) => console.log(error)
38
+ });
39
  }
40
  }
src/app/real-estate/listing/real-estate-listing.page.ts CHANGED
@@ -30,8 +30,11 @@ export class RealEstateListingPage implements OnInit {
30
  return ResolverHelper.extractData<RealEstateListingModel>(resolvedRouteData.data, RealEstateListingModel);
31
  })
32
  )
33
- .subscribe((state) => {
34
- this.listing = state;
35
- }, (error) => console.log(error));
 
 
 
36
  }
37
  }
30
  return ResolverHelper.extractData<RealEstateListingModel>(resolvedRouteData.data, RealEstateListingModel);
31
  })
32
  )
33
+ .subscribe({
34
+ next: (state) => {
35
+ this.listing = state;
36
+ },
37
+ error: (error) => console.log(error)
38
+ });
39
  }
40
  }
src/app/showcase/app-shell/data-store-combined/data-store-combined.page.ts CHANGED
@@ -31,7 +31,8 @@ export class DataStoreCombinedPage implements OnInit {
31
  this.tasksCombinedDataStore = new DataStore(shellModel);
32
  this.tasksCombinedDataStore.load(dataSource);
33
 
34
- this.tasksCombinedDataStore.state.subscribe(data => {
 
35
  this.tasks = data;
36
  });
37
  }
31
  this.tasksCombinedDataStore = new DataStore(shellModel);
32
  this.tasksCombinedDataStore.load(dataSource);
33
 
34
+ this.tasksCombinedDataStore.state
35
+ .subscribe(data => {
36
  this.tasks = data;
37
  });
38
  }
src/app/showcase/route-resolvers-ux/progressive-shell-resolvers/progressive-shell-resolvers.page.ts CHANGED
@@ -24,8 +24,11 @@ export class ProgressiveShellResovlersPage implements OnInit {
24
  // Extract data for this page
25
  switchMap((resolvedRouteData) => resolvedRouteData['data'].state)
26
  )
27
- .subscribe((state: any) => {
28
- this.routeResolveData = state;
29
- }, (error) => console.log(error));
 
 
 
30
  }
31
  }
24
  // Extract data for this page
25
  switchMap((resolvedRouteData) => resolvedRouteData['data'].state)
26
  )
27
+ .subscribe({
28
+ next: (state: any) => {
29
+ this.routeResolveData = state;
30
+ },
31
+ error: (error) => console.log(error)
32
+ });
33
  }
34
  }
src/app/travel/details/travel-details.page.ts CHANGED
@@ -35,9 +35,12 @@ export class TravelDetailsPage implements OnInit {
35
  return ResolverHelper.extractData<TravelDetailsModel>(resolvedRouteData.data['dataStore'], TravelDetailsModel);
36
  })
37
  )
38
- .subscribe((state) => {
39
- this.details = state;
40
- }, (error) => console.log(error));
 
 
 
41
  }
42
 
43
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
35
  return ResolverHelper.extractData<TravelDetailsModel>(resolvedRouteData.data['dataStore'], TravelDetailsModel);
36
  })
37
  )
38
+ .subscribe({
39
+ next: (state) => {
40
+ this.details = state;
41
+ },
42
+ error: (error) => console.log(error)
43
+ });
44
  }
45
 
46
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
src/app/travel/listing/travel-listing.page.ts CHANGED
@@ -38,9 +38,12 @@ export class TravelListingPage implements OnInit {
38
  return ResolverHelper.extractData<TravelListingModel>(resolvedRouteData.data['dataStore'], TravelListingModel);
39
  })
40
  )
41
- .subscribe((state) => {
42
- this.listing = state;
43
- }, (error) => console.log(error));
 
 
 
44
  }
45
 
46
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
38
  return ResolverHelper.extractData<TravelListingModel>(resolvedRouteData.data['dataStore'], TravelListingModel);
39
  })
40
  )
41
+ .subscribe({
42
+ next: (state) => {
43
+ this.listing = state;
44
+ },
45
+ error: (error) => console.log(error)
46
+ });
47
  }
48
 
49
  // NOTE: Ionic only calls ngOnDestroy if the page was popped (ex: when navigating back)
src/app/user/friends/user-friends.page.ts CHANGED
@@ -43,12 +43,15 @@ export class UserFriendsPage implements OnInit {
43
  return ResolverHelper.extractData<UserFriendsModel>(resolvedRouteData.data, UserFriendsModel);
44
  })
45
  )
46
- .subscribe((state) => {
47
- this.data = state;
48
- this.friendsList = this.data.friends;
49
- this.followersList = this.data.followers;
50
- this.followingList = this.data.following;
51
- }, (error) => console.log(error));
 
 
 
52
  }
53
 
54
  segmentChanged(ev): void {
43
  return ResolverHelper.extractData<UserFriendsModel>(resolvedRouteData.data, UserFriendsModel);
44
  })
45
  )
46
+ .subscribe({
47
+ next: (state) => {
48
+ this.data = state;
49
+ this.friendsList = this.data.friends;
50
+ this.followersList = this.data.followers;
51
+ this.followingList = this.data.following;
52
+ },
53
+ error: (error) => console.log(error)
54
+ });
55
  }
56
 
57
  segmentChanged(ev): void {
src/app/user/profile/user-profile.page.ts CHANGED
@@ -48,12 +48,15 @@ export class UserProfilePage implements OnInit {
48
  return ResolverHelper.extractData<UserProfileModel>(resolvedRouteData.data, UserProfileModel);
49
  })
50
  )
51
- .subscribe((state) => {
52
- this.profile = state;
53
-
54
- // get translations for this page to use in the Language Chooser Alert
55
- this.getTranslations();
56
- }, (error) => console.log(error));
 
 
 
57
 
58
  this.translate.onLangChange.subscribe(() => this.getTranslations());
59
  }
48
  return ResolverHelper.extractData<UserProfileModel>(resolvedRouteData.data, UserProfileModel);
49
  })
50
  )
51
+ .subscribe({
52
+ next: (state) => {
53
+ this.profile = state;
54
+
55
+ // get translations for this page to use in the Language Chooser Alert
56
+ this.getTranslations();
57
+ },
58
+ error: (error) => console.log(error)
59
+ });
60
 
61
  this.translate.onLangChange.subscribe(() => this.getTranslations());
62
  }
src/app/video-playlist/video-playlist.page.ts CHANGED
@@ -40,13 +40,16 @@ export class VideoPlaylistPage implements OnInit {
40
  // Extract data for this page
41
  switchMap((resolvedRouteData) => resolvedRouteData['data'].state)
42
  )
43
- .subscribe((state: any) => {
44
- this.video_playlist_model = state;
45
- if (!state.isShell) {
46
- this.video_playlist_model.video_playlist = state.videos;
47
- this.video_playlist_model.selected_video = state.videos[0];
48
- }
49
- }, (error) => console.log(error));
 
 
 
50
  }
51
 
52
  playMedia(media) {
40
  // Extract data for this page
41
  switchMap((resolvedRouteData) => resolvedRouteData['data'].state)
42
  )
43
+ .subscribe({
44
+ next: (state: any) => {
45
+ this.video_playlist_model = state;
46
+ if (!state.isShell) {
47
+ this.video_playlist_model.video_playlist = state.videos;
48
+ this.video_playlist_model.selected_video = state.videos[0];
49
+ }
50
+ },
51
+ error: (error) => console.log(error)
52
+ });
53
  }
54
 
55
  playMedia(media) {
src/app/walkthrough/styles/walkthrough.page.scss CHANGED
@@ -54,7 +54,7 @@ ion-content {
54
  }
55
 
56
  .illustration-and-decoration-slide {
57
- .slide-inner-row {
58
  --ion-grid-column-padding: 0px;
59
 
60
  flex-flow: column;
@@ -141,9 +141,9 @@ ion-content {
141
  .last-slide {
142
  --page-vector-decoration-fill: var(--page-last-slide-background);
143
 
144
- .slide-inner-row {
145
  // In the last slide .swiper-pagination is hidden
146
- border-width: 0px;
147
  }
148
 
149
  .info-col {
@@ -207,8 +207,8 @@ ion-content {
207
  }
208
  }
209
 
210
- // ISSUE: .swiper-paggination gets rendered dynamically. That prevents styling the elements when using the default Angular ViewEncapsulation.None
211
- // (Angular doesn't add an '_ngcontent' attribute to the .swiper-paggination because it's dynamically rendered)
212
  // FIX: See: https://stackoverflow.com/a/36265072/1116959
213
  :host ::ng-deep .walkthrough-slides {
214
  .swiper-pagination {
54
  }
55
 
56
  .illustration-and-decoration-slide {
57
+ &.slide-inner-row {
58
  --ion-grid-column-padding: 0px;
59
 
60
  flex-flow: column;
141
  .last-slide {
142
  --page-vector-decoration-fill: var(--page-last-slide-background);
143
 
144
+ &.slide-inner-row {
145
  // In the last slide .swiper-pagination is hidden
146
+ border-width: 0px !important;
147
  }
148
 
149
  .info-col {
207
  }
208
  }
209
 
210
+ // ISSUE: .swiper-pagination gets rendered dynamically. That prevents styling the elements when using the default Angular ViewEncapsulation.None
211
+ // (Angular doesn't add an '_ngcontent' attribute to the .swiper-pagination because it's dynamically rendered)
212
  // FIX: See: https://stackoverflow.com/a/36265072/1116959
213
  :host ::ng-deep .walkthrough-slides {
214
  .swiper-pagination {
src/app/walkthrough/walkthrough.module.ts CHANGED
@@ -5,6 +5,8 @@ import { Routes, RouterModule } from '@angular/router';
5
 
6
  import { IonicModule } from '@ionic/angular';
7
 
 
 
8
  import { ComponentsModule } from '../components/components.module';
9
 
10
  import { WalkthroughPage } from './walkthrough.page';
@@ -22,7 +24,8 @@ const routes: Routes = [
22
  FormsModule,
23
  IonicModule,
24
  RouterModule.forChild(routes),
25
- ComponentsModule
 
26
  ],
27
  declarations: [WalkthroughPage]
28
  })
5
 
6
  import { IonicModule } from '@ionic/angular';
7
 
8
+ import { SwiperModule } from 'swiper/angular';
9
+
10
  import { ComponentsModule } from '../components/components.module';
11
 
12
  import { WalkthroughPage } from './walkthrough.page';
24
  FormsModule,
25
  IonicModule,
26
  RouterModule.forChild(routes),
27
+ ComponentsModule,
28
+ SwiperModule
29
  ],
30
  declarations: [WalkthroughPage]
31
  })
src/app/walkthrough/walkthrough.page.html CHANGED
@@ -7,9 +7,9 @@
7
  </ion-header>
8
 
9
  <ion-content>
10
- <ion-slides class="walkthrough-slides" pager="true">
11
- <ion-slide class="first-slide illustration-and-decoration-slide">
12
- <ion-row class="slide-inner-row">
13
  <ion-col class="illustration-col">
14
  <app-aspect-ratio [ratio]="{w:915, h:849}">
15
  <app-image-shell class="illustration-image" animation="spinner" [src]="'./assets/sample-images/walkthrough/walkthrough-illustration-1.svg'" [alt]="'walkthrough'"></app-image-shell>
@@ -33,9 +33,9 @@
33
  </div>
34
  </ion-col>
35
  </ion-row>
36
- </ion-slide>
37
- <ion-slide class="second-slide illustration-and-decoration-slide">
38
- <ion-row class="slide-inner-row">
39
  <ion-col class="illustration-col">
40
  <app-aspect-ratio [ratio]="{w:1096, h:806}">
41
  <app-image-shell class="illustration-image" animation="spinner" [src]="'./assets/sample-images/walkthrough/walkthrough-illustration-2.svg'" [alt]="'walkthrough'"></app-image-shell>
@@ -59,9 +59,9 @@
59
  </div>
60
  </ion-col>
61
  </ion-row>
62
- </ion-slide>
63
- <ion-slide class="third-slide illustration-and-decoration-slide">
64
- <ion-row class="slide-inner-row">
65
  <ion-col class="illustration-col">
66
  <app-aspect-ratio [ratio]="{w:918, h:703}">
67
  <app-image-shell class="illustration-image" animation="spinner" [src]="'./assets/sample-images/walkthrough/walkthrough-illustration-3.svg'" [alt]="'walkthrough'"></app-image-shell>
@@ -82,9 +82,9 @@
82
  </div>
83
  </ion-col>
84
  </ion-row>
85
- </ion-slide>
86
- <ion-slide class="last-slide illustration-and-decoration-slide">
87
- <ion-row class="slide-inner-row">
88
  <ion-col class="illustration-col">
89
  <app-aspect-ratio [ratio]="{w:924, h:819}">
90
  <app-image-shell class="illustration-image" animation="spinner" [src]="'./assets/sample-images/walkthrough/walkthrough-illustration-4.svg'" [alt]="'walkthrough'"></app-image-shell>
@@ -118,6 +118,6 @@
118
  </ion-row>
119
  </ion-col>
120
  </ion-row>
121
- </ion-slide>
122
- </ion-slides>
123
  </ion-content>
7
  </ion-header>
8
 
9
  <ion-content>
10
+ <swiper [pagination]="true" class="walkthrough-slides" (swiper)="setSwiperInstance($event)" (init)="swiperInit()" (slideChangeTransitionStart)="slideWillChange()">
11
+ <ng-template swiperSlide>
12
+ <ion-row class="slide-inner-row first-slide illustration-and-decoration-slide">
13
  <ion-col class="illustration-col">
14
  <app-aspect-ratio [ratio]="{w:915, h:849}">
15
  <app-image-shell class="illustration-image" animation="spinner" [src]="'./assets/sample-images/walkthrough/walkthrough-illustration-1.svg'" [alt]="'walkthrough'"></app-image-shell>
33
  </div>
34
  </ion-col>
35
  </ion-row>
36
+ </ng-template>
37
+ <ng-template swiperSlide>
38
+ <ion-row class="second-slide illustration-and-decoration-slide slide-inner-row">
39
  <ion-col class="illustration-col">
40
  <app-aspect-ratio [ratio]="{w:1096, h:806}">
41
  <app-image-shell class="illustration-image" animation="spinner" [src]="'./assets/sample-images/walkthrough/walkthrough-illustration-2.svg'" [alt]="'walkthrough'"></app-image-shell>
59
  </div>
60
  </ion-col>
61
  </ion-row>
62
+ </ng-template>
63
+ <ng-template swiperSlide>
64
+ <ion-row class="third-slide illustration-and-decoration-slide slide-inner-row">
65
  <ion-col class="illustration-col">
66
  <app-aspect-ratio [ratio]="{w:918, h:703}">
67
  <app-image-shell class="illustration-image" animation="spinner" [src]="'./assets/sample-images/walkthrough/walkthrough-illustration-3.svg'" [alt]="'walkthrough'"></app-image-shell>
82
  </div>
83
  </ion-col>
84
  </ion-row>
85
+ </ng-template>
86
+ <ng-template swiperSlide>
87
+ <ion-row class="last-slide illustration-and-decoration-slide slide-inner-row">
88
  <ion-col class="illustration-col">
89
  <app-aspect-ratio [ratio]="{w:924, h:819}">
90
  <app-image-shell class="illustration-image" animation="spinner" [src]="'./assets/sample-images/walkthrough/walkthrough-illustration-4.svg'" [alt]="'walkthrough'"></app-image-shell>
118
  </ion-row>
119
  </ion-col>
120
  </ion-row>
121
+ </ng-template>
122
+ </swiper>
123
  </ion-content>
src/app/walkthrough/walkthrough.page.ts CHANGED
@@ -1,7 +1,12 @@
1
- import { isPlatformBrowser } from '@angular/common';
2
- import { Component, AfterViewInit, ViewChild, HostBinding, PLATFORM_ID, Inject } from '@angular/core';
3
 
4
- import { IonSlides, MenuController } from '@ionic/angular';
 
 
 
 
 
 
5
 
6
  @Component({
7
  selector: 'app-walkthrough',
@@ -13,16 +18,17 @@ import { IonSlides, MenuController } from '@ionic/angular';
13
  ]
14
  })
15
  export class WalkthroughPage implements AfterViewInit {
 
16
 
17
- @ViewChild(IonSlides, { static: true }) slides: IonSlides;
18
 
19
  @HostBinding('class.first-slide-active') isFirstSlide = true;
20
 
21
  @HostBinding('class.last-slide-active') isLastSlide = false;
22
 
23
  constructor(
24
- @Inject(PLATFORM_ID) private platformId: object,
25
- public menu: MenuController
26
  ) { }
27
 
28
  // Disable side menu for this page
@@ -37,34 +43,46 @@ export class WalkthroughPage implements AfterViewInit {
37
 
38
  ngAfterViewInit(): void {
39
  // Accessing slides in server platform throw errors
40
- if (isPlatformBrowser(this.platformId)) {
 
41
 
42
- this.slides.ionSlidesDidLoad.subscribe(() => this.slides.update());
43
-
44
- // ViewChild is set
45
- this.slides.isBeginning().then(isBeginning => {
46
- this.isFirstSlide = isBeginning;
47
- });
48
- this.slides.isEnd().then(isEnd => {
49
- this.isLastSlide = isEnd;
50
  });
 
51
 
52
- // Subscribe to changes
53
- this.slides.ionSlideWillChange.subscribe(changes => {
54
- this.slides.isBeginning().then(isBeginning => {
55
- this.isFirstSlide = isBeginning;
56
- });
57
- this.slides.isEnd().then(isEnd => {
58
- this.isLastSlide = isEnd;
59
- });
60
  });
61
- }
 
 
 
 
 
62
  }
63
 
64
- skipWalkthrough(): void {
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  // Skip to the last slide
66
- this.slides.length().then(length => {
67
- this.slides.slideTo(length);
68
- });
69
  }
70
  }
1
+ import { Component, AfterViewInit, ViewChild, HostBinding, NgZone } from '@angular/core';
 
2
 
3
+ import { MenuController } from '@ionic/angular';
4
+ import { IonicSwiper } from '@ionic/angular';
5
+
6
+ import SwiperCore, { Pagination } from 'swiper';
7
+ import { SwiperComponent } from 'swiper/angular';
8
+
9
+ SwiperCore.use([Pagination, IonicSwiper]);
10
 
11
  @Component({
12
  selector: 'app-walkthrough',
18
  ]
19
  })
20
  export class WalkthroughPage implements AfterViewInit {
21
+ swiperRef: SwiperCore;
22
 
23
+ @ViewChild(SwiperComponent, { static: false }) swiper?: SwiperComponent;
24
 
25
  @HostBinding('class.first-slide-active') isFirstSlide = true;
26
 
27
  @HostBinding('class.last-slide-active') isLastSlide = false;
28
 
29
  constructor(
30
+ public menu: MenuController,
31
+ private ngZone: NgZone
32
  ) { }
33
 
34
  // Disable side menu for this page
43
 
44
  ngAfterViewInit(): void {
45
  // Accessing slides in server platform throw errors
46
+ // if (isPlatformBrowser(this.platformId)) {
47
+ this.swiperRef = this.swiper.swiperRef;
48
 
49
+ this.swiperRef.on('slidesLengthChange', () => {
50
+ // ? We need to use ngZone because the change happens outside Angular
51
+ // (see: https://swiperjs.com/angular#swiper-component-events)
52
+ this.ngZone.run(() => {
53
+ this.markSlides(this.swiperRef);
 
 
 
54
  });
55
+ });
56
 
57
+ this.swiperRef.on('slideChange', () => {
58
+ // ? We need to use ngZone because the change happens outside Angular
59
+ // (see: https://swiperjs.com/angular#swiper-component-events)
60
+ this.ngZone.run(() => {
61
+ this.markSlides(this.swiperRef);
 
 
 
62
  });
63
+ });
64
+ // }
65
+ }
66
+
67
+ public setSwiperInstance(swiper: SwiperCore): void {
68
+ // console.log('setSwiperInstance');
69
  }
70
 
71
+ public swiperInit(): void {
72
+ // console.log('swiperInit');
73
+ }
74
+
75
+ public slideWillChange(): void {
76
+ // console.log('slideWillChange');
77
+ }
78
+
79
+ public markSlides(swiper: SwiperCore): void {
80
+ this.isFirstSlide = (swiper.isBeginning || swiper.activeIndex === 0);
81
+ this.isLastSlide = swiper.isEnd;
82
+ }
83
+
84
+ public skipWalkthrough(): void {
85
  // Skip to the last slide
86
+ this.swiperRef.slideTo(this.swiperRef.slides.length - 1);
 
 
87
  }
88
  }
src/assets/icon/favicon.ico CHANGED
Binary file
src/global.scss CHANGED
@@ -12,6 +12,11 @@
12
  @import "~@ionic/angular/css/text-transformation.css";
13
  @import "~@ionic/angular/css/flex-utils.css";
14
 
 
 
 
 
 
15
  // EXTRA GLOBAL STYLES
16
  // Add custom Ionic Colors
17
  @import "theme/custom-ion-colors.scss";
12
  @import "~@ionic/angular/css/text-transformation.css";
13
  @import "~@ionic/angular/css/flex-utils.css";
14
 
15
+ @import "~swiper/scss";
16
+ @import "~swiper/scss/autoplay";
17
+ @import "~swiper/scss/pagination";
18
+ @import "~@ionic/angular/css/ionic-swiper";
19
+
20
  // EXTRA GLOBAL STYLES
21
  // Add custom Ionic Colors
22
  @import "theme/custom-ion-colors.scss";
src/manifest.webmanifest CHANGED
@@ -1,7 +1,7 @@
1
  {
2
- "name": "Ionic5FullApp",
3
  "description": "The most advanced and complete Mobile & PWA Ionic starter app template",
4
- "short_name": "Ionic5FullApp",
5
  "theme_color": "#1C1C1C",
6
  "background_color": "#000000",
7
  "display": "standalone",
1
  {
2
+ "name": "Ionic6FullApp-PRO",
3
  "description": "The most advanced and complete Mobile & PWA Ionic starter app template",
4
+ "short_name": "Ionic6FullApp-PRO",
5
  "theme_color": "#1C1C1C",
6
  "background_color": "#000000",
7
  "display": "standalone",