diff --git a/app/actions/navigation.js b/app/actions/navigation.js index 94ab126c73..d4cd69ad26 100644 --- a/app/actions/navigation.js +++ b/app/actions/navigation.js @@ -360,24 +360,22 @@ export function dismissOverlay(componentId) { }; } -export function applyTheme() { +export function applyTheme(componentId) { return (dispatch, getState) => { const theme = getTheme(getState()); - EphemeralStore.getNavigationComponentIds().forEach((componentId) => { - Navigation.mergeOptions(componentId, { - topBar: { - backButton: { - color: theme.sidebarHeaderTextColor, - }, - background: { - color: theme.sidebarHeaderBg, - }, - title: { - color: theme.sidebarHeaderTextColor, - }, + Navigation.mergeOptions(componentId, { + topBar: { + backButton: { + color: theme.sidebarHeaderTextColor, }, - }); + background: { + color: theme.sidebarHeaderBg, + }, + title: { + color: theme.sidebarHeaderTextColor, + }, + }, }); }; } diff --git a/app/components/file_attachment_list/file_attachment_document/index.js b/app/components/file_attachment_list/file_attachment_document/index.js index 753d2d8ff1..31fe7d916f 100644 --- a/app/components/file_attachment_list/file_attachment_document/index.js +++ b/app/components/file_attachment_list/file_attachment_document/index.js @@ -16,4 +16,4 @@ function mapDispatchToProps(dispatch) { }; } -export default connect(null, mapDispatchToProps)(FileAttachmentDocument); +export default connect(null, mapDispatchToProps, null, {forwardRef: true})(FileAttachmentDocument); diff --git a/app/components/markdown/markdown_image/markdown_image.js b/app/components/markdown/markdown_image/markdown_image.js index 526bb4a824..b8a7bac8e5 100644 --- a/app/components/markdown/markdown_image/markdown_image.js +++ b/app/components/markdown/markdown_image/markdown_image.js @@ -18,6 +18,7 @@ import { import FormattedText from 'app/components/formatted_text'; import ProgressiveImage from 'app/components/progressive_image'; import CustomPropTypes from 'app/constants/custom_prop_types'; +import {getCurrentServerUrl} from 'app/init/credentials'; import mattermostManaged from 'app/mattermost_managed'; import BottomSheet from 'app/utils/bottom_sheet'; import ImageCacheManager from 'app/utils/image_cache_manager'; @@ -42,7 +43,7 @@ export default class MarkdownImage extends React.Component { imagesMetadata: PropTypes.object, linkDestination: PropTypes.string, isReplyPost: PropTypes.bool, - serverURL: PropTypes.string.isRequired, + serverURL: PropTypes.string, source: PropTypes.string.isRequired, errorTextStyle: CustomPropTypes.Style, }; @@ -95,11 +96,15 @@ export default class MarkdownImage extends React.Component { this.mounted = false; } - getSource = () => { + getSource = async () => { let source = this.props.source; + let serverUrl = this.props.serverURL; + if (!serverUrl) { + serverUrl = await getCurrentServerUrl(); + } if (source.startsWith('/')) { - source = this.props.serverURL + '/' + source; + source = serverUrl + source; } return source; diff --git a/app/components/markdown/markdown_link/markdown_link.js b/app/components/markdown/markdown_link/markdown_link.js index abcdc7b760..3aa9ff3100 100644 --- a/app/components/markdown/markdown_link/markdown_link.js +++ b/app/components/markdown/markdown_link/markdown_link.js @@ -9,6 +9,7 @@ import {intlShape} from 'react-intl'; import CustomPropTypes from 'app/constants/custom_prop_types'; import {DeepLinkTypes} from 'app/constants'; +import {getCurrentServerUrl} from 'app/init/credentials'; import mattermostManaged from 'app/mattermost_managed'; import BottomSheet from 'app/utils/bottom_sheet'; import {preventDoubleTap} from 'app/utils/tap'; @@ -24,7 +25,7 @@ export default class MarkdownLink extends PureComponent { children: CustomPropTypes.Children.isRequired, href: PropTypes.string.isRequired, onPermalinkPress: PropTypes.func, - serverURL: PropTypes.string.isRequired, + serverURL: PropTypes.string, siteURL: PropTypes.string.isRequired, }; @@ -38,7 +39,7 @@ export default class MarkdownLink extends PureComponent { intl: intlShape.isRequired, }; - handlePress = preventDoubleTap(() => { + handlePress = preventDoubleTap(async () => { const {href, onPermalinkPress, serverURL, siteURL} = this.props; const url = normalizeProtocol(href); @@ -46,7 +47,12 @@ export default class MarkdownLink extends PureComponent { return; } - const match = matchDeepLink(url, serverURL, siteURL); + let serverUrl = serverURL; + if (!serverUrl) { + serverUrl = await getCurrentServerUrl(); + } + + const match = matchDeepLink(url, serverUrl, siteURL); if (match) { if (match.type === DeepLinkTypes.CHANNEL) { this.props.actions.handleSelectChannelByName(match.channelName, match.teamName); diff --git a/app/components/markdown/markdown_table_image/markdown_table_image.js b/app/components/markdown/markdown_table_image/markdown_table_image.js index b2fdfd5f3e..1a21d0e52a 100644 --- a/app/components/markdown/markdown_table_image/markdown_table_image.js +++ b/app/components/markdown/markdown_table_image/markdown_table_image.js @@ -7,6 +7,7 @@ import {intlShape} from 'react-intl'; import {Text} from 'react-native'; import CustomPropTypes from 'app/constants/custom_prop_types'; +import {getCurrentServerUrl} from 'app/init/credentials'; import {preventDoubleTap} from 'app/utils/tap'; export default class MarkdownTableImage extends React.PureComponent { @@ -17,7 +18,7 @@ export default class MarkdownTableImage extends React.PureComponent { children: PropTypes.node.isRequired, source: PropTypes.string.isRequired, textStyle: CustomPropTypes.Style.isRequired, - serverURL: PropTypes.string.isRequired, + serverURL: PropTypes.string, theme: PropTypes.object.isRequired, }; @@ -40,11 +41,16 @@ export default class MarkdownTableImage extends React.PureComponent { actions.goToScreen(screen, title, passProps); }); - getImageSource = () => { + getImageSource = async () => { let source = this.props.source; + let serverUrl = this.props.serverURL; + + if (!serverUrl) { + serverUrl = await getCurrentServerUrl(); + } if (source.startsWith('/')) { - source = `${this.props.serverURL}/${source}`; + source = serverUrl + source; } return source; diff --git a/app/components/sidebars/main/teams_list/index.js b/app/components/sidebars/main/teams_list/index.js index b6c187e213..f72990601e 100644 --- a/app/components/sidebars/main/teams_list/index.js +++ b/app/components/sidebars/main/teams_list/index.js @@ -4,14 +4,12 @@ import {bindActionCreators} from 'redux'; import {connect} from 'react-redux'; -import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general'; import {getCurrentTeamId, getMySortedTeamIds, getJoinableTeamIds} from 'mattermost-redux/selectors/entities/teams'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {showModal} from 'app/actions/navigation'; import {handleTeamChange} from 'app/actions/views/select_team'; import {getCurrentLocale} from 'app/selectors/i18n'; -import {removeProtocol} from 'app/utils/url'; import TeamsList from './teams_list'; @@ -20,7 +18,6 @@ function mapStateToProps(state) { return { currentTeamId: getCurrentTeamId(state), - currentUrl: removeProtocol(getCurrentUrl(state)), hasOtherJoinableTeams: getJoinableTeamIds(state).length > 0, teamIds: getMySortedTeamIds(state, locale), theme: getTheme(state), diff --git a/app/components/sidebars/main/teams_list/teams_list.js b/app/components/sidebars/main/teams_list/teams_list.js index d8f7b28578..afc9272271 100644 --- a/app/components/sidebars/main/teams_list/teams_list.js +++ b/app/components/sidebars/main/teams_list/teams_list.js @@ -17,8 +17,10 @@ import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import FormattedText from 'app/components/formatted_text'; import {DeviceTypes, ListTypes, ViewTypes} from 'app/constants'; +import {getCurrentServerUrl} from 'app/init/credentials'; import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; +import {removeProtocol} from 'app/utils/url'; import tracker from 'app/utils/time_tracker'; import telemetry from 'app/telemetry'; @@ -38,7 +40,6 @@ export default class TeamsList extends PureComponent { }).isRequired, closeChannelDrawer: PropTypes.func.isRequired, currentTeamId: PropTypes.string.isRequired, - currentUrl: PropTypes.string.isRequired, hasOtherJoinableTeams: PropTypes.bool, teamIds: PropTypes.array.isRequired, theme: PropTypes.object.isRequired, @@ -51,9 +52,17 @@ export default class TeamsList extends PureComponent { constructor(props) { super(props); + this.state = { + serverUrl: '', + }; + MaterialIcon.getImageSource('close', 20, props.theme.sidebarHeaderTextColor).then((source) => { this.closeButton = source; }); + + getCurrentServerUrl().then((url) => { + this.setState({serverUrl: removeProtocol(url)}); + }); } selectTeam = (teamId) => { @@ -75,13 +84,14 @@ export default class TeamsList extends PureComponent { }); }; - goToSelectTeam = preventDoubleTap(() => { + goToSelectTeam = preventDoubleTap(async () => { const {intl} = this.context; - const {currentUrl, theme, actions} = this.props; + const {theme, actions} = this.props; + const {serverUrl} = this.state; const screen = 'SelectTeam'; const title = intl.formatMessage({id: 'mobile.routes.selectTeam', defaultMessage: 'Select Team'}); const passProps = { - currentUrl, + currentUrl: serverUrl, theme, }; const options = { @@ -117,6 +127,7 @@ export default class TeamsList extends PureComponent { renderItem = ({item}) => { return ( @@ -155,6 +166,7 @@ export default class TeamsList extends PureComponent { {moreAction} { Navigation.events().registerComponentDidAppearListener(({componentId}) => { EphemeralStore.addNavigationComponentId(componentId); }); + + Navigation.events().registerComponentDidDisappearListener(({componentId}) => { + EphemeralStore.removeNavigationComponentId(componentId); + }); }); diff --git a/app/screens/index.js b/app/screens/index.js index 3228745d07..a4f5b62a5f 100644 --- a/app/screens/index.js +++ b/app/screens/index.js @@ -66,7 +66,7 @@ export function registerScreens(store, Provider) { Navigation.registerComponent('SelectTeam', () => wrapper(require('app/screens/select_team').default), () => require('app/screens/select_team').default); Navigation.registerComponent('SelectTimezone', () => wrapper(require('app/screens/settings/timezone/select_timezone').default), () => require('app/screens/settings/timezone/select_timezone').default); Navigation.registerComponent('Settings', () => wrapper(require('app/screens/settings/general').default), () => require('app/screens/settings/general').default); - Navigation.registerComponent('SidebarSettings', () => wrapper(require('app/screens//settings/sidebar').default), () => require('app/screens/settings/sidebar').default); + Navigation.registerComponent('SidebarSettings', () => wrapper(require('app/screens/settings/sidebar').default), () => require('app/screens/settings/sidebar').default); Navigation.registerComponent('SSO', () => wrapper(require('app/screens/sso').default), () => require('app/screens/sso').default); Navigation.registerComponent('Table', () => wrapper(require('app/screens/table').default), () => require('app/screens/table').default); Navigation.registerComponent('TableImage', () => wrapper(require('app/screens/table_image').default), () => require('app/screens/table_image').default); diff --git a/app/screens/permalink/permalink.js b/app/screens/permalink/permalink.js index 3d59012b0b..2f76d25009 100644 --- a/app/screens/permalink/permalink.js +++ b/app/screens/permalink/permalink.js @@ -267,41 +267,43 @@ export default class Permalink extends PureComponent { const {formatMessage} = intl; let focusChannelId = channelId; - const post = await actions.getPostThread(focusedPostId, false); - if (post.error && (!postIds || !postIds.length)) { - if (this.mounted && isPermalink && post.error.message.toLowerCase() !== 'network request failed') { - this.setState({ - error: formatMessage({ - id: 'permalink.error.access', - defaultMessage: 'Permalink belongs to a deleted message or to a channel to which you do not have access.', - }), - title: formatMessage({ - id: 'mobile.search.no_results', - defaultMessage: 'No Results Found', - }), - }); - } else if (this.mounted) { - this.setState({error: post.error.message, retry: true}); + if (focusedPostId) { + const post = await actions.getPostThread(focusedPostId, false); + if (post.error && (!postIds || !postIds.length)) { + if (this.mounted && isPermalink && post.error.message.toLowerCase() !== 'network request failed') { + this.setState({ + error: formatMessage({ + id: 'permalink.error.access', + defaultMessage: 'Permalink belongs to a deleted message or to a channel to which you do not have access.', + }), + title: formatMessage({ + id: 'mobile.search.no_results', + defaultMessage: 'No Results Found', + }), + }); + } else if (this.mounted) { + this.setState({error: post.error.message, retry: true}); + } + + return; } - return; - } - - if (!channelId) { - const focusedPost = post.data && post.data.posts ? post.data.posts[focusedPostId] : null; - focusChannelId = focusedPost ? focusedPost.channel_id : ''; - if (focusChannelId) { - const {data: channel} = await actions.getChannel(focusChannelId); - if (!this.props.myMembers[focusChannelId] && channel && channel.type === General.OPEN_CHANNEL) { - await actions.joinChannel(currentUserId, channel.team_id, channel.id); + if (!channelId) { + const focusedPost = post.data && post.data.posts ? post.data.posts[focusedPostId] : null; + focusChannelId = focusedPost ? focusedPost.channel_id : ''; + if (focusChannelId) { + const {data: channel} = await actions.getChannel(focusChannelId); + if (!this.props.myMembers[focusChannelId] && channel && channel.type === General.OPEN_CHANNEL) { + await actions.joinChannel(currentUserId, channel.team_id, channel.id); + } } } - } - await actions.getPostsAround(focusChannelId, focusedPostId, 10); + await actions.getPostsAround(focusChannelId, focusedPostId, 10); - if (this.mounted) { - this.setState({loading: false}); + if (this.mounted) { + this.setState({loading: false}); + } } }; diff --git a/app/screens/settings/display_settings/display_settings.js b/app/screens/settings/display_settings/display_settings.js index 363ecc24f7..c134f79976 100644 --- a/app/screens/settings/display_settings/display_settings.js +++ b/app/screens/settings/display_settings/display_settings.js @@ -3,6 +3,7 @@ import React, {PureComponent} from 'react'; import PropTypes from 'prop-types'; +import {Navigation} from 'react-native-navigation'; import {intlShape} from 'react-intl'; import { Platform, @@ -19,6 +20,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; export default class DisplaySettings extends PureComponent { static propTypes = { actions: PropTypes.shape({ + applyTheme: PropTypes.func.isRequired, goToScreen: PropTypes.func.isRequired, }).isRequired, componentId: PropTypes.string, @@ -35,6 +37,15 @@ export default class DisplaySettings extends PureComponent { showClockDisplaySettings: false, }; + componentDidMount() { + this.navigationEventListener = Navigation.events().bindComponent(this); + } + + componentDidAppear() { + const {actions, componentId} = this.props; + actions.applyTheme(componentId); + } + closeClockDisplaySettings = () => { this.setState({showClockDisplaySettings: false}); }; @@ -44,7 +55,7 @@ export default class DisplaySettings extends PureComponent { const {intl} = this.context; if (Platform.OS === 'ios') { - const screen = 'ClockDisplay'; + const screen = 'ClockDisplaySettings'; const title = intl.formatMessage({id: 'user.settings.display.clockDisplay', defaultMessage: 'Clock Display'}); actions.goToScreen(screen, title); return; diff --git a/app/screens/settings/display_settings/display_settings.test.js b/app/screens/settings/display_settings/display_settings.test.js index 4d91bf1c82..8f485108dc 100644 --- a/app/screens/settings/display_settings/display_settings.test.js +++ b/app/screens/settings/display_settings/display_settings.test.js @@ -16,6 +16,7 @@ jest.mock('react-intl'); describe('DisplaySettings', () => { const baseProps = { actions: { + applyTheme: jest.fn(), goToScreen: jest.fn(), }, theme: Preferences.THEMES.default, diff --git a/app/screens/settings/display_settings/index.js b/app/screens/settings/display_settings/index.js index 12d4b321fe..ab325072aa 100644 --- a/app/screens/settings/display_settings/index.js +++ b/app/screens/settings/display_settings/index.js @@ -7,7 +7,7 @@ import {connect} from 'react-redux'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; import {isTimezoneEnabled} from 'mattermost-redux/selectors/entities/timezone'; -import {goToScreen} from 'app/actions/navigation'; +import {applyTheme, goToScreen} from 'app/actions/navigation'; import {getAllowedThemes} from 'app/selectors/theme'; import {isThemeSwitchingEnabled} from 'app/utils/theme'; @@ -28,6 +28,7 @@ function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ goToScreen, + applyTheme, }, dispatch), }; } diff --git a/app/screens/settings/general/index.js b/app/screens/settings/general/index.js index 09cc293abf..146d2b866c 100644 --- a/app/screens/settings/general/index.js +++ b/app/screens/settings/general/index.js @@ -9,7 +9,7 @@ import {getCurrentUrl, getConfig} from 'mattermost-redux/selectors/entities/gene import {getJoinableTeams} from 'mattermost-redux/selectors/entities/teams'; import {getTheme} from 'mattermost-redux/selectors/entities/preferences'; -import {goToScreen, dismissModal} from 'app/actions/navigation'; +import {applyTheme, goToScreen, dismissModal} from 'app/actions/navigation'; import {purgeOfflineStore} from 'app/actions/views/root'; import {removeProtocol} from 'app/utils/url'; @@ -32,6 +32,7 @@ function mapStateToProps(state) { function mapDispatchToProps(dispatch) { return { actions: bindActionCreators({ + applyTheme, clearErrors, purgeOfflineStore, goToScreen, diff --git a/app/screens/settings/general/settings.js b/app/screens/settings/general/settings.js index 94cbdba279..65f18e67b3 100644 --- a/app/screens/settings/general/settings.js +++ b/app/screens/settings/general/settings.js @@ -25,6 +25,7 @@ import LocalConfig from 'assets/config'; class Settings extends PureComponent { static propTypes = { actions: PropTypes.shape({ + applyTheme: PropTypes.func.isRequired, clearErrors: PropTypes.func.isRequired, purgeOfflineStore: PropTypes.func.isRequired, goToScreen: PropTypes.func.isRequired, @@ -50,6 +51,11 @@ class Settings extends PureComponent { this.navigationEventListener = Navigation.events().bindComponent(this); } + componentDidAppear() { + const {actions, componentId} = this.props; + actions.applyTheme(componentId); + } + navigationButtonPressed({buttonId}) { if (buttonId === 'close-settings') { this.props.actions.dismissModal(); diff --git a/app/screens/settings/theme/index.js b/app/screens/settings/theme/index.js index 13b93c3bf2..cb353f8a02 100644 --- a/app/screens/settings/theme/index.js +++ b/app/screens/settings/theme/index.js @@ -11,7 +11,6 @@ import {savePreferences} from 'mattermost-redux/actions/preferences'; import {getAllowedThemes, getCustomTheme} from 'app/selectors/theme'; import {isLandscape, isTablet} from 'app/selectors/device'; -import {applyTheme} from 'app/actions/navigation'; import Theme from './theme'; @@ -28,7 +27,6 @@ const mapStateToProps = (state) => ({ const mapDispatchToProps = (dispatch) => ({ actions: bindActionCreators({ savePreferences, - applyTheme, }, dispatch), }); diff --git a/app/screens/settings/theme/theme.js b/app/screens/settings/theme/theme.js index 364cac0661..1521323458 100644 --- a/app/screens/settings/theme/theme.js +++ b/app/screens/settings/theme/theme.js @@ -26,7 +26,6 @@ export default class Theme extends React.PureComponent { static propTypes = { actions: PropTypes.shape({ savePreferences: PropTypes.func.isRequired, - applyTheme: PropTypes.func.isRequired, }).isRequired, componentId: PropTypes.string, allowedThemes: PropTypes.arrayOf(PropTypes.object), @@ -66,7 +65,7 @@ export default class Theme extends React.PureComponent { userId, teamId, allowedThemes, - actions: {savePreferences, applyTheme}, + actions: {savePreferences}, } = this.props; const {customTheme} = this.state; const selectedTheme = allowedThemes.concat(customTheme).find((theme) => theme.key === key); @@ -77,8 +76,7 @@ export default class Theme extends React.PureComponent { name: teamId, value: JSON.stringify(selectedTheme), }]); - applyTheme(); - } + }; renderAllowedThemeTiles = () => { const {theme, allowedThemes, isLandscape, isTablet} = this.props; diff --git a/app/screens/settings/theme/theme.test.js b/app/screens/settings/theme/theme.test.js index 0c33caa99a..38dc10bce5 100644 --- a/app/screens/settings/theme/theme.test.js +++ b/app/screens/settings/theme/theme.test.js @@ -3,29 +3,14 @@ import React from 'react'; import {shallow} from 'enzyme'; -import {Navigation} from 'react-native-navigation'; - -import configureMockStore from 'redux-mock-store'; -import thunk from 'redux-thunk'; import Preferences from 'mattermost-redux/constants/preferences'; -import {applyTheme} from 'app/actions/navigation'; -import EphemeralStore from 'app/store/ephemeral_store'; - import Theme from './theme'; import ThemeTile from './theme_tile'; jest.mock('react-intl'); -jest.mock('react-native-navigation', () => ({ - Navigation: { - mergeOptions: jest.fn(), - }, -})); - -const mockStore = configureMockStore([thunk]); - const allowedThemes = [ { type: 'Mattermost', @@ -141,7 +126,6 @@ describe('Theme', () => { const baseProps = { actions: { savePreferences: jest.fn(), - applyTheme: jest.fn(), }, allowedThemes, isLandscape: false, @@ -159,57 +143,4 @@ describe('Theme', () => { expect(wrapper.getElement()).toMatchSnapshot(); expect(wrapper.find(ThemeTile)).toHaveLength(4); }); - - test('should apply new theme to all navigation components that have appeared', () => { - const componentIds = ['component-1', 'component-2', 'component-3']; - componentIds.forEach((componentId) => { - EphemeralStore.addNavigationComponentId(componentId); - }); - - const store = mockStore({ - entities: { - preferences: { - myPreferences: { - theme: {}, - }, - }, - teams: { - currentTeamId: 'current-team-id', - }, - general: { - config: {}, - }, - }, - }); - baseProps.actions.applyTheme.mockImplementation(() => { - store.dispatch(applyTheme()); - }); - - const wrapper = shallow( - , - ); - - const theme = allowedThemes[0]; - wrapper.instance().setTheme(theme.key); - expect(baseProps.actions.applyTheme).toHaveBeenCalledTimes(1); - - const options = { - topBar: { - backButton: { - color: theme.sidebarHeaderTextColor, - }, - background: { - color: theme.sidebarHeaderBg, - }, - title: { - color: theme.sidebarHeaderTextColor, - }, - }, - }; - expect(Navigation.mergeOptions.mock.calls).toEqual([ - [componentIds[2], options], - [componentIds[1], options], - [componentIds[0], options], - ]); - }); }); diff --git a/app/store/ephemeral_store.js b/app/store/ephemeral_store.js index a0df7d125c..997abfbb0f 100644 --- a/app/store/ephemeral_store.js +++ b/app/store/ephemeral_store.js @@ -19,6 +19,13 @@ class EphemeralStore { } this.navigationComponentIdStack.unshift(componentId); + }; + + removeNavigationComponentId = (componentId) => { + const index = this.navigationComponentIdStack.indexOf(componentId); + if (index >= 0) { + this.navigationComponentIdStack.splice(index, 1); + } } } diff --git a/app/utils/url.js b/app/utils/url.js index e0feebc8fe..a30fd4d319 100644 --- a/app/utils/url.js +++ b/app/utils/url.js @@ -99,7 +99,7 @@ export function getScheme(url) { } export function matchDeepLink(url, serverURL, siteURL) { - if (!url || !serverURL || !siteURL) { + if (!url || (!serverURL && !siteURL)) { return null; } diff --git a/test/setup.js b/test/setup.js index f60c19e476..0ce5659147 100644 --- a/test/setup.js +++ b/test/setup.js @@ -33,6 +33,11 @@ jest.mock('NativeModules', () => { END: 'END', }, }, + RNKeychainManager: { + SECURITY_LEVEL_ANY: 'ANY', + SECURITY_LEVEL_SECURE_SOFTWARE: 'SOFTWARE', + SECURITY_LEVEL_SECURE_HARDWARE: 'HARDWARE', + }, }; }); jest.mock('NativeEventEmitter');