Skip to content

Commit 9d9615b

Browse files
Merge pull request #323 from mxenabled/lr/version-number
feat(version): add an optional version number string that can be revealed
2 parents 4c786c0 + 0a28e4f commit 9d9615b

10 files changed

Lines changed: 95 additions & 4 deletions

File tree

src/Connect.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TokenContext } from '@kyper/tokenprovider'
1010
import { usePrevious } from '@kyper/hooks'
1111

1212
import * as connectActions from 'src/redux/actions/Connect'
13+
import { setWidgetVersion } from 'src/redux/actions/App'
1314
import { addAnalyticPath, removeAnalyticPath } from 'src/redux/reducers/analyticsSlice'
1415

1516
import { isConsentEnabled, loadUserFeatures } from 'src/redux/reducers/userFeaturesSlice'
@@ -144,6 +145,7 @@ export const Connect: React.FC<ConnectProps> = ({
144145
dispatch(loadProfiles(props.profiles))
145146
dispatch(loadUserFeatures(props.userFeatures))
146147
dispatch(loadExperimentalFeatures(props?.experimentalFeatures || {}))
148+
dispatch(setWidgetVersion(props?.version || null))
147149

148150
// Also important to note that this is a race condition between connect
149151
// mounting and the master data loading the client data. It just so happens

src/__tests__/ConnectWidget-test.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,4 +168,15 @@ describe('ConnectWidget', () => {
168168
{ timeout: 15000 },
169169
)
170170
}, 35000)
171+
172+
it('stores version metadata in app state from the top-level version prop', async () => {
173+
render(<ConnectWidgetWithoutReduxProvider {...defaultProps} version="abcdef1234567" />, {
174+
apiValue: apiValueMock,
175+
store: activeStore,
176+
})
177+
178+
await waitFor(() => {
179+
expect(activeStore.getState().app.version).toBe('abcdef1234567')
180+
})
181+
})
171182
})

src/redux/actions/App.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
export const ActionTypes = {
22
SESSION_IS_TIMED_OUT: 'app/session_is_timed_out',
33
HUMAN_EVENT_HAPPENED: 'app/human_event_happened',
4+
SET_WIDGET_VERSION: 'app/set_widget_version',
45
}
56

7+
export const setWidgetVersion = (version) => ({
8+
type: ActionTypes.SET_WIDGET_VERSION,
9+
payload: version,
10+
})
11+
612
export const dispatcher = (dispatch) => ({
713
markSessionTimedOut: () => dispatch({ type: ActionTypes.SESSION_IS_TIMED_OUT }),
814
handleHumanEvent: () => dispatch({ type: ActionTypes.HUMAN_EVENT_HAPPENED }),

src/redux/actions/__tests__/app-test.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { dispatcher as appDispatcher, ActionTypes } from 'src/redux/actions/App'
1+
import { dispatcher as appDispatcher, ActionTypes, setWidgetVersion } from 'src/redux/actions/App'
22
import { createReduxActionUtils } from 'src/utilities/Test'
33

44
const { actions, expectDispatch, resetDispatch } = createReduxActionUtils(appDispatcher)
@@ -12,4 +12,11 @@ describe('app Dispatcher', () => {
1212
actions.markSessionTimedOut()
1313
expectDispatch({ type: ActionTypes.SESSION_IS_TIMED_OUT })
1414
})
15+
16+
it('should create SET_WIDGET_VERSION action', () => {
17+
expect(setWidgetVersion('abc1234')).toEqual({
18+
type: ActionTypes.SET_WIDGET_VERSION,
19+
payload: 'abc1234',
20+
})
21+
})
1522
})

src/redux/reducers/App.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,23 @@ export const defaultState = {
66
// credential stuffing. See https://gitlab.mx.com/mx/connect/issues/279
77
// wether or not we consider a human has used the app.
88
humanEvent: false,
9+
version: null,
910
}
1011

1112
const markSessionTimedOut = (state) => ({ ...state, sessionIsTimedOut: true })
1213

1314
const handleHumanEvent = (state) => ({ ...state, humanEvent: true })
1415

16+
const setWidgetVersion = (state, action) => ({ ...state, version: action.payload })
17+
1518
export const app = (state = defaultState, action) => {
1619
switch (action.type) {
1720
case ActionTypes.SESSION_IS_TIMED_OUT:
1821
return markSessionTimedOut(state)
1922
case ActionTypes.HUMAN_EVENT_HAPPENED:
2023
return handleHumanEvent(state)
24+
case ActionTypes.SET_WIDGET_VERSION:
25+
return setWidgetVersion(state, action)
2126
default:
2227
return state
2328
}

src/redux/reducers/__tests__/app-test.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ActionTypes } from 'src/redux/actions/App'
1+
import { ActionTypes, setWidgetVersion } from 'src/redux/actions/App'
22
import { app as reducer, defaultState } from 'src/redux/reducers/App'
33

44
const { SESSION_IS_TIMED_OUT } = ActionTypes
@@ -15,4 +15,10 @@ describe('app reducers', () => {
1515
expect(reducer(undefined, action).sessionIsTimedOut).toBe(true)
1616
})
1717
})
18+
19+
describe('SET_WIDGET_VERSION', () => {
20+
it('should store the widget version', () => {
21+
expect(reducer(undefined, setWidgetVersion('abc1234')).version).toBe('abc1234')
22+
})
23+
})
1824
})

src/redux/selectors/app.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export const getSessionIsTimedOut = (state) => state.app.sessionIsTimedOut
2+
3+
export const getWidgetVersion = (state) => state.app.version

