Diff to HTML by rtfpessoa

Files changed (62) hide show
  1. .firebaserc +14 -1
  2. DIFF.md +51 -0
  3. FIREBASE.md +74 -0
  4. README.md +1 -1
  5. angular.json +3 -28
  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-routing.module.ts +5 -5
  18. src/app/app.component.ts +43 -42
  19. src/app/components/countdown-timer/countdown-timer.component.ts +6 -5
  20. src/app/components/rating-input/rating-input.component.html +5 -1
  21. src/app/components/rating-input/rating-input.component.ts +4 -12
  22. src/app/components/show-hide-password/show-hide-password.component.html +6 -6
  23. src/app/components/show-hide-password/show-hide-password.component.ts +4 -11
  24. src/app/deals/details/deals-details.module.ts +4 -1
  25. src/app/deals/details/deals-details.page.html +4 -4
  26. src/app/deals/details/deals-details.page.ts +1 -1
  27. src/app/deals/details/styles/deals-details.page.scss +3 -3
  28. src/app/fashion/details/fashion-details.module.ts +4 -1
  29. src/app/fashion/details/fashion-details.page.html +4 -4
  30. src/app/fashion/details/styles/fashion-details.page.scss +2 -2
  31. src/app/firebase/auth/firebase-auth-definitions.ts +6 -0
  32. src/app/firebase/auth/firebase-auth.module.ts +21 -5
  33. src/app/firebase/auth/firebase-auth.service.ts +442 -105
  34. src/app/firebase/auth/profile/firebase-profile.module.ts +7 -5
  35. src/app/firebase/auth/profile/firebase-profile.page.ts +16 -9
  36. src/app/firebase/auth/sign-in/firebase-sign-in.module.ts +11 -8
  37. src/app/firebase/auth/sign-in/firebase-sign-in.page.ts +102 -126
  38. src/app/firebase/auth/sign-up/firebase-sign-up.module.ts +11 -8
  39. src/app/firebase/auth/sign-up/firebase-sign-up.page.html +1 -1
  40. src/app/firebase/auth/sign-up/firebase-sign-up.page.ts +104 -122
  41. src/app/firebase/crud/firebase-crud.module.ts +6 -5
  42. src/app/firebase/crud/firebase-crud.service.ts +44 -23
  43. src/app/firebase/crud/listing/firebase-listing.page.ts +1 -1
  44. src/app/forms/filters/forms-filters.page.ts +1 -1
  45. src/app/forms/validations/styles/forms-validations.page.scss +1 -1
  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 +23 -17
  49. src/app/getting-started/styles/getting-started.page.scss +6 -6
  50. src/app/pipes/time-ago.pipe.ts +2 -2
  51. src/app/user/friends/styles/user-friends.page.scss +0 -1
  52. src/app/user/friends/user-friends.page.ts +10 -6
  53. src/app/validators/password.validator.ts +5 -4
  54. src/app/walkthrough/styles/walkthrough.page.scss +5 -5
  55. src/app/walkthrough/walkthrough.module.ts +3 -1
  56. src/app/walkthrough/walkthrough.page.html +14 -14
  57. src/app/walkthrough/walkthrough.page.ts +48 -33
  58. src/assets/icon/HOW-TO.md +0 -13
  59. src/assets/icon/favicon.ico +0 -0
  60. src/assets/sample-data/travel/listing.json +1 -1
  61. src/global.scss +5 -0
  62. src/manifest.webmanifest +2 -2
.firebaserc CHANGED
@@ -2,6 +2,19 @@
2
  "projects": {
3
  "dev": "dev-ion4fullpwa",
4
  "prod": "ion4fullpwa",
5
- "beginners": "ion5fullapp-beginners"
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  }
7
  }
2
  "projects": {
3
  "dev": "dev-ion4fullpwa",
4
  "prod": "ion4fullpwa",
5
+ "beginners": "ion5fullapp-beginners",
6
+ "basic": "basic-ionic-6-full-app"
7
+ },
8
+ "targets": {
9
+ "basic-ionic-6-full-app": {
10
+ "hosting": {
11
+ "06-2022-release": [
12
+ "basic-ionic-6-full-app"
13
+ ],
14
+ "12-2021-release": [
15
+ "basic-ionic-5-full-app"
16
+ ]
17
+ }
18
+ }
19
  }
20
  }
DIFF.md ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Generate diff file
2
+ *BASIC vs PRO (**no /android or /ios configs**)*
3
+ ```bash
4
+ git diff basic-version..pro-version --diff-filter=ADCMRT ':!package-lock.json' ':!dist' ':!.gradle/*' ':!android' ':!ios' ':!*.diff' ':!*.png' ':!*.svg' > diff/basic-vs-pro_changelog.diff
5
+ ```
6
+
7
+ *BASIC vs PRO (**android config**)*
8
+ ```bash
9
+ git diff basic-version..pro-version --diff-filter=ADCMRT -- android/ > diff/basic-vs-pro_android-changelog.diff
10
+ ```
11
+
12
+ *BASIC vs PRO (**ios config**)*
13
+ ```bash
14
+ git diff basic-version..pro-version --diff-filter=ADCMRT -- ios/ > diff/basic-vs-pro_ios-changelog.diff
15
+ ```
16
+
17
+ *Last BASIC update (**12-2021**) vs Current BASIC update (**06-2022**)*
18
+ ```bash
19
+ git diff 12-2021_basic-update..basic-version --diff-filter=ADCMRT ':!package-lock.json' ':!dist' ':!.gradle/*' ':!android' ':!ios' ':!*.diff' ':!*.png' ':!*.svg' > diff/last-basic-update-vs-current-basic_changelog.diff
20
+ ```
21
+
22
+ *Last BASIC update (**12-2021**) vs Current BASIC update (**06-2022**) (**android config**)*
23
+ ```bash
24
+ git diff 12-2021_basic-update..basic-version --diff-filter=ADCMRT -- android/ > diff/last-basic-update-vs-current-basic_android-changelog.diff
25
+ ```
26
+
27
+ *Last BASIC update (**12-2021**) vs Current BASIC update (**06-2022**) (**ios config**)*
28
+ ```bash
29
+ git diff 12-2021_basic-update..basic-version --diff-filter=ADCMRT -- ios/ > diff/last-basic-update-vs-current-basic_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/basic-vs-pro_changelog.diff
41
+
42
+ diff2html --style side --input file -- diff/basic-vs-pro_android-changelog.diff
43
+
44
+ diff2html --style side --input file -- diff/basic-vs-pro_ios-changelog.diff
45
+
46
+ diff2html --style side --input file -- diff/last-basic-update-vs-current-basic_changelog.diff
47
+
48
+ diff2html --style side --input file -- diff/last-basic-update-vs-current-basic_android-changelog.diff
49
+
50
+ diff2html --style side --input file -- diff/last-basic-update-vs-current-basic_ios-changelog.diff
51
+ ```
FIREBASE.md ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Prepare assets to deploy
2
+ Before deploying the app to Firebase Hosting, run `ionic build --prod`
3
+
4
+ # Multiple Environments with Firebase
5
+ For managing one site across different environments, we recommend multiple projects for promoting best practices of each environment having its own set of Firebase resources.
6
+
7
+ For example for this repo we will have two firebase projects:
8
+ - dev-ion4fullpwa
9
+ - ion4fullpwa
10
+
11
+ ## Check available alias
12
+ Before deploying to Firebase Hosting make sure you are using the correct alias (dev, prod)
13
+ `firebase use` will list all the alias available
14
+ ```
15
+ * dev (dev-ion4fullpwa)
16
+ prod (ion4fullpwa)
17
+ pro (pro-ion4fullpwa)
18
+ ```
19
+
20
+ ## Create alias
21
+ If you don't see these alias (dev, prod), you should create them
22
+ `firebase use --add`
23
+ ```
24
+ ? Which project do you want to add? ion4fullpwa
25
+ ? What alias do you want to use for this project? (e.g. staging) prod
26
+ ```
27
+
28
+ ## Select alias (switching environments)
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 = 12-2021-release
48
+ - RESOURCE_IDENTIFIER (the SITE_ID) = basic-ionic-5-full-app
49
+ ``` bash
50
+ firebase target:apply hosting 12-2021-release basic-ionic-5-full-app -P basic
51
+ ```
52
+
53
+ - TARGET_NAME = 06-2022-release
54
+ - RESOURCE_IDENTIFIER (the SITE_ID) = basic-ionic-6-full-app
55
+ ``` bash
56
+ firebase target:apply hosting 06-2022-release basic-ionic-6-full-app -P basic
57
+ ```
58
+
59
+
60
+ ## [Configure your `firebase.json` file to use deploy targets](https://firebase.google.com/docs/cli/targets#configure_your_firebasejson_file_to_use_deploy_targets)
61
+ Don't forget to configure this before deploying to the new target
62
+
63
+
64
+ ## Deploy to specific target (i.e.: different site)
65
+ ``` bash
66
+ firebase deploy --only hosting:06-2022-release -P basic
67
+ ```
68
+
69
+
70
+ ## Create channel
71
+ - CHANNEL_ID = dev
72
+ ``` bash
73
+ firebase hosting:channel:deploy dev --only 06-2022-release -P basic
74
+ ```
README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Ionic 6 Full App
2
  The most advanced and complete Mobile & PWA starter app template.
3
 
4
  # Documentation
1
+ # Ionic 6 Full App BASIC Version
2
  The most advanced and complete Mobile & PWA starter app template.
3
 
4
  # Documentation
angular.json CHANGED
@@ -105,8 +105,7 @@
105
  "production": {
106
  "browserTarget": "app:build:production"
107
  },
108
- "ci": {
109
- }
110
  }
111
  },
112
  "extract-i18n": {
@@ -123,36 +122,12 @@
123
  "src/**/*.html"
124
  ]
125
  }
126
- },
127
- "ionic-cordova-build": {
128
- "builder": "@ionic/angular-toolkit:cordova-build",
129
- "options": {
130
- "browserTarget": "app:build"
131
- },
132
- "configurations": {
133
- "production": {
134
- "browserTarget": "app:build:production"
135
- }
136
- }
137
- },
138
- "ionic-cordova-serve": {
139
- "builder": "@ionic/angular-toolkit:cordova-serve",
140
- "options": {
141
- "cordovaBuildTarget": "app:ionic-cordova-build",
142
- "devServerTarget": "app:serve"
143
- },
144
- "configurations": {
145
- "production": {
146
- "cordovaBuildTarget": "app:ionic-cordova-build:production",
147
- "devServerTarget": "app:serve:production"
148
- }
149
- }
150
  }
151
  }
152
  }
153
  },
154
  "cli": {
155
- "defaultCollection": "@ionic/angular-toolkit"
156
  },
157
  "schematics": {
158
  "@ionic/angular-toolkit:component": {
@@ -162,4 +137,4 @@
162
  "styleext": "scss"
163
  }
164
  }
165
- }
105
  "production": {
106
  "browserTarget": "app:build:production"
107
  },
108
+ "ci": {}
 
109
  }
110
  },
111
  "extract-i18n": {
122
  "src/**/*.html"
123
  ]
124
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  }
126
  }
127
  }
128
  },
129
  "cli": {
130
+ "defaultCollection": "@angular-eslint/schematics"
131
  },
132
  "schematics": {
133
  "@ionic/angular-toolkit:component": {
137
  "styleext": "scss"
138
  }
139
  }
140
+ }
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: 'www',
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: 'Ionic6FullAppBasic',
6
  webDir: 'www',
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": "www",
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": "www",
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": "www",
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-BASIC",
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>",
@@ -19,32 +19,33 @@
19
  "@angular/animations": "^13.1.1",
20
  "@angular/common": "^13.1.1",
21
  "@angular/core": "^13.1.1",
22
- "@angular/fire": "^6.1.5",
23
  "@angular/forms": "^13.1.1",
24
  "@angular/platform-browser": "^13.1.1",
25
  "@angular/platform-browser-dynamic": "^13.1.1",
26
  "@angular/router": "^13.1.1",
27
  "@angular/service-worker": "^13.1.1",
