I want to show intro slider when user just installed the app (only once)
Logic
When user install the app store item named intro with value of false
When user open the app check intro item, if value is false show intro page if value is true continue with routing and load main page route.
Issue
I have tried couple of ways to achieve this goal but all failed as result of storage not having any item named intro.
Code
What I've tried so far: (all placed in app.component.ts file)
1
import { NativeStorage } from '#ionic-native/native-storage/ngx';
constructor(
public navCtrl: NavController,
private storage: NativeStorage,
) {
this.letsTest();
}
letsTest(){
console.log('fired');
if (this.storage.getItem('intro') == undefined) { // if not getting intro add and redirect to intro route
this.storage.setItem('intro', false);
this.navCtrl.navigateRoot('/intro');
} else { // otherwise do normal routing and load main page
console.log('exists', this.storage.getItem('intro'));
}
}
Result: Failed.
2
import { NativeStorage } from '#ionic-native/native-storage/ngx';
constructor(
public navCtrl: NavController,
private storage: NativeStorage,
) {
this.letsTest();
}
letsTest(){
console.log('fired');
//get intro
const intro = this.storage.getItem('intro');
// if not exist add it and reload to intro route
if (!intro){
console.log('intro was undefined so we set new false value in storage');
this.storage.setItem('intro', false);
this.navCtrl.navigateRoot('/intro');
} else {//if found get it and check value of it
this.storage.getItem('intro').then(
data=>{
console.log('just data', data);
//if value is true means user already saw intro do normal routing and load main page
if(data == true){
console.log('exists', data);
}else{
// if value is not true (false) then load intro page first
this.navCtrl.navigateRoot('/intro');
console.log('ionViewWillEnter checked', data);
}
},
error => {
console.log('some error happened', error);
}
)
}
}
Result: Failed.
Console debug
This is what I get in console (Code 2 = ITEM_NOT_FOUND)
__zone_symbol__state: 0
__zone_symbol__value: NativeStorageError
code: 2
exception: null
source: "Native"
__proto__: Error
constructor: ƒ (code, source, exception)
ITEM_NOT_FOUND: 2
JSON_ERROR: 5
NATIVE_WRITE_FAILED: 1
NULL_REFERENCE: 3
UNDEFINED_TYPE: 4
WRONG_PARAMETER: 6
arguments: null
caller: null
length: 3
name: "NativeStorageError"
prototype: Error {constructor: ƒ}
__proto__: ƒ ()
[[FunctionLocation]]: NativeStorageError.js:6
[[Scopes]]: Scopes[1]
__proto__: Object
__proto__: Object
Question
How can I check intro item from native local storage and redirect user based on it's value?
import {Storage} from "#ionic/storage";
constructor( public storage: Storage){
this.storage.get('intro').then(value => {
if(value){
//nav to home page
}else{
this.setIntro(true)
}
},reason => {
this.setIntro(true)
}).catch(err=>{
console.log(err)
})
}
setIntro(bool:boolean){
this.storage.set('intro',bool).then(()=>{
//nav to intro page
}).catch(err=>{
console.log(err)
})
}
The problem with your code is likely something to do with you using NativeStorage instead of Ionic Storage, which is the recommended option.
There are several tutorials out there for this.
I think this is a good one:
Ionic Intro Slider for New Users | AngularFirebase
That content is pro but you can get 3 free lessons, it was a bit confusing but refreshing the page got it loaded for me normally.
The idea is to use an Route Guard instead of doing it in the page. So you would generate a guard and use something like this inside of it:
// ...omitted
import { Storage } from '#ionic/storage';
#Injectable({
providedIn: 'root'
})
export class TutorialGuard implements CanActivate {
constructor(private storage: Storage, private router: Router) {}
async canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Promise<boolean> {
const isComplete = await this.storage.get('tutorialComplete');
if (!isComplete) {
this.router.navigateByUrl('/tutorial');
}
return isComplete;
}
}
Then you can just put that guard on your homepage route like:
import { Routes, RouterModule } from '#angular/router';
import { TutorialGuard } from './guards/tutorial.guard';
const routes: Routes = [
{
path: '',
loadChildren: './tabs/tabs.module#TabsPageModule',
canActivate: [TutorialGuard] // <-- apply here
},
{
path: 'tutorial',
loadChildren: './tutorial/tutorial.module#TutorialPageModule'
}
];
#NgModule(...)
export class AppRoutingModule {}
And in the tutorial / intro code you would set the isComplete value once the user reached the end of the intro slides:
async finish() {
await this.storage.set('tutorialComplete', true);
this.router.navigateByUrl('/');
}
Related
I'm working on a small personal app. I'll explain what I did until now and in the end my problem and my question.
I have created a Node server and an Angular app.
When the Angular app is booting I'm checking if the user is logged in (via http get request to the server, the request is made in app.component.ts)
ngOnInit(): void {
this.authService.checkIfUserSignedIn();
}
Inside the checkIfUserSignedIn method after that I'm getting the relevant authentication information I notify to the interested components with the auth state.
this.userAuthDetailsSubject.next(this.userAuthDetails);
Additionally, I'm having an AuthGuard that restrict the entry to the "create-list" component only to authenticated users.
In the AuthGurad I'm checking the auth state:
const authStatus = this.authService.isAuth();
return authStatus;
In the menu html component I have the following code:
<span routerLink="create-list" *ngIf="userIsAuthenticated"> New List</span>
Which works fine.
My problem is when i'm visiting manually localhost:4200/create-list
The AuthGuard is probably loaded before auth state is updated and therefore the user has no access to the "create-list" component, although he is signed in eventually.
I thought about two solutions but I'm not sure if they are good and how to implement them, and would like to hear your opinion.
using localStorage (It may be an overkill solution for this tiny problem)
make the HTTP get request to the server (for the auth state) inside the authGuard or maybe subscribe to an observer in the auth service (if so, how to implement that?)
Any ideas/solutions?
canActivate (AuthGuard):
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | import("#angular/router").UrlTree | import("rxjs").Observable<boolean | import("#angular/router").UrlTree> | Promise<boolean | import("#angular/router").UrlTree> {
const authStatus = this.authService.isAuth();
if (authStatus) {
return true;
} else {
this.router.navigate(['/login']);
}
}
auth.service.ts
#Injectable()
export class AuthService {
userAuthDetailsSubject = new Subject<UserAuthDetails>();
userAuthDetails: UserAuthDetails = null;
private isAuthenticated = false;
constructor(#Inject(DOCUMENT) private document: Document, private http: HttpClient) {
};
public isAuth(): boolean {
console.log({
isAuth: this.isAuthenticated
})
return this.isAuthenticated;
}
signIn() {
// redirect to signin..
this.document.location.href = '/auth/google';
}
signOut() {
this.document.location.href = '/auth/logout';
}
checkIfUserSignedIn() {
this.http.get<any>('/auth/current_user').subscribe(res => {
if (res) {
this.isAuthenticated = true;
console.log('assigning true to isAuth')
this.userAuthDetails = {
displayName: res.displayName,
email: res.email,
uid: res._id
};
this.userAuthDetailsSubject.next(this.userAuthDetails);
} else {
console.log('User not authenticated')
}
})
}
}
For this particular problem you can make the 'isAuthenticated' field a subject just like 'userAuthDetailsSubject' and update its value when the server responds.
auth.service.ts
checkIfUserSignedIn() {
this.http.get<any>('/auth/current_user').subscribe(res => {
if (res) {
this.isAuthenticated.next(true); //update the value
console.log('assigning true to isAuth')
this.userAuthDetails = {
displayName: res.displayName,
email: res.email,
uid: res._id
};
this.userAuthDetailsSubject.next(this.userAuthDetails);
} else {
console.log('User not authenticated')
}
})
}
Now change your authguard so it does not return true or false synchronously.
canActivate (AuthGuard):
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot):
boolean | import("#angular/router").UrlTree |
import("rxjs").Observable<boolean | import("#angular/router").UrlTree>| Promise<boolean | import("#angular/router").UrlTree> {
return this.authService.isAuth().subscribe((logged)=>{
if (logged) {
return true;
} else {
this.router.navigate(['/login']);
return false;
}
})
}
Off topic:
Why do you use import("#angular/router").UrlTree? You can use import like import { UrlTree } from '#angular/router';
CanActive support UrlTree return. return this.router.createUrlTree(['/login']); and not create a new async process in your canActivate
On Topic:
If you call direct link, you have to resolve authentication. If you call link or F5 reload browser will lost every data from memory. If you use any token to auth it be worth saving into localStore and restore from here.
Ofc, After authentication if you open new tab, this new tab will new without auth default like you used F5 on current tab. It lives a separate life for each tabs.
My auth is based on NGRX
so when the page starts loading I got all roles and
then get logged in.
but when I start using route guard,
route gourd start working before user data get loading
how can I wait for user load action to be done then start using canActivate
I try below solution but it's not working
export class AuthGuard implements CanActivate, OnDestroy {
private unsubscribe: Subject<any>;
constructor(private store: Store<AppState>, private router: Router,
private alertService: ToastrService, private authService: AuthService) {
this.unsubscribe = new Subject();
}
ngOnDestroy(): void {
this.unsubscribe.next();
this.unsubscribe.complete();
}
getFromStoreOrAPI(): Observable<any> {
return this.store.pipe(
select(isUserLoaded),
tap((data: boolean) => {
if (!data) {
this.store.dispatch(new UserRequested());
}
}),
take(1)
);
}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
return this.getFromStoreOrAPI().pipe(
switchMap(() =>
this.store.pipe(
select(isLoggedIn),
map(loggedIn => {
if (!loggedIn) {
this.router.navigateByUrl('/auth/login');
return false;
} else {
this.store.pipe(
select(currentUserRoles),
map((userRoles: Role[]) => {
//.......
}),
takeUntil(this.unsubscribe)
).subscribe();
}
}),
)
),
catchError(() => this.router.navigateByUrl('/auth/login'))
);
}
}
You can use filter to wait until loaded flag is true.
Here is approach I took with my auth.guard.ts :
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> {
return this.authFacade.loaded$.pipe(
filter(loaded => !!loaded),
mergeMap(() => this.authFacade.userAccount$),
map(userAccount => {
if (!userAccount) this.authFacade.redirectLoginPage(state.url);
return !!userAccount;
}),
first()
);
}
In my case, main app component is dispatching an action CheckAuth to check if user is already authenticated, and then set loaded flag.
It should work with some adaptation for your need. But main difference is the use of filter which avoid to continue the workflow if user checking is not done, and force waiting for the value.
Of course, be sure to set loaded value in all the case after receiving response (authenticated or not), or in case of any error.
Here is a potential adaptation for your case :
authLoaded$ = this.store.pipe(select(authLoaded));
authAccount$ = this.store.pipe(select(authAccount));
canActivate(...) {
return userLoaded$.pipe(
tap(loaded => {
if (!loaded) {
this.store.dispatch(new UserRequested());
}
}),
filter(loaded => !!loaded),
mergeMap(() => authAccount$),
map(authAccount => {
if (!authAccount.loggedIn) {
this.router.navigateByUrl('/auth/login');
return false;
}
if (!authAccount.roles?.length) {
this.router.navigateByUrl('/auth/forbidden');
return false;
}
// do some extra stuff...
return true;
}),
first()
);
}
I renamed isUserLoaded to authLoaded to clearly indicate the status of authentication loading (you can use also ready or not for instance). But not necessary user.
I created also a new selector authAccount which returns an object with at least 2 things :
loggedIn : true/false if user is logged in
roles: array of user roles.
But you can add of course user property, with user details.
This is a composed selector from different parts of your state.
With it, your code is more clear and maintable, you receive a complete status of your current authentication user.
Maybe some typos is possible, I wrote the code directly in my answer without testing it.
Hope this will help you.
I have an app made in ionic 3 in which I created a provider to centralize access to LoadingController.
I have implemented the provider as the code shown below, and I thought it'd be enough to control loading indicators for everywhere in the app.
I don't know how, but sometimes there are multiple instances of the indicator being instantiated, even with the if (!this.isShowing()) verification before instantiating a new one.
Can someone help me figure out what is happening? Thanks in advance.
import { Injectable } from '#angular/core';
import { LoadingController, Loading, Platform } from 'ionic-angular';
import { BehaviorSubject } from 'rxjs';
export enum LoadingStatus {
SHOWING,
DISMISSED,
}
#Injectable()
export class LoadingProvider {
private loading: Loading = null;
private status: BehaviorSubject<LoadingStatus> = new BehaviorSubject(LoadingStatus.DISMISSED);
constructor(private loadingCtrl: LoadingController, private platform: Platform) {
this.platform.ready().then(() => {
this.status.next(LoadingStatus.DISMISSED);
});
}
async show(content?: string) {
if (!this.isShowing()) {
this.create(content);
await this.loading.present();
}
}
async dismiss() {
if (this.isShowing()) {
await this.loading.dismiss();
this.loading = null;
}
}
private create(content?: string) {
this.loading = this.loadingCtrl.create({
content: content ? content : 'Carregando...',
showBackdrop: true,
enableBackdropDismiss: true,
});
this.loading.didEnter.subscribe(() => {
if (this.status.getValue() === LoadingStatus.DISMISSED) {
this.updateLoadingStatus(LoadingStatus.SHOWING);
}
});
this.loading.didLeave.subscribe(() => {
if (this.status.getValue() === LoadingStatus.SHOWING) {
this.updateLoadingStatus(LoadingStatus.DISMISSED);
}
});
}
private async updateLoadingStatus(status: LoadingStatus) {
this.status.next(status);
}
private isShowing(): boolean {
return this.status.getValue() === LoadingStatus.SHOWING;
}
}
You're not updating your loading status until after the loader enters. If the entering is asynchronous, you've got a possibility for a race condition:
show() is called
A loader is created
show() is called again by something else
A second loader is created
The first loader enters, updating the status
In an angular 5 app, there is a route guard that check from an API if an object exists:
//guard.ts excerpt
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<boolean> | Promise<boolean> | boolean {
return this.clientService.get(next.params.id).switchMap( data => {
return Observable.of(true);
})
.catch( err => Observable.of(false))
}
//route.ts excerpt
{ path: ':id', canActivate: [ ClientDetailGuard ], component: ClientDetail }
this works perfect, but I am wondering if is there a way to pass the data retrieved from my service to next the route/component (ClientDetail), so I won't need to call the service again this again.
I tried to add
next.data.client = data;
before the return of Observable(true) but in the component, the ActivatedRoute's data does not have this value set.
Or should I use something like Resolve?
I know I can achieve this using some state container or a shared service to store/retrieve data, but I wouldn't like to do this at this time, as long as the app is not complex.
I could do this using a Resolver instead of a guard
//route.ts
{ path: ':id', resolve: { client: ClientDetailResolver }, component: ClientDetail }
//resolver.ts
#Injectable()
export class ClientDetailResolver implements Resolve {
constructor(private clientService: ClientService, private router: Router, public location: Location) {
}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): Observable<any>|Promise<any>|any {
return this.clientService.get(route.params.id)
.catch( err => {
//handle error
const path = this.location.path();
this.router.navigate(["error", err.status], { skipLocationChange: true })
.then( () => {
this.location.replaceState(path);
});
return Observable.empty();
})
}
}
You seem to be under-estimating the power of services. Services are the best way to save/store data or states between components. You can set the data from any component, pull the data from any component. You don't have to worry about putting data in for the next route, instead you go to the next route and subscribe to your data on ngOnInit and boom, got everything you need. Here is an example of just how simple it really is.
Example of service
import { Injectable } from '#angular/core';
import { BehaviorSubject, Subscription } from 'rxjs';
#Injectable()
export class AppService {
alertData = new BehaviorSubject({
type: '',
message: '',
timer: 0,
showing: false
});
constructor() {}
setAlertData(data: AlertModel) {
this.alertData.next(data);
}
}
Example of using service
this.subscription$.push(this._as.alertData.subscribe((data: AlertModel) => {
this.alertData = data;
if (data.showing) {
this.alertIsShowing = true;
}
else {
this.alertIsShowing = false;
}
}));
Current routing configuration:
//...
{
path: 'foo/:id',
component: SomeComponent,
canActivate: [SomeGuard]
},
//...
Then in guard I call permission service to get access for component:
#Injectable()
export class SomeGuard implements CanActivate {
constructor(private service: Service) {
}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
const id = parseInt(next.params['id']);
return this.service.getPermissions(id).then(permissions => {
if (permissions.canView) {
return true;
}
return false;
});
}
}
But in the component I utilize the same permissions endpoint, which means I call it twice in a row to get to one page:
//...
constructor(private route: ActivatedRoute,
private service: Service) {
}
ngOnInit() {
this.id = parseInt(this.route.snapshot.params['id']);
this.service.getPermissions(this.id).then(permissions => {
// ...
});
}
//...
So it would be great to just save the permissions data in the route and utilize it by both guard and the component. I tried using resolve, but it turns out resolve only activates after the guards, which is no good. So how can i save permissions data?
This looks like the kind of task for a caching service. Permissions do not change often so they are the perfect candidate for caching. That way even multiple visits to the same resource would not trigger multiple HTTP requests for permission checks.
Edit: Since you need permissions to be loaded each time, you could listen for RouteNavigationStart and clear the cache. If this becomes too cumbersome to maintain in the PermissionsService you could extract the logic into a separate service.
You could something like this in the service you use to get your permissions:
// Permissions service
private permissionCache;
constructor(
router: Router,
) {
// clear cache when a route navigation starts
router.events
.filter(event => event instanceof NavigationStart)
.subscribe(event => this.permissionCache = {})
}
getPermissions(id) {
if (permissionCache[id]) {
return Observable.of(permissionCache[id]);
} else {
// get the permissions
permissionCache[id] = response;
return Observable.of(response);
}
});