@mindful-web/marko-web-omeda-identity-x 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.eslintignore +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +86 -0
  4. package/add-integration-hooks.js +119 -0
  5. package/api/queries/customer-by-encrypted-id.js +10 -0
  6. package/browser/.eslintrc.js +1 -0
  7. package/browser/index.js +43 -0
  8. package/browser/rapid-identify.vue +34 -0
  9. package/components/identify.marko +16 -0
  10. package/components/identify.marko.js +60 -0
  11. package/components/marko.json +6 -0
  12. package/external-id/find-encrypted-customer-id.js +10 -0
  13. package/external-id/get-encrypted-customer-id.js +7 -0
  14. package/external-id/get-omeda-namespaces.js +16 -0
  15. package/external-id/is-demographic-id.js +8 -0
  16. package/external-id/is-deployment-type-id.js +8 -0
  17. package/external-id/is-encrypted-customer-id.js +10 -0
  18. package/external-id/is-omeda-namespace.js +18 -0
  19. package/external-id/is-product-id.js +8 -0
  20. package/index.js +261 -0
  21. package/integration-hooks/index.js +9 -0
  22. package/integration-hooks/on-authentication-success.js +59 -0
  23. package/integration-hooks/on-login-link-sent.js +106 -0
  24. package/integration-hooks/on-user-profile-update.js +54 -0
  25. package/marko.json +6 -0
  26. package/middleware/README.md +12 -0
  27. package/middleware/rapid-identify.js +56 -0
  28. package/middleware/resync-customer-data.js +69 -0
  29. package/middleware/set-identity-cookie.js +77 -0
  30. package/middleware/set-olytics-cookie.js +25 -0
  31. package/middleware/set-promo-source.js +20 -0
  32. package/middleware/strip-olytics-param.js +23 -0
  33. package/omeda-data/get-answered-question-map.js +19 -0
  34. package/omeda-data/get-omeda-customer-record.js +46 -0
  35. package/omeda-data/get-omeda-linked-fields.js +66 -0
  36. package/omeda-data/index.js +19 -0
  37. package/omeda-data/set-omeda-data.js +35 -0
  38. package/omeda-data/set-omeda-demographics.js +137 -0
  39. package/omeda-data/set-omeda-deployment-types.js +51 -0
  40. package/omeda-data/set-omeda-products.js +50 -0
  41. package/omeda-data/update-identity-x.js +68 -0
  42. package/package.json +33 -0
  43. package/rapid-identify.js +194 -0
  44. package/routes/rapid-identify.js +70 -0
  45. package/utils/append-data.js +24 -0
  46. package/utils/build-behavior.js +32 -0
  47. package/utils/extract-promo-code.js +8 -0
  48. package/utils/get-cookie-id.js +11 -0
  49. package/utils/resync-cookie.js +25 -0
  50. package/validation/props.js +20 -0
  51. package/validation/schemas/append-behavior.js +7 -0
  52. package/validation/schemas/append-demographic.js +8 -0
  53. package/validation/schemas/append-promo-code.js +5 -0
  54. package/validation/schemas/behavior.js +7 -0
  55. package/validation/schemas/hook-behavior.js +12 -0
  56. package/validation/schemas/hook-demographic.js +16 -0
  57. package/validation/schemas/hook-promo-code.js +12 -0
  58. package/validation/schemas/index.js +19 -0
  59. package/validation/schemas/should-await.js +7 -0
