Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 118 additions & 14 deletions packages/browser/core/src/core/auth/StitchAuthListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,44 +15,148 @@
*/

import StitchAuth from "./StitchAuth";
import StitchUser from "./StitchUser";

/**
* StitchAuthListener is an interface for taking action whenever
* a particular [[StitchAppClient]]'s authentication state changes.
*
* Implement [[onAuthEvent]] to handle these events. You can register
* your listener with [[StitchAuth]] using [[StitchAuth.addAuthListener]].
* Implement the methods in this interface to handle these events. You can
* register your listener with [[StitchAuth]] using
* [[StitchAuth.addAuthListener]].
*
* StitchAuth calls registered listeners when:
* - a user is added to the device for the first time
* - a user logs in
* - a user logs out
* - a user is linked to another identity
* - a listener is registered
* - active user is switched
* - the active user is changed
*
* Some actions may trigger multiple events. For instance. Logging into a
* user for the first time will trigger [[onUserAdded]], [[onUserLoggedIn]],
* and [[onActiveUserChanged]].
*
* @note The callbacks in this interface are called asynchronously. This means
* that if many auth events are happening at the same time, events that
* come in may not necessarily reflect the current state of
* authentication. In other words, although these methods will be
* called after events happen, those events may be stale by the time the
* listener method is called. Always check the state of [[StitchAuth]]
* object for the true authentication state.
*
* @see
* - [[StitchAuth]]
*/
export default interface StitchAuthListener {
/**
* onAuthEvent is called any time a notable event regarding authentication happens. These events are:
* @deprecated Use the other event methods for more detailed information
* about the auth event that has occurred.
*
* onAuthEvent is called any time the following events occur
* * When a user logs in.
* * When a user logs out.
* * When a user is linked to another identity.
* * When a listener is registered. This is to handle the case where during registration an event happens that the registerer would otherwise miss out on.
* * When a listener is registered. This is to handle the case where during
* registration an event happens that the registerer would otherwise miss
* out on.
* * When the active user has been switched.
*
* The [[StitchAuth]] instance itself is passed to this callback. This can be used to read the current state of authentication.
* The [[StitchAuth]] instance itself is passed to this callback. This can be
* used to read the current state of authentication.
*
* ### Note
* Specific event details are deliberately not provided here because the events could be stale by the time they are handled.
* @param auth The instance of StitchAuth where the event happened. It should
* be used to infer the current state of authentication.
*/
onAuthEvent?(auth: StitchAuth);

/**
* Called whenever a user is added to the device for the first time. If this
* is as part of a login, this method will be called before
* [[onUserLoggedIn]], and [[onActiveUserChanged]] are called.
*
* For example, a user could log in then log out before the first login event is handled.
* @param auth The instance of [[StitchAuth]] where the user was added. It
* can be used to infer the current state of authentication.
* @param addedUser The user that was added to the device.
*/
onUserAdded?(auth: StitchAuth, addedUser: StitchUser)

/**
* Called whenever a user is linked to a new identity.
*
* The intention is that you would treat this callback as a trigger to refresh the relevant parts of your app based
* on the new, current auth state.
*
* @param auth The instance of StitchAuth where the event happened. It should be used to infer the current state of authentication.
* @param auth The instance of [[StitchAuth]] where the user was linked. It
* can be used to infer the current state of authentication.
* @param linkedUser The user that was linked to a new identity.
*/
onUserLinked?(auth: StitchAuth, linkedUser: StitchUser)

/**
* Called whenever a user is logged in. This will be called before
* [[onActiveUserChanged]] is called.
*
* @note If an anonymous user was already logged in on the device, and you
* log in with an [[AnonymousCredential]], this method will not be
* called, as the underlying [[StitchAuth]] will reuse the anonymous
* user's existing session, and will thus only trigger
* [[onActiveUserChanged]].
*
* @param auth The instance of [[StitchAuth]] where the user was logged in.
* It can be used to infer the current state of authentication.
* @param loggedInUser The user that was logged in.
*/
onUserLoggedIn?(auth: StitchAuth, loggedInUser: StitchUser)

/**
* Called whenever a user is logged out. The user logged out is not
* necessarily the active user. If the user logged out was the active user,
* then [[onActiveUserChanged]] will be called after this method. If the user
* was an anonymous user, that user will also be removed and
* [[onUserRemoved]] will also be called.
*
* @param auth The instance of [[StitchAuth]] where the user was logged out.
* It can be used to infer the current state of authentication.
* @param loggedOutUser The user that was logged out.
*/
onUserLoggedOut?(auth: StitchAuth, loggedOutUser: StitchUser)

/**
* Called whenever the active user changes. This may be due to a call to
* [[StitchAuth.loginWithCredential]], [[StitchAuth.switchToUserWithId]],
* [[StitchAuth.logout]], [[StitchAuth.logoutUserWithId]],
* [[StitchAuth.removeUser]], or [[StitchAuth.removeUserWithId]].
*
* This may also occur on a normal request if a user's session is invalidated
* and they are forced to log out.
*
* @param auth The instance of [[StitchAuth]] where the active user changed.
* It can be used to infer the current state of authentication.
* @param currentActiveUser The active user after the change.
* @param previousActiveUser The active user before the change.
*/
onActiveUserChanged?(
auth: StitchAuth,
currentActiveUser: StitchUser | undefined,
previousActiveUser: StitchUser | undefined
)

/**
* Called whenever a user is removed from the list of users on the device.
*
* @param auth The instance of [[StitchAuth]] where the user was removed. It
* can be used to infer the current state of authentication.
* @param removedUser The user that was removed.
*/
onUserRemoved?(auth: StitchAuth, removedUser: StitchUser)

/**
* Called whenever this listener is registered for the first time. This can
* be useful to infer the state of authentication, because any events that
* occurred before the listener was registered will not be seen by the
* listener.
*
* @param auth The instance of [[StitchAuth]] where the listener was
* registered. It can be used to infer the current state of
* authentication.
*/
onAuthEvent(auth: StitchAuth);
onListenerRegistered?(auth: StitchAuth)
}
7 changes: 4 additions & 3 deletions packages/browser/core/src/core/auth/StitchUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@ export default interface StitchUser extends CoreStitchUser {

/**
* Whether or not this user is logged in. If the user is logged in, it can be
* switched to without reauthenticating. NOTE: this is not a dynamic
* property, this is the state of whether or not the user was logged in at
* the time this user object was created.
* switched to without reauthenticating.
*
* @note This is not a dynamic property, this is the state of whether or not
* the user was logged in at the time this user object was created.
*/
readonly isLoggedIn: boolean;

Expand Down
134 changes: 132 additions & 2 deletions packages/browser/core/src/core/auth/internal/StitchAuthImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { detect } from "detect-browser";
import "cross-fetch/polyfill"
import {
AuthInfo,
AuthEvent,
AuthEventKind,
CoreStitchAuth,
CoreStitchUser,
DeviceFields,
Expand Down Expand Up @@ -83,6 +85,7 @@ interface PartialWindow {
export default class StitchAuthImpl extends CoreStitchAuth<StitchUser>
implements StitchAuth {
private readonly listeners: Set<StitchAuthListener> = new Set();
private readonly synchronousListeners: Set<StitchAuthListener> = new Set();
public static injectedFetch?: any;

/**
Expand Down Expand Up @@ -271,20 +274,47 @@ export default class StitchAuthImpl extends CoreStitchAuth<StitchUser>
public addAuthListener(listener: StitchAuthListener) {
this.listeners.add(listener);

// Trigger the onUserLoggedIn event in case some event happens and
// Trigger the ListenerRegistered event in case some event happens and
// this caller would miss out on this event other wise.

// dispatch a legacy deprecated auth event
this.onAuthEvent(listener);

// dispatch a new style auth event
this.dispatchAuthEvent({
kind: AuthEventKind.ListenerRegistered,
});
}

public addSynchronousAuthListener(listener: StitchAuthListener) {
this.listeners.add(listener);

// Trigger the ListenerRegistered event in case some event happens and
// this caller would miss out on this event other wise.

// dispatch a legacy deprecated auth event
this.onAuthEvent(listener);

// dispatch a new style auth event
this.dispatchAuthEvent({
kind: AuthEventKind.ListenerRegistered,
});
}

public removeAuthListener(listener: StitchAuthListener) {
this.listeners.delete(listener);
}

/**
* Dispatch method for the deprecated auth listener method onAuthEvent.
*/
public onAuthEvent(listener?: StitchAuthListener) {
if (listener) {
const auth = this;
const _ = new Promise(resolve => {
listener.onAuthEvent(auth);
if (listener.onAuthEvent) {
listener.onAuthEvent(auth);
}
resolve(undefined);
});
} else {
Expand All @@ -294,6 +324,106 @@ export default class StitchAuthImpl extends CoreStitchAuth<StitchUser>
}
}

/**
* Utility function used to force the compiler to enforce an exhaustive
* switch statment in dispatchAuthEvent at compile-time.
* @see https://www.typescriptlang.org/docs/handbook/advanced-types.html
*/
private assertNever(x: never): never {
throw new Error("unexpected object: " + x);
}

/**
* Dispatch method for the new auth listener methods.
* @param event the discriminated union representing the auth event
*/
public dispatchAuthEvent(event: AuthEvent<StitchUser>) {
switch(event.kind) {
case AuthEventKind.ActiveUserChanged:
this.dispatchBlockToListeners((listener: StitchAuthListener) => {
if (listener.onActiveUserChanged) {
listener.onActiveUserChanged(
this,
event.currentActiveUser,
event.previousActiveUser
);
}
});
break;
case AuthEventKind.ListenerRegistered:
this.dispatchBlockToListeners((listener: StitchAuthListener) => {
if (listener.onListenerRegistered) {
listener.onListenerRegistered(this);
}
});
break;
case AuthEventKind.UserAdded:
this.dispatchBlockToListeners((listener: StitchAuthListener) => {
if (listener.onUserAdded) {
listener.onUserAdded(this, event.addedUser);
}
});
break;
case AuthEventKind.UserLinked:
this.dispatchBlockToListeners((listener: StitchAuthListener) => {
if (listener.onUserLinked) {
listener.onUserLinked(this, event.linkedUser);
}
})
break;
case AuthEventKind.UserLoggedIn:
this.dispatchBlockToListeners((listener: StitchAuthListener) => {
if (listener.onUserLoggedIn) {
listener.onUserLoggedIn(
this,
event.loggedInUser
);
}
});
break;
case AuthEventKind.UserLoggedOut:
this.dispatchBlockToListeners((listener: StitchAuthListener) => {
if (listener.onUserLoggedOut) {
listener.onUserLoggedOut(
this,
event.loggedOutUser
);
}
});
break;
case AuthEventKind.UserRemoved:
this.dispatchBlockToListeners((listener: StitchAuthListener) => {
if (listener.onUserRemoved) {
listener.onUserRemoved(this, event.removedUser);
}
});
break;
default:
// compiler trick to force this switch to be exhaustive. if the above
// switch statement doesn't check all AuthEventKinds, event will not
// be of type never
return this.assertNever(event);
}
}

/**
* Dispatches the provided block to all auth listeners, including the
* synchronous and asynchronous ones.
* @param block The block to dispatch to listeners.
*/
private dispatchBlockToListeners(block: (StitchAuthListener) => void) {
// Dispatch to all synchronous listeners
this.synchronousListeners.forEach(block);

// Dispatch to all asynchronous listeners
this.listeners.forEach(listener => {
const _ = new Promise(resolve => {
block(listener);
resolve(undefined);
})
});
}

private cleanupRedirect() {
this.jsdomWindow.history.replaceState(null, "", this.pageRootUrl());

Expand Down
Loading