28
- "@capacitor/android": "^3.3.3",
29
- "@capacitor/app": "^1.0.7",
30
- "@capacitor/core": "^3.3.3",
31
- "@capacitor/geolocation": "^1.3.0",
32
- "@capacitor/haptics": "^1.1.3",
33
- "@capacitor/ios": "^3.3.3",
34
- "@capacitor/keyboard": "^1.2.0",
35
- "@capacitor/share": "^1.0.7",
36
- "@capacitor/splash-screen": "^1.2.0",
37
- "@capacitor/status-bar": "^1.0.6",
38
- "@capacitor/storage": "^1.0.2",
39
- "@ionic/angular": "^6.0.0",
 
40
  "@types/core-js": "^2.5.5",
41
  "angular-pipes": "^10.0.0",
42
- "capacitor-firebase-auth": "^3.0.0",
43
  "core-js": "^2.6.11",
44
- "dayjs": "^1.10.7",
45
- "firebase": "^8.6.8",
46
- "google-libphonenumber": "^3.2.25",
47
  "rxjs": "~6.6.6",
 
48
  "tslib": "^2.0.0",
49
  "zone.js": "~0.11.4"
50
  },
@@ -62,15 +63,17 @@
62
  "@angular/compiler": "^13.1.1",
63
  "@angular/compiler-cli": "^13.1.1",
64
  "@angular/language-service": "~13.1.1",
65
- "@capacitor/cli": "^3.0.2",
66
- "@commitlint/cli": "^11.0.0",
67
- "@commitlint/config-angular": "^11.0.0",
68
- "@ionic/angular-toolkit": "^5.0.3",
 
69
  "@types/node": "^17.0.2",
70
- "@typescript-eslint/eslint-plugin": "5.3.0",
71
- "@typescript-eslint/parser": "5.3.0",
72
  "@webcomponents/webcomponentsjs": "^2.6.0",
73
- "eslint": "^8.5.0",
 
74
  "husky": "^4.3.0",
75
  "ts-node": "^8.10.1",
76
  "typescript": "~4.5.4"
1
  {
2
  "name": "IonicFullApp-BASIC",
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>",
19
  "@angular/animations": "^13.1.1",
20
  "@angular/common": "^13.1.1",
21
  "@angular/core": "^13.1.1",
22
+ "@angular/fire": "^7.4.1",
23
  "@angular/forms": "^13.1.1",
24
  "@angular/platform-browser": "^13.1.1",
25
  "@angular/platform-browser-dynamic": "^13.1.1",
26
  "@angular/router": "^13.1.1",
27
  "@angular/service-worker": "^13.1.1",
28
+ "@capacitor-firebase/authentication": "^0.3.1",
29
+ "@capacitor/android": "^3.5.1",
30
+ "@capacitor/app": "^1.1.1",
31
+ "@capacitor/core": "^3.5.1",
32
+ "@capacitor/geolocation": "^1.3.1",
33
+ "@capacitor/haptics": "^1.1.4",
34
+ "@capacitor/ios": "^3.5.1",
35
+ "@capacitor/keyboard": "^1.2.2",
36
+ "@capacitor/share": "^1.1.2",
37
+ "@capacitor/splash-screen": "^1.2.2",
38
+ "@capacitor/status-bar": "^1.0.8",
39
+ "@capacitor/storage": "^1.2.5",
40
+ "@ionic/angular": "^6.1.9",
41
  "@types/core-js": "^2.5.5",
42
  "angular-pipes": "^10.0.0",
 
43
  "core-js": "^2.6.11",
44
+ "dayjs": "^1.11.3",
45
+ "firebase": "^9.8.3",
46
+ "google-libphonenumber": "^3.2.28",
47
  "rxjs": "~6.6.6",
48
+ "swiper": "^8.2.2",
49
  "tslib": "^2.0.0",
50
  "zone.js": "~0.11.4"
51
  },
63
  "@angular/compiler": "^13.1.1",
64
  "@angular/compiler-cli": "^13.1.1",
65
  "@angular/language-service": "~13.1.1",
66
+ "@capacitor/cli": "^3.5.1",
67
+ "@commitlint/cli": "^17.0.2",
68
+ "@commitlint/config-angular": "^17.0.0",
69
+ "@ionic/angular-toolkit": "^6.1.0",
70
+ "@ionic/cli": "6.19.1",
71
  "@types/node": "^17.0.2",
72
+ "@typescript-eslint/eslint-plugin": "^5.27.1",
73
+ "@typescript-eslint/parser": "^5.27.1",
74
  "@webcomponents/webcomponentsjs": "^2.6.0",
75
+ "cordova-res": "0.15.4",
76
+ "eslint": "^8.17.0",
77
  "husky": "^4.3.0",
78
  "ts-node": "^8.10.1",
79
  "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-routing.module.ts CHANGED
@@ -77,11 +77,11 @@ const routes: Routes = [
77
  @NgModule({
78
  imports: [
79
  RouterModule.forRoot(routes, {
80
- initialNavigation: 'enabled',
81
- scrollPositionRestoration: 'enabled',
82
- anchorScrolling: 'enabled',
83
- relativeLinkResolution: 'legacy'
84
- })
85
  ],
86
  exports: [RouterModule]
87
  })
77
  @NgModule({
78
  imports: [
79
  RouterModule.forRoot(routes, {
80
+ initialNavigation: 'enabled',
81
+ scrollPositionRestoration: 'enabled',
82
+ anchorScrolling: 'enabled',
83
+ relativeLinkResolution: 'legacy'
84
+ })
85
  ],
86
  exports: [RouterModule]
87
  })
src/app/app.component.ts CHANGED
@@ -14,49 +14,50 @@ import { Storage } from '@capacitor/storage';
14
  })