src/views/search/Search.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@ import { CloseOutline } from '@kyper/icon/CloseOutline'
1818
import { Search as SearchIcon } from '@kyper/icon/Search'
1919
import InputAdornment from '@mui/material/InputAdornment'
2020
import { TextField } from 'src/privacy/input'
21-
import { IconButton } from '@mui/material'
21+
import { IconButton, Snackbar } from '@mui/material'
2222

2323
import { __ } from 'src/utilities/Intl'
2424
import * as connectActions from 'src/redux/actions/Connect'
2525
import { selectConnectConfig } from 'src/redux/reducers/configSlice'
26+
import { getWidgetVersion } from 'src/redux/selectors/app'
2627
import { getMembers } from 'src/redux/selectors/Connect'
2728

2829
import { AnalyticEvents, PageviewInfo } from 'src/const/Analytics'
@@ -131,6 +132,7 @@ export const Search = React.forwardRef((_, navigationRef) => {
131132
useAnalyticsPath(...PageviewInfo.CONNECT_SEARCH, {}, false)
132133
const [state, dispatch] = useReducer(reducer, initialState)
133134
const [ariaLiveRegionMessage, setAriaLiveRegionMessage] = useState('')
135+
const [headerClicks, setHeaderClicks] = useState(0)
134136
const searchInput = useRef('')
135137
const sendAnalyticsEvent = useAnalyticsEvent()
136138
const postMessageFunctions = useContext(PostMessageContext)
@@ -140,6 +142,7 @@ export const Search = React.forwardRef((_, navigationRef) => {
140142
// Redux
141143
const reduxDispatch = useDispatch()
142144
const connectConfig = useSelector(selectConnectConfig)
145+
const widgetVersion = useSelector(getWidgetVersion)
143146
const connectedMembers = useSelector(getMembers)
144147
const usePopularOnly = useSelector((state) => {
145148
const clientProfile = state.profiles.clientProfile || {}
@@ -331,13 +334,21 @@ export const Search = React.forwardRef((_, navigationRef) => {
331334
component={'h2'}
332335
data-test="search-header"
333336
id="connect-search-header"
337+
onClick={() => setHeaderClicks((prev) => prev + 1)}
334338
style={inlineStyles.headerText}
335339
tabIndex={-1}
336340
truncate={false}
337341
variant="H2"
338342
>
339343
{__('Select your institution')}
340344
</Text>
345+
{/* This version is a hidden feature unless a user is told how to find it */}
346+
<Snackbar
347+
autoHideDuration={6000}
348+
message={widgetVersion}
349+
onClose={() => setHeaderClicks(0)}
350+
open={headerClicks >= 5 && Boolean(widgetVersion)}
351+
/>
341352
<TextField
342353
InputProps={{
343354
startAdornment: (

src/views/search/__tests__/Search-test.js

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import React from 'react'
2-
import { render, screen, waitFor, fireEvent } from 'src/utilities/testingLibrary'
2+
import {
3+
render,
4+
screen,
5+
waitFor,
6+
fireEvent,
7+
createTestReduxStore,
8+
} from 'src/utilities/testingLibrary'
39
import { FAVORITE_INSTITUTIONS, SEARCHED_INSTITUTIONS } from 'src/services/mockedData'
410
import { Search, buildSearchQuery, getSuggestedInstitutions } from 'src/views/search/Search'
511
import { VERIFY_MODE, TAX_MODE, AGG_MODE } from 'src/const/Connect'
@@ -8,6 +14,7 @@ import { __ } from 'src/utilities/Intl'
814
import { ApiProvider } from 'src/context/ApiContext'
915
import { apiValue } from 'src/const/apiProviderMock'
1016
import { InstitutionStatusField } from 'src/utilities/institutionStatus'
17+
import { setWidgetVersion } from 'src/redux/actions/App'
1118

1219
describe('Search View', () => {
1320
describe('Search component', () => {
@@ -77,6 +84,39 @@ describe('Search View', () => {
7784
expect(screen.getByText(__('No results found for ”%1”', searchTerm))).toBeInTheDocument()
7885
})
7986
})
87+
88+
it('shows version after clicking the header five times', async () => {
89+
const ref = React.createRef()
90+
const store = createTestReduxStore()
91+
store.dispatch(setWidgetVersion('abcdef1234567'))
92+
93+
render(<Search {...defaultProps} ref={ref} />, { store })
94+
95+
const header = await screen.findByText('Select your institution')
96+
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
97+
98+
for (let i = 0; i < 5; i++) {
99+
fireEvent.click(header)
100+
}
101+
102+
expect(screen.getByRole('alert')).toHaveTextContent('abcdef1234567')
103+
})
104+
105+
it('does not show version snackbar after five clicks when version is not provided', async () => {
106+
const ref = React.createRef()
107+
const store = createTestReduxStore()
108+
109+
render(<Search {...defaultProps} ref={ref} />, { store })
110+
111+
const header = await screen.findByText('Select your institution')
112+
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
113+
114+
for (let i = 0; i < 5; i++) {
115+
fireEvent.click(header)
116+
}
117+
118+
expect(screen.queryByRole('alert')).not.toBeInTheDocument()
119+
})
80120
})
81121

82122
describe('buildSearchQuery function', () => {

typings/connectProps.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ interface ConnectProps {
4444
memberPollingMilliseconds?: number
4545
useWebSockets?: boolean
4646
}
47+
version?: string
4748
}
4849
interface ClientConfigType {
4950
_initialValues: string

0 commit comments

Comments
 (0)