"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.UserManagerInstance = exports.ANONYMOUS_USERNAME = void 0; function _camelCase() { const data = _interopRequireDefault(require("lodash/camelCase")); _camelCase = function () { return data; }; return data; } function _isEmpty() { const data = _interopRequireDefault(require("lodash/isEmpty")); _isEmpty = function () { return data; }; return data; } function _snakeCase() { const data = _interopRequireDefault(require("lodash/snakeCase")); _snakeCase = function () { return data; }; return data; } function _internal() { const data = require("./internal"); _internal = function () { return data; }; return data; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } const ANONYMOUS_USERNAME = 'anonymous'; exports.ANONYMOUS_USERNAME = ANONYMOUS_USERNAME; class UserManagerInstance { constructor() { _defineProperty(this, "_currentUser", null); _defineProperty(this, "_getSessionLock", new (_internal().Semaphore)()); _defineProperty(this, "_interactiveAuthenticationCallbackAsync", void 0); _defineProperty(this, "getCachedUserDataAsync", async () => { await this._getSessionLock.acquire(); try { const currentUser = this._currentUser; // If user is cached and there is an accessToken or sessionSecret, return the user if (currentUser && (currentUser.accessToken || currentUser.sessionSecret)) { return currentUser; } const userData = await this._readUserData(); // // No token, no session, no current user. Need to login if (!(userData !== null && userData !== void 0 && userData.sessionSecret)) { return null; } return userData; } catch (e) { _internal().Logger.global.warn(e); return null; } finally { this._getSessionLock.release(); } }); } static getGlobalInstance() { if (!__globalInstance) { __globalInstance = new UserManagerInstance(); } return __globalInstance; } initialize() { this._currentUser = null; this._getSessionLock = new (_internal().Semaphore)(); } /** * Get the account and project name using a user and Expo config. * This will validate if the owner field is set when using a robot account. */ getProjectOwner(user, exp) { if (user.kind === 'robot' && !exp.owner) { throw new (_internal().XDLError)('ROBOT_OWNER_ERROR', 'The "owner" manifest property is required when using robot users. See: https://docs.expo.dev/versions/latest/config/app/#owner'); } return exp.owner || user.username; } /** * Logs in a user for a given login type. * * Valid login types are: * - "user-pass": Username and password authentication * * If the login type is "user-pass", we directly make the request to www * to login a user. */ async loginAsync(loginType, loginArgs) { if (loginType === 'user-pass') { if (!loginArgs) { throw new Error(`The 'user-pass' login type requires a username and password.`); } const apiAnonymous = _internal().ApiV2.clientForUser(); const loginResp = await apiAnonymous.postAsync('auth/loginAsync', { username: loginArgs.username, password: loginArgs.password, otp: loginArgs.otp }); if (loginResp.error) { throw new (_internal().XDLError)('INVALID_USERNAME_PASSWORD', loginResp['error_description']); } const user = await this._getProfileAsync({ currentConnection: 'Username-Password-Authentication', sessionSecret: loginResp.sessionSecret }); return user; } else { throw new Error(`Invalid login type provided. Must be 'user-pass'.`); } } async registerAsync(userData, user = null) { let actor = user; if (!actor) { actor = await this.getCurrentUserAsync(); } if (actor) { await this.logoutAsync(); actor = null; } try { // Create or update the profile let registeredUser = await this.createOrUpdateUserAsync({ connection: 'Username-Password-Authentication', // Always create/update username password email: userData.email, givenName: userData.givenName, familyName: userData.familyName, username: userData.username, password: userData.password }); registeredUser = await this.loginAsync('user-pass', { username: userData.username, password: userData.password }); return registeredUser; } catch (e) { console.error(e); throw new (_internal().XDLError)('REGISTRATION_ERROR', 'Error registering user: ' + e.message); } } /** * Ensure user is logged in and has a valid token. * * If there are any issues with the login, this method throws. */ async ensureLoggedInAsync() { if (_internal().ConnectionStatus.isOffline()) { throw new (_internal().XDLError)('NETWORK_REQUIRED', "Can't verify user without network access"); } let user = await this.getCurrentUserAsync({ silent: true }); if (!user && this._interactiveAuthenticationCallbackAsync) { user = await this._interactiveAuthenticationCallbackAsync(); } if (!user) { throw new (_internal().XDLError)('NOT_LOGGED_IN', 'Not logged in'); } return user; } setInteractiveAuthenticationCallback(callback) { this._interactiveAuthenticationCallbackAsync = callback; } async _readUserData() { let auth = await _internal().UserSettings.getAsync('auth', null); if ((0, _isEmpty().default)(auth)) { // XXX(ville): // We sometimes read an empty string from ~/.expo/state.json, // even though it has valid credentials in it. // We don't know why. // An empty string can't be parsed as JSON, so an empty default object is returned. // In this case, retrying usually helps. auth = await _internal().UserSettings.getAsync('auth', null); } if (typeof auth === 'undefined') { return null; } return auth; } /** * Returns cached user data without hitting our backend. Only works for 'Username-Password-Authentication' flow. Does not work with 'Access-Token-Authentication' flow. */ /** * Get the current user based on the available token. * If there is no current token, returns null. */ async getCurrentUserAsync(options) { await this._getSessionLock.acquire(); try { const currentUser = this._currentUser; // If user is cached and there is an accessToken or sessionSecret, return the user if (currentUser && (currentUser.accessToken || currentUser.sessionSecret)) { return currentUser; } if (_internal().ConnectionStatus.isOffline()) { return null; } const data = await this._readUserData(); const accessToken = _internal().UserSettings.accessToken(); // No token, no session, no current user. Need to login if (!accessToken && !(data !== null && data !== void 0 && data.sessionSecret)) { return null; } try { if (accessToken) { return await this._getProfileAsync({ accessToken, currentConnection: 'Access-Token-Authentication' }); } return await this._getProfileAsync({ currentConnection: data === null || data === void 0 ? void 0 : data.currentConnection, sessionSecret: data === null || data === void 0 ? void 0 : data.sessionSecret }); } catch (e) { if (!(options && options.silent)) { _internal().Logger.global.warn('Fetching the user profile failed'); _internal().Logger.global.warn(e); } if (e.code === 'UNAUTHORIZED_ERROR') { return null; } throw e; } } finally { this._getSessionLock.release(); } } /** * Get the current user and check if it's a robot. * If the user is not a robot, it will throw an error. */ async getCurrentUserOnlyAsync() { const user = await this.getCurrentUserAsync(); if (user && user.kind !== 'user') { throw new (_internal().XDLError)('ROBOT_ACCOUNT_ERROR', 'This action is not supported for robot users.'); } return user; } /** * Get the current user and check if it's a robot. * If the user is not a robot, it will throw an error. */ async getCurrentRobotUserOnlyAsync() { const user = await this.getCurrentUserAsync(); if (user && user.kind !== 'robot') { throw new (_internal().XDLError)('USER_ACCOUNT_ERROR', 'This action is not supported for normal users.'); } return user; } async getCurrentUsernameAsync() { const token = _internal().UserSettings.accessToken(); if (token) { const user = await this.getCurrentUserAsync(); if (user !== null && user !== void 0 && user.username) { return user.username; } } const data = await this._readUserData(); if (data !== null && data !== void 0 && data.username) { return data.username; } return null; } async getSessionAsync() { const token = _internal().UserSettings.accessToken(); if (token) { return { accessToken: token }; } const data = await this._readUserData(); if (data !== null && data !== void 0 && data.sessionSecret) { return { sessionSecret: data.sessionSecret }; } return null; } /** * Create or update a user. */ async createOrUpdateUserAsync(userData) { var _currentUser; let currentUser = this._currentUser; if (!currentUser) { // attempt to get the current user currentUser = await this.getCurrentUserAsync(); } if (((_currentUser = currentUser) === null || _currentUser === void 0 ? void 0 : _currentUser.kind) === 'robot') { throw new (_internal().XDLError)('ROBOT_ACCOUNT_ERROR', 'This action is not available for robot users'); } const api = _internal().ApiV2.clientForUser(currentUser); const { user: updatedUser } = await api.postAsync('auth/createOrUpdateUser', { userData: _prepareAuth0Profile(userData) }); this._currentUser = { ...this._currentUser, ..._parseAuth0Profile(updatedUser), kind: 'user' }; return this._currentUser; } /** * Logout */ async logoutAsync() { var _this$_currentUser, _this$_currentUser2; if (((_this$_currentUser = this._currentUser) === null || _this$_currentUser === void 0 ? void 0 : _this$_currentUser.kind) === 'robot') { throw new (_internal().XDLError)('ROBOT_ACCOUNT_ERROR', 'This action is not available for robot users'); } // Only send logout events events for users without access tokens if (this._currentUser && !((_this$_currentUser2 = this._currentUser) !== null && _this$_currentUser2 !== void 0 && _this$_currentUser2.accessToken)) { _internal().Analytics.logEvent('Logout', { userId: this._currentUser.userId, currentConnection: this._currentUser.currentConnection }); } this._currentUser = null; // Delete saved auth info await _internal().UserSettings.deleteKeyAsync('auth'); } async getFeatureGatingAsync() { const user = await this.ensureLoggedInAsync(); const api = _internal().ApiV2.clientForUser(user); const { featureGates } = await api.getAsync('auth/user-feature-gates'); return new (_internal().FeatureGating)(featureGates, new (_internal().FeatureGateEnvOverrides)()); } /** * Forgot Password */ async forgotPasswordAsync(usernameOrEmail) { const apiAnonymous = _internal().ApiV2.clientForUser(); return apiAnonymous.postAsync('auth/forgotPasswordAsync', { usernameOrEmail }); } /** * Get profile given token data. Errors if token is not valid or if no * user profile is returned. * * This method is called by all public authentication methods of `UserManager` * except `logoutAsync`. Therefore, we use this method as a way to: * - update the UserSettings store with the current token and user id * - update UserManager._currentUser * - Fire login analytics events * * Also updates UserManager._currentUser. * * @private */ async _getProfileAsync({ currentConnection, sessionSecret, accessToken }) { let user; const api = _internal().ApiV2.clientForUser({ sessionSecret, accessToken }); user = await api.getAsync('auth/userInfo'); if (!user) { throw new Error('Unable to fetch user.'); } user = { ..._parseAuth0Profile(user), // We need to inherit the "robot" type only, the rest is considered "user" but returned as "person". kind: user.user_type === 'robot' ? 'robot' : 'user', currentConnection, sessionSecret, accessToken }; // Create a "username" to use in current terminal UI (e.g. expo whoami) if (user.kind === 'robot') { user.username = user.givenName ? `${user.givenName} (robot)` : 'robot'; } // note: do not persist the authorization token, must be env-var only if (!accessToken) { await _internal().UserSettings.setAsync('auth', { userId: user.userId, username: user.username, currentConnection, sessionSecret }); } // If no currentUser, or currentUser.id differs from profiles // user id, that means we have a new login if ((!this._currentUser || this._currentUser.userId !== user.userId) && user.username && user.username !== '') { if (!accessToken) { // Only send login events for users without access tokens _internal().Analytics.logEvent('Login', { userId: user.userId, currentConnection: user.currentConnection }); } _internal().UnifiedAnalytics.identifyUser(user.userId, { userId: user.userId, currentConnection: user.currentConnection, username: user.username, userType: user.kind, primaryAccountId: user.primaryAccountId }); _internal().Analytics.identifyUser(user.userId, { userId: user.userId, currentConnection: user.currentConnection, username: user.username, userType: user.kind, primaryAccountId: user.primaryAccountId }); } this._currentUser = user; return user; } } exports.UserManagerInstance = UserManagerInstance; let __globalInstance; var _default = UserManagerInstance.getGlobalInstance(); /** Private Methods **/ exports.default = _default; function _parseAuth0Profile(rawProfile) { if (!rawProfile || typeof rawProfile !== 'object') { return rawProfile; } return Object.keys(rawProfile).reduce((p, key) => { p[(0, _camelCase().default)(key)] = _parseAuth0Profile(rawProfile[key]); return p; }, {}); } function _prepareAuth0Profile(niceProfile) { if (typeof niceProfile !== 'object') { return niceProfile; } return Object.keys(niceProfile).reduce((p, key) => { p[(0, _snakeCase().default)(key)] = _prepareAuth0Profile(niceProfile[key]); return p; }, {}); } //# sourceMappingURL=User.js.map