Commit 47c3b515 authored by Hugo SUBTIL's avatar Hugo SUBTIL
Browse files

Merge branch 'features/US466-mail-token-analysis' into 'dev'

features/US466-mail-token-analysis

See merge request web-et-numerique/llle_project/ecolyo!375
parents f47ce3c8 5421ac52
/* eslint-disable react/display-name */
import React from 'react'
import { mount } from 'enzyme'
import * as reactRedux from 'react-redux'
import { Provider } from 'react-redux'
import AnalysisView from 'components/Analysis/AnalysisView'
import { globalStateData } from '../../../tests/__mocks__/globalStateData.mock'
import { profileData } from '../../../tests/__mocks__/profile.mock'
import Header from 'components/Header/Header'
import {
createMockStore,
mockInitialEcolyoState,
} from '../../../tests/__mocks__/store'
import * as globalActions from 'store/global/global.actions'
import * as profileActions from 'store/profile/profile.actions'
jest.mock('cozy-ui/transpiled/react/I18n', () => {
return {
useI18n: jest.fn(() => {
return {
t: (str: string) => str,
}
}),
}
})
jest.mock('components/Header/CozyBar', () => () => <div id="cozybar"></div>)
jest.mock('components/Analysis/MonthlyAnalysis', () => () => (
<div id="monthlyanalysis"></div>
))
const useSelectorSpy = jest.spyOn(reactRedux, 'useSelector')
const useDispatchSpy = jest.spyOn(reactRedux, 'useDispatch')
const toggleAnalysisNotificationSpy = jest.spyOn(
globalActions,
'toggleAnalysisNotification'
)
const updateProfileSpy = jest.spyOn(profileActions, 'updateProfile')
describe('AnalysisView component', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let store: any
beforeEach(() => {
store = createMockStore(mockInitialEcolyoState)
useSelectorSpy.mockClear()
useDispatchSpy.mockClear()
toggleAnalysisNotificationSpy.mockClear()
updateProfileSpy.mockClear()
})
it('should be rendered correctly', () => {
useSelectorSpy.mockReturnValue({
global: globalStateData,
profile: profileData,
})
useDispatchSpy.mockReturnValue(jest.fn())
const wrapper = mount(
<Provider store={store}>
<AnalysisView />
</Provider>
)
expect(wrapper.find('#cozybar')).toBeTruthy()
expect(wrapper.find(Header)).toBeTruthy()
expect(wrapper.find('#monthlyanalysis')).toBeTruthy()
})
it('should update profile and toggle analysis notification to false if notification is true', () => {
useSelectorSpy.mockReturnValue({
global: {
...globalStateData,
analysisNotification: true,
},
profile: {
...profileData,
haveSeenLastAnalysis: false,
},
})
useDispatchSpy.mockReturnValue(jest.fn())
const wrapper = mount(
<Provider store={store}>
<AnalysisView />
</Provider>
)
expect(wrapper.find('#cozybar')).toBeTruthy()
expect(wrapper.find(Header)).toBeTruthy()
expect(wrapper.find('#monthlyanalysis')).toBeTruthy()
expect(updateProfileSpy).toBeCalledTimes(1)
expect(updateProfileSpy).toHaveBeenCalledWith({
haveSeenLastAnalysis: true,
})
expect(toggleAnalysisNotificationSpy).toBeCalledTimes(1)
expect(toggleAnalysisNotificationSpy).toHaveBeenCalledWith(false)
})
})
/* eslint-disable react/display-name */
import React from 'react'
import { mount } from 'enzyme'
import * as reactRedux from 'react-redux'
import { Provider } from 'react-redux'
import AnalysisView from 'components/Analysis/AnalysisView'
import { globalStateData } from '../../../tests/__mocks__/globalStateData.mock'
import { profileData } from '../../../tests/__mocks__/profile.mock'
import Header from 'components/Header/Header'
import {
createMockStore,
mockInitialEcolyoState,
} from '../../../tests/__mocks__/store'
import * as globalActions from 'store/global/global.actions'
import * as profileActions from 'store/profile/profile.actions'
jest.mock('cozy-ui/transpiled/react/I18n', () => {
return {
useI18n: jest.fn(() => {
return {
t: (str: string) => str,
}
}),
}
})
jest.mock('components/Header/CozyBar', () => () => <div id="cozybar"></div>)
jest.mock('components/Analysis/MonthlyAnalysis', () => () => (
<div id="monthlyanalysis"></div>
))
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({
pathname: 'ecolyo.cozy.localhost:8080/#/analysis',
}),
}))
const useSelectorSpy = jest.spyOn(reactRedux, 'useSelector')
const useDispatchSpy = jest.spyOn(reactRedux, 'useDispatch')
const toggleAnalysisNotificationSpy = jest.spyOn(
globalActions,
'toggleAnalysisNotification'
)
const updateProfileSpy = jest.spyOn(profileActions, 'updateProfile')
describe('AnalysisView component', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let store: any
beforeEach(() => {
store = createMockStore(mockInitialEcolyoState)
useSelectorSpy.mockClear()
useDispatchSpy.mockClear()
toggleAnalysisNotificationSpy.mockClear()
updateProfileSpy.mockClear()
})
it('should be rendered correctly', () => {
useSelectorSpy.mockReturnValue({
global: globalStateData,
profile: profileData,
})
useDispatchSpy.mockReturnValue(jest.fn())
const wrapper = mount(
<Provider store={store}>
<AnalysisView />
</Provider>
)
expect(wrapper.find('#cozybar')).toBeTruthy()
expect(wrapper.find(Header)).toBeTruthy()
expect(wrapper.find('#monthlyanalysis')).toBeTruthy()
})
it('should update profile and toggle analysis notification to false if notification is true', () => {
useSelectorSpy.mockReturnValue({
global: {
...globalStateData,
analysisNotification: true,
},
profile: {
...profileData,
haveSeenLastAnalysis: false,
},
})
useDispatchSpy.mockReturnValue(jest.fn())
const wrapper = mount(
<Provider store={store}>
<AnalysisView />
</Provider>
)
expect(wrapper.find('#cozybar')).toBeTruthy()
expect(wrapper.find(Header)).toBeTruthy()
expect(wrapper.find('#monthlyanalysis')).toBeTruthy()
expect(updateProfileSpy).toBeCalledTimes(2)
expect(updateProfileSpy).toHaveBeenCalledWith({
haveSeenLastAnalysis: true,
})
expect(toggleAnalysisNotificationSpy).toBeCalledTimes(2)
expect(toggleAnalysisNotificationSpy).toHaveBeenCalledWith(false)
})
})
......@@ -11,8 +11,13 @@ import MonthlyAnalysis from 'components/Analysis/MonthlyAnalysis'
import './analysisView.scss'
import DateNavigator from 'components/DateNavigator/DateNavigator'
import { DateTime } from 'luxon'
import { useLocation } from 'react-router-dom'
import UsageEventService from 'services/usageEvent.service'
import { useClient } from 'cozy-client'
import { UsageEventType } from 'enum/usageEvent.enum'
const AnalysisView: React.FC = () => {
const client = useClient()
const [headerHeight, setHeaderHeight] = useState<number>(0)
const {
global: { analysisNotification },
......@@ -23,21 +28,69 @@ const AnalysisView: React.FC = () => {
const [currentAnalysisDate, setCurrentAnalysisDate] = useState<DateTime>(
monthlyAnalysisDate
)
const { mailToken } = useSelector((state: AppStore) => state.ecolyo.profile)
const dispatch = useDispatch()
const defineHeaderHeight = useCallback((height: number) => {
setHeaderHeight(height)
}, [])
// Handle email report comeback
const { search } = useLocation()
const query = new URLSearchParams(search)
const paramToken = query.get('token')
useEffect(() => {
const updateAnalysisNotification = () => {
if (analysisNotification) {
dispatch(updateProfile({ haveSeenLastAnalysis: true }))
dispatch(
updateProfile({
haveSeenLastAnalysis: true,
})
)
dispatch(toggleAnalysisNotification(false))
}
// Save usageevent came back from email
if (paramToken && mailToken && paramToken === mailToken) {
UsageEventService.addEventIfDoesntExist(
client,
{
type: UsageEventType.REPORT_FROM_EMAIL,
target: 'analysis',
result: '1',
},
{
type: UsageEventType.REPORT_FROM_EMAIL,
eventDate: {
$lt: DateTime.local()
.setZone('utc', {
keepLocalTime: true,
})
.endOf('month')
.toString(),
$gt: DateTime.local()
.setZone('utc', {
keepLocalTime: true,
})
.startOf('month')
.toString(),
},
}
)
}
}
updateAnalysisNotification()
}, [dispatch, analysisNotification, monthlyAnalysisDate, selectedDate])
}, [
dispatch,
analysisNotification,
monthlyAnalysisDate,
selectedDate,
paramToken,
mailToken,
client,
])
return (
<>
......
[
{
"ecogestureHash": "",
"challengeHash": "",
"duelHash": "",
"quizHash": "",
"isFirstConnection": true,
"GCUApprovalDate": null,
"lastConnectionDate": "0000-01-01T00:00:00.000Z",
"haveSeenOldFluidModal": false,
"haveSeenLastAnalysis": true,
"sendAnalysisNotification": false,
"monthlyAnalysisDate": "0000-01-01T00:00:00.000Z",
"isProfileTypeCompleted": false,
"tutorial": {
"isWelcomeSeen": false
}
}
]
[
{
"ecogestureHash": "",
"challengeHash": "",
"mailToken": "",
"duelHash": "",
"quizHash": "",
"isFirstConnection": true,
"GCUApprovalDate": null,
"lastConnectionDate": "0000-01-01T00:00:00.000Z",
"haveSeenOldFluidModal": false,
"haveSeenLastAnalysis": true,
"sendAnalysisNotification": false,
"monthlyAnalysisDate": "0000-01-01T00:00:00.000Z",
"isProfileTypeCompleted": false,
"tutorial": {
"isWelcomeSeen": false
}
}
]
......@@ -9,4 +9,5 @@ export enum DaccEvent {
NAVIGATION_ACTION_DAILY = 'navigation-action-daily',
EVENT_DURATION = 'event-duration',
QUIZ_STARS = 'quiz-stars',
SUMMARY_SUBSCRIPTION_MONTHLY = 'summary-subscription-monthly',
}
......@@ -14,4 +14,5 @@ export enum UsageEventType {
ACTION_CHANGE_EVENT = 'ActionChangeEvent',
ACTION_END_EVENT = 'ActionEndEvent',
PROFILE_SET_EVENT = 'ProfileSetEvent',
REPORT_FROM_EMAIL = 'ReportFromEvent',
}
import { DateTime } from 'luxon'
import { ProfileType } from './profileType.model'
interface Tutorial {
isWelcomeSeen: boolean
}
export interface ProfileEntity {
id: string
ecogestureHash: string
challengeHash: string
duelHash: string
quizHash: string
explorationHash: string
isFirstConnection: boolean
GCUApprovalDate: string | null
lastConnectionDate: string
haveSeenLastAnalysis: boolean
haveSeenOldFluidModal: string | boolean
sendAnalysisNotification: boolean
monthlyAnalysisDate: string
isProfileTypeCompleted: boolean
tutorial: Tutorial
_id?: string
_rev?: string
}
export interface Profile
extends Omit<
ProfileEntity,
| 'GCUApprovalDate'
| 'haveSeenOldFluidModal'
| 'lastConnectionDate'
| 'monthlyAnalysisDate'
> {
GCUApprovalDate: DateTime | null
lastConnectionDate: DateTime
haveSeenOldFluidModal: DateTime | boolean
monthlyAnalysisDate: DateTime
}
import { DateTime } from 'luxon'
import { ProfileType } from './profileType.model'
interface Tutorial {
isWelcomeSeen: boolean
}
export interface ProfileEntity {
id: string
ecogestureHash: string
challengeHash: string
duelHash: string
quizHash: string
explorationHash: string
isFirstConnection: boolean
GCUApprovalDate: string | null
lastConnectionDate: string
haveSeenLastAnalysis: boolean
haveSeenOldFluidModal: string | boolean
sendAnalysisNotification: boolean
monthlyAnalysisDate: string
isProfileTypeCompleted: boolean
tutorial: Tutorial
mailToken: string
_id?: string
_rev?: string
}
export interface Profile
extends Omit<
ProfileEntity,
| 'GCUApprovalDate'
| 'haveSeenOldFluidModal'
| 'lastConnectionDate'
| 'monthlyAnalysisDate'
> {
GCUApprovalDate: DateTime | null
lastConnectionDate: DateTime
haveSeenOldFluidModal: DateTime | boolean
monthlyAnalysisDate: DateTime
}
......@@ -52,7 +52,7 @@ export default class MailService {
}
h2 {
font-size: 22px;
font-weight: 300;
}
.title {
......@@ -132,7 +132,7 @@ export default class MailService {
<tbody>
<tr>
<td
class="headerImg"
class="headerImg"
valign="middle"
align="center"
>
......@@ -244,7 +244,7 @@ export default class MailService {
style="color:#FFFFFF; font-size:16px;"
>
<p>
Bravo, vous faites partie des utilisateurs d’Ecolyo.
Bravo, vous faites partie des utilisateurs d’Ecolyo.
</p>
</div>
<div
......@@ -289,7 +289,7 @@ export default class MailService {
1 - Appuyez sur les trois petits points du menu en haut à droite.
</p>
<p style="margin: 0;">
2 - Sélectionnez "Ajouter à l'écran d'accueil".
2 - Sélectionnez "Ajouter à l'écran d'accueil".
</p>
<p style="margin: 0 0 1rem 0;">
3 - Nommez la page et appuyez sur "Ajouter". Un raccourci vers la page web est apparu sur l'écran d'accueil de votre smartphone.
......@@ -346,7 +346,7 @@ export default class MailService {
height="20"
/>
<span>OK j’ai compris</span>
</a>
</a>
</td>
</tr>
<tr>
......@@ -354,7 +354,7 @@ export default class MailService {
</tr>
<tr>
<td align="center">
<a class="link" href="${clientUrl}">${clientUrl}</a>
<a class="link" href="${clientUrl}">${clientUrl}</a>
</td>
</tr>
<tr>
......@@ -434,11 +434,18 @@ export default class MailService {
</html>
`
}
public CreateBodyMonthlyReport(username: string, clientUrl: string) {
public CreateBodyMonthlyReport(
username: string,
clientUrl: string,
token?: string
) {
let unsubscribeUrl
if (!clientUrl.includes('analysis')) {
unsubscribeUrl = clientUrl + '/#/unsubscribe'
clientUrl = clientUrl + '/#/analysis'
if (token) {
clientUrl += '?token=' + token
}
} else {
unsubscribeUrl = clientUrl.replace('analysis', 'unsubscribe')
}
......@@ -476,7 +483,7 @@ export default class MailService {
}
h2 {
font-size: 22px;
font-weight: 300;
}
.title {
......@@ -556,7 +563,7 @@ export default class MailService {
<tbody>
<tr>
<td
class="headerImg"
class="headerImg"
valign="middle"
align="center"
>
......@@ -679,7 +686,7 @@ export default class MailService {
</div>
</td>
</tr>
</tbody>
</table>
<table class="w580"
......@@ -702,7 +709,7 @@ export default class MailService {
height="20"
/>
<span>Voir mon bilan</span>
</a>
</a>
</td>
</tr>
<tr>
......
import {
Client,
QueryDefinition,
QueryResult,
Q,
MongoSelector,
} from 'cozy-client'
import { USAGEEVENT_DOCTYPE } from 'doctypes'
import { DateTime } from 'luxon'
import {
AddEventParams,
UsageEvent,
UsageEventCreationEntity,
UsageEventEntity,
} from 'models'
export default class UsageEventService {
/**
* addEvent
* @param {Client} client
* @param {AddEventParams} params
* @returns {Promise<UsageEvent>} usageEvent added
*/
static async addEvent(
client: Client,
params: AddEventParams
): Promise<UsageEvent> {
const usageEvent: UsageEventCreationEntity = {
...params,
eventDate: DateTime.local()
.setZone('utc', {
keepLocalTime: true,
})
.toString(),
startDate: params.startDate ? params.startDate.toString() : undefined,
aggregated: false,
}
const { data }: QueryResult<UsageEventEntity> = await client.create(
USAGEEVENT_DOCTYPE,
usageEvent
)
return this.parseUsageEventEntityToUsageEvent(data)
}
/**
* updateUsageEventsAggregated
* @param {Client} client
* @param {string[]} ids
* @returns {Promise<boolean>} return true if all events are updated
*/
static async updateUsageEventsAggregated(
client: Client,
events: UsageEvent[]
): Promise<boolean> {
for (const event of events) {
try {
await client.save({
...event,
aggregated: true,
})
} catch (error) {
console.log(error)
}
}
return true
}
/**
* getEvents
* @param {Client} client
* @param {MongoSelector} filterParams
* @returns {Promise<UsageEvent[]>} usageEvent added
*/
static async getEvents(
client: Client,
filterParams: MongoSelector
): Promise<UsageEvent[]> {
const query: QueryDefinition = Q(USAGEEVENT_DOCTYPE)
.where(filterParams)
.sortBy([{ eventDate: 'asc' }])
const {
data: usageEventEntities,
}: QueryResult<UsageEventEntity[]> = await client.query(query)
const usageEvents: UsageEvent[] = usageEventEntities.map(
(usageEventEntity: UsageEventEntity) => {
return this.parseUsageEventEntityToUsageEvent(usageEventEntity)
}
)