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