Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: #220 terms and conditions modal first time login dev portal #241

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 13 additions & 11 deletions packages/elements/src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface ModalProps {

export interface ModalHeaderProps {
title: string
afterClose: () => void
afterClose?: () => void
}

export const ModalFooter: React.SFC<{ footerItems: React.ReactNode }> = ({ footerItems }) => (
Expand All @@ -34,15 +34,17 @@ export const ModalBody: React.SFC<{ body: React.ReactNode }> = ({ body }) => (
export const ModalHeader: React.SFC<ModalHeaderProps> = ({ title, afterClose }) => (
<header className="modal-card-head">
<h4 className="modal-card-title is-4">{title}</h4>
<button
className="delete"
aria-label="close"
data-test="modal-close-button"
onClick={event => {
event.preventDefault()
afterClose && afterClose()
}}
/>
{afterClose && (
<button
className="delete"
aria-label="close"
data-test="modal-close-button"
onClick={event => {
event.preventDefault()
afterClose && afterClose()
}}
/>
)}
</header>
)

Expand Down Expand Up @@ -92,7 +94,7 @@ export const Modal: React.FunctionComponent<ModalProps> = ({
) : (
<>
{HeaderComponent && <HeaderComponent />}
{!HeaderComponent && afterClose && title && <ModalHeader title={title} afterClose={afterClose} />}
{!HeaderComponent && title && <ModalHeader title={title} afterClose={afterClose} />}
{children && <ModalBody body={children} />}
{footerItems && <ModalFooter footerItems={footerItems} />}
</>
Expand Down
3 changes: 3 additions & 0 deletions packages/marketplace/src/actions/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ export const authClear = actionCreator<void>(ActionTypes.AUTH_CLEAR)
export const checkFirstTimeLogin = actionCreator<void>(ActionTypes.CHECK_FIRST_TIME_LOGIN)
export const toggleFirstLogin = actionCreator<boolean>(ActionTypes.TOGGLE_FIRST_LOGIN)
export const userAcceptTermAndCondition = actionCreator<void>(ActionTypes.USER_ACCEPT_TERM_AND_CONDITION)
export const checkTermsAcceptedWithCookie = actionCreator<void>(ActionTypes.CHECK_TERM_ACCEPTED_WITH_COOKIE)
export const setTermsAcceptedWithCookie = actionCreator<boolean>(ActionTypes.SET_TERMS_ACCEPTED_WITH_COOKIE)
export const setTermsAcceptedState = actionCreator<boolean>(ActionTypes.SET_TERMS_ACCEPTED_STATE)
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ exports[`Menu should match a snapshot 1`] = `
</Unknown>
</React.Fragment>
}
tapOutsideToDissmiss={true}
title="Terms and Conditions"
visible={true}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { Button, Modal, ModalProps } from '@reapit/elements'

export type TermsAndConditionsModalProps = {
onAccept: () => void
onDecline: () => void
onDecline?: () => void
text?: string
tapOutsideToDissmiss?: boolean
} & Pick<ModalProps, 'visible' | 'afterClose'>

const placeholderText = `
Expand All @@ -23,17 +24,21 @@ export const TermsAndConditionsModal: React.FunctionComponent<TermsAndConditions
onAccept,
onDecline,
text = placeholderText,
tapOutsideToDissmiss = true,
}) => {
return (
<Modal
title="Terms and Conditions"
visible={visible}
afterClose={afterClose}
tapOutsideToDissmiss={tapOutsideToDissmiss}
footerItems={
<>
<Button variant="secondary" type="button" onClick={onDecline}>
Decline
</Button>
{onDecline && (
<Button variant="secondary" type="button" onClick={onDecline}>
Decline
</Button>
)}
<Button dataTest="buttonAcceptTermsAndConditions" variant="primary" type="button" onClick={onAccept}>
Accept
</Button>
Expand Down
3 changes: 3 additions & 0 deletions packages/marketplace/src/constants/action-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const ActionTypes = {
CHECK_FIRST_TIME_LOGIN: 'CHECK_FIRST_TIME_LOGIN',
TOGGLE_FIRST_LOGIN: 'TOGGLE_FIRST_LOGIN',
USER_ACCEPT_TERM_AND_CONDITION: 'USER_ACCEPT_TERM_AND_CONDITION',
CHECK_TERM_ACCEPTED_WITH_COOKIE: 'CHECK_TERM_ACCEPTED_WITH_COOKIE',
SET_TERMS_ACCEPTED_WITH_COOKIE: 'SET_TERMS_ACCEPTED_WITH_COOKIE',
SET_TERMS_ACCEPTED_STATE: 'SET_TERMS_ACCEPTED_STATE',

// Error actions
ERROR_THROWN_COMPONENT: 'ERROR_THROWN_COMPONENT',
Expand Down
22 changes: 21 additions & 1 deletion packages/marketplace/src/core/private-route-wrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,28 @@ import { RouteComponentProps } from 'react-router-dom'
import { connect } from 'react-redux'
import { ReduxState } from 'src/types/core'
import Menu from '@/components/ui/menu'
import TermsAndConditionsModal from '@/components/ui/terms-and-conditions-modal'
import { Loader, Section, FlexContainerBasic, AppNavContainer } from '@reapit/elements'
import { LoginType, RefreshParams, getTokenFromQueryString, redirectToOAuth } from '@reapit/cognito-auth'
import { Dispatch } from 'redux'
import { withRouter, Redirect } from 'react-router'
import { authSetRefreshSession } from '../actions/auth'
import { getDefaultRouteByLoginType, getDefaultPathByLoginType } from '@/utils/auth-route'
import { authSetRefreshSession, checkTermsAcceptedWithCookie, setTermsAcceptedWithCookie } from '../actions/auth'
import { getCookieString, COOKIE_FIRST_TIME_LOGIN } from '@/utils/cookie'

const { Suspense } = React

export interface PrivateRouteWrapperConnectActions {
setRefreshSession: (refreshParams: RefreshParams) => void
checkTermsAcceptedWithCookie: () => void
setTermsAcceptedWithCookie: () => void
}

export interface PrivateRouteWrapperConnectState {
hasSession: boolean
isDesktopMode: boolean
loginType: LoginType
isTermAccepted: boolean
}

export type PrivateRouteWrapperProps = PrivateRouteWrapperConnectState &
Expand All @@ -35,11 +39,17 @@ export const PrivateRouteWrapper: React.FunctionComponent<PrivateRouteWrapperPro
hasSession,
loginType,
location,
checkTermsAcceptedWithCookie,
setTermsAcceptedWithCookie,
isTermAccepted,
}) => {
React.useEffect(checkTermsAcceptedWithCookie, [])

const params = new URLSearchParams(location.search)
const state = params.get('state')
const type =
state && state.includes('ADMIN') ? 'ADMIN' : state && state.includes('DEVELOPER') ? 'DEVELOPER' : loginType

const firstLoginCookie = getCookieString(COOKIE_FIRST_TIME_LOGIN)
const route = getDefaultRouteByLoginType(type, firstLoginCookie)
const cognitoClientId = process.env.COGNITO_CLIENT_ID_MARKETPLACE as string
Expand All @@ -64,6 +74,11 @@ export const PrivateRouteWrapper: React.FunctionComponent<PrivateRouteWrapperPro
return (
<AppNavContainer>
<Menu />
<TermsAndConditionsModal
visible={!isTermAccepted}
onAccept={setTermsAcceptedWithCookie}
tapOutsideToDissmiss={false}
/>
<FlexContainerBasic isScrollable flexColumn>
<Suspense
fallback={
Expand All @@ -83,10 +98,15 @@ const mapStateToProps = (state: ReduxState): PrivateRouteWrapperConnectState =>
hasSession: !!state.auth.loginSession || !!state.auth.refreshSession,
loginType: state.auth.loginType,
isDesktopMode: state?.auth?.refreshSession?.mode === 'DESKTOP',
isTermAccepted: state.auth.isTermAccepted,
})

const mapDispatchToProps = (dispatch: Dispatch): PrivateRouteWrapperConnectActions => ({
setRefreshSession: refreshParams => dispatch(authSetRefreshSession(refreshParams)),
checkTermsAcceptedWithCookie: () => {
dispatch(checkTermsAcceptedWithCookie())
},
setTermsAcceptedWithCookie: () => dispatch(setTermsAcceptedWithCookie(true)),
})

export default withRouter(connect(mapStateToProps, mapDispatchToProps)(PrivateRouteWrapper))
12 changes: 12 additions & 0 deletions packages/marketplace/src/reducers/__tests__/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,16 @@ describe('auth reducer', () => {
const newState = authReducer(undefined, { type: ActionTypes.TOGGLE_FIRST_LOGIN as ActionType, data })
expect(newState.firstLogin).toEqual(data)
})

it('should set isTermAccepted state when SET_TERMS_ACCEPTED_STATE is called ', () => {
const newState = authReducer(undefined, {
type: ActionTypes.SET_TERMS_ACCEPTED_STATE as ActionType,
data: true,
})
const expected = {
...defaultState(),
isTermAccepted: true,
}
expect(newState).toEqual(expected)
})
})
9 changes: 9 additions & 0 deletions packages/marketplace/src/reducers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
authSetRefreshSession,
authChangeLoginType,
toggleFirstLogin,
setTermsAcceptedState,
} from '../actions/auth'
import { LoginSession, RefreshParams, LoginType, getSessionCookie } from '@reapit/cognito-auth'
import { COOKIE_SESSION_KEY_MARKETPLACE } from '../constants/api'
Expand All @@ -18,6 +19,7 @@ export interface AuthState {
loginType: LoginType
loginSession: LoginSession | null
refreshSession: RefreshParams | null
isTermAccepted: boolean
}

export const defaultState = (): AuthState => {
Expand All @@ -26,6 +28,7 @@ export const defaultState = (): AuthState => {
error: false,
loginSession: null,
firstLogin: false,
isTermAccepted: true,
loginType: refreshSession ? refreshSession.loginType : 'CLIENT',
refreshSession,
}
Expand Down Expand Up @@ -83,6 +86,12 @@ const authReducer = (state: AuthState = defaultState(), action: Action<any>): Au
}
}

if (isType(action, setTermsAcceptedState)) {
return {
...state,
isTermAccepted: action.data,
}
}
return state
}

Expand Down
97 changes: 94 additions & 3 deletions packages/marketplace/src/sagas/__tests__/auth.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import MockDate from 'mockdate'
import { put, all, takeLatest, call, fork } from '@redux-saga/core/effects'
import { select, put, all, takeLatest, call, fork } from '@redux-saga/core/effects'
import { setUserSession, removeSession, LoginParams, LoginSession, redirectToLogout } from '@reapit/cognito-auth'
import { Action } from '@/types/core'
import { cloneableGenerator } from '@redux-saga/testing-utils'
import { getCookieString, setCookieString, COOKIE_FIRST_TIME_LOGIN } from '@/utils/cookie'
import {
getCookieString,
setCookieString,
COOKIE_FIRST_TIME_LOGIN,
COOKIE_TERMS_ACCEPTED,
COOKIE_MAX_AGE_INFINITY,
} from '@/utils/cookie'
import authSagas, {
doLogin,
doLogout,
Expand All @@ -15,12 +21,23 @@ import authSagas, {
setFirstTimeLogin,
checkFirstTimeLoginListen,
checkFirstTimeLogin,
setTermsAcceptedWithCookieHelper,
checkTermsAcceptedWithCookieHelper,
checkTermsAcceptedListen,
setTermsAcceptedListen,
} from '../auth'
import ActionTypes from '../../constants/action-types'
import { authLoginSuccess, authLogoutSuccess, authLoginFailure, toggleFirstLogin } from '../../actions/auth'
import {
authLoginSuccess,
authLogoutSuccess,
authLoginFailure,
toggleFirstLogin,
setTermsAcceptedState,
} from '../../actions/auth'
import Routes from '../../constants/routes'
import { ActionType } from '../../types/core'
import { COOKIE_SESSION_KEY_MARKETPLACE } from '../../constants/api'
import { selectLoginType } from '@/selector/auth'

jest.mock('../../utils/session')
jest.mock('../../core/router', () => ({
Expand All @@ -31,6 +48,25 @@ jest.mock('../../core/router', () => ({
jest.mock('../../core/store')
jest.mock('@reapit/cognito-auth')

/* mock to make new Date() a consistent value */
const RealDate = Date

function mockDate() {
global.Date = class extends RealDate {
constructor() {
super()
return new RealDate('2017-06-13T04:41:20') as Date
}
} as any
}

beforeEach(() => {
mockDate()
})
afterEach(() => {
global.Date = RealDate
})

export const mockLoginSession = {
userName: '[email protected]',
accessTokenExpiry: 2,
Expand Down Expand Up @@ -143,6 +179,8 @@ describe('auth thunks', () => {
fork(clearAuthListen),
fork(checkFirstTimeLoginListen),
fork(setFirstLoginListen),
fork(checkTermsAcceptedListen),
fork(setTermsAcceptedListen),
]),
)
expect(gen.next().done).toBe(true)
Expand Down Expand Up @@ -178,4 +216,57 @@ describe('auth thunks', () => {
expect(gen.next().done).toBe(true)
})
})

describe('checkTermsAcceptedWithCookie', () => {
it('should run correctly with developer login type', () => {
const gen = cloneableGenerator(checkTermsAcceptedWithCookieHelper)()
expect(gen.next().value).toEqual(select(selectLoginType))
expect(gen.next('DEVELOPER').value).toEqual(call(getCookieString, COOKIE_TERMS_ACCEPTED))
expect(gen.next('2019-12-18T16:30:00').value).toEqual(put(setTermsAcceptedState(true)))
expect(gen.next().done).toBe(true)
})

it('should run correctly with other login type', () => {
const gen = cloneableGenerator(checkTermsAcceptedWithCookieHelper)()
expect(gen.next().value).toEqual(select(selectLoginType))
expect(gen.next('CLIENT').done).toBe(true)
})
})

describe('setTermsAcceptedWithCookieHelper', () => {
it('should run correctly with true', () => {
const gen = cloneableGenerator(setTermsAcceptedWithCookieHelper)({ data: true })
expect(gen.next().value).toEqual(
call(setCookieString, COOKIE_TERMS_ACCEPTED, new Date(), COOKIE_MAX_AGE_INFINITY),
)
expect(gen.next().value).toEqual(put(setTermsAcceptedState(true)))
expect(gen.next().done).toBe(true)
})

it('should run correctly with false', () => {
const gen = cloneableGenerator(setTermsAcceptedWithCookieHelper)({ data: false })
expect(gen.next().value).toEqual(put(setTermsAcceptedState(false)))
expect(gen.next().done).toBe(true)
})
})

describe('checkTermsAcceptedListen', () => {
it('should run correctly', () => {
const gen = checkTermsAcceptedListen()
expect(gen.next().value).toEqual(
takeLatest(ActionTypes.CHECK_TERM_ACCEPTED_WITH_COOKIE, checkTermsAcceptedWithCookieHelper),
)
expect(gen.next().done).toBe(true)
})
})

describe('setTermsAcceptedListen', () => {
it('should run correctly', () => {
const gen = setTermsAcceptedListen()
expect(gen.next().value).toEqual(
takeLatest<Action<boolean>>(ActionTypes.SET_TERMS_ACCEPTED_WITH_COOKIE, setTermsAcceptedWithCookieHelper),
)
expect(gen.next().done).toBe(true)
})
})
})
Loading