@@ -0,0 +1,194 @@
1
+ const gql = require('graphql-tag');
2
+ const { get, getAsArray } = require('@mindful-web/object-path');
3
+ const isOmedaDemographicId = require('./external-id/is-demographic-id');
4
+ const isDeploymentTypeId = require('./external-id/is-deployment-type-id');
5
+ const isProductId = require('./external-id/is-product-id');
6
+
7
+ const ALPHA3_CODE = gql`
8
+ query GetAlpha3Code($alpha2: String!) {
9
+ localeCountry(input: { code: $alpha2 }) { id alpha3 }
10
+ }
11
+ `;
12
+
13
+ const getAlpha3CodeFor = async (alpha2, identityX) => {
14
+ const { data } = await identityX.client.query({
15
+ query: ALPHA3_CODE,
16
+ variables: { alpha2 },
17
+ });
18
+ return get(data, 'localeCountry.alpha3');
19
+ };
20
+
21
+ /**
22
+ *
23
+ * @param {object} params
24
+ * @param {string} params.brandKey The Omeda brand key
25
+ * @param {number} params.productId The Omeda product ID to associate with the identification
26
+ * @param {object} params.appUser The IdentityX user
27
+ * @param {string} [params.promoCode] An optional code to send to Omeda for tracking the
28
+ * acquisition source
29
+ * @param {IdentityX} params.identityX The Marko web IdentityX service
30
+ * @param {function} params.omedaRapidIdentify The Omeda rapid identifcation action
31
+ */
32
+ module.exports = async ({
33
+ brandKey,
34
+ productId,
35
+ appUser,
36
+
37
+ promoCode,
38
+
39
+ deploymentTypes = [],
40
+
41
+ appendBehaviors,
42
+ appendDemographics,
43
+ appendPromoCodes,
44
+
45
+ behavior,
46
+
47
+ identityX,
48
+ omedaRapidIdentify,
49
+ } = {}) => {
50
+ const {
51
+ givenName,
52
+ familyName,
53
+ organization,
54
+ organizationTitle,
55
+ countryCode,
56
+ regionCode,
57
+ postalCode,
58
+ street,
59
+ addressExtra,
60
+ city,
61
+ phoneNumber,
62
+ mobileNumber,
63
+ } = appUser;
64
+
65
+ let alpha3;
66
+ if (countryCode) {
67
+ // omeda requires and ISO alpha3 country code.
68
+ alpha3 = await getAlpha3CodeFor(countryCode, identityX);
69
+ }
70
+
71
+ const behaviors = [{ id: behavior.id, attributes: getAsArray(behavior, 'attributes') }];
72
+ const demographics = getAsArray(appUser, 'customSelectFieldAnswers').filter((select) => {
73
+ const { field, hasAnswered } = select;
74
+ if (!field.active || !field.externalId || !hasAnswered) return false;
75
+ return isOmedaDemographicId({ externalId: field.externalId, brandKey })
76
+ && select.answers.some((answer) => answer.externalIdentifier);
77
+ }).map((select) => {
78
+ const { field } = select;
79
+ const { identifier } = field.externalId;
80
+ const writeInValue = select.answers.reduce((v, answer) => (v || answer.writeInValue), null);
81
+ return {
82
+ id: parseInt(identifier.value, 10),
83
+ values: select.answers.map((answer) => answer.externalIdentifier).filter((v) => v),
84
+ ...(writeInValue && { writeInValue }),
85
+ };
86
+ });
87
+
88
+ getAsArray(appUser, 'customTextFieldAnswers').forEach((text) => {
89
+ const { field, hasAnswered } = text;
90
+ const { externalId } = field;
91
+ if (!field.active || !externalId || !hasAnswered) return;
92
+
93
+ const { identifier } = field.externalId;
94
+ const id = parseInt(identifier.value, 10);
95
+ if (isOmedaDemographicId({ externalId, brandKey })) {
96
+ demographics.push({ id, values: [`${text.value}`] });
97
+ }
98
+ });
99
+
100
+ const subscriptions = [];
101
+ getAsArray(appUser, 'customBooleanFieldAnswers').forEach((boolean) => {
102
+ const { field, hasAnswered } = boolean;
103
+ const { externalId } = field;
104
+ if (!field.active || !externalId || !hasAnswered) return;
105
+
106
+ const { identifier } = field.externalId;
107
+ const id = parseInt(identifier.value, 10);
108
+
109
+ if (isOmedaDemographicId({ externalId, brandKey })) {
110
+ demographics.push({ id, values: [`${boolean.value}`] });
111
+ }
112
+
113
+ if (isDeploymentTypeId({ externalId, brandKey })) {
114
+ deploymentTypes.push({ id, optedIn: boolean.answer });
115
+ }
116
+
117
+ if (isProductId({ externalId, brandKey })) {
118
+ subscriptions.push({ id, receive: boolean.answer });
119
+ }
120
+ });
121
+
122
+ // Append custom demographic answer, if specified
123
+ if (appendDemographics && appendDemographics.length) {
124
+ appendDemographics.forEach(({ demographicId, valueIds, writeInValue }) => {
125
+ demographics.push({
126
+ id: demographicId,
127
+ values: valueIds.map((v) => `${v}`),
128
+ ...(writeInValue && { writeInValue }),
129
+ });
130
+ });
131
+ }
132
+
133
+ // Append custom behavior, if specified
134
+ if (appendBehaviors && appendBehaviors.length) {
135
+ appendBehaviors.forEach(({ behaviorId, attributes }) => {
136
+ behaviors.push({
137
+ id: behaviorId,
138
+ // Only pass attributes through if they are present
139
+ ...(attributes && attributes.length && {
140
+ attributes: attributes.map((attr) => ({
141
+ id: attr.id,
142
+ ...(attr.valueId && { valueId: attr.valueId }),
143
+ ...(attr.value && { value: attr.value }),
144
+ })),
145
+ }),
146
+ });
147
+ });
148
+ }
149
+
150
+ const promoCodes = [];
151
+ if (promoCode) promoCodes.push(promoCode);
152
+
153
+ // Append promo codes, if specified
154
+ if (appendPromoCodes && appendPromoCodes.length) {
155
+ appendPromoCodes.forEach((code) => promoCodes.push(code.promoCode));
156
+ }
157
+
158
+ const { id, encryptedCustomerId } = await omedaRapidIdentify({
159
+ email: appUser.email,
160
+ productId,
161
+ ...(givenName && { firstName: givenName }),
162
+ ...(familyName && { lastName: familyName }),
163
+ ...(organization && { companyName: organization }),
164
+ ...(organizationTitle && { title: organizationTitle }),
165
+ ...(alpha3 && { countryCode: alpha3 }),
166
+ ...(regionCode && { regionCode }),
167
+ ...(postalCode && { postalCode }),
168
+ ...(street && { street }),
169
+ ...(addressExtra && { addressExtra }),
170
+ ...(city && { city }),
171
+ ...(mobileNumber && { mobileNumber }),
172
+ ...(phoneNumber && { phoneNumber }),
173
+ ...(demographics.length && { demographics }),
174
+ ...(behaviors.length && { behaviors }),
175
+ ...(deploymentTypes.length && { deploymentTypes }),
176
+ ...(promoCodes.length && { promoCode: [...promoCodes].pop() }),
177
+ ...(subscriptions.length && { subscriptions }),
178
+ });
179
+
180
+ const namespace = { provider: 'omeda', tenant: brandKey.toLowerCase(), type: 'customer' };
181
+ await Promise.all([
182
+ identityX.addExternalUserId({
183
+ userId: appUser.id,
184
+ identifier: { value: `${id}` },
185
+ namespace,
186
+ }),
187
+ identityX.addExternalUserId({
188
+ userId: appUser.id,
189
+ identifier: { value: encryptedCustomerId, type: 'encrypted' },
190
+ namespace,
191
+ }),
192
+ ]);
193
+ return { id, encryptedCustomerId };
194
+ };
@@ -0,0 +1,70 @@
1
+ const { Router } = require('express');
2
+ const { getAsObject } = require('@mindful-web/object-path');
3
+ const { asyncRoute } = require('@mindful-web/utils');
4
+ const jsonErrorHandler = require('@mindful-web/marko-web/express/json-error-handler');
5
+
6
+ const tokenCookie = require('@mindful-web/marko-web-identity-x/utils/token-cookie');
7
+ const olyticsCookie = require('@mindful-web/marko-web-omeda/olytics/customer-cookie');
8
+
9
+ const findEncryptedId = require('../external-id/find-encrypted-customer-id');
10
+ const extractPromoCode = require('../utils/extract-promo-code');
11
+
12
+ module.exports = ({
13
+ brandKey,
14
+ idxOmedaRapidIdentifyProp = '$idxOmedaRapidIdentify',
15
+ omedaPromoCodeCookieName = 'omeda_promo_code',
16
+ omedaPromoCodeDefault,
17
+ } = {}) => {
18
+ if (!brandKey) throw new Error('An Omeda brand key is required to use this middleware.');
19
+ const router = Router();
20
+ router.get('/', asyncRoute(async (req, res) => {
21
+ const idxOmedaRapidIdentify = req[idxOmedaRapidIdentifyProp];
22
+ if (!idxOmedaRapidIdentify) throw new Error(`Unable to find the IdentityX+Omeda rapid identifier on the request using ${idxOmedaRapidIdentifyProp}`);
23
+
24
+ const promoCode = extractPromoCode({
25
+ omedaPromoCodeCookieName,
26
+ omedaPromoCodeDefault,
27
+ cookies: req.cookies,
28
+ });
29
+
30
+ const data = {
31
+ userId: null,
32
+ encryptedId: null,
33
+ source: null,
34
+ didResetCookie: false,
35
+ brandKey: brandKey.toLowerCase(),
36
+ };
37
+ const idxUserExists = tokenCookie.exists(req);
38
+ if (!idxUserExists) return res.json(data);
39
+
40
+ const context = await req.identityX.loadActiveContext();
41
+ const user = getAsObject(context, 'user');
42
+ if (!user.id) return res.json(data);
43
+ data.userId = user.id;
44
+
45
+ // determine if an encrypted ID already exists for this user and brand.
46
+ const encryptedId = findEncryptedId({ externalIds: user.externalIds, brandKey });
47
+ if (encryptedId) {
48
+ data.encryptedId = encryptedId;
49
+ data.source = 'existing';
50
+ } else {
51
+ // no omeda identifier found for this user, rapidly identify.
52
+ const { encryptedCustomerId } = await idxOmedaRapidIdentify({ user, promoCode });
53
+ data.encryptedId = encryptedCustomerId;
54
+ data.source = 'new';
55
+ }
56
+
57
+ // finally, reset the olytics cookie if it isn't present
58
+ // or doesn't match the current encrypted id.
59
+ const currentEncId = olyticsCookie.parseFrom(req);
60
+
61
+ if (!currentEncId || currentEncId !== data.encryptedId) {
62
+ olyticsCookie.setTo(res, data.encryptedId);
63
+ data.didResetCookie = true;
64
+ }
65
+ return res.json(data);
66
+ }));
67
+
68
+ router.use(jsonErrorHandler());
69
+ return router;
70
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Returns the Omeda behaviors, demographics, or promo codes that should be appended to the API call
3
+ * based on the supplied event hook.
4
+ */
5
+ module.exports = ({
6
+ behaviors = [],
7
+ demographics = [],
8
+ promoCodes = [],
9
+ }) => (hookName) => {
10
+ const appendBehaviors = behaviors
11
+ .filter(({ hook }) => hook === hookName)
12
+ .map(({ hook, ...rest }) => rest);
13
+ const appendDemographics = demographics
14
+ .filter(({ hook }) => hook === hookName)
15
+ .map(({ hook, ...rest }) => rest);
16
+ const appendPromoCodes = promoCodes
17
+ .filter(({ hook }) => hook === hookName)
18
+ .map(({ hook, ...rest }) => rest);
19
+ return {
20
+ ...(appendBehaviors.length && { appendBehaviors }),
21
+ ...(appendDemographics.length && { appendDemographics }),
22
+ ...(appendPromoCodes.length && { appendPromoCodes }),
23
+ };
24
+ };
@@ -0,0 +1,32 @@
1
+ const { get } = require('@mindful-web/object-path');
2
+
3
+ const getBehavior = (event, behaviors) => {
4
+ switch (event) {
5
+ case 'onLoginLinkSent':
6
+ return behaviors.logIn;
7
+ case 'onAuthenticationSuccess':
8
+ return behaviors.verifyEmail;
9
+ case 'onUserProfileUpdate':
10
+ return behaviors.submitProfile;
11
+ default:
12
+ return undefined;
13
+ }
14
+ };
15
+
16
+ const buildAttributes = (attrs, data) => Object.keys(attrs).reduce((arr, key) => {
17
+ const attr = attrs[key];
18
+ const valueId = get(attrs, `${key}.valueIds.${data[key]}`, attr.valueId);
19
+ if (!valueId) return arr;
20
+ return [...arr, { id: attr.id, valueId }];
21
+ }, []);
22
+
23
+ /**
24
+ * Returns an Omeda Behavior ID and attribute map for the supplied IdentityX event data.
25
+ * @returns Object|undefined
26
+ */
27
+ module.exports = ({ behaviors, behaviorAttributes }) => (event, data) => {
28
+ const behaviorId = getBehavior(event, behaviors);
29
+ if (!behaviorId) return undefined;
30
+ const attributes = buildAttributes(behaviorAttributes, data);
31
+ return { id: behaviorId, attributes };
32
+ };
@@ -0,0 +1,8 @@
1
+ const { get } = require('@mindful-web/object-path');
2
+
3
+ module.exports = ({
4
+ promoCode,
5
+ omedaPromoCodeCookieName = 'omeda_promo_code',
6
+ omedaPromoCodeDefault,
7
+ cookies = {},
8
+ } = {}) => promoCode || get(cookies, omedaPromoCodeCookieName) || omedaPromoCodeDefault;
@@ -0,0 +1,11 @@
1
+ module.exports = (value, type) => {
2
+ if (!value) return null;
3
+ const trimmed = `${value.replace(/^"/, '').replace(/"$/, '')}`.trim();
4
+ if (type === 'anon') {
5
+ return /^[a-z0-9-]{36}$/i.test(trimmed) ? trimmed : null;
6
+ }
7
+ if (type === 'enc') {
8
+ return /^[a-z0-9]{15}$/i.test(trimmed) ? trimmed : null;
9
+ }
10
+ return null;
11
+ };
@@ -0,0 +1,25 @@
1
+ const COOKIE_NAME = '__idx_omeda_sync';
2
+ const defaultMaxAge = 7 * 24 * 60 * 60 * 1000; // 1 week
3
+
4
+ const parseFrom = (req) => {
5
+ try {
6
+ return JSON.parse(req.cookies[COOKIE_NAME]);
7
+ } catch (e) {
8
+ return false;
9
+ }
10
+ };
11
+
12
+ const setTo = ({
13
+ res,
14
+ value,
15
+ maxAge = defaultMaxAge,
16
+ }) => {
17
+ const v = JSON.stringify(value);
18
+ res.cookie(COOKIE_NAME, v, { maxAge, httpOnly: false });
19
+ return true;
20
+ };
21
+
22
+ module.exports = {
23
+ parseFrom,
24
+ setTo,
25
+ };
@@ -0,0 +1,20 @@
1
+ const Joi = require('@parameter1/joi');
2
+ const IdXConfig = require('@mindful-web/marko-web-identity-x/config');
3
+ const validHooks = require('@mindful-web/marko-web-identity-x/hooks');
4
+
5
+ module.exports = {
6
+ behaviorAttribute: Joi.object({
7
+ id: Joi.number().required(),
8
+ valueId: Joi.number(),
9
+ value: Joi.string(),
10
+ }).xor('valueId', 'value'),
11
+ behaviorId: Joi.number(),
12
+ brandKey: Joi.string().trim().lowercase(),
13
+ clientKey: Joi.string().trim().lowercase(),
14
+ demographicId: Joi.number(),
15
+ hook: Joi.string().valid(...validHooks),
16
+ idxConfig: Joi.object().instance(IdXConfig),
17
+ promoCode: Joi.string(),
18
+ valueId: Joi.number(),
19
+ writeInValue: Joi.string(),
20
+ };
@@ -0,0 +1,7 @@
1
+ const Joi = require('@parameter1/joi');
2
+ const { behaviorAttribute, behaviorId } = require('../props');
3
+
4
+ module.exports = Joi.object({
5
+ behaviorId: behaviorId.required(),
6
+ attributes: Joi.array().items(behaviorAttribute).default([]),
7
+ });
@@ -0,0 +1,8 @@
1
+ const Joi = require('@parameter1/joi');
2
+ const { demographicId, valueId } = require('../props');
3
+
4
+ module.exports = Joi.object({
5
+ demographicId: demographicId.required(),
6
+ valueIds: Joi.array().items(valueId.required()).required(),
7
+ writeInValue: Joi.string(),
8
+ });
@@ -0,0 +1,5 @@
1
+ const Joi = require('@parameter1/joi');
2
+
3
+ module.exports = Joi.object({
4
+ promoCode: Joi.string().required(),
5
+ });
@@ -0,0 +1,7 @@
1
+ const Joi = require('@parameter1/joi');
2
+ const { behaviorAttribute, behaviorId } = require('../props');
3
+
4
+ module.exports = Joi.object({
5
+ id: behaviorId.required(),
6
+ attributes: Joi.array().items(behaviorAttribute).default([]),
7
+ });
@@ -0,0 +1,12 @@
1
+ const Joi = require('@parameter1/joi');
2
+ const { behaviorId, hook } = require('../props');
3
+
4
+ /**
5
+ * @typedef HookBehaviorSchema
6
+ * @prop {import('@mindful-web/marko-web-identity-x/hooks').HookTypeEnum} hook
7
+ * @prop {number} behaviorId
8
+ */
9
+ module.exports = Joi.object({
10
+ hook: hook.required(),
11
+ behaviorId: behaviorId.required(),
12
+ });
@@ -0,0 +1,16 @@
1
+ const Joi = require('@parameter1/joi');
2
+ const { demographicId, valueId, hook } = require('../props');
3
+
4
+ /**
5
+ * @typedef HookDemographicSchema
6
+ * @prop {import('@mindful-web/marko-web-identity-x/hooks').HookTypeEnum} hook
7
+ * @prop {number} demographicId
8
+ * @prop {number[]} valueIds
9
+ * @prop {?string} writeInValue
10
+ */
11
+ module.exports = Joi.object({
12
+ hook: hook.required(),
13
+ demographicId: demographicId.required(),
14
+ valueIds: Joi.array().items(valueId.required()).required(),
15
+ writeInValue: Joi.string(),
16
+ });
@@ -0,0 +1,12 @@
1
+ const Joi = require('@parameter1/joi');
2
+ const { hook } = require('../props');
3
+
4
+ /**
5
+ * @typedef HookPromoCodeSchema
6
+ * @prop {import('@mindful-web/marko-web-identity-x/hooks').HookTypeEnum} hook
7
+ * @prop {string} promoCode
8
+ */
9
+ module.exports = Joi.object({
10
+ hook: hook.required(),
11
+ promoCode: Joi.string().required(),
12
+ });
@@ -0,0 +1,19 @@
1
+ const appendBehavior = require('./append-behavior');
2
+ const appendDemographic = require('./append-demographic');
3
+ const appendPromoCode = require('./append-promo-code');
4
+ const behavior = require('./behavior');
5
+ const hookBehavior = require('./hook-behavior');
6
+ const hookDemographic = require('./hook-demographic');
7
+ const hookPromoCode = require('./hook-promo-code');
8
+ const shouldAwait = require('./should-await');
9
+
10
+ module.exports = {
11
+ appendBehavior,
12
+ appendDemographic,
13
+ appendPromoCode,
14
+ behavior,
15
+ hookBehavior,
16
+ hookDemographic,
17
+ hookPromoCode,
18
+ shouldAwait,
19
+ };
@@ -0,0 +1,7 @@
1
+ const Joi = require('@parameter1/joi');
2
+
3
+ module.exports = Joi.object({
4
+ onLoginLinkSent: Joi.boolean().default(false),
5
+ onAuthenticationSuccess: Joi.boolean().default(true),
6
+ onUserProfileUpdate: Joi.boolean().default(false),
7
+ }).default();