Skip to content

Commit

Permalink
feat: #220 terms and conditions modal first time login dev portal (#241)
Browse files Browse the repository at this point in the history
* feat: #220 terms and condition modal when dev first login dev portal

* feat: #220 update test badges

* feat: #220 update terms and conditions ui

* fix: #220 update badges
  • Loading branch information
vuhuucuong authored Feb 12, 2020
1 parent 0db1fb3 commit c924abd
Show file tree
Hide file tree
Showing 18 changed files with 248 additions and 31 deletions.
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

0 comments on commit c924abd

Please sign in to comment.