Adding Automatic Logout to Angular Applications
At Picnic we have a clear goal: make the online supermarket of tomorrow available today. In our journey to change the way people do their groceries, the jewel in the crown is our automated fulfillment center — a combination of conveyor belts, robot shuttles, totes (crates), picking stations and, most importantly, our shoppers, who make sure our customer’s orders are picked and made ready for delivery.
We are always looking for new ways to make this system better, identifying what can help the shoppers in their day to day work, while ensuring everything runs smoothly and efficiently.
Recently we worked on one of those improvements. Until now, if a picking station was empty but a user was still logged in, that station would continue to receive totes. An unmanned open station is a clear bottleneck: it creates a line of “stuck” totes that our system has no way of knowing if they’ll be taken care of. Our solution was quite straightforward: if an active station does not have any user input for a certain amount of time, the logged in user should be logged out and the station should no longer be active. With that idea we went to the drawing board.
Think big, build small
The concept was simple enough but, as most of us know, simple problems can sometimes end up with very complex solutions. To avoid overengineering we identified two minimum requirements for this feature:
- Starting a countdown once a user logged in on a station, triggering this “automated log out”.
- Restart the countdown upon any “user input”.
For the first part we need a countdown that triggers an action at the end of it. If you’re familiar with JavaScript, the first thing that may come to mind is a clear and simple setTimeOut
. However, since our application is built in Angular we need to make sure that we build things the Angular way.
You can follow along with the implementation described in this post on this demo app
Using timer and interval
We can’t talk about working with Angular without talking about RxJS — so that’s our first stop when building this feature. Checking the options, we see two promising candidates: timer
and interval
.
Both emit a value after a given amount of time, but for interval
this emission is incremental, whereas timer
only emits once after the count is over. Similar to these two, we also have the option of using delay
, but while delay
shifts the emit of an existing observable, timer
is its own observable, making it easier to track down when and if it needs to be cleared.
Knowing already how we are going to handle this logout countdown, and given the fact that we want to use this in multiple parts of our application, we opted to start our logout countdown from a shared service. In this service we implement the following function:
startLogoutCountdown(timeToLogout: number) {
// timeToLogout needs to be a value in milliseconds
timer(timeToLogout).subscribe(()=> ourLogoutFunction());
}
This raises an issue: we can’t easily clear this observable in case we want to cancel or restart our countdown. Luckily, since logout.service
is a shared service, we can declare a variable in the service and assign the subscription
we are creating. We can then call unsubscribe()
to clear timer
from that variable. Our code now looks like this:
logoutCountdown!: Subscription;
storedTimeToLogout!: number;
startLogoutCountDown(timeToLogout: number) {
this.storedTimeToLogout = timeToLogout;
this.logoutCountdown = this.createCountdown(timeToLogout).subscribe(()=> this.ourLogoutFunction());
}
createCountdown(timeToLogout: number): Observable{
return timer(timeToLogout);
}
cancelLogout() {
this.logoutCountdown.unsubscribe();
}
ourLogoutFunction() {
// call our specific log out logic
alert('USER LOGGED OUT!');
}
And there we have it! A logout countdown that can be used from any part of the application and that will log out the user after it’s over. However, it will be a bit harsh to log out someone without prior knowledge — how will a user know that they are being logged out because their station was inactive for too long and not because of some error?
To solve this, we first show a dialog to the user once the application countdown is over. They have a last chance to stay logged in if the station is still in use. If we don’t receive any user interaction from the dialog, our logout function will be called.
startLogoutCountDown(timeToLogout: number) {
this.storedTimeToLogout = timeToLogout;
this.logoutCountdown = this.createCountdown(timeToLogout).subscribe(() =>
this.matDialog.open(LogoutConfirmComponent)
);
}
The dialog shows the user how much time is left before the logout, starting at 30 seconds.
We once again have to implement a countdown, only this time we need something that allows us to incrementally modify the count that we show to the user. Incrementally, you said? If last time timer
was the right solution for our needs, it looks like interval
is the way to go for this new functionality. In our dialog component ngOnInit
we want to start that interval as such:
count = 30;
private destroyed$ = new Subject();
constructor(
private changeDetection: ChangeDetectorRef,
private logoutService: LogoutService,
private matDialogRef: MatDialogRef
) {}
ngOnInit(): void {
interval(1000)
.pipe(takeUntil(this.destroyed$))
.subscribe(() => {
if (this.count === 1) {
this.logoutService.ourLogoutFunction();
this.destroyed$.next();
}
this.count--;
this.changeDetection.detectChanges();
});
}
Cool! We are now letting the user know that they are about to be logged out if they don’t react to it — but what if they do?
For this we can use the close
event from our dialog. We add a click
event to our “Stay Logged In” button that will:
- call the
close
event of our dialog - call the restart method for our countdown
restartCountdown() {
this.matDialogRef.close();
this.logoutService.restartCountdown();
}
Let’s implement this new function in our logout.service
:
restartCountdown() {
// we want to be sure that there is an active subscription that we can clear out
if (this.logoutCountdown) {
this.cancelLogout();
this.startLogoutCountDown(this.storedTimeToLogout);
}
}
We will also use this function for restarting the countdown anytime we receive user input, our second requirement. That will be the last bit we need to implement. As we already have the necessary functions and variables in our shared service, the only thing missing is making sure that on any part of the application where the logout countdown is started, we are listening to click
events and calling our restart method:
@HostListener('document:click')
onClick() {
this.logoutService.restartCountdown();
}
And that’s it! Like this one, there are a lot of interesting features and initiatives that we are constantly developing and improving. We are always trying to live up to the challenge and provide our shoppers with the right tools, so Picnic’s customers can have the best experience possible. We hope you enjoyed this post and we have inspired you on how to build the next feature in your project — and, who knows, maybe help us build our next one?
Recent blog posts
We're strong believers in learning from each other, so
our employees write about what interests them.