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