From 6587e0d4be80f2d84f0c544f5524d25133a59de0 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Mon, 22 Jan 2024 16:31:25 -0300 Subject: [PATCH] chore: split UserProvider into two: User and Authentication (#31513) --- .../AuthenticationProvider.tsx | 118 ++++++++++++++++++ .../hooks/useLDAPAndCrowdCollisionWarning.tsx | 2 +- .../client/providers/MeteorProvider.tsx | 45 +++---- .../providers/UserProvider/UserProvider.tsx | 100 +-------------- .../meteor/client/sidebar/Sidebar.stories.tsx | 6 +- .../src/MockedAppRootBuilder.tsx | 4 - .../mock-providers/src/MockedUserContext.tsx | 5 - .../ui-contexts/src/AuthenticationContext.tsx | 26 ++++ packages/ui-contexts/src/UserContext.ts | 21 ---- .../ui-contexts/src/hooks/useLoginServices.ts | 6 +- .../src/hooks/useLoginWithPassword.ts | 4 +- .../src/hooks/useLoginWithService.ts | 6 +- .../src/hooks/useLoginWithToken.ts | 4 +- packages/ui-contexts/src/index.ts | 3 +- 14 files changed, 185 insertions(+), 165 deletions(-) create mode 100644 apps/meteor/client/providers/AuthenticationProvider/AuthenticationProvider.tsx rename apps/meteor/client/providers/{UserProvider => AuthenticationProvider}/hooks/useLDAPAndCrowdCollisionWarning.tsx (93%) create mode 100644 packages/ui-contexts/src/AuthenticationContext.tsx diff --git a/apps/meteor/client/providers/AuthenticationProvider/AuthenticationProvider.tsx b/apps/meteor/client/providers/AuthenticationProvider/AuthenticationProvider.tsx new file mode 100644 index 000000000000..10fe5c9d0747 --- /dev/null +++ b/apps/meteor/client/providers/AuthenticationProvider/AuthenticationProvider.tsx @@ -0,0 +1,118 @@ +import type { LoginService } from '@rocket.chat/ui-contexts'; +import { AuthenticationContext, useSetting } from '@rocket.chat/ui-contexts'; +import { Meteor } from 'meteor/meteor'; +import type { ContextType, ReactElement, ReactNode } from 'react'; +import React, { useMemo } from 'react'; + +import { createReactiveSubscriptionFactory } from '../../lib/createReactiveSubscriptionFactory'; +import { useLDAPAndCrowdCollisionWarning } from './hooks/useLDAPAndCrowdCollisionWarning'; + +const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1); + +const config: Record> = { + 'apple': { title: 'Apple', icon: 'apple' }, + 'facebook': { title: 'Facebook', icon: 'facebook' }, + 'twitter': { title: 'Twitter', icon: 'twitter' }, + 'google': { title: 'Google', icon: 'google' }, + 'github': { title: 'Github', icon: 'github' }, + 'github_enterprise': { title: 'Github Enterprise', icon: 'github' }, + 'gitlab': { title: 'Gitlab', icon: 'gitlab' }, + 'dolphin': { title: 'Dolphin', icon: 'dophin' }, + 'drupal': { title: 'Drupal', icon: 'drupal' }, + 'nextcloud': { title: 'Nextcloud', icon: 'nextcloud' }, + 'tokenpass': { title: 'Tokenpass', icon: 'tokenpass' }, + 'meteor-developer': { title: 'Meteor', icon: 'meteor' }, + 'wordpress': { title: 'WordPress', icon: 'wordpress' }, + 'linkedin': { title: 'Linkedin', icon: 'linkedin' }, +}; + +export type LoginMethods = keyof typeof Meteor extends infer T ? (T extends `loginWith${string}` ? T : never) : never; + +type AuthenticationProviderProps = { + children: ReactNode; +}; + +const AuthenticationProvider = ({ children }: AuthenticationProviderProps): ReactElement => { + const isLdapEnabled = useSetting('LDAP_Enable'); + const isCrowdEnabled = useSetting('CROWD_Enable'); + + const loginMethod: LoginMethods = (isLdapEnabled && 'loginWithLDAP') || (isCrowdEnabled && 'loginWithCrowd') || 'loginWithPassword'; + + useLDAPAndCrowdCollisionWarning(); + + const contextValue = useMemo( + (): ContextType => ({ + loginWithToken: (token: string): Promise => + new Promise((resolve, reject) => + Meteor.loginWithToken(token, (err) => { + if (err) { + return reject(err); + } + resolve(undefined); + }), + ), + loginWithPassword: (user: string | { username: string } | { email: string } | { id: string }, password: string): Promise => + new Promise((resolve, reject) => { + Meteor[loginMethod](user, password, (error) => { + if (error) { + reject(error); + return; + } + + resolve(); + }); + }), + loginWithService: ({ service, clientConfig = {} }: T): (() => Promise) => { + const loginMethods = { + 'meteor-developer': 'MeteorDeveloperAccount', + } as const; + + const loginWithService = `loginWith${loginMethods[service] || capitalize(String(service || ''))}`; + + const method: (config: unknown, cb: (error: any) => void) => Promise = (Meteor as any)[loginWithService] as any; + + if (!method) { + return () => Promise.reject(new Error('Login method not found')); + } + + return () => + new Promise((resolve, reject) => { + method(clientConfig, (error: any): void => { + if (!error) { + resolve(true); + return; + } + reject(error); + }); + }); + }, + queryAllServices: createReactiveSubscriptionFactory(() => + ServiceConfiguration.configurations + .find( + { + showButton: { $ne: false }, + }, + { + sort: { + service: 1, + }, + }, + ) + .fetch() + .map( + ({ appId: _, ...service }) => + ({ + title: capitalize(String((service as any).service || '')), + ...service, + ...(config[(service as any).service] ?? {}), + } as any), + ), + ), + }), + [loginMethod], + ); + + return ; +}; + +export default AuthenticationProvider; diff --git a/apps/meteor/client/providers/UserProvider/hooks/useLDAPAndCrowdCollisionWarning.tsx b/apps/meteor/client/providers/AuthenticationProvider/hooks/useLDAPAndCrowdCollisionWarning.tsx similarity index 93% rename from apps/meteor/client/providers/UserProvider/hooks/useLDAPAndCrowdCollisionWarning.tsx rename to apps/meteor/client/providers/AuthenticationProvider/hooks/useLDAPAndCrowdCollisionWarning.tsx index fbcabc2825fb..afb0eea54fda 100644 --- a/apps/meteor/client/providers/UserProvider/hooks/useLDAPAndCrowdCollisionWarning.tsx +++ b/apps/meteor/client/providers/AuthenticationProvider/hooks/useLDAPAndCrowdCollisionWarning.tsx @@ -2,7 +2,7 @@ import { useSetting } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { useEffect } from 'react'; -import type { LoginMethods } from '../UserProvider'; +import type { LoginMethods } from '../AuthenticationProvider'; export function useLDAPAndCrowdCollisionWarning() { const isLdapEnabled = useSetting('LDAP_Enable'); diff --git a/apps/meteor/client/providers/MeteorProvider.tsx b/apps/meteor/client/providers/MeteorProvider.tsx index aa12af905521..9ae22651f1f3 100644 --- a/apps/meteor/client/providers/MeteorProvider.tsx +++ b/apps/meteor/client/providers/MeteorProvider.tsx @@ -3,6 +3,7 @@ import React from 'react'; import { OmnichannelRoomIconProvider } from '../components/RoomIcon/OmnichannelRoomIcon/provider/OmnichannelRoomIconProvider'; import ActionManagerProvider from './ActionManagerProvider'; +import AuthenticationProvider from './AuthenticationProvider/AuthenticationProvider'; import AuthorizationProvider from './AuthorizationProvider'; import AvatarUrlProvider from './AvatarUrlProvider'; import { CallProvider } from './CallProvider'; @@ -36,27 +37,29 @@ const MeteorProvider: FC = ({ children }) => ( - - - - - - - - - - - {children} - - - - - - - - - - + + + + + + + + + + + + {children} + + + + + + + + + + + diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx index 8552de8b6130..371a3b2659e3 100644 --- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx +++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx @@ -1,7 +1,7 @@ import type { IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; import { useLocalStorage } from '@rocket.chat/fuselage-hooks'; -import type { LoginService, SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; -import { UserContext, useEndpoint, useSetting } from '@rocket.chat/ui-contexts'; +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import { UserContext, useEndpoint } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import type { ContextType, ReactElement, ReactNode } from 'react'; import React, { useEffect, useMemo } from 'react'; @@ -15,7 +15,6 @@ import { createReactiveSubscriptionFactory } from '../../lib/createReactiveSubsc import { useCreateFontStyleElement } from '../../views/account/accessibility/hooks/useCreateFontStyleElement'; import { useDeleteUser } from './hooks/useDeleteUser'; import { useEmailVerificationWarning } from './hooks/useEmailVerificationWarning'; -import { useLDAPAndCrowdCollisionWarning } from './hooks/useLDAPAndCrowdCollisionWarning'; import { useUpdateAvatar } from './hooks/useUpdateAvatar'; import { useUpdateCustomUserStatus } from './hooks/useUpdateCustomUserStatus'; @@ -23,25 +22,6 @@ const getUserId = (): string | null => Meteor.userId(); const getUser = (): IUser | null => Meteor.user() as IUser | null; -const capitalize = (str: string): string => str.charAt(0).toUpperCase() + str.slice(1); - -const config: Record> = { - 'apple': { title: 'Apple', icon: 'apple' }, - 'facebook': { title: 'Facebook', icon: 'facebook' }, - 'twitter': { title: 'Twitter', icon: 'twitter' }, - 'google': { title: 'Google', icon: 'google' }, - 'github': { title: 'Github', icon: 'github' }, - 'github_enterprise': { title: 'Github Enterprise', icon: 'github' }, - 'gitlab': { title: 'Gitlab', icon: 'gitlab' }, - 'dolphin': { title: 'Dolphin', icon: 'dophin' }, - 'drupal': { title: 'Drupal', icon: 'drupal' }, - 'nextcloud': { title: 'Nextcloud', icon: 'nextcloud' }, - 'tokenpass': { title: 'Tokenpass', icon: 'tokenpass' }, - 'meteor-developer': { title: 'Meteor', icon: 'meteor' }, - 'wordpress': { title: 'WordPress', icon: 'wordpress' }, - 'linkedin': { title: 'Linkedin', icon: 'linkedin' }, -}; - const logout = (): Promise => new Promise((resolve, reject) => { const user = getUser(); @@ -56,16 +36,11 @@ const logout = (): Promise => }); }); -export type LoginMethods = keyof typeof Meteor extends infer T ? (T extends `loginWith${string}` ? T : never) : never; - type UserProviderProps = { children: ReactNode; }; const UserProvider = ({ children }: UserProviderProps): ReactElement => { - const isLdapEnabled = useSetting('LDAP_Enable'); - const isCrowdEnabled = useSetting('CROWD_Enable'); - const userId = useReactiveValue(getUserId); const user = useReactiveValue(getUser); const [userLanguage, setUserLanguage] = useLocalStorage('userLanguage', ''); @@ -76,9 +51,6 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => { const createFontStyleElement = useCreateFontStyleElement(); createFontStyleElement(user?.settings?.preferences?.fontSize); - const loginMethod: LoginMethods = (isLdapEnabled && 'loginWithLDAP') || (isCrowdEnabled && 'loginWithCrowd') || 'loginWithPassword'; - - useLDAPAndCrowdCollisionWarning(); useEmailVerificationWarning(user ?? undefined); useUpdateCustomUserStatus(); @@ -103,75 +75,9 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => { return ChatRoom.find(query, options).fetch(); }), - loginWithToken: (token: string): Promise => - new Promise((resolve, reject) => - Meteor.loginWithToken(token, (err) => { - if (err) { - return reject(err); - } - resolve(undefined); - }), - ), - loginWithPassword: (user: string | { username: string } | { email: string } | { id: string }, password: string): Promise => - new Promise((resolve, reject) => { - Meteor[loginMethod](user, password, (error) => { - if (error) { - reject(error); - return; - } - - resolve(); - }); - }), logout, - loginWithService: ({ service, clientConfig = {} }: T): (() => Promise) => { - const loginMethods = { - 'meteor-developer': 'MeteorDeveloperAccount', - } as const; - - const loginWithService = `loginWith${loginMethods[service] || capitalize(String(service || ''))}`; - - const method: (config: unknown, cb: (error: any) => void) => Promise = (Meteor as any)[loginWithService] as any; - - if (!method) { - return () => Promise.reject(new Error('Login method not found')); - } - - return () => - new Promise((resolve, reject) => { - method(clientConfig, (error: any): void => { - if (!error) { - resolve(true); - return; - } - reject(error); - }); - }); - }, - queryAllServices: createReactiveSubscriptionFactory(() => - ServiceConfiguration.configurations - .find( - { - showButton: { $ne: false }, - }, - { - sort: { - service: 1, - }, - }, - ) - .fetch() - .map( - ({ appId: _, ...service }) => - ({ - title: capitalize(String((service as any).service || '')), - ...service, - ...(config[(service as any).service] ?? {}), - } as any), - ), - ), }), - [userId, user, loginMethod], + [userId, user], ); useEffect(() => { diff --git a/apps/meteor/client/sidebar/Sidebar.stories.tsx b/apps/meteor/client/sidebar/Sidebar.stories.tsx index f147ed86b4e4..d8c5788bae86 100644 --- a/apps/meteor/client/sidebar/Sidebar.stories.tsx +++ b/apps/meteor/client/sidebar/Sidebar.stories.tsx @@ -1,5 +1,5 @@ import type { ISetting } from '@rocket.chat/core-typings'; -import type { LoginService, SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; +import type { SubscriptionWithRoom } from '@rocket.chat/ui-contexts'; import { UserContext, SettingsContext } from '@rocket.chat/ui-contexts'; import type { Meta, Story } from '@storybook/react'; import type { ObjectId } from 'mongodb'; @@ -98,10 +98,6 @@ const userContextValue: ContextType = { querySubscription: () => [() => () => undefined, () => undefined], queryRoom: () => [() => () => undefined, () => undefined], - queryAllServices: () => [() => (): void => undefined, (): LoginService[] => []], - loginWithService: () => () => Promise.reject('loginWithService not implemented'), - loginWithPassword: async () => Promise.reject('loginWithPassword not implemented'), - loginWithToken: async () => Promise.reject('loginWithToken not implemented'), logout: () => Promise.resolve(), }; diff --git a/packages/mock-providers/src/MockedAppRootBuilder.tsx b/packages/mock-providers/src/MockedAppRootBuilder.tsx index 15a4db77eb10..1ec9ff09c283 100644 --- a/packages/mock-providers/src/MockedAppRootBuilder.tsx +++ b/packages/mock-providers/src/MockedAppRootBuilder.tsx @@ -77,11 +77,7 @@ export class MockedAppRootBuilder { }; private user: ContextType = { - loginWithPassword: () => Promise.reject(new Error('not implemented')), logout: () => Promise.reject(new Error('not implemented')), - loginWithService: () => () => Promise.reject(new Error('not implemented')), - loginWithToken: () => Promise.reject(new Error('not implemented')), - queryAllServices: () => [() => () => undefined, () => []], queryPreference: () => [() => () => undefined, () => undefined], queryRoom: () => [() => () => undefined, () => undefined], querySubscription: () => [() => () => undefined, () => undefined], diff --git a/packages/mock-providers/src/MockedUserContext.tsx b/packages/mock-providers/src/MockedUserContext.tsx index 10abe3915b42..973a6768846e 100644 --- a/packages/mock-providers/src/MockedUserContext.tsx +++ b/packages/mock-providers/src/MockedUserContext.tsx @@ -1,4 +1,3 @@ -import type { LoginService } from '@rocket.chat/ui-contexts'; import { UserContext } from '@rocket.chat/ui-contexts'; import React from 'react'; import type { ContextType } from 'react'; @@ -23,10 +22,6 @@ const userContextValue: ContextType = { querySubscription: () => [() => () => undefined, () => undefined], queryRoom: () => [() => () => undefined, () => undefined], - queryAllServices: () => [() => (): void => undefined, (): LoginService[] => []], - loginWithService: () => () => Promise.reject('loginWithService not implemented'), - loginWithPassword: async () => Promise.reject('loginWithPassword not implemented'), - loginWithToken: async () => Promise.reject('loginWithToken not implemented'), logout: () => Promise.resolve(), }; diff --git a/packages/ui-contexts/src/AuthenticationContext.tsx b/packages/ui-contexts/src/AuthenticationContext.tsx new file mode 100644 index 000000000000..6c5db9836e60 --- /dev/null +++ b/packages/ui-contexts/src/AuthenticationContext.tsx @@ -0,0 +1,26 @@ +import { createContext } from 'react'; + +export type LoginService = { + clientConfig: unknown; + + title: string; + service: 'meteor-developer'; + + buttonLabelText?: string; + icon?: string; +}; + +export type AuthenticationContextValue = { + loginWithPassword: (user: string | { username: string } | { email: string } | { id: string }, password: string) => Promise; + loginWithToken: (user: string) => Promise; + + queryAllServices(): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => LoginService[]]; + loginWithService(service: T): () => Promise; +}; + +export const AuthenticationContext = createContext({ + queryAllServices: () => [() => (): void => undefined, (): LoginService[] => []], + loginWithService: () => () => Promise.reject('loginWithService not implemented'), + loginWithPassword: async () => Promise.reject('loginWithPassword not implemented'), + loginWithToken: async () => Promise.reject('loginWithToken not implemented'), +}); diff --git a/packages/ui-contexts/src/UserContext.ts b/packages/ui-contexts/src/UserContext.ts index 14b9644e6a3c..001df9ab8ecd 100644 --- a/packages/ui-contexts/src/UserContext.ts +++ b/packages/ui-contexts/src/UserContext.ts @@ -25,16 +25,6 @@ export type FindOptions = { sort?: Sort; }; -export type LoginService = { - clientConfig: unknown; - - title: string; - service: 'meteor-developer'; - - buttonLabelText?: string; - icon?: string; -}; - export type UserContextValue = { userId: string | null; user: IUser | null; @@ -56,13 +46,7 @@ export type UserContextValue = { query: SubscriptionQuery, options?: FindOptions, ) => [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => SubscriptionWithRoom[]]; - - loginWithPassword: (user: string | { username: string } | { email: string } | { id: string }, password: string) => Promise; - loginWithToken: (user: string) => Promise; logout: () => Promise; - - queryAllServices(): [subscribe: (onStoreChange: () => void) => () => void, getSnapshot: () => LoginService[]]; - loginWithService(service: T): () => Promise; }; export const UserContext = createContext({ @@ -72,10 +56,5 @@ export const UserContext = createContext({ querySubscription: () => [() => (): void => undefined, (): undefined => undefined], queryRoom: () => [() => (): void => undefined, (): undefined => undefined], querySubscriptions: () => [() => (): void => undefined, (): [] => []], - - queryAllServices: () => [() => (): void => undefined, (): LoginService[] => []], - loginWithService: () => () => Promise.reject('loginWithService not implemented'), - loginWithPassword: async () => Promise.reject('loginWithPassword not implemented'), - loginWithToken: async () => Promise.reject('loginWithToken not implemented'), logout: () => Promise.resolve(), }); diff --git a/packages/ui-contexts/src/hooks/useLoginServices.ts b/packages/ui-contexts/src/hooks/useLoginServices.ts index e14812bee04b..6cfcb2cf1747 100644 --- a/packages/ui-contexts/src/hooks/useLoginServices.ts +++ b/packages/ui-contexts/src/hooks/useLoginServices.ts @@ -1,11 +1,11 @@ import { useContext, useMemo } from 'react'; import { useSyncExternalStore } from 'use-sync-external-store/shim'; -import type { LoginService } from '../UserContext'; -import { UserContext } from '../UserContext'; +import type { LoginService } from '../AuthenticationContext'; +import { AuthenticationContext } from '../AuthenticationContext'; export const useLoginServices = (): LoginService[] => { - const { queryAllServices } = useContext(UserContext); + const { queryAllServices } = useContext(AuthenticationContext); const [subscribe, getSnapshot] = useMemo(() => queryAllServices(), [queryAllServices]); return useSyncExternalStore(subscribe, getSnapshot); }; diff --git a/packages/ui-contexts/src/hooks/useLoginWithPassword.ts b/packages/ui-contexts/src/hooks/useLoginWithPassword.ts index 8ca2cc0b07de..fc0e01418ff8 100644 --- a/packages/ui-contexts/src/hooks/useLoginWithPassword.ts +++ b/packages/ui-contexts/src/hooks/useLoginWithPassword.ts @@ -1,8 +1,8 @@ import { useContext } from 'react'; -import { UserContext } from '../UserContext'; +import { AuthenticationContext } from '../AuthenticationContext'; export const useLoginWithPassword = (): (( user: string | { username: string } | { email: string } | { id: string }, password: string, -) => Promise) => useContext(UserContext).loginWithPassword; +) => Promise) => useContext(AuthenticationContext).loginWithPassword; diff --git a/packages/ui-contexts/src/hooks/useLoginWithService.ts b/packages/ui-contexts/src/hooks/useLoginWithService.ts index 58320ca9db44..e2ca7c3f90f9 100644 --- a/packages/ui-contexts/src/hooks/useLoginWithService.ts +++ b/packages/ui-contexts/src/hooks/useLoginWithService.ts @@ -1,10 +1,10 @@ import { useContext, useMemo } from 'react'; -import type { LoginService } from '../UserContext'; -import { UserContext } from '../UserContext'; +import type { LoginService } from '../AuthenticationContext'; +import { AuthenticationContext } from '../AuthenticationContext'; export const useLoginWithService = (service: T): (() => Promise) => { - const { loginWithService } = useContext(UserContext); + const { loginWithService } = useContext(AuthenticationContext); return useMemo(() => { return loginWithService(service); diff --git a/packages/ui-contexts/src/hooks/useLoginWithToken.ts b/packages/ui-contexts/src/hooks/useLoginWithToken.ts index 68efde730ad0..92c3c78a23a6 100644 --- a/packages/ui-contexts/src/hooks/useLoginWithToken.ts +++ b/packages/ui-contexts/src/hooks/useLoginWithToken.ts @@ -1,5 +1,5 @@ import { useContext } from 'react'; -import { UserContext } from '../UserContext'; +import { AuthenticationContext } from '../AuthenticationContext'; -export const useLoginWithToken = (): ((token: string) => Promise) => useContext(UserContext).loginWithToken; +export const useLoginWithToken = (): ((token: string) => Promise) => useContext(AuthenticationContext).loginWithToken; diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index fb2f2b84d377..e34ddd869431 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -1,4 +1,5 @@ export { AttachmentContext, AttachmentContextValue } from './AttachmentContext'; +export { AuthenticationContext, AuthenticationContextValue, LoginService } from './AuthenticationContext'; export { AuthorizationContext, AuthorizationContextValue } from './AuthorizationContext'; export { AvatarUrlContext, AvatarUrlContextValue } from './AvatarUrlContext'; export { ConnectionStatusContext, ConnectionStatusContextValue } from './ConnectionStatusContext'; @@ -12,7 +13,7 @@ export { SettingsContext, SettingsContextValue, SettingsContextQuery } from './S export { ToastMessagesContext, ToastMessagesContextValue } from './ToastMessagesContext'; export { TooltipContext, TooltipContextValue } from './TooltipContext'; export { TranslationContext, TranslationContextValue } from './TranslationContext'; -export { UserContext, UserContextValue, LoginService } from './UserContext'; +export { UserContext, UserContextValue } from './UserContext'; export { DeviceContext, Device, IExperimentalHTMLAudioElement, DeviceContextValue } from './DeviceContext'; export { ActionManagerContext, IActionManager } from './ActionManagerContext';