src/modules/identity.js
/**
* Identity SDK version
*
* Note: This require is replaced by an inline version string by babel-plugin-inline-package-json
*
* @type {String}
*/
const version = require('../../package.json').version; // eslint-disable-line
import {checkIsLocal} from '../helpers/local';
import IdentityAPI from './identity-api';
import IdentityConfig from './identity-config';
import IdentityCookie from './identity-cookie';
import IdentityNative from './identity-native';
import IdentityServer from './identity-server';
import IdentityUser from './identity-user';
import Logger from './logger';
import Observable from './observable';
import IdentityRecord from './identity-record';
import IdentityRedirect from './identity-redirect';
import IdentityCrossApp from './identity-crossapp';
import IdentityWindow from './identity-window';
import IdentityLaunchdarkly from './identity-launchdarkly';
/**
* NBC Identity SDK class
*
* Note: The default export of this SDK is an instance of this class
* which is globally exposed as 'Identity' in the browser.
*/
class Identity extends Observable {
/**
* Identity constructor
*/
constructor() {
super();
/**
* Read-only properties exposed by getters below
* @type {String}
*/
this._state = 'uninitialized';
/**
* @type {String}
*/
this._native = false;
/**
* @type {Object}
*/
this._nativeController = null;
/**
* @type {String}
*/
this._result = null;
/**
* @type {String}
*/
this._message = null;
/**
* @type {String}
*/
this._token = null;
/**
* @type {Boolean}
*/
this._topbar = true;
/**
* @type {Object}
*/
this._user = null;
/**
* @type {Object}
*/
this._redirectController = null;
/**
* @type {Object}
*/
this._popUpController = null;
/**
* @type {Object}
*/
this._iframeController = null;
/**
* @type {Object}
*/
this._cookieHelper = null;
/**
* Private properties
* @type {Logger}
*/
this._logger = new Logger('Identity');
/**
* @type {String}
*/
this._env = null;
/**
* @type {String}
*/
this._key = null;
/**
* @type {IdentityConfig}
*/
this._config = null;
/**
* @type {IdentityServer}
*/
this._server = null;
/**
* @type {IdentityAPI}
*/
this._api = null;
/**
* @type {Boolean}
*/
this._useCookie = /(\.nbc\.com)/.test(location.hostname) || checkIsLocal();
/**
* @type {Boolean}
*/
this._additionalDisabled = false;
/**
* @type {Boolean}
*/
this._isVPPARemoved = false;
/**
* @type {Boolean}
*/
this._enablePopUp = false;
/**
* @type {Boolean}
*/
this._enableIframe = false;
/**
* @type {Boolean}
*/
this._initEnablePopUp = false;
/**
* @type {Array}
*/
this._optIns = [];
// Bind this to be used elsewhere
this._updateSession = this._updateSession.bind(this);
this._onResult = this._onResult.bind(this);
this._onPopUpMessage = this._onPopUpMessage.bind(this);
this._onIframeMessage = this._onIframeMessage.bind(this);
this._onPopUpClose = this._onPopUpClose.bind(this);
this._onIframeClose = this._onIframeClose.bind(this);
this._ldOnChange = this._ldOnChange.bind(this);
}
/**
* Identity SDK version
*
* @type {String}
*/
get version() {
return version;
}
/**
* Identity SDK state
*
* Can be one of:
* - 'uninitialized'
* - 'initializing'
* - 'unauthenticated'
* - 'authenticated'
* - 'unauthenticating'
* - 'unableToFetchUserData'
* - 'cross-app-unavailable'
* - 'pop-up-blocked'
*
* Changes to this value will be published as 'state' events.
*
* @type {String}
*/
get state() {
return this._state;
}
/**
* Identity SDK state message
*
* @type {String}
*/
get message() {
return this._message;
}
/**
* Authenticate result
*
* Can be one of:
* - null - No result
* - 'registered' - Registered as a new user
* - 'logged in' - Logged in as existing user
* - 'completed' - User profile has been completed
* - 'cancelled' - Authentication was cancelled by user
* - 'logged out' - Logged out user
* - 'logout cancelled' - Logout cancelled by user
* - 'cross app login cancelled' - Cross app login cancelled by user
*
* @type {String}
*/
get result() {
return this._result;
}
/**
* Current Identity user session token, or null
*
* @type {String}
*/
get token() {
return this._token;
}
/**
* Current authenticated Identity user, or null (read-only)
*
* @type {IdentityUser}
*/
get user() {
return this._user;
}
/**
* Additional Information bypass state
*
* @type {Boolean}
*/
get additionalInformationDisabled() {
return this._additionalDisabled;
}
/**
* Log arguments with Identity prefix
*/
_log(...args) {
this._logger.log(...args);
}
/**
* Set Identity state and notify state observers of new value
*
* Optionally also set result, token and/or user before notifying observers of state change.
* This ensures a consistent set of Identity property values when observers are called.
*
* @param {String} newState - New Identity state
* @param {Object} [options] - Options
* @param {Boolean} [options.result] - New Identity result
* @param {Boolean} [options.token] - New Identity token
* @param {Boolean} [options.user] - New Identity user
*/
_setState(newState, options) {
this._log(`state: ${newState}`, options);
this._state = newState;
if (options) {
if ('message' in options) {
this._message = options.message;
}
if ('result' in options) {
this._result = options.result;
}
if ('token' in options) {
this._token = options.token;
}
if ('user' in options) {
this._user = options.user;
}
}
this.notify('state', newState);
}
/**
* Set Identity result and notify result observers of new value
*
* @param {String} result - New Identity result
*/
_setResult(result) {
this._log(`result: ${result}`);
this._result = result;
this.notify('result', result);
}
/**
* Set Identity token and notify token observers of new value
*
* @param {String} token - New Identity token
*/
_setToken(token) {
this._log(`token: ${token}`);
this._token = token;
this.notify('token', token);
}
/**
* Set Identity profile and notify profile observers of new value
*
* @param {String} user - New Identity profile
*/
_setProfile(user) {
this._log('profile: ', user);
this._profile = user;
this.notify('profile', user);
}
/**
* Update Identity session token, user and state
*
* @param {String} [newToken=null] - New Identity session token
* @param {IdentityUser} [storedUser=null] - Restored Identity user
* @param {Boolean} [setState=true] - set the Identity state
* @return {Promise<Identity, Error>} - Promise of Identity with updated token, user and state
*/
_updateSession(newToken = null, storedUser = null, setState = true) {
const handleAuth = (token, user) => {
const userData = {...user};
this._optIns.forEach((optIn) => userData[optIn] = true);
const storedUser = new IdentityUser(this._config, userData);
this._setState('authenticated', {
token,
user: storedUser,
});
this._log('Stored', IdentityRecord.store(this._env, this._key, this._token, this._user));
return this;
};
if (!newToken) {
IdentityRecord.clear();
if (setState && this._state !== 'unauthenticated') {
this._setState('unauthenticated', {
token: null,
user: null,
});
}
return Promise.resolve(this);
}
if (storedUser) {
this._setState('authenticated', {
token: newToken,
user: storedUser,
});
return Promise.resolve(this);
}
const unableToFetch = () => {
this._setState('unableToFetchUserData', {
token: null,
user: null,
});
return this._updateSession(null, null, false);
};
switch (this._result) {
case 'registered':
case 'logged in':
case 'completed':
case 'user cancelled':
case 'logout cancelled': {
const storedProfile = this._profile;
this._profile = null;
const profileCookie = this._cookieHelper.getCookie('profile');
if (profileCookie) this._cookieHelper.removeCookie('profile');
const userProfile = storedProfile || profileCookie;
if (!userProfile) break;
const profileData = (typeof userProfile === 'string') ? JSON.parse(userProfile) : userProfile;
if (!profileData._id) break;
return handleAuth(newToken, profileData);
}
}
return this._apiCall('getUserInfo', {
newToken,
userData: null,
}).then((newUser) => {
return handleAuth(newToken, newUser);
}).catch((error) => {
// Fallback to updating session to unauthenticated (e.g: when token is invalid)
this._log(`${error.name} fetching user by token: ${error.message}`, newToken, error);
// Remove the token cookie
this._cookieHelper.removeCookie('token');
// Update state and session
return unableToFetch();
});
}
/**
* Restore the user session if possible
*
* @param {Object} config
* @param {String} env
* @param {String} key
* @param {String} token = null
* @return {Promise}
*/
_restoreRecord(config, env, key, token = null) {
const record = IdentityRecord.restore(config, env, key, token);
if (record) {
this._log('Restored', record);
return this._updateSession(record.token, record.user);
} else {
return this._updateSession(token);
}
}
/**
* Handle receiving messages from pop-up window
*
* @param {String} data - JSON data passed by the pop-up
*/
_onPopUpMessage(data) {
if (!data) return;
const {
token,
result,
user,
} = data;
if (token) this._setToken(token);
if (user) this._setProfile(user);
if (result) this._setResult(result);
}
/**
* Fire callback when pop-up closes
*
* @param {Boolean} fromWindow
*/
_onPopUpClose(fromWindow) {
this._log('Pop-up closed');
if (!fromWindow &&
!(this.result === 'logged in' || this.result === 'registered')) this._setResult('user cancelled');
if (!this._enablePopUp) this._popUpController.remove();
}
/**
* Handle receiving messages from iframe window
*
* @param {String} data - JSON data passed by the iframe
*/
_onIframeMessage(data) {
if (!data) return;
const {
token,
result,
user,
} = data;
if (token) this._setToken(token);
if (user) this._setProfile(user);
if (result) this._setResult(result);
}
/**
* Fire callback when iframe closes
*
* @param {Boolean} fromWindow
*/
_onIframeClose(fromWindow) {
this._log('Iframe closed');
this._iframeController.closeIframe();
if (!fromWindow &&
!(this.result === 'logged in' || this.result === 'registered')) this._setResult('user cancelled');
if (!this._enableIframe) this._iframeController.remove();
}
/**
* Result change handler
*
* @param {Boolean} tokenIsParam - Whether the Identity token is passed as a parameter
* @return {Promise<Identity, Error>} - Promise of Identity with updated token, user and state
*/
_onResult(tokenIsParam = false) {
// Remove user session
if (this._result === 'logged out') {
this._log('Remove user session');
// Remove the token cookie
this._cookieHelper.removeCookie('token');
return this._updateSession();
}
// Update user session
const notCancelled = this._result && !/cancelled/.test(this._result);
if (this._token && (notCancelled || tokenIsParam)) {
this._log('Update user session');
return this._updateSession(this._token);
}
// Restore user session
if (this._token || !this._useCookie) {
this._log('Restore user session');
return this._restoreRecord(this._config, this._env, this._key, this._token);
}
// No user session
this._log('No user session available');
return this._updateSession();
}
/**
* Check Launch Darkly Popup Feature flag and populate local flag
*
* @param {String} name - Launch Darkly flag key
* @return {Boolean}
*/
_ldCheckFlag(name) {
const ldFlag = this._launchdarklyController.getFlag(name);
switch (name) {
case 'popup-feature':
this._enablePopUp = (typeof ldFlag === 'boolean') ? ldFlag : this._initEnablePopUp;
break;
case 'additional-information-screen-show':
this._additionalDisabled = (typeof ldFlag === 'boolean') ? !ldFlag : false;
break;
case 'remove-registration-vppa-checkbox':
this._isVPPARemoved = (typeof ldFlag === 'boolean') ? ldFlag : false;
break;
}
return ldFlag;
}
/**
* React to Launch Darkly changes
*/
_ldOnChange() {
this._shouldEnablePopUp(this._enablePopUp, true);
this._ldCheckFlag('additional-information-screen-show');
this._ldCheckFlag('remove-registration-vppa-checkbox');
}
/**
* Make sure pop-up should be used
*
* @param {Boolean} enablePopUp
* @param {Boolean} checkLD
* @return {Boolean}
*/
_shouldEnablePopUp(enablePopUp, checkLD = false) {
if (this._native) return false;
const initPopUp = () => {
if (this._popUpController && !this._popUpController.isInit) {
this._popUpController.init();
}
};
if (checkLD) {
const ldPopupFeature = this._ldCheckFlag('popup-feature');
if (ldPopupFeature) initPopUp();
if (typeof ldPopupFeature === 'boolean') return ldPopupFeature;
}
if (enablePopUp) initPopUp();
return enablePopUp;
}
/**
* Check user agent to see if iFrame modal is supported
* iOS 12 and lower is not supported
* If version is not found in the userAgent check if its iPhone or iPad and don't support those
* If neither version nor iPhone / iPad are found, set version to 13 to pass
*
* @return {Boolean}
*/
_isIframeSupported() {
const getIOSVersion = () => {
const {userAgent} = window.navigator;
const start = userAgent.indexOf('OS ');
const isIOS = userAgent.indexOf('iPhone') > -1 || userAgent.indexOf('iPad') > -1;
const version = (isIOS && start > -1) ? window.Number(userAgent.substr(start + 3, 3).replace('_', '.')) : null;
if (version) return version;
if (isIOS) return 12;
return 13;
};
const version = getIOSVersion();
return version && version > 12;
}
/**
* Make sure iframe should be used
*
* @param {Boolean} enableIframe
* @param {Boolean} checkLD
* @return {Boolean}
*/
_shouldEnableIframe(enableIframe, checkLD = false) {
const initIframe = () => {
if (this._iframeController && !this._iframeController.isInit) {
this._iframeController.init();
}
};
if (enableIframe) initIframe();
return enableIframe;
}
/**
* Determine the enabled user flow
*
* @param {Object} [properties]
* @param {Boolean} [properties.enableIframe]
* @param {Boolean} [properties.enablePopUp]
* @param {Boolean} isInit
* @return {String} User flow: 'redirect' | 'popup' | 'iframe'
*/
_determineUserFlow(properties, isInit = false) {
const isIframeSupported = this._isIframeSupported();
const shouldEnableIframe = this._shouldEnableIframe(properties.enableIframe);
this._enableIframe = isIframeSupported && shouldEnableIframe;
// Fallback to popup if iFrame is not supported
const enablePopUp = (shouldEnableIframe && !isIframeSupported) ? true : properties.enablePopUp;
const shouldEnablePopUp = this._shouldEnablePopUp(enablePopUp, true);
this._enablePopUp = shouldEnablePopUp;
this._initEnablePopUp = shouldEnablePopUp;
if (shouldEnablePopUp) return 'popup';
if (this._enableIframe) {
if (!isInit) {
const topbarEnabled = this._isTopbarEnabled(true);
this._redirectController.updateGlobalProp('topbar', topbarEnabled);
}
return 'iframe';
}
return 'redirect';
}
/**
* Make sure properties should be requested
*
* @param {Array} properties
* @return {Boolean}
*/
_shouldRequestProperties(properties = []) {
const allowedProperties = [];
properties.forEach((name) => {
const ldPropertyCheck = this._ldCheckFlag(`additional-information-${name}`);
if (ldPropertyCheck === false) return;
allowedProperties.push(name);
});
return allowedProperties;
}
/**
* Check if topbar is enabled based on topbar and enableIframe attributes
*
* @param {Boolean} enableIframe
*
* @return {Boolean}
*/
_isTopbarEnabled(enableIframe = this._enableIframe) {
return this._topbar || (this._topbar === undefined && !enableIframe);
}
/**
* Send API call with specific headers
* @param {String} name
* @param {Object} args
*
* @return {Promise}
*/
_apiCall(name, args) {
if (!this._api || !this._api[name]) return Promise.reject();
if (!args.options) args.options = {};
if (!args.options.headers) args.options.headers = {};
if (this._isVPPARemoved) {
args.options.headers['X-IDM-Bypass-VPPA'] = true;
delete args.options.headers['vppa_re_opt_in'];
}
this._log('API Call', name);
return this._api[name](...Object.values(args));
}
/**
* Initialize SDK for use
*
* This will start by fetching the {@link IdentityConfig} based on the specified env.
*
* If the token option is specified then that value will be used.
*
* Else the URL fragment will be parsed for the token, result and hash values passed to this web app upon returning
* from authentication and the original URL fragment (hash) value will be restored.
*
* If no token is specified then token and user will be restored from local storage, provided the data has not
* timed out according to the {@link IdentityConfig#sessionTimeout} and/or {@link IdentityConfig#userTimeout}.
*
* If a token is available but no user data (e.g: upon authentication or user data cache timeout)
* then the user data will be fetched from the IDM server and persisted before resolving this promise.
*
* After initialization the Identity properties (e.g: {@link Identity#state}, {@link Identity#result},
* {@link Identity#token} and {@link Identity#user}) can be examined.
*
* @param {String} key - NBC App key, e.g: example
* @param {Object} [options={}] - Options
* @param {Boolean} [options.debug=false] - If true then enable logging
* @param {String} [options.env='production'] - Environment to use, e.g: dev/stage/acc/production
* @param {Boolean} [options.useLocalConfig=false] - Flag that loads local config file if true
* @param {Boolean} [options.native=false] - Enable native analytics bridge
* @param {String} [options.token=null] - Identity user session token
* @param {String} [options.topbar=true] - If false then the topbar is not displayed
* @param {Boolean} [options.useLocalConfig=false] - Load config from local directory
* @param {Boolean} [options.enablePopUp=false] - Open authenticate in windowed pop-up instead of redirect
* on all authenticate and unauthenticate calls
* @param {Boolean} [options.disableCookie=false] - Disable the use of the standard cookie logic
* @param {String} [options.configLocationHost] - Call config from custom host location
* @return {Promise<Identity, Error>} - Promise of initialized Identity
*/
initialize(key, {
debug = false,
env = 'production',
native = false,
token = null,
topbar,
useLocalConfig = false,
enablePopUp = false,
enableIframe = false,
disableCookie = false,
configLocationHost = null,
} = {}) {
this._logger.enabled = debug;
this._env = env;
this._key = key;
this._native = native;
if (this._native) this._nativeController = new IdentityNative();
this._server = new IdentityServer({sdkVersion: version, env, useLocalConfig});
this._launchdarklyController = new IdentityLaunchdarkly({env});
this._topbar = topbar;
this._useLocalConfig = useLocalConfig;
if (disableCookie) this._useCookie = !disableCookie;
this._log({version, env, key, debug});
this._setState('initializing');
return IdentityConfig.request({server: this._server, key, configLocationHost}).then((config) => {
this._log('config', config);
this._config = config;
this._server.setConfig(config);
this._api = new IdentityAPI({server: this._server, config, useCookie: this._useCookie});
this._cookieHelper = new IdentityCookie(config);
this._cookieHelper.setEnabled(this._useCookie);
this._popUpController = new IdentityWindow({
debug,
onMessage: this._onPopUpMessage,
onClose: this._onPopUpClose,
});
this._iframeController = new IdentityWindow({
debug,
config,
onMessage: this._onIframeMessage,
onClose: this._onIframeClose,
});
const userFlow = this._determineUserFlow({enableIframe, enablePopUp}, true);
const topbarEnabled = this._isTopbarEnabled(userFlow === 'iframe');
this._redirectController = new IdentityRedirect({
debug,
env,
key,
native,
topbar: topbarEnabled,
useLocalConfig,
version,
configLocationHost,
}, this._server, this._popUpController, this._iframeController);
// Token was passed through the initialize parameter
const tokenIsParam = token !== null;
// Set the cookie to the passed token
if (tokenIsParam) this._cookieHelper.setCookie('token', token);
// Grab token from cookie
// Remove token for testing
// this._cookieHelper.removeCookie('token');
token = this._cookieHelper.getCookie('token');
// Start the cookie observer
this._cookieSub = this._cookieHelper.subscribe('token', (newToken) => {
if (newToken === this.token && this.state === 'authenticated') return;
this._updateSession(newToken);
});
// No token specified, parse parameters from URL fragment
if (!tokenIsParam) {
const params = this._redirectController.parseUrlParameters();
if (params) {
if (params.token) token = params.token;
this._result = params.result;
this._optIns = params.optIns;
}
}
this._token = token;
this.on('result', this._onResult);
// Initialize Launch Darkly
if (config.launchdarkly && config.launchdarkly.enabled) {
this._launchdarklyController.init(key, config.launchdarkly, {}, this._ldOnChange).then(() => {
this._shouldEnablePopUp(this._enablePopUp, true);
this._ldCheckFlag('additional-information-screen-show');
this._ldCheckFlag('remove-registration-vppa-checkbox');
this._onResult(tokenIsParam);
}).catch(() => {
this._onResult(tokenIsParam);
});
}
return this._onResult(tokenIsParam);
}).catch((error) => {
this._log(`${error.name} initializing: ${error.message}`, error);
this._setState('uninitialized');
throw error;
});
}
/**
* Authenticate user
*
* This will redirect the web browser or view to https://id.nbc.com/sdk/authenticate/ where an attempt will be made
* to take all necessary steps to result in a user with the specified properties after being redirected back.
*
* If no properties are specified then all properties that have "request: true" in the config will be requested.
* If the user had already previously submitted valid property values for all of those however,
* the additional properties screen will be skipped, to allow for quick re-authentication.
* If some, but not all of the values were previously submitted then those will shown besides the invalid ones.
*
* If properties are specified in this call then only those specific properties will be requested.
* In that case, even if the user had already previously submitted valid property values they will be re-requested,
* allowing the user to either specify or review and change those values.
* Only properties defined by the App's IdentityConfig can be requested.
*
* If an Identity or Facebook token is specified then the authentication page will attempt to use those.
*
* After the Authenticate page redirects back and this App re-initializes {@link Identity#result} can be used
* to determine the authentication result.
*
* @example
* // Default authentication based on App IdentityConfig:
* Identity.authenticate();
*
* // Or alternatively (e.g: opt-in at a later time) request specific properties:
* Identity.authenticate({
* properties: ['sponsorOptIn']
* });
*
* @param {Object} [options={}] - Options
* @param {String[]} [options.properties] - Keys of specific user properties to request
* @param {String} [options.origin] - Auth origin, e.g: NBC Login, Vote, BAT, ...
* @param {String} [options.redirectUrl=location.href] - URL to redirect back to after authentication
* @param {String} [options.token=this.token] - Identity token to (try to) use
* @param {String} [options.facebookToken] - Facebook access token to (try to) use
* @param {String} [options.marketingReferrer] - Referrer string based on show phase, e.g: S16_The Voice_BAT
* @param {String} [options.propertyEdit] - Request that the passed properties be editable
* @param {String} [options.enablePopUp] - Open authenticate in windowed pop-up instead of redirect.
* Overwrites initialize param
* @param {String} [options.defaultPage] - Open authenticate to a specific page e.g: signUp, signupWithEmail, signIn
*
* @return {Error}
*/
authenticate({
properties = [],
origin = null,
redirectUrl = location.href,
token = this.token,
facebookToken,
marketingReferrer = null,
propertyEdit = false,
enableIframe = this._enableIframe,
enablePopUp = this._enablePopUp,
defaultPage = null,
} = {}) {
// Default page whitelist
const validDefaultPages = [
'signUp',
'signupWithEmail',
'signIn',
];
if (defaultPage && validDefaultPages.indexOf(defaultPage) === -1) {
return window.console.error(
// eslint-disable-next-line max-len
`Identity.authenticate error: make sure the defaultPage property is set to one of the following: ${validDefaultPages.join(', ')}`,
);
}
const userFlow = this._determineUserFlow({enableIframe, enablePopUp});
// Redirect user to authenticate
this._redirectController.trigger({
properties: this._shouldRequestProperties(properties),
origin,
redirectUrl,
token,
facebookToken,
marketingReferrer,
propertyEdit,
enableIframe: userFlow === 'iframe',
enablePopUp: userFlow === 'popup',
requestPrompt: !this._native && !this._useCookie && !this._token,
defaultPage,
});
}
/**
* ManageProfile user
* @param {Object} [options={}] - Options
* @param {String[]} [options.properties] - Keys of specific user properties to request
* @param {String} [options.marketingReferrer] - Referrer string based on show phase, e.g: S16_The Voice_BAT
* @param {String} [options.propertyEdit] - Request that the passed properties be editable
* @param {String} [options.defaultPage] - Open authenticate to a specific page e.g: signUp, signupWithEmail, signIn
*
*/
manageProfile({
marketingReferrer = null,
defaultPage = null,
} = {}) {
this.authenticate({
properties: ['email', 'firstName', 'lastName', 'gender', 'phone', 'zipCode'],
marketingReferrer,
defaultPage,
propertyEdit: true,
});
}
/**
* Unauthenticate user
* @param {Object} [options={}] - Options
* @param {String} [options.origin] - Auth origin, e.g: NBC Login, Vote, BAT, ...
* @param {String} [options.redirectUrl=location.href] - URL to redirect back to after authentication
* @param {String} [options.token=this.token] - Identity token to (try to) use
* @param {Boolean} [options.redirect] - Flag to redirect user to the auth app on unauthenticate
* @param {String} [options.enablePopUp] - Open authenticate in windowed pop-up instead of redirect.
* Overwrites initialize param
* @return {Promise<Identity, Error>} - Promise of unauthenticated Identity
*/
unauthenticate({
origin = null,
redirectUrl = location.href,
token = this.token,
redirect = true,
enablePopUp = this._enablePopUp,
enableIframe = this._enableIframe,
} = {}) {
this._setState('unauthenticating', {result: null});
// Unauthenticate without redirect
if (!redirect) {
return this._apiCall('unauthenticate', {
token: this._token,
native: this._nativeController,
}).catch((error) => {
this._log('Unauthenticate failed', error);
// Continue regardless; this should be a non fatal error
}).then(
() => this._updateSession()
);
}
const userFlow = this._determineUserFlow({enableIframe, enablePopUp});
// Redirect user to logout screen
this._redirectController.trigger({
logout: true,
origin,
redirectUrl,
token,
enableIframe: userFlow === 'iframe',
enablePopUp: userFlow === 'popup',
});
}
/**
* Current status of the IDM system
*
* Example usage:
* Identity.getHealth('dev', 2000).then(status => {
* console.log(status);
* });
*
* @param {String} env - Environment to use, e.g: dev/stage/acc/production
* @param {number} timeout - Timeout limit on call response
*
* @return {Promise<Status, Error>} - Promise of IDM status boolean
*/
getHealth(env = null, timeout = 10000) {
const environment = env||this._env;
let server = this._server;
return new Promise((resolve) => {
if (!server) {
if (!environment) {
return resolve('env parameter missing');
}
server = new IdentityServer({sdkVersion: version, env: environment});
}
const endpoint = `https://${environment !== 'production' ? `${environment}-` : ''}id.nbc.com/status/`;
const limit = setTimeout(() => resolve(false), timeout);
server.call(endpoint, {
method: 'GET',
}).then((response) => {
clearTimeout(limit);
const status = ('status' in response) ? response.status : false;
return resolve(status);
}).catch(() => {
clearTimeout(limit);
return resolve(false);
});
});
}
/**
* Initialize the native bridge to check for cross application credentials
* When credentials exist, redirect to the authenticate app to ask the user to sign in using those saved credentials
* When credentials don't exist, update state and do nothing
*
* @param {Object} [options={}]
* @param {String} [options.origin]
* @param {String} [options.redirectUrl=location.href]
*/
crossAppPrompt({
origin = null,
redirectUrl = location.href,
enablePopUp = this._enablePopUp,
enableIframe = this._enableIframe,
} = {}) {
this._origin = origin;
this._redirectUrl = redirectUrl;
if (this._nativeController) {
// Initialize the cross app bridge
this._crossAppBridge = new IdentityCrossApp(this._nativeController);
if (this._crossAppInitialized) {
// Check the keychain for a user session token
this._crossAppBridge.command('get', {});
} else {
this._crossAppBridge.init().then(() => {
this._crossAppInitialized = true;
const {nativeWrapper} = window;
if (nativeWrapper.os === 'iOS') {
this._crossAppBridge.on('get', (message) => {
const {token, value} = message.payload;
this._crossAppToken = token;
const getPromise = new Promise((resolve, reject) => {
if (token && value) {
// Validate the token by fetching user info
this._apiCall('getUserInfo', {
token: this._crossAppToken,
userData: null,
}).then((userData) => {
// Save userData to a cookie to use in authenticate app
this._cookieHelper.setCookie('userData', JSON.stringify(userData));
return resolve(this._crossAppToken);
}).catch(() => {
// Remove the invalid token and data from the keychain
this._removeKeychainData(this._crossAppToken);
return reject('Invalid token.');
});
} else {
// The keychain does not contain a payload
return reject('The keychain is empty.');
}
});
getPromise.then((token) => {
// Store the crossApp credentials in a cookie to use on authenticate
this._cookieHelper.setCookie('crossAppToken', token);
this._cookieHelper.setCookie('crossApp', JSON.stringify(message.payload));
const userFlow = this._determineUserFlow({enableIframe, enablePopUp});
// Redirect user to authenticate
this._redirectController.trigger({
crossApp: true,
iframeEnabled: userFlow === 'iframe',
enablePopUp: userFlow === 'popup',
origin: this._origin,
redirectUrl: this._redirectUrl,
});
}).catch((message) => this._setState('cross-app-unavailable', {
message,
}));
});
// Check the keychain for a user session token
this._crossAppBridge.command('get', {});
} else {
this._setState('cross-app-unavailable', {
message: 'crossAppPrompt call should only be used on iOS devices.',
});
}
}).catch(() => this._setState('cross-app-unavailable', {
message: 'The native bridge could not be initialized.',
}));
}
}
}
/**
* Clear the user data from localStorage
*/
clearUser() {
IdentityRecord.clear();
}
}
export default Identity;