15
  export class AppComponent {
16
  appPages = [
17
- {
18
- title: 'Categories',
19
- url: '/app/categories',
20
- ionicIcon: 'list-outline'
21
- },
22
- {
23
- title: 'Profile',
24
- url: '/app/user',
25
- ionicIcon: 'person-outline'
26
- },
27
- {
28
- title: 'Contact Card',
29
- url: '/contact-card',
30
- customIcon: './assets/custom-icons/side-menu/contact-card.svg'
31
- },
32
- {
33
- title: 'Notifications',
34
- url: '/app/notifications',
35
- ionicIcon: 'notifications-outline'
36
- }
37
  ];
 
38
  accountPages = [
39
- {
40
- title: 'Log In',
41
- url: '/auth/login',
42
- ionicIcon: 'log-in-outline'
43
- },
44
- {
45
- title: 'Sign Up',
46
- url: '/auth/signup',
47
- ionicIcon: 'person-add-outline'
48
- },
49
- {
50
- title: 'Getting Started',
51
- url: '/getting-started',
52
- ionicIcon: 'rocket-outline'
53
- },
54
- {
55
- title: '404 page',
56
- url: '/page-not-found',
57
- ionicIcon: 'alert-circle-outline'
58
- }
59
- ];
60
 
61
  constructor(private router: Router) {
62
  this.initializeApp();
@@ -70,7 +71,7 @@ export class AppComponent {
70
  }
71
  }
72
 
73
- openTutorial() {
74
  // save key to mark the walkthrough as NOT visited because the user wants to check it out
75
  Storage.set({
76
  key: 'visitedWalkthrough',
14
  })
15
  export class AppComponent {
16
  appPages = [
17
+ {
18
+ title: 'Categories',
19
+ url: '/app/categories',
20
+ ionicIcon: 'list-outline'
21
+ },
22
+ {
23
+ title: 'Profile',
24
+ url: '/app/user',
25
+ ionicIcon: 'person-outline'
26
+ },
27
+ {
28
+ title: 'Contact Card',
29
+ url: '/contact-card',
30
+ customIcon: './assets/custom-icons/side-menu/contact-card.svg'
31
+ },
32
+ {
33
+ title: 'Notifications',
34
+ url: '/app/notifications',
35
+ ionicIcon: 'notifications-outline'
36
+ }
37
  ];
38
+
39
  accountPages = [
40
+ {
41
+ title: 'Log In',
42
+ url: '/auth/login',
43
+ ionicIcon: 'log-in-outline'
44
+ },
45
+ {
46
+ title: 'Sign Up',
47
+ url: '/auth/signup',
48
+ ionicIcon: 'person-add-outline'
49
+ },
50
+ {
51
+ title: 'Getting Started',
52
+ url: '/getting-started',
53
+ ionicIcon: 'rocket-outline'
54
+ },
55
+ {
56
+ title: '404 page',
57
+ url: '/page-not-found',
58
+ ionicIcon: 'alert-circle-outline'
59
+ }
60
+ ];
61
 
62
  constructor(private router: Router) {
63
  this.initializeApp();
71
  }
72
  }
73
 
74
+ public openTutorial(): void {
75
  // save key to mark the walkthrough as NOT visited because the user wants to check it out
76
  Storage.set({
77
  key: 'visitedWalkthrough',
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/components/rating-input/rating-input.component.html CHANGED
@@ -1,3 +1,7 @@
1
  <ion-button class="rating-icon" fill="clear" shape="round" *ngFor="let r of range; let i = index" (click)="rate(i + 1)">
2
- <ion-icon slot="icon-only" [name]="value === undefined ? (r === 1 ? 'star' : (r === 2 ? 'star-half' : 'star-outline')) : (value > i ? (value < i+1 ? 'star-half' : 'star') : 'star-outline')"></ion-icon>
 
 
 
 
3
  </ion-button>
1
  <ion-button class="rating-icon" fill="clear" shape="round" *ngFor="let r of range; let i = index" (click)="rate(i + 1)">
2
+ <ion-icon slot="icon-only"
3
+ [name]="value === undefined ?
4
+ 'star-outline' :
5
+ (value > i ? (value < i+1 ? 'star-half' : 'star') : 'star-outline')">
6
+ </ion-icon>
7
  </ion-button>
src/app/components/rating-input/rating-input.component.ts CHANGED
@@ -15,23 +15,15 @@ export class RatingInputComponent implements ControlValueAccessor, OnInit {
15
  @Input() readOnly = false;
16
 
17
  range: Array<number>;
18
- innerValue: any;
19
- propagateChange: any = () => {}; // Noop function
20
 
21
  ngOnInit() {
22
- const states: Array<number> = [];
23
 
24
  for (let i = 0; i < this.max; i++) {
25
- if (this.innerValue > i && this.innerValue < i + 1) {
26
- states[i] = 2;
27
- } else if (this.innerValue > i) {
28
- states[i] = 1;
29
- } else {
30
- states[i] = 0;
31
- }
32
  }
33
-
34
- this.range = states;
35
  }
36
 
37
  get value(): any {
15
  @Input() readOnly = false;
16
 
17
  range: Array<number>;
18
+ innerValue: any; // the value of the control
19
+ propagateChange: any = () => {};
20
 
21
  ngOnInit() {
22
+ this.range = []; // the amout of stars
23
 
24
  for (let i = 0; i < this.max; i++) {
25
+ this.range[i] = 1;
 
 
 
 
 
 
26
  }
 
 
27
  }
28
 
29
  get value(): any {
src/app/components/show-hide-password/show-hide-password.component.html CHANGED
@@ -1,10 +1,10 @@
1
  <ng-content></ng-content>
2
  <a class="type-toggle" (click)="toggleShow()">
3
- <ion-icon class="show-option" [hidden]="show" name="eye-off-outline"></ion-icon>
4
- <ion-icon class="hide-option" [hidden]="!show" name="eye-outline"></ion-icon>
5
- <!-- In case you want to use text instead of icons -->
6
  <!--
7
- <span class="show-option" [hidden]="show">show</span>
8
- <span class="hide-option" [hidden]="!show">hide</span>
9
- -->
10
  </a>
1
  <ng-content></ng-content>
2
  <a class="type-toggle" (click)="toggleShow()">
3
+ <ion-icon class="show-option" [hidden]="showPassword" name="eye-off-outline"></ion-icon>
4
+ <ion-icon class="hide-option" [hidden]="!showPassword" name="eye-outline"></ion-icon>
5
+ <!-- In case you want to use text instead of icons -->
6
  <!--
7
+ <span class="show-option" [hidden]="showPassword">show</span>
8
+ <span class="hide-option" [hidden]="!showPassword">hide</span>
9
+ -->
10
  </a>
src/app/components/show-hide-password/show-hide-password.component.ts CHANGED
@@ -1,27 +1,20 @@
1
  import { Component, ContentChild } from '@angular/core';
2
-
3
  import { IonInput } from '@ionic/angular';
4
 
5
  @Component({
6
  selector: 'app-show-hide-password',
7
  templateUrl: './show-hide-password.component.html',
8
- styleUrls: [
9
- './show-hide-password.component.scss'
10
- ]
11
  })
12
  export class ShowHidePasswordComponent {
13
- show = false;
14
 
15
  @ContentChild(IonInput) input: IonInput;
16
 
17
  constructor() {}
18
 
19
  toggleShow() {
20
- this.show = !this.show;
21
- if (this.show) {
22
- this.input.type = 'text';
23
- } else {
24
- this.input.type = 'password';
25
- }
26
  }
27
  }
1
  import { Component, ContentChild } from '@angular/core';
 
2
  import { IonInput } from '@ionic/angular';
3
 
4
  @Component({
5
  selector: 'app-show-hide-password',
6
  templateUrl: './show-hide-password.component.html',
7
+ styleUrls: ['./show-hide-password.component.scss']
 
 
8
  })
9
  export class ShowHidePasswordComponent {
10
+ showPassword = false;
11
 
12
  @ContentChild(IonInput) input: IonInput;
13
 
14
  constructor() {}
15
 
16
  toggleShow() {
17
+ this.showPassword = !this.showPassword;
18
+ this.input.type = this.showPassword ? 'text' : 'password';
 
 
 
 
19
  }
20
  }
src/app/deals/details/deals-details.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
  import { PipesModule } from '../../pipes/pipes.module';
10
 
@@ -29,7 +31,8 @@ const routes: Routes = [
29
  IonicModule,
30
  RouterModule.forChild(routes),
31
  ComponentsModule,
32
- PipesModule
 
33
  ],
34
  declarations: [
35
  DealsDetailsPage
5
 
6
  import { IonicModule } from '@ionic/angular';
7
 
8
+ import { SwiperModule } from 'swiper/angular';
9
+
10
  import { ComponentsModule } from '../../components/components.module';
11
  import { PipesModule } from '../../pipes/pipes.module';
12
 
31
  IonicModule,
32
  RouterModule.forChild(routes),
33
  ComponentsModule,
34
+ PipesModule,
35
+ SwiperModule
36
  ],
37
  declarations: [
38
  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
@@ -14,7 +14,7 @@ export class DealsDetailsPage implements OnInit {
14
  details: any;
15
  slidesOptions: any = {
16
  zoom: {
17
- toggle: false // Disable zooming to prevent weird double tap zomming on slide images
18
  }
19
  };
20
 
14
  details: any;
15
  slidesOptions: any = {
16
  zoom: {
17
+ toggle: false // Disable zooming to prevent weird double tap zooming on slide images
18
  }
19
  };
20
 
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/fashion/details/fashion-details.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 { FashionService } from '../fashion.service';
@@ -27,7 +29,8 @@ const routes: Routes = [
27
  FormsModule,
28
  IonicModule,
29
  RouterModule.forChild(routes),
30
- ComponentsModule
 
31
  ],
32
  declarations: [
33
  FashionDetailsPage
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 { FashionService } from '../fashion.service';
29
  FormsModule,
30
  IonicModule,
31
  RouterModule.forChild(routes),
32
+ ComponentsModule,
33
+ SwiperModule
34
  ],
35
  declarations: [
36
  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 class="" *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/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/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,160 +1,497 @@
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 { FirebaseProfileModel } from './profile/firebase-profile.model';
5
- import { Platform } from '@ionic/angular';
 
6
  import { filter, map } from 'rxjs/operators';
7
 
8
- import firebase from 'firebase/app';
9
- import { cfaSignIn, cfaSignOut } from 'capacitor-firebase-auth';
10
- import { isPlatformBrowser } from '@angular/common';
 
 
 
 
 
 
 
 
11
 
12
- @Injectable()
13
- export class FirebaseAuthService {
14
 
15
- currentUser: firebase.User;
16
- userProviderAdditionalInfo: any;
17
- redirectResult: Subject<any> = new Subject<any>();
 
 
 
 
 
18
 
19
  constructor(
20
- public angularFire: AngularFireAuth,
 
21
  public platform: Platform,
 
 
 
22
  @Inject(PLATFORM_ID) private platformId: object
23
  ) {
24
  if (isPlatformBrowser(this.platformId)) {
25
- this.angularFire.onAuthStateChanged((user) => {
26
- if (user) {
27
- // User is signed in.
28
- this.currentUser = user;
29
- } else {
30
- // No user is signed in.
31
- this.currentUser = null;
32
- }
33
- });
34
 
35
- if (!this.platform.is('capacitor')) {
36
- // when using signInWithRedirect, this listens for the redirect results
37
- this.angularFire.getRedirectResult()
38
- .then((result) => {
39
- // result.credential.accessToken gives you the Provider Access Token. You can use it to access the Provider API.
40
- if (this.currentUser) {
41
- this.redirectResult.next(this.currentUser);
42
  }
43
- }, (error) => {
44
- this.redirectResult.next({error: error.code});
45
  });
46
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  }
48
  }
49
 
50
- getRedirectResult(): Observable<any> {
51
- return this.redirectResult.asObservable();
52
  }
53
 
54
- getPhotoURL(signInProviderId: string, photoURL: string): string {
55
- // Default imgs are too small and our app needs a bigger image
56
- switch (signInProviderId) {
57
- case 'facebook.com':
58
- return photoURL + '?height=400';
59
- case 'password':
60
- return 'https://s3-us-west-2.amazonaws.com/ionicthemes/otros/avatar-placeholder.png';
61
- case 'twitter.com':
62
- return photoURL.replace('_normal', '_400x400');
63
- case 'google.com':
64
- return photoURL.split('=')[0];
65
- default:
66
- return photoURL;
67
- }
 
 
 
 
 
 
 
 
68
  }
69
 
70
- signOut(): Observable<any> {
 
 
 
 
71
  if (this.platform.is('capacitor')) {
72
- return cfaSignOut();
73
  } else {
74
- return from(this.angularFire.signOut());
 
 
 
 
 
 
 
 
75
  }
76
  }
77
 
78
- signInWithEmail(email: string, password: string): Promise<firebase.auth.UserCredential> {
79
- return this.angularFire.signInWithEmailAndPassword(email, password);
 
 
80
  }
81
 
82
- signUpWithEmail(email: string, password: string): Promise<firebase.auth.UserCredential> {
83
- return this.angularFire.createUserWithEmailAndPassword(email, password);
 
 
84
  }
85
 
86
- socialSignIn(providerName: string, scopes?: Array<string>): Observable<any> {
87
- if (this.platform.is('capacitor')) {
88
- return cfaSignIn(providerName);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  } else {
90
- const provider = new firebase.auth.OAuthProvider(providerName);
 
 
91
 
92
- if (scopes) {
93
- scopes.forEach(scope => {
94
- provider.addScope(scope);
95
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  }
97
- if (this.platform.is('desktop')) {
98
- return from(this.angularFire.signInWithPopup(provider));
99
- } else {
100
- // web but not desktop, for example mobile PWA
101
- return from(this.angularFire.signInWithRedirect(provider));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  }
 
 
 
 
 
 
 
103
  }
104
  }
105
 
106
- signInWithFacebook() {
107
- const provider = new firebase.auth.FacebookAuthProvider();
108
  const scopes = ['email'];
109
- return this.socialSignIn(provider.providerId, scopes);
 
 
110
  }
111
 
112
- signInWithGoogle() {
113
- const provider = new firebase.auth.GoogleAuthProvider();
114
  const scopes = ['profile', 'email'];
115
- return this.socialSignIn(provider.providerId, scopes);
 
 
116
  }
117
 
118
- signInWithTwitter() {
119
- const provider = new firebase.auth.TwitterAuthProvider();
120
  const scopes = ['name', 'email'];
121
- return this.socialSignIn(provider.providerId, scopes);
 
 
122
  }
123
 
124
- signInWithApple() {
125
- const provider = new firebase.auth.OAuthProvider('apple.com');
126
  const scopes = ['name', 'email'];
127
- return this.socialSignIn(provider.providerId, scopes);
 
 
128
  }
129
 
130
- public getProfileData(): Observable<any> {
131
- // we need to do this differentiation because there is a bug in
132
- // platform capacitor ios when executing this.angularFire.user
133
- if (this.platform.is('capacitor')) {
134
- return of(this.setUserModelForProfile());
135
- } else {
136
- return this.angularFire.user
137
- .pipe(
138
- filter((user: firebase.User) => user !== null),
139
- map((user: firebase.User) => {
140
- return this.setUserModelForProfile();
141
- })
142
- );
143
- }
144
  }
145
 
146
- private setUserModelForProfile(): FirebaseProfileModel {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  const userModel = new FirebaseProfileModel();
148
- const provierData = this.currentUser.providerData[0];
149
- const userData = this.userProviderAdditionalInfo ? this.userProviderAdditionalInfo : provierData;
150
- userModel.image = this.getPhotoURL(provierData.providerId, provierData.photoURL);
151
- userModel.name = userData.name || userData.displayName || 'What\'s your name?';
152
- userModel.role = 'How would you describe yourself?';
153
- userModel.description = userData.description || 'Anything else you would like to share with the world?';
154
- userModel.phoneNumber = userData.phoneNumber || 'Is there a number where I can reach you?';
155
- userModel.email = userData.email || 'Where can I send you emails?';
156
- userModel.provider = (provierData.providerId !== 'password') ? provierData.providerId : 'Credentials';
 
157
 
158
  return userModel;
159
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
  }
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 { FirebaseProfileModel } from './profile/firebase-profile.model';
19
+ import { SignInProvider } from './firebase-auth-definitions';
20
 
 
 
21
 
22
+ @Injectable({
23
+ providedIn: 'root'
24
+ })
25
+ export class FirebaseAuthService implements OnDestroy {
26
+ currentUser: User;
27
+ authLoader: HTMLIonLoadingElement;
28
+ redirectResultSubject: Subject<any> = new Subject<any>();
29
+ authStateSubject: Subject<AuthStateChange> = new Subject<AuthStateChange>();
30
 
31
  constructor(
32
+ public router: Router,
33
+ public route: ActivatedRoute,
34
  public platform: Platform,
35
+ private ngZone: NgZone,
36
+ public loadingController: LoadingController,
37
+ public location: Location,
38
  @Inject(PLATFORM_ID) private platformId: object
39
  ) {
40
  if (isPlatformBrowser(this.platformId)) {
41
+ FirebaseAuthentication.removeAllListeners().then(() => {
42
+ FirebaseAuthentication.addListener('authStateChange', (change: AuthStateChange) => {
43
+ this.ngZone.run(() => {
44
+ this.authStateSubject.next(change);
45
+ });
 
 
 
 
46
 
47
+ if (change?.user) {
48
+ // ? User is signed in.
49
+ this.currentUser = change.user;
50
+ } else {
51
+ // ? No user is signed in.
52
+ this.currentUser = null;
 
53
  }
 
 
54
  });
55
+ });
56
+
57
+ // ? We should only listen for firebase auth redirect results when we have the flag 'auth-redirect' in the query params
58
+ this.route.queryParams.subscribe(params => {
59
+ const authProvider = params['auth-redirect'];
60
+
61
+ if (authProvider) {
62
+ // ? Show a loader while we receive the getRedirectResult notification
63
+ this.presentLoading(authProvider);
64
+
65
+ // ? When using signInWithRedirect, this listens for the redirect results
66
+ const auth = getAuth();
67
+ getRedirectResult(auth)
68
+ .then((result: UserCredential) => {
69
+ // ? result.credential.accessToken gives you the Provider Access Token. You can use it to access the Provider API.
70
+ // const credential = FacebookAuthProvider.credentialFromResult(result);
71
+ // const token = credential.accessToken;
72
+
73
+ let credential: any;
74
+
75
+ if (result && result !== null) {
76
+ switch (result.providerId) {
77
+ case SignInProvider.apple:
78
+ credential = OAuthProvider.credentialFromResult(result);
79
+ break;
80
+ case SignInProvider.facebook:
81
+ credential = FacebookAuthProvider.credentialFromResult(result);
82
+ break;
83
+ case SignInProvider.google:
84
+ credential = GoogleAuthProvider.credentialFromResult(result);
85
+ break;
86
+ case SignInProvider.twitter:
87
+ credential = TwitterAuthProvider.credentialFromResult(result);
88
+ break;
89
+ }
90
+
91
+ const signInResult = this.createSignInResult(result.user, credential);
92
+
93
+ this.dismissLoading();
94
+
95
+ this.redirectResultSubject.next(signInResult);
96
+ } else {
97
+ throw new Error('Could not get user from redirect result');
98
+ }
99
+ }, (reason) => {
100
+ console.log('Promise rejected', reason);
101
+
102
+ // ? Clear redirection loading
103
+ this.clearAuthWithProvidersRedirection();
104
+ }).catch((error) => {
105
+ // ? Clear redirection loading
106
+ this.clearAuthWithProvidersRedirection();
107
+
108
+ // ? Handle Errors here
109
+ // const errorCode = error.code;
110
+ // const errorMessage = error.message;
111
+ // ? The email of the user's account used.
112
+ // const email = error.email;
113
+ // ?AuthCredential type that was used.
114
+ // const credential = FacebookAuthProvider.credentialFromError(error);
115
+
116
+ let errorResult = {error: 'undefined'};
117
+
118
+ if (error && (error.code || error.message)) {
119
+ errorResult = {error: (error.code ? error.code : error.message)};
120
+ }
121
+
122
+ this.redirectResultSubject.next(errorResult);
123
+ });
124
+ }
125
+ });
126
  }
127
  }
128
 
129
+ ngOnDestroy(): void {
130
+ this.dismissLoading();
131
  }
132
 
133
+ public async signOut(): Promise<string> {
134
+ const signOutPromise = new Promise<string>((resolve, reject) => {
135
+ // * 1. Sign out on the native layer
136
+ FirebaseAuthentication.signOut()
137
+ .then((nativeResult) => {
138
+ // * 2. Sign out on the web layer
139
+ const auth = getAuth();
140
+ signOut(auth)
141
+ .then((webResult) => {
142
+ // ? Sign-out successful
143
+ resolve('Successfully sign out from native and web');
144
+ }).catch((webError) => {
145
+ // ? An error happened
146
+ reject(`Web auth sign out error: ${webError}`);
147
+ });
148
+ })
149
+ .catch((nativeError) => {
150
+ reject(`Native auth sign out error: ${nativeError}`);
151
+ });
152
+ });
153
+
154
+ return signOutPromise;
155
  }
156
 
157
+ private async socialSignIn(provider: AuthProvider, scopes?: Array<string>): Promise<SignInResult> {
158
+ this.presentLoading(provider.providerId);
159
+
160
+ let authResult: SignInResult = null;
161
+
162
  if (this.platform.is('capacitor')) {
163
+ authResult = await this.nativeAuth(provider, scopes);
164
  } else {
165
+ authResult = await this.webAuth(provider);
166
+ }
167
+
168
+ this.dismissLoading();
169
+
170
+ if (authResult !== null) {
171
+ return authResult;
172
+ } else {
173
+ return Promise.reject('Could not perform social sign in, authResult is null');
174
  }
175
  }
176
 
177
+ private prepareForAuthWithProvidersRedirection(authProviderId: string): void {
178
+ // ? Before invoking auth provider redirect flow, add a flag to the path.
179
+ // ? The presence of the flag in the path indicates we should wait for the auth redirect to complete
180
+ this.location.replaceState(this.location.path(), 'auth-redirect=' + authProviderId, this.location.getState());
181
  }
182
 
183
+ private clearAuthWithProvidersRedirection(): void {
184
+ // ? Remove auth-redirect param from url
185
+ this.location.replaceState(this.router.url.split('?')[0], '');
186
+ this.dismissLoading();
187
  }
188
 
189
+ private async presentLoading(authProviderId?: string): Promise<void> {
190
+ const authProviderCapitalized = authProviderId[0].toUpperCase() + authProviderId.slice(1);
191
+
192
+ this.loadingController.create({
193
+ message: authProviderId ? 'Signing in with ' + authProviderCapitalized : 'Signing in ...',
194
+ duration: 4000
195
+ }).then((loader) => {
196
+ this.authLoader = loader;
197
+ this.authLoader.present();
198
+ });
199
+ }
200
+
201
+ private async dismissLoading(): Promise<void> {
202
+ if (this.authLoader) {
203
+ await this.authLoader.dismiss();
204
+ }
205
+ }
206
+
207
+ private async webAuth(provider: AuthProvider, scopes?: Array<string>): Promise<SignInResult> {
208
+ // ? Scopes for Firebase JS SDK auth
209
+ if (scopes) {
210
+ let providerWithScopes: any;
211
+
212
+ switch (provider.providerId) {
213
+ case SignInProvider.apple:
214
+ providerWithScopes = (provider as OAuthProvider);
215
+ break;
216
+ case SignInProvider.facebook:
217
+ providerWithScopes = (provider as FacebookAuthProvider);
218
+ break;
219
+ case SignInProvider.google:
220
+ providerWithScopes = (provider as GoogleAuthProvider);
221
+ break;
222
+ case SignInProvider.twitter:
223
+ providerWithScopes = (provider as TwitterAuthProvider);
224
+ break;
225
+ }
226
+
227
+ scopes.forEach(scope => {
228
+ providerWithScopes.addScope(scope);
229
+ });
230
+
231
+ provider = providerWithScopes;
232
+ }
233
+
234
+ const auth = getAuth();
235
+ let webAuthUserCredential: UserCredential = null;
236
+
237
+ if (this.platform.is('desktop')) {
238
+ webAuthUserCredential = await signInWithPopup(auth, provider);
239
  } else {
240
+ // ? Web but not desktop, for example mobile PWA
241
+ this.prepareForAuthWithProvidersRedirection(provider.providerId);
242
+ return signInWithRedirect(auth, provider);
243
 
244
+ // ? If you prefer to use signInWithPopup in every scenario, just un-comment this line
245
+ // webAuthUserCredential = await signInWithPopup(auth, provider);
246
+ }
247
+
248
+ if (webAuthUserCredential && webAuthUserCredential !== null) {
249
+ let webCredential: OAuthCredential = null;
250
+
251
+ switch (provider.providerId) {
252
+ case SignInProvider.apple:
253
+ webCredential = OAuthProvider.credentialFromResult(webAuthUserCredential);
254
+ break;
255
+ case SignInProvider.facebook:
256
+ webCredential = FacebookAuthProvider.credentialFromResult(webAuthUserCredential);
257
+ break;
258
+ case SignInProvider.google:
259
+ webCredential = GoogleAuthProvider.credentialFromResult(webAuthUserCredential);
260
+ break;
261
+ case SignInProvider.twitter:
262
+ webCredential = TwitterAuthProvider.credentialFromResult(webAuthUserCredential);
263
+ break;
264
  }
265
+
266
+ return this.createSignInResult(webAuthUserCredential.user, webCredential);
267
+ } else {
268
+ return Promise.reject('null webAuthUserCredential');
269
+ }
270
+ }
271
+
272
+ private async nativeAuth(provider: AuthProvider, scopes?: Array<string>): Promise<SignInResult> {
273
+ let nativeAuthResult: SignInResult = null;
274
+
275
+ // ? Scopes for Firebase native SDK (iOS and Android)
276
+ // TODO: Scopes for Firebase native SDK auth is a work in progress yet
277
+ // (see: https://github.com/robingenz/capacitor-firebase/issues/32)
278
+
279
+
280
+ // * 1. Sign in on the native layer
281
+ switch (provider.providerId) {
282
+ case SignInProvider.apple:
283
+ nativeAuthResult = await FirebaseAuthentication.signInWithApple();
284
+ break;
285
+ case SignInProvider.facebook:
286
+ nativeAuthResult = await FirebaseAuthentication.signInWithFacebook();
287
+ break;
288
+ case SignInProvider.google:
289
+ nativeAuthResult = await FirebaseAuthentication.signInWithGoogle();
290
+ break;
291
+ case SignInProvider.twitter:
292
+ nativeAuthResult = await FirebaseAuthentication.signInWithTwitter();
293
+ break;
294
+ }
295
+
296
+ // ? Once we have the user authenticated on the native layer, authenticate it in the web layer
297
+ if (nativeAuthResult && nativeAuthResult !== null) {
298
+ const auth = getAuth();
299
+ let nativeCredential: OAuthCredential = null;
300
+
301
+ switch (provider.providerId) {
302
+ case SignInProvider.apple:
303
+ const provider = new OAuthProvider(SignInProvider.apple);
304
+ nativeCredential = provider.credential({
305
+ idToken: nativeAuthResult.credential?.idToken,
306
+ rawNonce: nativeAuthResult.credential?.nonce
307
+ });
308
+ break;
309
+ case SignInProvider.facebook:
310
+ nativeCredential = FacebookAuthProvider.credential(
311
+ nativeAuthResult.credential?.accessToken
312
+ );
313
+ break;
314
+ case SignInProvider.google:
315
+ nativeCredential = GoogleAuthProvider.credential(nativeAuthResult.credential?.idToken, nativeAuthResult.credential?.accessToken);
316
+ break;
317
+ case SignInProvider.twitter:
318
+ try {
319
+ nativeCredential = TwitterAuthProvider.credential(nativeAuthResult.credential?.accessToken, nativeAuthResult.credential?.secret);
320
+ break;
321
+ } catch (error) {
322
+ console.error(error);
323
+ }
324
  }
325
+
326
+ // * 2. Sign in on the web layer using the access token we got from the native sign in
327
+ const webAuthResult = await signInWithCredential(auth, nativeCredential);
328
+
329
+ return this.createSignInResult(webAuthResult.user, nativeCredential);
330
+ } else {
331
+ return Promise.reject('null nativeAuthResult');
332
  }
333
  }
334
 
335
+ public async signInWithFacebook(): Promise<SignInResult> {
336
+ const provider = new FacebookAuthProvider();
337
  const scopes = ['email'];
338
+
339
+ // ? When we use the redirect authentication flow, the code below the socialSignIn() invocation does not get executed as we leave the current page
340
+ return this.socialSignIn(provider, scopes);
341
  }
342
 
343
+ public async signInWithGoogle(): Promise<SignInResult> {
344
+ const provider = new GoogleAuthProvider();
345
  const scopes = ['profile', 'email'];
346
+
347
+ // ? When we use the redirect authentication flow, the code below the socialSignIn() invocation does not get executed as we leave the current page
348
+ return this.socialSignIn(provider, scopes);
349
  }
350
 
351
+ public async signInWithTwitter(): Promise<SignInResult> {
352
+ const provider = new TwitterAuthProvider();
353
  const scopes = ['name', 'email'];
354
+
355
+ // ? When we use the redirect authentication flow, the code below the socialSignIn() invocation does not get executed as we leave the current page
356
+ return this.socialSignIn(provider, scopes);
357
  }
358
 
359
+ public async signInWithApple(): Promise<SignInResult> {
360
+ const provider = new OAuthProvider('apple.com');
361
  const scopes = ['name', 'email'];
362
+
363
+ // ? When we use the redirect authentication flow, the code below the socialSignIn() invocation does not get executed as we leave the current page
364
+ return this.socialSignIn(provider, scopes);
365
  }
366
 
367
+ public async signInWithEmail(email: string, password: string): Promise<SignInResult> {
368
+ // ? Show a loader while we attempt to perform the login
369
+ this.presentLoading('email');
370
+
371
+ const auth = getAuth();
372
+ const credential = await signInWithEmailAndPassword(auth, email, password);
373
+
374
+ this.dismissLoading();
375
+
376
+ return this.createSignInResultFromUserCredential(credential);
 
 
 
 
377
  }
378
 
379
+ public async signUpWithEmail(email: string, password: string): Promise<SignInResult> {
380
+ // ? Show a loader while we attempt to perform the signup
381
+ this.presentLoading('email');
382
+
383
+ const auth = getAuth();
384
+ const credential = await createUserWithEmailAndPassword(auth, email, password);
385
+
386
+ this.dismissLoading();
387
+
388
+ return this.createSignInResultFromUserCredential(credential);
389
+ }
390
+
391
+ public get redirectResult$(): Observable<any> {
392
+ return this.redirectResultSubject.asObservable();
393
+ }
394
+
395
+ public get authState$(): Observable<AuthStateChange> {
396
+ return this.authStateSubject.asObservable();
397
+ }
398
+
399
+ public getProfileData(): Observable<FirebaseProfileModel> {
400
+ const auth = getAuth();
401
+ return of(auth.currentUser)
402
+ .pipe(
403
+ filter((user: FirebaseUser) => user != null),
404
+ map((user: FirebaseUser) => {
405
+ const userResult = this.createUserResult(user);
406
+ return this.setUserModelForProfile(userResult);
407
+ })
408
+ );
409
+ }
410
+
411
+ private setUserModelForProfile(userResult?: (User | null)): FirebaseProfileModel {
412
  const userModel = new FirebaseProfileModel();
413
+
414
+ if (userResult) {
415
+ userModel.image = this.getPhotoURL(userResult.providerId, userResult.photoUrl);
416
+ userModel.name = userResult.displayName || 'What\'s your name?';
417
+ userModel.role = 'How would you describe yourself?';
418
+ userModel.description = 'Anything else you would like to share with the world?';
419
+ userModel.phoneNumber = userResult.phoneNumber || 'Is there a number where I can reach you?';
420
+ userModel.email = userResult.email || 'Where can I send you emails?';
421
+ userModel.provider = (userResult.providerId !== 'password') ? userResult.providerId : 'Credentials';
422
+ }
423
 
424
  return userModel;
425
  }
426
+
427
+ private getPhotoURL(signInProviderId: string, photoURL: string): string {
428
+ // ? Default imgs are too small and our app needs a bigger image
429
+ switch (signInProviderId) {
430
+ case SignInProvider.facebook:
431
+ return photoURL + '?height=400';
432
+ case SignInProvider.twitter:
433
+ return photoURL.replace('_normal', '_400x400');
434
+ case SignInProvider.google:
435
+ return photoURL.split('=')[0];
436
+ case 'password':
437
+ return 'https://s3-us-west-2.amazonaws.com/ionicthemes/otros/avatar-placeholder.png';
438
+ default:
439
+ return photoURL;
440
+ }
441
+ }
442
+
443
+ // * Aux methods inspired on the @capacitor-firebase/authentication library
444
+
445
+ // (see: https://github.com/robingenz/capacitor-firebase/blob/a51927ff3acce94cedcd7bfc218952bb106db904/packages/authentication/src/web.ts#L297)
446
+ private createSignInResultFromUserCredential(credential: UserCredential): SignInResult {
447
+ const userResult = this.createUserResult(credential.user);
448
+ const result: SignInResult = {
449
+ user: userResult,
450
+ credential: null,
451
+ };
452
+ return result;
453
+ }
454
+
455
+ private createSignInResult(user: FirebaseUser, credential: FirebaseAuthCredential | null): SignInResult {
456
+ const userResult = this.createUserResult(user);
457
+ const credentialResult = this.createCredentialResult(credential);
458
+ const result: SignInResult = {
459
+ user: userResult,
460
+ credential: credentialResult,
461
+ };
462
+ return result;
463
+ }
464
+
465
+ private createUserResult(user: FirebaseUser | null): User | null {
466
+ if (!user) {
467
+ return null;
468
+ }
469
+ const result: User = {
470
+ displayName: user.displayName,
471
+ email: user.email,
472
+ emailVerified: user.emailVerified,
473
+ isAnonymous: user.isAnonymous,
474
+ phoneNumber: user.phoneNumber,
475
+ photoUrl: user.photoURL,
476
+ providerId: user.providerId,
477
+ tenantId: user.tenantId,
478
+ uid: user.uid,
479
+ };
480
+ return result;
481
+ }
482
+
483
+ private createCredentialResult(credential: FirebaseAuthCredential | null): AuthCredential | null {
484
+ if (!credential) {
485
+ return null;
486
+ }
487
+ const result: AuthCredential = {
488
+ providerId: credential.providerId,
489
+ };
490
+ if (credential instanceof OAuthCredential) {
491
+ result.accessToken = credential.accessToken;
492
+ result.idToken = credential.idToken;
493
+ result.secret = credential.secret;
494
+ }
495
+ return result;
496
+ }
497
  }
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,5 +1,6 @@
1
  import { Component, OnInit } from '@angular/core';
2
  import { ActivatedRoute, Router } from '@angular/router';
 
3
  import { FirebaseAuthService } from '../firebase-auth.service';
4
 
5
  @Component({
@@ -21,19 +22,25 @@ export class FirebaseProfilePage implements OnInit {
21
  }
22
 
23
  ngOnInit() {
24
-
25
  this.route.data.subscribe(routeData => {
26
  this.user = routeData['data'];
27
  });
28
  }
29
 
30
- signOut() {
31
- this.authService.signOut().subscribe(() => {
32
- // Sign-out successful.
33
- // Replace state as we are no longer authorized to access profile page.
34
- this.router.navigate(['firebase/auth/sign-in'], { replaceUrl: true });
35
- }, (error) => {
36
- console.log('signout error', error);
37
- });
 
 
 
 
 
 
 
38
  }
39
  }
1
  import { Component, OnInit } from '@angular/core';
2
  import { ActivatedRoute, Router } from '@angular/router';
3
+
4
  import { FirebaseAuthService } from '../firebase-auth.service';
5
 
6
  @Component({
22
  }
23
 
24
  ngOnInit() {
 
25
  this.route.data.subscribe(routeData => {
26
  this.user = routeData['data'];
27
  });
28
  }
29
 
30
+ public async signOut(): Promise<void> {
31
+ try {
32
+ // * 1. Sign out on the native layer
33
+ await this.authService.signOut()
34
+ .then((result) => {
35
+ // ? Sign-out successful
36
+ // ? Replace state as we are no longer authorized to access profile page
37
+ this.router.navigate(['firebase/auth/sign-in'], { replaceUrl: true });
38
+ })
39
+ .catch((error) => {
40
+ console.log('userProfile - signOut() - error', error);
41
+ });
42
+ } finally {
43
+ console.log('userProfile - signOut() - finally');
44
+ }
45
  }
46
  }
src/app/firebase/auth/sign-in/firebase-sign-in.module.ts CHANGED
@@ -2,16 +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
- import { map } from 'rxjs/operators';
10
 
11
- // Firebase guard to redirect logged in users to profile
12
- const redirectLoggedInToProfile = (next) => map(user => {
13
- // when queryParams['auth-redirect'] don't redirect because we want
14
- // the component to handle the redirection
15
  if (user !== null && !next.queryParams['auth-redirect']) {
16
  return ['firebase/auth/profile'];
17
  } else {
@@ -23,8 +27,7 @@ const routes: Routes = [
23
  {
24
  path: '',
25
  component: FirebaseSignInPage,
26
- canActivate: [AngularFireAuthGuard],
27
- data: { authGuardPipe: redirectLoggedInToProfile }
28
  }
29
  ];
30
 
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 { FirebaseAuthService } from '../firebase-auth.service';
@@ -14,10 +14,9 @@ import { FirebaseAuthService } from '../firebase-auth.service';
14
  './styles/firebase-sign-in.page.scss'
15
  ]
16
  })
17
- export class FirebaseSignInPage implements OnDestroy {
18
  loginForm: FormGroup;
19
  submitError: string;
20
- redirectLoader: HTMLIonLoadingElement;
21
  authRedirectResult: Subscription;
22
 
23
  validation_messages = {
@@ -33,11 +32,8 @@ export class FirebaseSignInPage implements OnDestroy {
33
 
34
  constructor(
35
  public router: Router,
36
- public route: ActivatedRoute,
37
- public authService: FirebaseAuthService,
38
- private ngZone: NgZone,
39
- public loadingController: LoadingController,
40
- public location: Location
41
  ) {
42
  this.loginForm = new FormGroup({
43
  'email': new FormControl('', Validators.compose([
@@ -50,9 +46,9 @@ export class FirebaseSignInPage implements OnDestroy {
50
  ]))
51
  });
52
 
53
- // Get firebase authentication redirect result invoken when using signInWithRedirect()
54
- // signInWithRedirect() is only used when client is in web but not desktop
55
- this.authRedirectResult = this.authService.getRedirectResult()
56
  .subscribe(result => {
57
  if (result.error) {
58
  this.manageAuthWithProvidersErrors(result.error);
@@ -61,143 +57,123 @@ export class FirebaseSignInPage implements OnDestroy {
61
  }
62
  });
63
 
64
- // Check if url contains our custom 'auth-redirect' param, then show a loader while we receive the getRedirectResult notification
65
- this.route.queryParams.subscribe(params => {
66
- const authProvider = params['auth-redirect'];
67
- if (authProvider) {
68
- this.presentLoading(authProvider);
 
69
  }
70
  });
71
  }
72
 
73
- ngOnDestroy(): void {
74
- this.dismissLoading();
75
- }
76
-
77
- // Once the auth provider finished the authentication flow, and the auth redirect completes,
78
- // hide the loader and redirect the user to the profile page
79
- redirectLoggedUserToProfilePage() {
80
- this.dismissLoading();
81
- // As we are calling the Angular router navigation inside a subscribe method, the navigation will be triggered outside Angular zone.
82
- // That's why we need to wrap the router navigation call inside an ngZone wrapper
83
- this.ngZone.run(() => {
84
- // Get previous URL from our custom History Helper
85
- // If there's no previous page, then redirect to profile
86
- // const previousUrl = this.historyHelper.previousUrl || 'firebase/auth/profile';
87
- const previousUrl = 'firebase/auth/profile';
88
 
89
- // No need to store in the navigation history the sign-in page with redirect params (it's justa a mandatory mid-step)
90
- // Navigate to profile and replace current url with profile
91
- this.router.navigate([previousUrl], { replaceUrl: true });
92
- });
 
 
 
 
 
 
 
 
 
93
  }
94
 
95
- async presentLoading(authProvider?: string) {
96
- const authProviderCapitalized = authProvider[0].toUpperCase() + authProvider.slice(1);
97
- this.loadingController.create({
98
- message: authProvider ? 'Signing in with ' + authProviderCapitalized : 'Signin in ...',
99
- duration: 4000
100
- }).then((loader) => {
101
- const currentUrl = this.location.path();
102
- if (currentUrl.includes('auth-redirect')) {
103
- this.redirectLoader = loader;
104
- this.redirectLoader.present();
105
- }
106
- });
107
- }
108
 
109
- async dismissLoading() {
110
- if (this.redirectLoader) {
111
- await this.redirectLoader.dismiss();
 
 
 
 
 
 
 
 
 
112
  }
113
  }
114
 
115
- // Before invoking auth provider redirect flow, present a loading indicator and add a flag to the path.
116
- // The precense of the flag in the path indicates we should wait for the auth redirect to complete.
117
- prepareForAuthWithProvidersRedirection(authProvider: string) {
118
- this.presentLoading(authProvider);
119
 
120
- this.location.replaceState(this.location.path(), 'auth-redirect=' + authProvider, this.location.getState());
 
 
 
 
 
 
 
 
 
 
 
 
121
  }
122
 
123
- manageAuthWithProvidersErrors(errorMessage: string) {
124
- this.submitError = errorMessage;
125
- // remove auth-redirect param from url
126
- this.location.replaceState(this.router.url.split('?')[0], '');
127
- this.dismissLoading();
128
- }
129
 
130
- resetSubmitError() {
131
- this.submitError = null;
 
 
 
 
 
 
 
 
 
132
  }
133
 
134
- signInWithEmail() {
135
  this.resetSubmitError();
136
- this.authService.signInWithEmail(this.loginForm.value['email'], this.loginForm.value['password'])
137
- .then(user => {
138
- // navigate to user profile
139
- this.redirectLoggedUserToProfilePage();
140
- })
141
- .catch(error => {
142
- this.submitError = error.message;
143
- this.dismissLoading();
144
- });
145
- }
146
 
147
- doFacebookLogin(): void {
148
- this.resetSubmitError();
149
- this.prepareForAuthWithProvidersRedirection('facebook');
150
-
151
- this.authService.signInWithFacebook()
152
- .subscribe((result) => {
153
- // This gives you a Facebook Access Token. You can use it to access the Facebook API.
154
- // const token = result.credential.accessToken;
155
- this.redirectLoggedUserToProfilePage();
156
- }, (error) => {
157
- this.manageAuthWithProvidersErrors(error.message);
158
- });
159
  }
160
 
161
- doGoogleLogin(): void {
162
- this.resetSubmitError();
163
- this.prepareForAuthWithProvidersRedirection('google');
164
-
165
- this.authService.signInWithGoogle()
166
- .subscribe((result) => {
167
- // This gives you a Google Access Token. You can use it to access the Google API.
168
- // var token = result.credential.accessToken;
169
- this.redirectLoggedUserToProfilePage();
170
- }, (error) => {
171
- console.log(error);
172
- this.manageAuthWithProvidersErrors(error.message);
 
173
  });
174
  }
175
 
176
- doTwitterLogin(): void {
177
- this.resetSubmitError();
178
- this.prepareForAuthWithProvidersRedirection('twitter');
179
-
180
- this.authService.signInWithTwitter()
181
- .subscribe((result) => {
182
- // This gives you a Twitter Access Token. You can use it to access the Twitter API.
183
- // var token = result.credential.accessToken;
184
- this.redirectLoggedUserToProfilePage();
185
- }, (error) => {
186
- console.log(error);
187
- this.manageAuthWithProvidersErrors(error.message);
188
- });
189
  }
190
 
191
- doAppleLogin(): void {
192
- this.resetSubmitError();
193
- this.prepareForAuthWithProvidersRedirection('apple');
194
-
195
- this.authService.signInWithApple()
196
- .subscribe((result) => {
197
- this.redirectLoggedUserToProfilePage();
198
- }, (error) => {
199
- console.log(error);
200
- this.manageAuthWithProvidersErrors(error.message);
201
- });
202
  }
203
  }
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 { FirebaseAuthService } from '../firebase-auth.service';
14
  './styles/firebase-sign-in.page.scss'
15
  ]
16
  })
17
+ export class FirebaseSignInPage {
18
  loginForm: FormGroup;
19
  submitError: string;
 
20
  authRedirectResult: Subscription;
21
 
22
  validation_messages = {
32
 
33
  constructor(
34
  public router: Router,
35
+ public firebaseAuthService: FirebaseAuthService,
36
+ private ngZone: NgZone
 
 
 
37
  ) {
38
  this.loginForm = new FormGroup({
39
  'email': new FormControl('', Validators.compose([
46
  ]))
47
  });
48
 
49
+ // ? Get firebase authentication redirect result invoked when using signInWithRedirect()
50
+ // ? signInWithRedirect() is only used when client is in web but not desktop. For example a PWA
51
+ this.authRedirectResult = this.firebaseAuthService.redirectResult$
52
  .subscribe(result => {
53
  if (result.error) {
54
  this.manageAuthWithProvidersErrors(result.error);
57
  }
58
  });
59
 
60
+ this.firebaseAuthService.authState$
61
+ .subscribe((stateChange: AuthStateChange) => {
62
+ if (!stateChange.user) {
63
+ this.manageAuthWithProvidersErrors('No user logged in');
64
+ } else {
65
+ this.redirectLoggedUserToProfilePage();
66
  }
67
  });
68
  }
69
 
70
+ public async doFacebookLogin(): Promise<void> {
71
+ this.resetSubmitError();
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
+ try {
74
+ await this.firebaseAuthService.signInWithFacebook()
75
+ .then((result: SignInResult) => {
76
+ // ? This gives you a Facebook Access Token. You can use it to access the Facebook API.
77
+ // const token = result.credential.accessToken;
78
+ this.redirectLoggedUserToProfilePage();
79
+ })
80
+ .catch((error) => {
81
+ this.manageAuthWithProvidersErrors(error.message);
82
+ });
83
+ } finally {
84
+ // ? Termination code goes here
85
+ }
86
  }
87
 
88
+ public async doGoogleLogin(): Promise<void> {
89
+ this.resetSubmitError();
 
 
 
 
 
 
 
 
 
 
 
90
 
91
+ try {
92
+ await this.firebaseAuthService.signInWithGoogle()
93
+ .then((result) => {
94
+ // ? This gives you a Google Access Token. You can use it to access the Google API.
95
+ // const token = result.credential.accessToken;
96
+ this.redirectLoggedUserToProfilePage();
97
+ })
98
+ .catch((error) => {
99
+ this.manageAuthWithProvidersErrors(error.message);
100
+ });
101
+ } finally {
102
+ // ? Termination code goes here
103
  }
104
  }
105
 
106
+ public async doTwitterLogin(): Promise<void> {
107
+ this.resetSubmitError();
 
 
108
 
109
+ try {
110
+ await this.firebaseAuthService.signInWithTwitter()
111
+ .then((result) => {
112
+ // ? This gives you a Twitter Access Token. You can use it to access the Twitter API.
113
+ // const token = result.credential.accessToken;
114
+ this.redirectLoggedUserToProfilePage();
115
+ })
116
+ .catch((error) => {
117
+ this.manageAuthWithProvidersErrors(error.message);
118
+ });
119
+ } finally {
120
+ // ? Termination code goes here
121
+ }
122
  }
123
 
124
+ public async doAppleLogin(): Promise<void> {
125
+ this.resetSubmitError();
 
 
 
 
126
 
127
+ try {
128
+ await this.firebaseAuthService.signInWithApple()
129
+ .then((result) => {
130
+ this.redirectLoggedUserToProfilePage();
131
+ })
132
+ .catch((error) => {
133
+ this.manageAuthWithProvidersErrors(error.message);
134
+ });
135
+ } finally {
136
+ // ? Termination code goes here
137
+ }
138
  }
139
 
140
+ public async signInWithEmail(): Promise<void> {
141
  this.resetSubmitError();
 
 
 
 
 
 
 
 
 
 
142
 
143
+ try {
144
+ await this.firebaseAuthService.signInWithEmail(this.loginForm.value['email'], this.loginForm.value['password'])
145
+ .then((result) => {
146
+ this.redirectLoggedUserToProfilePage();
147
+ })
148
+ .catch((error) => {
149
+ this.submitError = error.message;
150
+ });
151
+ } finally {
152
+ // ? Termination code goes here
153
+ }
 
154
  }
155
 
156
+ // ? Once the auth provider finished the authentication flow, and the auth redirect completes, hide the loader and redirect the user to the profile page
157
+ private redirectLoggedUserToProfilePage(): void {
158
+ // As we are calling the Angular router navigation inside a subscribe method, the navigation will be triggered outside Angular zone.
159
+ // That's why we need to wrap the router navigation call inside an ngZone wrapper
160
+ this.ngZone.run(() => {
161
+ // Get previous URL from our custom History Helper
162
+ // If there's no previous page, then redirect to profile
163
+ // const previousUrl = this.historyHelper.previousUrl || 'firebase/auth/profile';
164
+ const previousUrl = 'firebase/auth/profile';
165
+
166
+ // No need to store in the navigation history the sign-in page with redirect params (it's just a a mandatory mid-step)
167
+ // Navigate to profile and replace current url with profile
168
+ this.router.navigate([previousUrl], { replaceUrl: true });
169
  });
170
  }
171
 
172
+ private manageAuthWithProvidersErrors(errorMessage: string): void {
173
+ this.submitError = errorMessage;
 
 
 
 
 
 
 
 
 
 
 
174
  }
175
 
176
+ private resetSubmitError(): void {
177
+ this.submitError = null;
 
 
 
 
 
 
 
 
 
178
  }
179
  }
src/app/firebase/auth/sign-up/firebase-sign-up.module.ts CHANGED
@@ -2,16 +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 { FirebaseSignUpPage } from './firebase-sign-up.page';
7
  import { ComponentsModule } from '../../../components/components.module';
8
- import { map } from 'rxjs/operators';
9
- import { AngularFireAuthGuard } from '@angular/fire/auth-guard';
10
 
11
- // Firebase guard to redirect logged in users to profile
12
- const redirectLoggedInToProfile = (next) => map(user => {
13
- // when queryParams['auth-redirect'] don't redirect because we want
14
- // the component to handle the redirection
15
  if (user !== null && !next.queryParams['auth-redirect']) {
16
  return ['firebase/auth/profile'];
17
  } else {
@@ -23,8 +27,7 @@ const routes: Routes = [
23
  {
24
  path: '',
25
  component: FirebaseSignUpPage,
26
- canActivate: [AngularFireAuthGuard],
27
- data: { authGuardPipe: redirectLoggedInToProfile }
28
  }
29
  ];
30
 
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 { 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 {
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,14 @@
1
- import { Component, OnInit, NgZone, OnDestroy } 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',
@@ -14,11 +17,10 @@ import { Subscription } from 'rxjs';
14
  './styles/firebase-sign-up.page.scss'
15
  ]
16
  })
17
- export class FirebaseSignUpPage implements OnInit, OnDestroy {
18
  signupForm: FormGroup;
19
  matching_passwords_group: FormGroup;
20
  submitError: string;
21
- redirectLoader: HTMLIonLoadingElement;
22
  authRedirectResult: Subscription;
23
 
24
  validation_messages = {
@@ -39,13 +41,10 @@ export class FirebaseSignUpPage implements OnInit, OnDestroy {
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 +64,9 @@ export class FirebaseSignUpPage implements OnInit, OnDestroy {
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 +75,12 @@ export class FirebaseSignUpPage implements OnInit, OnDestroy {
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,131 +89,113 @@ export class FirebaseSignUpPage implements OnInit, OnDestroy {
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
- this.dismissLoading();
156
  });
 
 
 
157
  }
158
 
159
- doFacebookSignup(): void {
160
  this.resetSubmitError();
161
- this.prepareForAuthWithProvidersRedirection('facebook');
162
-
163
- this.authService.signInWithFacebook()
164
- .subscribe((result) => {
165
- // This gives you a Facebook Access Token. You can use it to access the Facebook API.
166
- // const token = result.credential.accessToken;
167
- this.redirectLoggedUserToProfilePage();
168
- }, (error) => {
169
- this.manageAuthWithProvidersErrors(error.message);
170
- });
171
- }
172
 
173
- doGoogleSignup(): void {
174
- this.resetSubmitError();
175
- this.prepareForAuthWithProvidersRedirection('google');
176
-
177
- this.authService.signInWithGoogle()
178
- .subscribe((result) => {
179
- // This gives you a Google Access Token. You can use it to access the Google API.
180
- // var token = result.credential.accessToken;
181
- this.redirectLoggedUserToProfilePage();
182
- }, (error) => {
183
- console.log(error);
184
- this.manageAuthWithProvidersErrors(error.message);
185
- });
186
  }
187
 
188
- doTwitterSignup(): void {
189
- this.resetSubmitError();
190
- this.prepareForAuthWithProvidersRedirection('twitter');
191
-
192
- this.authService.signInWithTwitter()
193
- .subscribe((result) => {
194
- // This gives you a Twitter Access Token. You can use it to access the Twitter API.
195
- // var token = result.credential.accessToken;
196
- this.redirectLoggedUserToProfilePage();
197
- }, (error) => {
198
- console.log(error);
199
- this.manageAuthWithProvidersErrors(error.message);
 
200
  });
201
  }
202
 
203
- doAppleSignup(): void {
204
- this.resetSubmitError();
205
- this.prepareForAuthWithProvidersRedirection('apple');
206
-
207
- this.authService.signInWithApple()
208
- .subscribe((result) => {
209
- this.redirectLoggedUserToProfilePage();
210
- }, (error) => {
211
- console.log(error);
212
- this.manageAuthWithProvidersErrors(error.message);
213
- });
214
  }
215
 
216
- ngOnDestroy(): void {
217
- this.dismissLoading();
218
  }
219
  }
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 { PasswordValidator } from '../../../validators/password.validator';
11
  import { FirebaseAuthService } from '../firebase-auth.service';
 
12
 
13
  @Component({
14
  selector: 'app-firebase-sign-up',
17
  './styles/firebase-sign-up.page.scss'
18
  ]
19
  })
20
+ export class FirebaseSignUpPage implements OnInit {
21
  signupForm: FormGroup;
22
  matching_passwords_group: FormGroup;
23
  submitError: string;
 
24
  authRedirectResult: Subscription;
25
 
26
  validation_messages = {
41
  };
42
 
43
  constructor(
 
 
44
  public menu: MenuController,
45
+ public router: Router,
46
+ public firebaseAuthService: FirebaseAuthService,
47
+ private ngZone: NgZone
 
48
  ) {
49
  this.matching_passwords_group = new FormGroup({
50
  'password': new FormControl('', Validators.compose([
64
  'matching_passwords': this.matching_passwords_group
65
  });
66
 
67
+ // ? Get firebase authentication redirect result invoked when using signInWithRedirect()
68
+ // ? signInWithRedirect() is only used when client is in web but not desktop. For example a PWA
69
+ this.authRedirectResult = this.firebaseAuthService.redirectResult$
70
  .subscribe(result => {
71
  if (result.error) {
72
  this.manageAuthWithProvidersErrors(result.error);
75
  }
76
  });
77
 
78
+ this.firebaseAuthService.authState$
79
+ .subscribe((stateChange: AuthStateChange) => {
80
+ if (!stateChange.user) {
81
+ this.manageAuthWithProvidersErrors('No user logged in');
82
+ } else {
83
+ this.redirectLoggedUserToProfilePage();
84
  }
85
  });
86
  }
89
  this.menu.enable(false);
90
  }
91
 
92
+ public async doFacebookSignup(): Promise<void> {
93
+ this.resetSubmitError();
 
 
 
 
 
 
 
 
 
 
94
 
95
+ try {
96
+ await this.firebaseAuthService.signInWithFacebook()
97
+ .then((result: SignInResult) => {
98
+ // ? This gives you a Facebook Access Token. You can use it to access the Facebook API.
99
+ // const token = result.credential.accessToken;
100
+ this.redirectLoggedUserToProfilePage();
101
+ })
102
+ .catch((error) => {
103
+ this.manageAuthWithProvidersErrors(error.message);
104
+ });
105
+ } finally {
106
+ // ? Termination code goes here
107
+ }
108
  }
109
 
110
+ public async doGoogleSignup(): Promise<void> {
111
+ this.resetSubmitError();
 
 
 
 
 
 
112
 
113
+ try {
114
+ await this.firebaseAuthService.signInWithGoogle()
115
+ .then((result) => {
116
+ // ? This gives you a Google Access Token. You can use it to access the Google API.
117
+ // const token = result.credential.accessToken;
118
+ this.redirectLoggedUserToProfilePage();
119
+ })
120
+ .catch((error) => {
121
+ this.manageAuthWithProvidersErrors(error.message);
122
+ });
123
+ } finally {
124
+ // ? Termination code goes here
125
  }
126
  }
127
 
128
+ public async doTwitterSignup(): Promise<void> {
129
+ this.resetSubmitError();
 
 
 
 
 
 
 
 
 
130
 
131
+ try {
132
+ await this.firebaseAuthService.signInWithTwitter()
133
+ .then((result) => {
134
+ // ? This gives you a Twitter Access Token. You can use it to access the Twitter API.
135
+ // const token = result.credential.accessToken;
136
+ this.redirectLoggedUserToProfilePage();
137
+ })
138
+ .catch((error) => {
139
+ this.manageAuthWithProvidersErrors(error.message);
140
+ });
141
+ } finally {
142
+ // ? Termination code goes here
143
+ }
144
  }
145
 
146
+ public async doAppleSignup(): Promise<void> {
147
  this.resetSubmitError();
148
+
149
+ try {
150
+ await this.firebaseAuthService.signInWithApple()
151
+ .then((result) => {
152
  this.redirectLoggedUserToProfilePage();
153
  })
154
+ .catch((error) => {
155
+ this.manageAuthWithProvidersErrors(error.message);
 
156
  });
157
+ } finally {
158
+ // ? Termination code goes here
159
+ }
160
  }
161
 
162
+ public async signUpWithEmail(): Promise<void> {
163
  this.resetSubmitError();
 
 
 
 
 
 
 
 
 
 
 
164
 
165
+ try {
166
+ await this.firebaseAuthService.signUpWithEmail(this.signupForm.value['email'], this.signupForm.value.matching_passwords.password)
167
+ .then((result) => {
168
+ this.redirectLoggedUserToProfilePage();
169
+ })
170
+ .catch((error) => {
171
+ this.submitError = error.message;
172
+ });
173
+ } finally {
174
+ // ? Termination code goes here
175
+ }
 
 
176
  }
177
 
178
+ // ? Once the auth provider finished the authentication flow, and the auth redirect completes, hide the loader and redirect the user to the profile page
179
+ private redirectLoggedUserToProfilePage(): void {
180
+ // As we are calling the Angular router navigation inside a subscribe method, the navigation will be triggered outside Angular zone.
181
+ // That's why we need to wrap the router navigation call inside an ngZone wrapper
182
+ this.ngZone.run(() => {
183
+ // Get previous URL from our custom History Helper
184
+ // If there's no previous page, then redirect to profile
185
+ // const previousUrl = this.historyHelper.previousUrl || 'firebase/auth/profile';
186
+ const previousUrl = 'firebase/auth/profile';
187
+
188
+ // No need to store in the navigation history the sign-in page with redirect params (it's justa a mandatory mid-step)
189
+ // Navigate to profile and replace current url with profile
190
+ this.router.navigate([previousUrl], { replaceUrl: true });
191
  });
192
  }
193
 
194
+ private manageAuthWithProvidersErrors(errorMessage: string): void {
195
+ this.submitError = errorMessage;
 
 
 
 
 
 
 
 
 
196
  }
197
 
198
+ private resetSubmitError(): void {
199
+ this.submitError = null;
200
  }
201
  }
src/app/firebase/crud/firebase-crud.module.ts CHANGED
@@ -3,8 +3,9 @@ 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 = [
@@ -30,8 +31,8 @@ const firebaseRoutes: Routes = [
30
  IonicModule,
31
  CommonModule,
32
  RouterModule.forChild(firebaseRoutes),
33
- AngularFireModule.initializeApp(environment.firebase),
34
- AngularFirestoreModule
35
- ],
36
  })
37
  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
  const firebaseRoutes: Routes = [
31
  IonicModule,
32
  CommonModule,
33
  RouterModule.forChild(firebaseRoutes),
34
+ provideFirebaseApp(() => initializeApp(environment.firebase)),
35
+ provideFirestore(() => getFirestore())
36
+ ]
37
  })
38
  export class FirebaseCrudModule {}
src/app/firebase/crud/firebase-crud.service.ts CHANGED
@@ -1,43 +1,64 @@
1
  import { Injectable } from '@angular/core';
2
- import { AngularFirestore } from '@angular/fire/firestore';
 
 
3
  import { Observable } from 'rxjs';
4
  import { map } from 'rxjs/operators';
 
5
  import * as dayjs from 'dayjs';
6
 
7
- @Injectable()
 
 
8
  export class FirebaseCrudService {
9
 
10
- constructor(private afs: AngularFirestore) {}
11
 
12
- /*
13
- Firebase User Listing Page
14
- */
15
- public getListingData(): Observable<any> {
16
- return this.afs.collection('users').valueChanges({ idField: 'id' })
17
- .pipe(
18
- map(actions => actions.map((user: any) => {
 
 
 
19
  const age = this.calcUserAge(user.birthdate);
 
20
  return { age, ...user };
21
- })
22
- )
23
  );
 
 
24
  }
25
 
26
- // Filter users by age
27
- public searchUsersByAge(lower: number, upper: number): Observable<any> {
28
- // we save the dateOfBirth in our DB so we need to calc the min and max dates valid for this query
29
  const minDate = (dayjs(Date.now()).subtract(upper, 'year')).unix();
30
  const maxDate = (dayjs(Date.now()).subtract(lower, 'year')).unix();
31
 
32
- const listingCollection = this.afs.collection('users', ref =>
33
- ref.orderBy('birthdate').startAt(minDate).endAt(maxDate));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
 
35
- return listingCollection.valueChanges({ idField: 'id' }).pipe(
36
- map(actions => actions.map((user: any) => {
37
- const age = this.calcUserAge(user.birthdate);
38
- return { age, ...user };
39
- })
40
- ));
41
  }
42
 
43
  private calcUserAge(dateOfBirth: number): number {
1
  import { Injectable } from '@angular/core';
2
+
3
+ import { Firestore, collection, collectionData, query, CollectionReference, orderBy, startAt, endAt } from '@angular/fire/firestore';
4
+
5
  import { Observable } from 'rxjs';
6
  import { map } from 'rxjs/operators';
7
+
8
  import * as dayjs from 'dayjs';
9
 
10
+ @Injectable({
11
+ providedIn: 'root'
12
+ })
13
  export class FirebaseCrudService {
14
 
15
+ constructor(private firestore: Firestore) {}
16
 
17
+ // * Firebase User Listing Page
18
+ public getListingData(): Observable<Array<any>> {
19
+ const rawData: Observable<Array<any>> = collectionData<any>(
20
+ query<any>(
21
+ collection(this.firestore, 'users') as CollectionReference<any>
22
+ ), { idField: 'id' }
23
+ )
24
+ .pipe(
25
+ map((users: Array<any>) => {
26
+ return users.map((user: any) => {
27
  const age = this.calcUserAge(user.birthdate);
28
+
29
  return { age, ...user };
30
+ });
31
+ })
32
  );
33
+
34
+ return rawData;
35
  }
36
 
37
+ // * Filter users by age
38
+ public searchUsersByAge(lower: number, upper: number): Observable<Array<any>> {
39
+ // ? We save the dateOfBirth in our DB so we need to calc the min and max dates valid for this query
40
  const minDate = (dayjs(Date.now()).subtract(upper, 'year')).unix();
41
  const maxDate = (dayjs(Date.now()).subtract(lower, 'year')).unix();
42
 
43
+ const filteredData: Observable<Array<any>> = collectionData<any>(
44
+ query<any>(
45
+ collection(this.firestore, 'users') as CollectionReference<any>,
46
+ orderBy('birthdate'),
47
+ startAt(minDate),
48
+ endAt(maxDate)
49
+ ), { idField: 'id' }
50
+ )
51
+ .pipe(
52
+ map((users: Array<any>) => {
53
+ return users.map((user: any) => {
54
+ const age = this.calcUserAge(user.birthdate);
55
+
56
+ return { age, ...user };
57
+ });
58
+ })
59
+ );
60
 
61
+ return filteredData;
 
 
 
 
 
62
  }
63
 
64
  private calcUserAge(dateOfBirth: number): number {
src/app/firebase/crud/listing/firebase-listing.page.ts CHANGED
@@ -23,7 +23,7 @@ export class FirebaseListingPage implements OnInit {
23
 
24
  searchSubject: ReplaySubject<any> = new ReplaySubject<any>(1);
25
  searchFiltersObservable: Observable<any> = this.searchSubject.asObservable();
26
- items: [];
27
 
28
 
29
  constructor(
23
 
24
  searchSubject: ReplaySubject<any> = new ReplaySubject<any>(1);
25
  searchFiltersObservable: Observable<any> = this.searchSubject.asObservable();
26
+ items: Array<any>;
27
 
28
 
29
  constructor(
src/app/forms/filters/forms-filters.page.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Component } from '@angular/core';
2
  import { FormGroup, FormControl } from '@angular/forms';
3
 
4
  import { counterRangeValidator } from '../../components/counter-input/counter-input.component';
1
+ import { Component} from '@angular/core';
2
  import { FormGroup, FormControl } from '@angular/forms';
3
 
4
  import { counterRangeValidator } from '../../components/counter-input/counter-input.component';
src/app/forms/validations/styles/forms-validations.page.scss CHANGED
@@ -83,7 +83,7 @@ ion-content {
83
  }
84
 
85
  .submit-btn {
86
- margin: var(--page-margin)
87
  }
88
  }
89
  }
83
  }
84
 
85
  .submit-btn {
86
+ margin: var(--page-margin);
87
  }
88
  }
89
  }
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,7 +1,12 @@
1
- import { Component, AfterViewInit, ViewChild, HostBinding } from '@angular/core';
2
  import { FormGroup, FormControl } from '@angular/forms';
3
 
4
- import { IonSlides, MenuController } from '@ionic/angular';
 
 
 
 
 
5
 
6
  @Component({
7
  selector: 'app-getting-started',
@@ -12,13 +17,16 @@ import { IonSlides, MenuController } from '@ionic/angular';
12
  './styles/getting-started.responsive.scss'
13
  ]
14
  })
15
- export class GettingStartedPage implements AfterViewInit {
16
- @ViewChild(IonSlides, { static: true }) slides: IonSlides;
17
  @HostBinding('class.last-slide-active') isLastSlide = false;
18
 
 
19
  gettingStartedForm: FormGroup;
20
 
21
- constructor(public menu: MenuController) {
 
 
 
22
  this.gettingStartedForm = new FormGroup({
23
  browsingCategory: new FormControl('men'),
24
  followingInterests: new FormGroup({
@@ -33,26 +41,24 @@ export class GettingStartedPage implements AfterViewInit {
33
  }
34
 
35
  // Disable side menu for this page
36
- ionViewDidEnter(): void {
37
  this.menu.enable(false);
38
  }
39
 
40
  // Restore to default when leaving this page
41
- ionViewDidLeave(): void {
42
  this.menu.enable(true);
43
  }
44
 
45
- ngAfterViewInit(): void {
46
- // ViewChild is set
47
- this.slides.isEnd().then(isEnd => {
48
- this.isLastSlide = isEnd;
49
- });
50
 
51
- // Subscribe to changes
52
- this.slides.ionSlideWillChange.subscribe(changes => {
53
- this.slides.isEnd().then(isEnd => {
54
- this.isLastSlide = isEnd;
55
- });
56
  });
57
  }
58
  }
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'),
32
  followingInterests: new FormGroup({
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/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/user/friends/styles/user-friends.page.scss CHANGED
@@ -2,7 +2,6 @@
2
  // Note: These ones were added by us and have nothing to do with Ionic CSS Custom Properties
3
  :host {
4
  --page-margin: var(--app-narrow-margin);
5
-
6
  --page-border-radius: var(--app-fair-radius);
7
  --page-segment-background: var(--app-background);
8
  --page-segment-indicator-height: 2px;
2
  // Note: These ones were added by us and have nothing to do with Ionic CSS Custom Properties
3
  :host {
4
  --page-margin: var(--app-narrow-margin);
 
5
  --page-border-radius: var(--app-fair-radius);
6
  --page-segment-background: var(--app-background);
7
  --page-segment-indicator-height: 2px;
src/app/user/friends/user-friends.page.ts CHANGED
@@ -28,12 +28,16 @@ export class UserFriendsPage implements OnInit {
28
  constructor(private route: ActivatedRoute) { }
29
 
30
  ngOnInit(): void {
31
- this.route.data.subscribe(routeData => {
32
- this.data = routeData['data'];
33
- this.friendsList = this.data.friends;
34
- this.followersList = this.data.followers;
35
- this.followingList = this.data.following;
36
- }, (error) => console.log(error));
 
 
 
 
37
  }
38
 
39
  segmentChanged(ev): void {
28
  constructor(private route: ActivatedRoute) { }
29
 
30
  ngOnInit(): void {
31
+ this.route.data
32
+ .subscribe({
33
+ next: (routeData) => {
34
+ this.data = routeData['data'];
35
+ this.friendsList = this.data.friends;
36
+ this.followersList = this.data.followers;
37
+ this.followingList = this.data.following;
38
+ },
39
+ error: (error) => console.log(error)
40
+ });
41
  }
42
 
43
  segmentChanged(ev): void {
src/app/validators/password.validator.ts CHANGED
@@ -6,17 +6,18 @@ export class PasswordValidator {
6
  // Otherwise, if the validation passes, we simply return null because there is no error.
7
 
8
  static areNotEqual(formGroup: FormGroup) {
9
- let val;
10
  let valid = true;
11
 
12
  for (const key in formGroup.controls) {
13
  if (formGroup.controls.hasOwnProperty(key)) {
14
  const control: FormControl = <FormControl>formGroup.controls[key];
15
 
16
- if (val === undefined) {
17
- val = control.value;
18
  } else {
19
- if (val !== control.value) {
 
20
  valid = false;
21
  break;
22
  }
6
  // Otherwise, if the validation passes, we simply return null because there is no error.
7
 
8
  static areNotEqual(formGroup: FormGroup) {
9
+ let firstControlValue: any;
10
  let valid = true;
11
 
12
  for (const key in formGroup.controls) {
13
  if (formGroup.controls.hasOwnProperty(key)) {
14
  const control: FormControl = <FormControl>formGroup.controls[key];
15
 
16
+ if (firstControlValue === undefined) {
17
+ firstControlValue = control.value;
18
  } else {
19
+ // check if the value of the first control is equal to the value of the second control
20
+ if (firstControlValue !== control.value) {
21
  valid = false;
22
  break;
23
  }
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
@@ -4,6 +4,7 @@ import { FormsModule } from '@angular/forms';
4
  import { Routes, RouterModule } from '@angular/router';
5
 
6
  import { IonicModule } from '@ionic/angular';
 
7
 
8
  import { ComponentsModule } from '../components/components.module';
9
 
@@ -24,7 +25,8 @@ const routes: Routes = [
24
  FormsModule,
25
  IonicModule,
26
  RouterModule.forChild(routes),
27
- ComponentsModule
 
28
  ],
29
  declarations: [WalkthroughPage],
30
  providers: [WalkthroughGuard]
4
  import { Routes, RouterModule } from '@angular/router';
5
 
6
  import { IonicModule } from '@ionic/angular';
7
+ import { SwiperModule } from 'swiper/angular';
8
 
9
  import { ComponentsModule } from '../components/components.module';
10
 
25
  FormsModule,
26
  IonicModule,
27
  RouterModule.forChild(routes),
28
+ ComponentsModule,
29
+ SwiperModule
30
  ],
31
  declarations: [WalkthroughPage],
32
  providers: [WalkthroughGuard]
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" [options]="slidesOptions">
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,8 +1,15 @@
1
- import { isPlatformBrowser } from '@angular/common';
2
- import { Component, AfterViewInit, ViewChild, HostBinding, PLATFORM_ID, Inject, OnInit } from '@angular/core';
3
- import { IonSlides, MenuController } from '@ionic/angular';
4
  import { Storage } from '@capacitor/storage';
5
 
 
 
 
 
 
 
 
 
6
  @Component({
7
  selector: 'app-walkthrough',
8
  templateUrl: './walkthrough.page.html',
@@ -13,21 +20,17 @@ import { Storage } from '@capacitor/storage';
13
  ]
14
  })
15
  export class WalkthroughPage implements AfterViewInit, OnInit {
16
- slidesOptions: any = {
17
- zoom: {
18
- toggle: false // Disable zooming to prevent weird double tap zomming on slide images
19
- }
20
- };
21
 
22
- @ViewChild(IonSlides, { static: true }) slides: IonSlides;
23
 
24
  @HostBinding('class.first-slide-active') isFirstSlide = true;
25
 
26
  @HostBinding('class.last-slide-active') isLastSlide = false;
27
 
28
  constructor(
29
- @Inject(PLATFORM_ID) private platformId: object,
30
- public menu: MenuController
31
  ) { }
32
 
33
  ngOnInit(): void {
@@ -50,34 +53,46 @@ export class WalkthroughPage implements AfterViewInit, OnInit {
50
 
51
  ngAfterViewInit(): void {
52
  // Accessing slides in server platform throw errors
53
- if (isPlatformBrowser(this.platformId)) {
54
- // ViewChild is set
55
-
56
- this.slides.ionSlidesDidLoad.subscribe(() => this.slides.update());
57
 
58
- this.slides.isBeginning().then(isBeginning => {
59
- this.isFirstSlide = isBeginning;
60
- });
61
- this.slides.isEnd().then(isEnd => {
62
- this.isLastSlide = isEnd;
63
  });
 
64
 
65
- // Subscribe to changes
66
- this.slides.ionSlideWillChange.subscribe(changes => {
67
- this.slides.isBeginning().then(isBeginning => {
68
- this.isFirstSlide = isBeginning;
69
- });
70
- this.slides.isEnd().then(isEnd => {
71
- this.isLastSlide = isEnd;
72
- });
73
  });
74
- }
 
75
  }
76
 
77
- skipWalkthrough(): void {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  // Skip to the last slide
79
- this.slides.length().then(length => {
80
- this.slides.slideTo(length);
81
- });
82
  }
83
  }
1
+ import { Component, AfterViewInit, ViewChild, HostBinding, OnInit, NgZone } from '@angular/core';
2
+
 
3
  import { Storage } from '@capacitor/storage';
4
 
5
+ import { MenuController } from '@ionic/angular';
6
+ import { IonicSwiper } from '@ionic/angular';
7
+
8
+ import SwiperCore, { Pagination } from 'swiper';
9
+ import { SwiperComponent } from 'swiper/angular';
10
+
11
+ SwiperCore.use([Pagination, IonicSwiper]);
12
+
13
  @Component({
14
  selector: 'app-walkthrough',
15
  templateUrl: './walkthrough.page.html',
20
  ]
21
  })
22
  export class WalkthroughPage implements AfterViewInit, OnInit {
23
+ swiperRef: SwiperCore;
 
 
 
 
24
 
25
+ @ViewChild(SwiperComponent, { static: false }) swiper?: SwiperComponent;
26
 
27
  @HostBinding('class.first-slide-active') isFirstSlide = true;
28
 
29
  @HostBinding('class.last-slide-active') isLastSlide = false;
30
 
31
  constructor(
32
+ public menu: MenuController,
33
+ private ngZone: NgZone
34
  ) { }
35
 
36
  ngOnInit(): void {
53
 
54
  ngAfterViewInit(): void {
55
  // Accessing slides in server platform throw errors
56
+ // if (isPlatformBrowser(this.platformId)) {
57
+ this.swiperRef = this.swiper.swiperRef;
 
 
58
 
59
+ this.swiperRef.on('slidesLengthChange', () => {
60
+ // ? We need to use ngZone because the change happens outside Angular
61
+ // (see: https://swiperjs.com/angular#swiper-component-events)
62
+ this.ngZone.run(() => {
63
+ this.markSlides(this.swiperRef);
64
  });
65
+ });
66
 
67
+ this.swiperRef.on('slideChange', () => {
68
+ // ? We need to use ngZone because the change happens outside Angular
69
+ // (see: https://swiperjs.com/angular#swiper-component-events)
70
+ this.ngZone.run(() => {
71
+ this.markSlides(this.swiperRef);
 
 
 
72
  });
73
+ });
74
+ // }
75
  }
76
 
77
+ public setSwiperInstance(swiper: SwiperCore): void {
78
+ // console.log('setSwiperInstance');
79
+ }
80
+
81
+ public swiperInit(): void {
82
+ // console.log('swiperInit');
83
+ }
84
+
85
+ public slideWillChange(): void {
86
+ // console.log('slideWillChange');
87
+ }
88
+
89
+ public markSlides(swiper: SwiperCore): void {
90
+ this.isFirstSlide = (swiper.isBeginning || swiper.activeIndex === 0);
91
+ this.isLastSlide = swiper.isEnd;
92
+ }
93
+
94
+ public skipWalkthrough(): void {
95
  // Skip to the last slide
96
+ this.swiperRef.slideTo(this.swiperRef.slides.length - 1);
 
 
97
  }
98
  }
src/assets/icon/HOW-TO.md DELETED
@@ -1,13 +0,0 @@
1
- ## Generate a multi size .ico file using ImageMagick
2
-
3
- First install ImageMagick
4
-
5
- ### Mac OS X
6
- ```
7
- brew install imagemagick
8
- ```
9
-
10
- Then run this command
11
- ```
12
- convert favicon.png -define icon:auto-resize:64,48,32,24,16 favicon.ico
13
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
src/assets/icon/favicon.ico CHANGED
Binary file
src/assets/sample-data/travel/listing.json CHANGED
@@ -5,7 +5,7 @@
5
  "slug": "tristan-narvaja",
6
  "image": "./assets/sample-images/travel/Travel1-64.47.png",
7
  "icon": "./assets/sample-images/travel/TravelIcon1.png",
8
- "name": "Tristán Narvaja ",
9
  "category": "Flea Market",
10
  "description": "Every Sunday from early morning until three in the afternoon enjoy the biggest market in Montevideo.",
11
  "rating": 4.8,
5
  "slug": "tristan-narvaja",
6
  "image": "./assets/sample-images/travel/Travel1-64.47.png",
7
  "icon": "./assets/sample-images/travel/TravelIcon1.png",
8
+ "name": "Tristán Narvaja",
9
  "category": "Flea Market",
10
  "description": "Every Sunday from early morning until three in the afternoon enjoy the biggest market in Montevideo.",
11
  "rating": 4.8,
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-BASIC",
3
  "description": "The most advanced and complete Mobile & PWA Ionic starter app template",
4
+ "short_name": "Ionic6FullApp-BASIC",
5
  "theme_color": "#1C1C1C",
6
  "background_color": "#000000",
7
  "display": "standalone",