diff --git a/docker-compose.yml b/docker-compose.yml index ca171deeb..2fb22bd57 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -342,6 +342,8 @@ services: IDENTITYX_APP_ID: ${EXAMPLE_IDENTITYX_APP_ID-60774dbe3dac5936323ae121} IDENTITYX_API_TOKEN: ${EXAMPLE_IDENTITYX_API_TOKEN-} + OMEDA_CLIENT_KEY: ${EXAMPLE_OMEDA_CLIENT_KEY-client_allu} + OMEDA_GRAPHQL_URI: ${EXAMPLE_OMEDA_GRAPHQL_URI-} OMEDA_BRAND_KEY: ${EXAMPLE_OMEDA_BRAND_KEY-allucd} OMEDA_APP_ID: ${EXAMPLE_OMEDA_APP_ID} OMEDA_INPUT_ID: ${EXAMPLE_OMEDA_INPUT_ID} diff --git a/packages/marko-web-identity-x/api/fragment-types.json b/packages/marko-web-identity-x/api/fragment-types.json new file mode 100644 index 000000000..89ff64791 --- /dev/null +++ b/packages/marko-web-identity-x/api/fragment-types.json @@ -0,0 +1 @@ +{"__schema":{"types":[{"kind":"INTERFACE","name":"FieldInterface","possibleTypes":[{"name":"SelectField"},{"name":"BooleanField"}]}]}} \ No newline at end of file diff --git a/packages/marko-web-identity-x/api/fragments/active-user.js b/packages/marko-web-identity-x/api/fragments/active-user.js index 53f2a279c..50bbb9a10 100644 --- a/packages/marko-web-identity-x/api/fragments/active-user.js +++ b/packages/marko-web-identity-x/api/fragments/active-user.js @@ -27,6 +27,9 @@ fragment ActiveUserFragment on AppUser { sort: { field: name, order: asc } }) { id + hasAnswered + answer + value field { id label @@ -38,7 +41,6 @@ fragment ActiveUserFragment on AppUser { identifier { value type } } } - value } customSelectFieldAnswers(input: { onlyActive: true diff --git a/packages/marko-web-identity-x/browser/form/fields/custom-boolean.vue b/packages/marko-web-identity-x/browser/form/fields/custom-boolean.vue index 6acb3226d..867984bbb 100644 --- a/packages/marko-web-identity-x/browser/form/fields/custom-boolean.vue +++ b/packages/marko-web-identity-x/browser/form/fields/custom-boolean.vue @@ -47,10 +47,10 @@ export default { computed: { given: { get() { - return this.value; + return Boolean(this.value); }, set(given) { - this.$emit('input', given); + this.$emit('input', Boolean(given)); }, }, }, diff --git a/packages/marko-web-identity-x/browser/profile.vue b/packages/marko-web-identity-x/browser/profile.vue index 1f14e47a1..43238498a 100644 --- a/packages/marko-web-identity-x/browser/profile.vue +++ b/packages/marko-web-identity-x/browser/profile.vue @@ -83,7 +83,7 @@ :id="fieldAnswer.id" :message="fieldAnswer.field.label" :required="fieldAnswer.field.required" - :value="fieldAnswer.value" + :value="fieldAnswer.answer" @input="onCustomBooleanChange(fieldAnswer.id)" /> @@ -373,8 +373,8 @@ export default { onCustomBooleanChange(id) { const objIndex = this.customBooleanFieldAnswers.findIndex((obj => obj.id === id)); - const value = !this.customBooleanFieldAnswers[objIndex].value; - this.customBooleanFieldAnswers[objIndex].value = value; + const answer = !this.customBooleanFieldAnswers[objIndex].answer; + this.customBooleanFieldAnswers[objIndex].answer = answer; this.user.customBooleanFieldAnswers = this.customBooleanFieldAnswers; }, diff --git a/packages/marko-web-identity-x/routes/profile.js b/packages/marko-web-identity-x/routes/profile.js index b1c744b60..caa9267ba 100644 --- a/packages/marko-web-identity-x/routes/profile.js +++ b/packages/marko-web-identity-x/routes/profile.js @@ -74,7 +74,9 @@ module.exports = asyncRoute(async (req, res) => { // only update custom questions when there some :) const customBooleanFieldsInput = customBooleanFieldAnswers.map(fieldAnswer => ({ fieldId: fieldAnswer.field.id, - value: fieldAnswer.value, + // can either be true, false or null. convert null to false. + // the form submit is effectively answers the question. + value: Boolean(fieldAnswer.answer), })); await identityX.client.mutate({ mutation: customBooleanFieldsMutation, diff --git a/packages/marko-web-identity-x/utils/create-client.js b/packages/marko-web-identity-x/utils/create-client.js index 8cb022cdd..40fcf801d 100644 --- a/packages/marko-web-identity-x/utils/create-client.js +++ b/packages/marko-web-identity-x/utils/create-client.js @@ -1,8 +1,9 @@ const fetch = require('node-fetch'); const { ApolloClient } = require('apollo-client'); -const { InMemoryCache } = require('apollo-cache-inmemory'); +const { InMemoryCache, IntrospectionFragmentMatcher } = require('apollo-cache-inmemory'); const { createHttpLink } = require('apollo-link-http'); const { setContext } = require('apollo-link-context'); +const introspectionQueryResultData = require('../api/fragment-types.json'); const rootConfig = { connectToDevTools: false, @@ -40,6 +41,8 @@ module.exports = ({ ...config, ...rootConfig, link: contextLink.concat(httpLink), - cache: new InMemoryCache(), + cache: new InMemoryCache({ + fragmentMatcher: new IntrospectionFragmentMatcher({ introspectionQueryResultData }), + }), }); }; diff --git a/packages/marko-web-omeda-identity-x/external-id/is-deployment-type-id.js b/packages/marko-web-omeda-identity-x/external-id/is-deployment-type-id.js new file mode 100644 index 000000000..4d7bf2d0c --- /dev/null +++ b/packages/marko-web-omeda-identity-x/external-id/is-deployment-type-id.js @@ -0,0 +1,8 @@ +const isOmedaNamespace = require('./is-omeda-namespace'); + +module.exports = ({ externalId, brandKey } = {}) => isOmedaNamespace({ + externalId, + brandKey, + type: 'deploymentType', + valueMatcher: id => parseInt(id, 10), +}); diff --git a/packages/marko-web-omeda-identity-x/integration-hooks/on-login-link-sent.js b/packages/marko-web-omeda-identity-x/integration-hooks/on-login-link-sent.js index 081de6709..66a9f622b 100644 --- a/packages/marko-web-omeda-identity-x/integration-hooks/on-login-link-sent.js +++ b/packages/marko-web-omeda-identity-x/integration-hooks/on-login-link-sent.js @@ -1,5 +1,6 @@ const gql = require('graphql-tag'); const { get, getAsArray } = require('@parameter1/base-cms-object-path'); +const isOmedaDeploymentTypeId = require('../external-id/is-deployment-type-id'); const isOmedaDemographicId = require('../external-id/is-demographic-id'); const FIELD_QUERY = gql` @@ -9,6 +10,7 @@ const FIELD_QUERY = gql` node { id name + type active externalId { id @@ -22,6 +24,11 @@ const FIELD_QUERY = gql` externalIdentifier } } + + ... on BooleanField { + whenTrue { type value } + whenFalse { type value } + } } } } @@ -41,6 +48,9 @@ const CUSTOMER_QUERY = gql` demographic { id description } value { id description } } + primaryEmailAddress { + optInStatus { deploymentTypeId status { id } } + } } } `; @@ -51,8 +61,14 @@ const SET_OMEDA_DATA = gql` } `; -const SET_OMEDA_DEMOGRAPHIC_DATA = gql` - mutation SetOmedaDemographicData($input: UpdateAppUserCustomSelectAnswersMutationInput!) { +const SET_OMEDA_BOOLEAN_FIELD_ANSWERS = gql` + mutation SetOmedaBooleanFieldAnswers($input: UpdateAppUserCustomBooleanAnswersMutationInput!) { + updateAppUserCustomBooleanAnswers(input: $input) { id } + } +`; + +const SET_OMEDA_SELECT_FIELD_ANSWERS = gql` + mutation SetOmedaSelectFieldAnswers($input: UpdateAppUserCustomSelectAnswersMutationInput!) { updateAppUserCustomSelectAnswers(input: $input) { id } } `; @@ -98,31 +114,95 @@ const setOmedaDemographics = async ({ return map; }, new Map()); - const answerMap = new Map(); + const booleanAnswerMap = new Map(); + const selectAnswerMap = new Map(); omedaLinkedFields.forEach((field) => { if (answeredQuestionMap.has(field.id)) return; const { value: demoId } = field.externalId.identifier; const valueIdSet = omedaCustomerDemoValuesMap.get(demoId); if (!valueIdSet) return; - field.options.forEach((option) => { - const { externalIdentifier } = option; - if (!externalIdentifier || !valueIdSet.has(externalIdentifier)) return; - if (!answerMap.has(field.id)) answerMap.set(field.id, new Set()); - answerMap.get(field.id).add(option.id); - }); + + if (field.type === 'select') { + field.options.forEach((option) => { + const { externalIdentifier } = option; + if (!externalIdentifier || !valueIdSet.has(externalIdentifier)) return; + if (!selectAnswerMap.has(field.id)) selectAnswerMap.set(field.id, new Set()); + selectAnswerMap.get(field.id).add(option.id); + }); + } + + if (field.type === 'boolean') { + const { whenTrue, whenFalse } = field; + if (whenTrue.type === 'INTEGER' && valueIdSet.has(`${whenTrue.value}`)) { + booleanAnswerMap.set(field.id, true); + return; + } + if (whenFalse.type === 'INTEGER' && valueIdSet.has(`${whenFalse.value}`)) { + booleanAnswerMap.set(field.id, false); + } + } }); - if (answerMap.size) { - const answers = []; - answerMap.forEach((optionIdSet, fieldId) => { - answers.push({ fieldId, optionIds: [...optionIdSet] }); - }); - await identityX.client.mutate({ - mutation: SET_OMEDA_DEMOGRAPHIC_DATA, - variables: { input: { id: user.id, answers } }, - context: { apiToken: identityX.config.getApiToken() }, - }); - } + await Promise.all([ + (async () => { + if (!selectAnswerMap.size) return; + const answers = []; + selectAnswerMap.forEach((optionIdSet, fieldId) => { + answers.push({ fieldId, optionIds: [...optionIdSet] }); + }); + await identityX.client.mutate({ + mutation: SET_OMEDA_SELECT_FIELD_ANSWERS, + variables: { input: { id: user.id, answers } }, + context: { apiToken: identityX.config.getApiToken() }, + }); + })(), + (async () => { + if (!booleanAnswerMap.size) return; + const answers = []; + booleanAnswerMap.forEach((value, fieldId) => { + answers.push({ fieldId, value }); + }); + await identityX.client.mutate({ + mutation: SET_OMEDA_BOOLEAN_FIELD_ANSWERS, + variables: { input: { id: user.id, answers } }, + context: { apiToken: identityX.config.getApiToken() }, + }); + })(), + ]); +}; + +const setOmedaDeploymentTypes = async ({ + identityX, + user, + omedaCustomer, + omedaLinkedFields, + answeredQuestionMap, +}) => { + const omedaDeploymentOptInMap = getAsArray(omedaCustomer, 'primaryEmailAddress.optInStatus').reduce((map, { deploymentTypeId, status }) => { + const optedIn = status.id === 'IN'; + map.set(`${deploymentTypeId}`, optedIn); + return map; + }, new Map()); + + const answerMap = new Map(); + omedaLinkedFields.forEach((field) => { + if (answeredQuestionMap.has(field.id)) return; + const { value: deploymentTypeId } = field.externalId.identifier; + const optedIn = omedaDeploymentOptInMap.get(deploymentTypeId); + if (optedIn == null) return; + answerMap.set(field.id, optedIn); + }); + if (!answerMap.size) return; + + const answers = []; + answerMap.forEach((value, fieldId) => { + answers.push({ fieldId, value }); + }); + await identityX.client.mutate({ + mutation: SET_OMEDA_BOOLEAN_FIELD_ANSWERS, + variables: { input: { id: user.id, answers } }, + context: { apiToken: identityX.config.getApiToken() }, + }); }; module.exports = async ({ @@ -147,21 +227,37 @@ module.exports = async ({ }), ]); - const omedaLinkedFields = getAsArray(data, 'fields.edges') - .map(edge => edge.node) - .filter((field) => { - if (!field.active || !field.externalId) return false; - return isOmedaDemographicId({ externalId: field.externalId, brandKey }); - }); + const omedaLinkedFields = { + demographic: [], + deploymentType: [], + }; + getAsArray(data, 'fields.edges').forEach((edge) => { + const { node: field } = edge; + const { externalId } = field; + if (!field.active || !externalId) return; + if (isOmedaDemographicId({ externalId, brandKey })) { + omedaLinkedFields.demographic.push(field); + } + if (field.type === 'boolean' && isOmedaDeploymentTypeId({ externalId, brandKey })) { + omedaLinkedFields.deploymentType.push(field); + } + }); - const answeredQuestionMap = user.customSelectFieldAnswers.reduce((map, select) => { - if (!select.hasAnswered) return map; - map.set(select.field.id, true); - return map; - }, new Map()); + const answeredQuestionMap = new Map(); + user.customSelectFieldAnswers.forEach((select) => { + if (!select.hasAnswered) return; + answeredQuestionMap.set(select.field.id, true); + }); + user.customBooleanFieldAnswers.forEach((boolean) => { + if (!boolean.hasAnswered) return; + answeredQuestionMap.set(boolean.field.id, true); + }); const hasAnsweredAllOmedaQuestions = omedaLinkedFields - .every(field => answeredQuestionMap.has(field.id)); + .demographic.every(field => answeredQuestionMap.has(field.id)) + && omedaLinkedFields + .deploymentType.every(field => answeredQuestionMap.has(field.id)); + if (user.verified && user.hasAnsweredAllOmedaQuestions) { return; } @@ -171,13 +267,22 @@ module.exports = async ({ const promises = []; if (!user.verified) promises.push(setOmedaData({ identityX, user, omedaCustomer })); if (!hasAnsweredAllOmedaQuestions) { - promises.push(setOmedaDemographics({ - identityX, - user, - omedaCustomer, - omedaLinkedFields, - answeredQuestionMap, - })); + promises.push((async () => { + await setOmedaDemographics({ + identityX, + user, + omedaCustomer, + omedaLinkedFields: omedaLinkedFields.demographic, + answeredQuestionMap, + }); + await setOmedaDeploymentTypes({ + identityX, + user, + omedaCustomer, + omedaLinkedFields: omedaLinkedFields.deploymentType, + answeredQuestionMap, + }); + })()); } await Promise.all(promises); }; diff --git a/packages/marko-web-omeda-identity-x/rapid-identify.js b/packages/marko-web-omeda-identity-x/rapid-identify.js index 72c7d6497..53b08e531 100644 --- a/packages/marko-web-omeda-identity-x/rapid-identify.js +++ b/packages/marko-web-omeda-identity-x/rapid-identify.js @@ -1,6 +1,7 @@ const gql = require('graphql-tag'); const { get, getAsArray } = require('@parameter1/base-cms-object-path'); const isOmedaDemographicId = require('./external-id/is-demographic-id'); +const isDeploymentTypeId = require('./external-id/is-deployment-type-id'); const ALPHA3_CODE = gql` query GetAlpha3Code($alpha2: String!) { @@ -67,6 +68,24 @@ module.exports = async ({ }; }); + const deploymentTypes = []; + getAsArray(appUser, 'customBooleanFieldAnswers').forEach((boolean) => { + const { field, hasAnswered } = boolean; + const { externalId } = field; + if (!field.active || !externalId || !hasAnswered) return; + + const { identifier } = field.externalId; + const id = parseInt(identifier.value, 10); + + if (isOmedaDemographicId({ externalId, brandKey })) { + demographics.push({ id, values: [`${boolean.value}`] }); + } + + if (isDeploymentTypeId({ externalId, brandKey })) { + deploymentTypes.push({ id, optedIn: boolean.answer }); + } + }); + const { id, encryptedCustomerId } = await omedaRapidIdentify({ email: appUser.email, productId, @@ -78,6 +97,7 @@ module.exports = async ({ ...(regionCode && { regionCode }), ...(postalCode && { postalCode }), ...(demographics.length && { demographics }), + ...(deploymentTypes.length && { deploymentTypes }), ...(promoCode && { promoCode }), }); diff --git a/packages/marko-web-omeda/rapid-identify.js b/packages/marko-web-omeda/rapid-identify.js index 994e6c330..38e278242 100644 --- a/packages/marko-web-omeda/rapid-identify.js +++ b/packages/marko-web-omeda/rapid-identify.js @@ -21,7 +21,10 @@ module.exports = async (omedaGraphQLClient, { countryCode, postalCode, + // deprecated, use `deploymentTypes` instead deploymentTypeIds, + + deploymentTypes, demographics, promoCode, @@ -39,6 +42,7 @@ module.exports = async (omedaGraphQLClient, { ...(postalCode && { postalCode }), ...(isArray(deploymentTypeIds) && deploymentTypeIds.length && { deploymentTypeIds }), + ...(isArray(deploymentTypes) && deploymentTypes.length && { deploymentTypes }), ...(isArray(demographics) && demographics.length && { demographics }), ...(promoCode && { promoCode }), diff --git a/services/example-website/config/omeda.js b/services/example-website/config/omeda.js index 77f2178af..a45b7d21d 100644 --- a/services/example-website/config/omeda.js +++ b/services/example-website/config/omeda.js @@ -1,5 +1,6 @@ module.exports = { brandKey: process.env.OMEDA_BRAND_KEY, + clientKey: process.env.OMEDA_CLIENT_KEY, appId: process.env.OMEDA_APP_ID, inputId: process.env.OMEDA_INPUT_ID, graphqlUri: 'https://graphql.omeda.parameter1.com/', diff --git a/services/example-website/index.js b/services/example-website/index.js index de70a15ff..ce859e355 100644 --- a/services/example-website/index.js +++ b/services/example-website/index.js @@ -23,6 +23,7 @@ module.exports = startServer({ const omedaConfig = getAsObject(siteConfig, 'omeda'); const idxConfig = getAsObject(siteConfig, 'identityX'); omedaIdentityX(app, { + clientKey: omedaConfig.clientKey, brandKey: omedaConfig.brandKey, appId: omedaConfig.appId, inputId: omedaConfig.inputId,