Search code examples
angularngrxngrx-storengrx-effects

How can I make a ngrx effects that gets the current state of email and send that to a AuthApiService if it succedes?


I am using Angular 16 with Google Identity and google one tap, to make users being able to login:
https://developers.google.com/identity/gsi/web/guides/overview

Currently I am working on the logout feature of the application, where I want to revoke the token in my .net backend.
To do that I want to dispatch an action when the user does that.

This is what I have tried:

This is revokeToken() from AuthApiService, where I send the information to the backend, which use the email (a string) to logout the user. It takes in a string email, which it should get when I dispatch the action.

revokeToken(email: string) {
    const header = new HttpHeaders().set('Content-type', 'application/json');
    return this.httpClient.delete(`api/v1/Auth/RevokeToken/` + email, { headers: header, withCredentials: true });
}

Therefore, I have created actions:

import { createActionGroup, emptyProps, props } from '@ngrx/store';
import { HttpErrorResponse } from '@angular/common/http';

export const AuthActions = createActionGroup({
  source: 'Auth',
  events: {
    'Logout Init': emptyProps(),
    'Logout Success': emptyProps(),
    'Logout Failed': props<{ error: HttpErrorResponse }>(),
  },
});

and reducers to implement this:

import { createFeature, createReducer, on } from '@ngrx/store';
import { AuthActions } from './auth.actions';
import { LoadingState } from '../../shared/models/loading-state.model';

export interface AuthState {
  token: string;
  email: string;
  fullName: string;
}

export const initialState: AuthState = {
  token: '',
  email: '',
  fullName: '',
};

export const authFeature = createFeature({
  name: 'auth',
  reducer: createReducer(
    initialState,

    on(AuthActions.logoutInit, (state) => ({
      ...state,
      ...LoadingState.loading(),
    })),
    on(AuthActions.logoutSuccess, (state) => ({
      ...state,
      token: '',
      email: '',
      fullName: '',
      ...LoadingState.loaded(),
    })),
    on(AuthActions.logoutFailed, (state, { error }) => ({
      ...state,
      ...LoadingState.failed(error),
    })),
  )
})

and I now need the effect. This is what I have tried:

revokeTokenAtLogout$ = createEffect(() => {
  return this.actions$.pipe(
    ofType(AuthActions.logoutInit),
    concatLatestFrom(() => this.store.select(selectEmail)),
    map(([action, email]) => {
      this.authApiService.revokeToken(email).pipe(
        AuthActions.logoutSuccess(),
        catchError(error => of(AuthActions.logoutFailed({error})))
      )
    })
   )
})

Which I know does not work.

I know what it should do though.

  1. It must listen on logoutInit,
  2. then get the current state of email from the store,
  3. then give that to the revokeToken as we call it,
  4. if it succeedes, logout and navigate back to the login screen.
  5. if it fails, return the logoutFailed action.

I am hopelessy stuck with it and been looking at it for days..

What I need help with is how to implement the effect correctly, so that it works as intended. Can anybody help me with this?

EDIT:
The solution for this problem is:

@Injectable()
export class AuthEffects {
  constructor(
    private readonly actions$: Actions,
    private readonly store: Store,
    private readonly authApiService: AuthApiService,
    private router: Router,
  ) {}

  revokeTokenAtLogout$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(AuthActions.logoutInit),
      concatLatestFrom(() => this.store.select(selectEmail)),
      switchMap(([, email]) =>
        this.authApiService.revokeToken(email).pipe(
          map(() => AuthActions.logoutSuccess()),
          catchError(error => of(AuthActions.logoutFailed({ error })))
        ),
      ),
    );
  });

  logoutNavigate$ = createEffect(() => {
    return this.actions$.pipe(
      ofType(AuthActions.logoutSuccess),
      tap(() => {
        this.router.navigate(['login']).then(() => window.location.reload());
      })
    );
  }, { dispatch: false })
}

Solution

  • When asking questions please define "does not work". Because, to me, the code matches the description:

    revokeTokenAtLogout$ = createEffect(() => {
      return this.actions$.pipe(
    // ✅ It must listen on logoutInit,
        ofType(AuthActions.logoutInit),
    // ✅ then get the current state of email from the store,    concatLatestFrom(() => this.store.select(selectEmail)),
        map(([action, email]) => {
    // ✅ then give that to the revokeToken as we call it,
          this.authApiService.revokeToken(email).pipe(
    // ✅ if it succeedes, logout and navigate back to the login screen., 
    // ⚠️ but wrap it in a `map`
            map(() => AuthActions.logoutSuccess()),
    // ✅ if it fails, return the logoutFailed action.
            catchError(error => of(AuthActions.logoutFailed({error})))
          )
        })
       )
    })
    

    What we don't see but can affect the outcome:

    • the email property is not set to a value
    • the logic behind the LoadingState.abc() methods in the reducer
    • the http response
    • how the store is registered