Skip to content
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
6 changes: 5 additions & 1 deletion src/components/form/fields/consentField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import React from 'react';

import styled from 'styled-components';

import { ConsentType } from '@reachfive/identity-core';

import { Checkbox } from '../formControlsComponent';
import { createField, type FieldComponentProps, type FieldDefinition } from '../fieldCreator';
import { MarkdownContent } from '../../miscComponent';

import { PathMapping } from '../../../core/mapping';
import { checked, empty, isValidatorError } from '../../../core/validation';
import { isRichFormValue } from '../../../helpers/utils';
import { ConsentType } from '@reachfive/identity-core';
import { snakeCasePath } from '../../../helpers/transformObjectProperties';

const Description = styled.div`
font-size: ${props => props.theme.smallTextFontSize}px;
Expand Down Expand Up @@ -80,6 +83,7 @@ export default function consentField({
...props,
required,
defaultValue: { granted: props.defaultValue ?? false },
mapping: new PathMapping(snakeCasePath(props.path ?? props.key)), // Consent key should be snake_case
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Le coeur du problème. PathMapping sert à faire la mapping entre le modèles et les champs d'un formulaire. Par défaut ça fait une conversion camelcase du le nom des propriétés...
(les joies de l'usine à gaz que sont les formulaire sur le SDK UI...)

format: {
bind: value => value,
unbind: value => value !== undefined
Expand Down
9 changes: 7 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,19 @@ export function createClient(creationConfig: Config): Client {

return fetch(`https://${creationConfig.domain}/identity/v1/config/consents?${toQueryString({ lang: language })}`)
.then(response => response.json())
.then(consentsVersions => {
.then((consentsVersions: Record<string, ConsentVersions>) => {
return fetch(`${remoteConfig.resourceBaseUrl}/${language}.json`)
.then(response => response.json())
.then(defaultI18n => {
const config = {
...creationConfig,
...remoteConfig,
consentsVersions: camelCaseProperties(consentsVersions) as Record<string, ConsentVersions>
consentsVersions: Object.fromEntries(
Object.entries(consentsVersions).map(([key, value]) => [
key, // consents keys should be snake_case
camelCaseProperties(value) as ConsentVersions
])
)
}
return new UiClient(config, coreClient, defaultI18n)
})
Expand Down
24 changes: 22 additions & 2 deletions src/widgets/profileEditor/profileEditorWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,23 @@ export default createWidget<ProfileEditorWidgetProps, ProfileEditorProps>({
accessToken,
fields: resolvedFields.map(({ path }) => path).join(',')
})
.then(profile => camelCaseProperties(profile) as Profile) /** @todo check api response key format in sdk core */
/**
* @todo this can be removed when https://github.com/ReachFive/identity-web-core-sdk/pull/260 will be merged
* L'idéal serait que la clé d'un consentement soit en valeur d'une propriété "key" et non une clé de la structure Map utilisée.
* le mieux serait même que `consents` soit un array et non un map.
* Mais bon, difficile de changer ça maintenant sans créer un breaking change pour les utilisateurs de l'api ou du sdk core...
*/
.then(({ consents, ...profile }) => ({
...profile,
consents: consents
? Object.fromEntries(
Object.entries(consents).map(([key, value]) => [
key,
camelCaseProperties(value) as UserConsent
])
)
: undefined
}) as Profile) /** @todo check api response key format in sdk core */
Comment on lines +138 to +154
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cette partie sert juste à palier au fait que le SDK Core format la réponse api en camelcase mais en faisant exception des concents et de toute la structure enfant.
J'ai fait une PR sur le SDK Core pour que seul la clé d'un consentement soit préservée en snakecase. Mais j'ai peur que ça introduise un breaking change pour d'éventuelles intégrations avec le SDK Core...

.then((profile: Profile) => {
const filteredProfileConsents = (profile.consents && Object.keys(profile.consents).length > 0) ? filterProfileConsents(fields, config.consentsVersions, profile.consents) : undefined;
const filteredOutConsentsProfile = { ...profile, consents: filteredProfileConsents };
Expand All @@ -156,7 +172,11 @@ export default createWidget<ProfileEditorWidgetProps, ProfileEditorProps>({
});

// Filter out the profile consents with different version than the one the given consent field own
const filterProfileConsents = (fields: (string | Field)[], consentsVersions: Record<string, ConsentVersions>, profileConsents: Record<string, UserConsent>) => {
const filterProfileConsents = (
fields: (string | Field)[],
consentsVersions: Record<string, ConsentVersions>,
profileConsents: Record<string, UserConsent>
) => {
return Object.keys(profileConsents)
.filter(profileConsentKey => {
const consentField = fields.map(f => typeof f === 'string' ? f : f.key).find(field => field.startsWith(`consents.${profileConsentKey}`));
Expand Down
47 changes: 43 additions & 4 deletions tests/widgets/profileEditor/profileEditorWidget.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,19 @@ const defaultConfig: Config = {
mfaSmsEnabled: false,
mfaEmailEnabled: false,
rbaEnabled: false,
consentsVersions: {},
consentsVersions: {
'optinTesting': {
key: 'optinTesting',
consentType: 'opt-in',
status: 'active',
versions: [{
versionId: 1,
title: 'Opt-in Testing v1',
language: 'fr',
description: 'This is just a test'
}]
}
},
passwordPolicy: {
minLength: 8,
minStrength: 2,
Expand Down Expand Up @@ -116,9 +128,21 @@ describe('DOM testing', () => {
test('default', async () => {
const user = userEvent.setup()

const profile = {
// @ts-expect-error partial Profile
const profile: Profile = {
givenName: 'John',
familyName: 'Do',
consents: {
optinTesting: {
granted: false,
consentType: 'opt-in',
consentVersion: {
versionId: 1,
language: 'fr'
},
date: '2021-01-01T00:00:00.000Z',
}
}
}

getUser.mockResolvedValue(profile as Profile)
Expand All @@ -128,14 +152,15 @@ describe('DOM testing', () => {
await generateComponent({
fields: [
'given_name',
'family_name'
'family_name',
'optinTesting'
]
})

expect(getUser).toBeCalledWith(
expect.objectContaining({
accessToken: 'azerty',
fields: 'givenName,familyName',
fields: 'givenName,familyName,consents.optinTesting',
})
)

Expand All @@ -152,6 +177,10 @@ describe('DOM testing', () => {
await userEvent.clear(familyNameInput)
await userEvent.type(familyNameInput, 'reachfive')

// const consentCheckbox = screen.getByTestId('consents.optinTesting.1')
const consentCheckbox = screen.getByLabelText(defaultConfig.consentsVersions['optinTesting'].versions[0].title)
await user.click(consentCheckbox)

const submitBtn = screen.getByRole('button', { name: 'save'})
expect(submitBtn).toBeInTheDocument()

Expand All @@ -163,6 +192,16 @@ describe('DOM testing', () => {
data: {
givenName: 'alice',
familyName: 'reachfive',
consents: {
optin_testing: { // consent key should be snakecase
granted: true,
consentType: 'opt-in',
consentVersion: {
versionId: 1,
language: 'fr'
}
}
}
}
})
)
Expand Down