@mindful-web/marko-web-identity-x 1.83.1 → 1.85.2

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 (35) hide show
  1. package/CLAUDE.md +302 -0
  2. package/api/mutations/progressive-profile-update.js +29 -0
  3. package/browser/index.js +13 -0
  4. package/browser/progressive-profile.vue +406 -0
  5. package/components/access.marko.js +2 -2
  6. package/components/comment-stream.marko.js +2 -2
  7. package/components/context.marko.js +2 -2
  8. package/components/form-access.marko.js +2 -2
  9. package/components/form-authenticate.marko.js +2 -2
  10. package/components/form-change-email.marko.js +2 -2
  11. package/components/form-login.marko.js +2 -2
  12. package/components/form-logout.marko.js +2 -2
  13. package/components/form-profile.marko.js +2 -2
  14. package/components/form-register.marko.js +2 -2
  15. package/components/google-init.marko.js +2 -2
  16. package/components/google-sign-in-button.marko.js +2 -2
  17. package/components/identify.marko.js +2 -2
  18. package/components/marko.json +16 -0
  19. package/components/non-auth-identify.marko +6 -1
  20. package/components/non-auth-identify.marko.js +3 -3
  21. package/components/progressive-profile-form.marko +127 -0
  22. package/components/progressive-profile-form.marko.js +147 -0
  23. package/components/subscribe.marko.js +2 -2
  24. package/config.js +98 -0
  25. package/package.json +3 -2
  26. package/routes/google.js +5 -0
  27. package/routes/index.js +3 -0
  28. package/routes/progressive-profile.js +296 -0
  29. package/service.js +31 -0
  30. package/utils/add-custom-attributes.js +19 -46
  31. package/utils/build-progressive-profile-payload.js +117 -0
  32. package/utils/get-missing-progressive-fields.js +114 -0
  33. package/utils/get-progressive-form-fields.js +67 -0
  34. package/utils/progressive-profile-cookies.js +120 -0
  35. package/utils/progressive-profile-telemetry.js +148 -0
@@ -4,7 +4,12 @@ $ const { identityX } = req;
4
4
  <if(Boolean(identityX))>
5
5
  <marko-web-resolve|{ resolved: user }| promise=identityX.getIdentityData()>
6
6
  <if(user && user.id)>
7
- <marko-web-resolve|{ resolved: fullUser }| promise=identityX.findUserById(user.id)>
7
+ <!--
8
+ Memoized: a progressive-profile page load resolves this same identity in the middleware and
9
+ again in `getIdentityData()`. Read-only, so a per-request cache is safe here -- see
10
+ `service.findUserByIdMemoized` for the one caller that must NOT use it.
11
+ -->
12
+ <marko-web-resolve|{ resolved: fullUser }| promise=identityX.findUserByIdMemoized(user.id)>
8
13
  <${input.renderBody} user=fullUser />
9
14
  </marko-web-resolve>
10
15
  </if>
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.1/components/non-auth-identify.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.2/components/non-auth-identify.marko",
6
6
  marko_component = require("./non-auth-identify.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  marko_dynamicTag = require("marko/dist/runtime/helpers/dynamic-tag"),
@@ -23,7 +23,7 @@ function render(input, out, __component, component, state) {
23
23
  renderBody: function(out, { resolved: user }) {
24
24
  if (user && user.id) {
25
25
  marko_web_resolve_tag({
26
- promise: identityX.findUserById(user.id),
26
+ promise: identityX.findUserByIdMemoized(user.id),
27
27
  renderBody: function(out, { resolved: fullUser }) {
28
28
  marko_dynamicTag(out, input.renderBody, function() {
29
29
  return {
@@ -45,7 +45,7 @@ marko_template._ = marko_renderer(render, {
45
45
  }, marko_component);
46
46
 
47
47
  marko_template.meta = {
48
- id: "/@mindful-web/marko-web-identity-x$1.83.1/components/non-auth-identify.marko",
48
+ id: "/@mindful-web/marko-web-identity-x$1.85.2/components/non-auth-identify.marko",
49
49
  component: "./non-auth-identify.marko",
50
50
  tags: [
51
51
  "@mindful-web/marko-core/components/resolve.marko"
@@ -0,0 +1,127 @@
1
+
2
+ import defaultValue from "@mindful-web/marko-core/utils/default-value";
3
+ import { getAsArray, getAsObject } from "@mindful-web/object-path";
4
+ import baseContentEntity from "@mindful-web/marko-web-p1-events/utils/base-content-entity";
5
+
6
+ import getProgressiveFormFields from '../utils/get-progressive-form-fields'
7
+
8
+ $ const { req: { identityX }, site, i18n } = out.global;
9
+ $ const { content } = input;
10
+ <!-- NOT named `state`: compiled Marko render functions receive a `state` parameter (component
11
+ state), so a top-level `$ const state` is a SyntaxError at require time — after a clean
12
+ compile. -->
13
+ $ const profileState = getAsObject(input, "state");
14
+
15
+ <!--
16
+ The progressive-profile gate form.
17
+
18
+ Mirrors `form-access.marko`, with one deliberate difference: where that template sets
19
+ `updateProfileOnSubmit = false` for identified-but-unauthenticated visitors (their answers go to
20
+ an anonymous content-access submission and never touch their AppUser record), this one writes.
21
+ That is the entire point of the feature -- the identified bucket is the majority of who we know.
22
+
23
+ Rendering is driven off `state.missingRows`, produced by the theme middleware via
24
+ `get-missing-progressive-fields.js`, so only unanswered questions appear and every one of them is
25
+ required. The rows are NOT read from the request anywhere; the write route re-derives what it
26
+ will accept from server config.
27
+ -->
28
+ $ const fieldRows = getAsArray(profileState, "missingRows");
29
+ $ const userId = profileState.userId;
30
+ $ const contentId = content && content.id;
31
+
32
+ <if(identityX && fieldRows.length && userId && contentId)>
33
+ $ const additionalEventData = {
34
+ ...defaultValue(input.additionalEventData, {}),
35
+ contentGateType: "progressive-profile",
36
+ ...(profileState.promoCode ? { promoCode: profileState.promoCode } : {}),
37
+ };
38
+ <!--
39
+ The default title names how many questions are about to be asked, which is the single biggest
40
+ lever on completion: most abandonment on a gate like this is fear of a long form.
41
+
42
+ Read from `state.missingCount` (the middleware already flattens it), so it is the number
43
+ actually being rendered rather than the number configured -- a site asking two questions shows
44
+ the one-question title to a reader who has already answered half of them. The `missingRows`
45
+ fallback covers a caller that builds the state by hand rather than via the theme middleware.
46
+
47
+ Each count is its own string rather than an interpolated number because both translation
48
+ layers here (the app-level `i18n` function and the Vue component's own `translate` map) are
49
+ flat key lookups with no placeholder support. Three variants covers it: a real form is one or
50
+ two questions, and anything longer reads better as "a few" than as a number.
51
+ -->
52
+ $ const questionCount = profileState.missingCount || fieldRows.reduce((total, row) => total + row.length, 0);
53
+ $ const defaultTitlesByCount = {
54
+ 1: "One quick question, then keep reading",
55
+ 2: "Two quick questions, then keep reading",
56
+ };
57
+ $ const defaultTitle = defaultTitlesByCount[questionCount] || "A few quick questions, then keep reading";
58
+
59
+ $ const title = defaultValue(input.title, i18n(defaultTitle));
60
+ $ const callToAction = defaultValue(input.callToAction, i18n("Knowing your role helps us cover the topics that matter to your work. We only ask once."));
61
+ $ const buttonLabel = defaultValue(input.buttonLabel, i18n("Continue Reading"));
62
+ $ const dismissLabel = defaultValue(input.dismissLabel, i18n("Not right now"));
63
+ $ const lang = site.config.lang || "en";
64
+
65
+ <marko-web-identity-x-context|{ hasUser, user, fields }| with-fields=true>
66
+ $ const contentEntity = (content.id && content.type) ? baseContentEntity(content.id, content.type) : null;
67
+
68
+ <!--
69
+ reCAPTCHA v3 site key, resolved the two ways the fleet already does it: `app.locals.recaptcha`
70
+ (set in each website repo's `start-server.js`) with a `process.env` fallback for the packages
71
+ that read it directly, as `marko-web-contact-us` does.
72
+ -->
73
+ $ const recaptcha = getAsObject(out, "global.recaptcha");
74
+ $ const recaptchaSiteKey = recaptcha.siteKey || process.env.RECAPTCHA_V3_SITE_KEY;
75
+
76
+ $ const buildProps = (activeUser) => {
77
+ const {
78
+ customSelectFieldAnswers,
79
+ customBooleanFieldAnswers,
80
+ customTextFieldAnswers,
81
+ } = getProgressiveFormFields({ fields, fieldRows, user: activeUser });
82
+ return {
83
+ fieldRows,
84
+ activeUser,
85
+ customSelectFieldAnswers,
86
+ customBooleanFieldAnswers,
87
+ customTextFieldAnswers,
88
+ contentEntity,
89
+ content: { id: content.id, type: content.type, name: content.name },
90
+ title,
91
+ callToAction,
92
+ buttonLabel,
93
+ dismissLabel,
94
+ dismissible: Boolean(profileState.dismissible),
95
+ recaptchaSiteKey,
96
+ userId,
97
+ audience: hasUser ? "authenticated" : "identified",
98
+ idxEntity: `identity-x.${identityX.config.getAppId()}.app-user*${userId}`,
99
+ fieldIds: fieldRows.reduce((arr, row) => [...arr, ...row.map((col) => col.id)], []),
100
+ additionalEventData,
101
+ loginSource: "progressiveProfile",
102
+ endpoints: identityX.config.getEndpoints(),
103
+ enableChangeEmail: identityX.config.get("enableChangeEmail"),
104
+ defaultFieldLabels: identityX.config.get("defaultFieldLabels"),
105
+ lang,
106
+ };
107
+ };
108
+
109
+ <div class="content-progressive-profile-idx__wrapper">
110
+ <if(!hasUser)>
111
+ <!--
112
+ Identified but not authenticated -- the majority of who we know, and the population this
113
+ feature exists for. Same wrapper `form-access.marko` uses to resolve the `__idx_idt`
114
+ identity into a full AppUser.
115
+ -->
116
+ <marko-web-identity-x-non-auth-identify|{ user: identifiedUser }|>
117
+ <if(identifiedUser)>
118
+ <marko-web-browser-component name="IdentityXProgressiveProfile" props=buildProps(identifiedUser) />
119
+ </if>
120
+ </marko-web-identity-x-non-auth-identify>
121
+ </if>
122
+ <else>
123
+ <marko-web-browser-component name="IdentityXProgressiveProfile" props=buildProps(user) />
124
+ </else>
125
+ </div>
126
+ </marko-web-identity-x-context>
127
+ </if>
@@ -0,0 +1,147 @@
1
+ // Compiled using marko@4.20.2 - DO NOT EDIT
2
+ "use strict";
3
+
4
+ var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.2/components/progressive-profile-form.marko",
6
+ marko_component = require("./progressive-profile-form.marko"),
7
+ marko_renderer = require("marko/dist/runtime/components/renderer"),
8
+ module_defaultValue = require("@mindful-web/marko-core/utils/default-value"),
9
+ defaultValue = module_defaultValue.default || module_defaultValue,
10
+ module_objectPath_module = require("@mindful-web/object-path"),
11
+ objectPath_module = module_objectPath_module.default || module_objectPath_module,
12
+ getAsArray = module_objectPath_module.getAsArray,
13
+ getAsObject = module_objectPath_module.getAsObject,
14
+ module_baseContentEntity = require("@mindful-web/marko-web-p1-events/utils/base-content-entity"),
15
+ baseContentEntity = module_baseContentEntity.default || module_baseContentEntity,
16
+ module_getProgressiveFormFields = require("../utils/get-progressive-form-fields"),
17
+ getProgressiveFormFields = module_getProgressiveFormFields.default || module_getProgressiveFormFields,
18
+ marko_web_browser_component_template = require("@mindful-web/marko-web/components/browser-component.marko"),
19
+ marko_loadTag = require("marko/dist/runtime/helpers/load-tag"),
20
+ marko_web_browser_component_tag = marko_loadTag(marko_web_browser_component_template),
21
+ marko_web_identity_x_non_auth_identify_template = require("./non-auth-identify.marko"),
22
+ marko_web_identity_x_non_auth_identify_tag = marko_loadTag(marko_web_identity_x_non_auth_identify_template),
23
+ marko_web_identity_x_context_template = require("./context.marko"),
24
+ marko_web_identity_x_context_tag = marko_loadTag(marko_web_identity_x_context_template);
25
+
26
+ function render(input, out, __component, component, state) {
27
+ var data = input;
28
+
29
+ const { req: { identityX }, site, i18n } = out.global;
30
+
31
+ const { content } = input;
32
+
33
+ const profileState = getAsObject(input, "state");
34
+
35
+ const fieldRows = getAsArray(profileState, "missingRows");
36
+
37
+ const userId = profileState.userId;
38
+
39
+ const contentId = content && content.id;
40
+
41
+ if (((identityX && fieldRows.length) && userId) && contentId) {
42
+ const additionalEventData = {
43
+ ...defaultValue(input.additionalEventData, {}),
44
+ contentGateType: "progressive-profile",
45
+ ...(profileState.promoCode ? { promoCode: profileState.promoCode } : {}),
46
+ };
47
+
48
+ const questionCount = profileState.missingCount || fieldRows.reduce((total, row) => total + row.length, 0);
49
+
50
+ const defaultTitlesByCount = {
51
+ 1: "One quick question, then keep reading",
52
+ 2: "Two quick questions, then keep reading",
53
+ };
54
+
55
+ const defaultTitle = defaultTitlesByCount[questionCount] || "A few quick questions, then keep reading";
56
+
57
+ const title = defaultValue(input.title, i18n(defaultTitle));
58
+
59
+ const callToAction = defaultValue(input.callToAction, i18n("Knowing your role helps us cover the topics that matter to your work. We only ask once."));
60
+
61
+ const buttonLabel = defaultValue(input.buttonLabel, i18n("Continue Reading"));
62
+
63
+ const dismissLabel = defaultValue(input.dismissLabel, i18n("Not right now"));
64
+
65
+ const lang = site.config.lang || "en";
66
+
67
+ marko_web_identity_x_context_tag({
68
+ withFields: true,
69
+ renderBody: function(out, { hasUser, user, fields }) {
70
+ const contentEntity = (content.id && content.type) ? baseContentEntity(content.id, content.type) : null;
71
+
72
+ const recaptcha = getAsObject(out, "global.recaptcha");
73
+
74
+ const recaptchaSiteKey = recaptcha.siteKey || process.env.RECAPTCHA_V3_SITE_KEY;
75
+
76
+ const buildProps = (activeUser) => {
77
+ const {
78
+ customSelectFieldAnswers,
79
+ customBooleanFieldAnswers,
80
+ customTextFieldAnswers,
81
+ } = getProgressiveFormFields({ fields, fieldRows, user: activeUser });
82
+ return {
83
+ fieldRows,
84
+ activeUser,
85
+ customSelectFieldAnswers,
86
+ customBooleanFieldAnswers,
87
+ customTextFieldAnswers,
88
+ contentEntity,
89
+ content: { id: content.id, type: content.type, name: content.name },
90
+ title,
91
+ callToAction,
92
+ buttonLabel,
93
+ dismissLabel,
94
+ dismissible: Boolean(profileState.dismissible),
95
+ recaptchaSiteKey,
96
+ userId,
97
+ audience: hasUser ? "authenticated" : "identified",
98
+ idxEntity: `identity-x.${identityX.config.getAppId()}.app-user*${userId}`,
99
+ fieldIds: fieldRows.reduce((arr, row) => [...arr, ...row.map((col) => col.id)], []),
100
+ additionalEventData,
101
+ loginSource: "progressiveProfile",
102
+ endpoints: identityX.config.getEndpoints(),
103
+ enableChangeEmail: identityX.config.get("enableChangeEmail"),
104
+ defaultFieldLabels: identityX.config.get("defaultFieldLabels"),
105
+ lang,
106
+ };
107
+ };
108
+
109
+ out.w("<div class=\"content-progressive-profile-idx__wrapper\">");
110
+
111
+ if (!hasUser) {
112
+ marko_web_identity_x_non_auth_identify_tag({
113
+ renderBody: function(out, { user: identifiedUser }) {
114
+ if (identifiedUser) {
115
+ marko_web_browser_component_tag({
116
+ name: "IdentityXProgressiveProfile",
117
+ props: buildProps(identifiedUser)
118
+ }, out, __component, "3");
119
+ }
120
+ }
121
+ }, out, __component, "2");
122
+ } else {
123
+ marko_web_browser_component_tag({
124
+ name: "IdentityXProgressiveProfile",
125
+ props: buildProps(user)
126
+ }, out, __component, "4");
127
+ }
128
+
129
+ out.w("</div>");
130
+ }
131
+ }, out, __component, "0");
132
+ }
133
+ }
134
+
135
+ marko_template._ = marko_renderer(render, {
136
+ e_: marko_componentType
137
+ }, marko_component);
138
+
139
+ marko_template.meta = {
140
+ id: "/@mindful-web/marko-web-identity-x$1.85.2/components/progressive-profile-form.marko",
141
+ component: "./progressive-profile-form.marko",
142
+ tags: [
143
+ "@mindful-web/marko-web/components/browser-component.marko",
144
+ "./non-auth-identify.marko",
145
+ "./context.marko"
146
+ ]
147
+ };
@@ -2,7 +2,7 @@
2
2
  "use strict";
3
3
 
4
4
  var marko_template = module.exports = require("marko/dist/html").t(__filename),
5
- marko_componentType = "/@mindful-web/marko-web-identity-x$1.83.1/components/subscribe.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.2/components/subscribe.marko",
6
6
  marko_component = require("./subscribe.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -73,7 +73,7 @@ marko_template._ = marko_renderer(render, {
73
73
  }, marko_component);
74
74
 
75
75
  marko_template.meta = {
76
- id: "/@mindful-web/marko-web-identity-x$1.83.1/components/subscribe.marko",
76
+ id: "/@mindful-web/marko-web-identity-x$1.85.2/components/subscribe.marko",
77
77
  component: "./subscribe.marko",
78
78
  tags: [
79
79
  "@mindful-web/marko-web/components/browser-component.marko",
package/config.js CHANGED
@@ -5,6 +5,80 @@ const defaultAddRequestAttributes = require('./utils/add-custom-attributes');
5
5
 
6
6
  const { log } = console;
7
7
 
8
+ /**
9
+ * The only column types the progressive-profile gate may ask for.
10
+ *
11
+ * A strict subset of what `custom-column.vue` can draw, and narrower than
12
+ * `get-missing-progressive-fields.js` supports, for two independent reasons:
13
+ *
14
+ * - **`built-in` is unwritable.** The shipped `progressiveProfileUpdate` mutation is
15
+ * attributes-only (core/root member fields were cut on review), so a built-in column renders,
16
+ * accepts a value, and is then silently dropped: the user submits, gets `changed: false`, and is
17
+ * re-gated forever. This is the only one of these checks whose runtime failure is *silent*.
18
+ * - **`custom-boolean` is unsatisfiable.** `custom-column.vue` binds the checkbox to
19
+ * `customField.answer` but its change handler writes `customField.value`, so the box never
20
+ * registers as checked — and a *required* checkbox that cannot be checked traps the user behind
21
+ * HTML5 validation. The diff util, the payload builder and the mutation all handle booleans
22
+ * correctly; only the shared renderer does not, and that is fixed in the separate shared-code
23
+ * PR rather than here. Remove `custom-boolean` from this list once it is.
24
+ *
25
+ * No coverage is lost: every field the fleet-wide impact analysis measured is a select attribute.
26
+ */
27
+ const PROGRESSIVE_PROFILE_TYPES = ['custom-select', 'custom-text'];
28
+ const PROGRESSIVE_PROFILE_UNWRITABLE_TYPES = ['built-in'];
29
+
30
+ /**
31
+ * Fails the boot rather than shipping a configuration that presents to a real visitor as a gate
32
+ * they cannot get past, or that collects nothing while appearing to work. Failing the deploy beats
33
+ * discovering either as a loop.
34
+ *
35
+ * @param {import('.').CustomColumnDefinition[][]} fieldRows
36
+ * @param {string[]} activeCustomFieldIds
37
+ * @param {string} [apiToken]
38
+ */
39
+ const validateProgressiveProfileFieldRows = (fieldRows, activeCustomFieldIds, apiToken) => {
40
+ const bail = (msg) => { throw new Error(`Unable to configure IdentityX: ${msg}`); };
41
+ const allowed = new Set(PROGRESSIVE_PROFILE_TYPES);
42
+ const active = new Set(activeCustomFieldIds);
43
+ const rows = getAsArray({ fieldRows }, 'fieldRows');
44
+
45
+ if (rows.length) {
46
+ // Every write on the identified path goes through `getOrgUserApiToken()`.
47
+ if (!apiToken) {
48
+ bail('`progressiveProfileFieldRows` is configured but no `apiToken` was provided. Progressive profiling writes with the org API token.');
49
+ }
50
+ // The submit route scores every submission with reCAPTCHA v3, the same way every other write
51
+ // form on the fleet does. Without keys, every submit scores as unverified and every answer is
52
+ // silently discarded -- the gate would look like it works and collect nothing at all.
53
+ if (!process.env.RECAPTCHA_V3_SITE_KEY || !process.env.RECAPTCHA_V3_SECRET_KEY) {
54
+ bail('`progressiveProfileFieldRows` is configured but reCAPTCHA is not. Set RECAPTCHA_V3_SITE_KEY and RECAPTCHA_V3_SECRET_KEY.');
55
+ }
56
+ }
57
+
58
+ rows.forEach((row, ridx) => {
59
+ getAsArray({ row }, 'row').forEach((col, cidx) => {
60
+ const at = `progressiveProfileFieldRows[${ridx}][${cidx}]`;
61
+ if (!col || typeof col !== 'object') bail(`${at} is not a field definition.`);
62
+ const { id, type } = col;
63
+
64
+ if (PROGRESSIVE_PROFILE_UNWRITABLE_TYPES.includes(type)) {
65
+ bail(`${at} is a "${type}" field. Progressive profiling can only write member attributes (${PROGRESSIVE_PROFILE_TYPES.join(', ')}); a "${type}" field would render, accept an answer, and then be silently discarded, re-gating the user on every page.`);
66
+ }
67
+ if (!allowed.has(type)) {
68
+ // Anything else falls through to `custom-column.vue`'s final `<pre v-else>` debug block,
69
+ // printing raw field metadata to a real visitor.
70
+ bail(`${at} has unsupported type "${type}". Progressive profiling supports: ${PROGRESSIVE_PROFILE_TYPES.join(', ')}.`);
71
+ }
72
+ if (!id) bail(`${at} has no "id". Custom fields are addressed by id, not key.`);
73
+ if (active.size && !active.has(id)) {
74
+ // `routes/profile.js` filters custom answers through this allowlist, so an id missing
75
+ // from it is silently discarded on any profile submit -- and the gate never clears.
76
+ bail(`${at} ("${id}") is not present in "activeCustomFieldIds". Add it, or the answer will be silently discarded.`);
77
+ }
78
+ });
79
+ });
80
+ };
81
+
8
82
  class IdentityXConfiguration {
9
83
  /**
10
84
  *
@@ -14,6 +88,10 @@ class IdentityXConfiguration {
14
88
  * @param {string[]} [options.requiredServerFields] Required fields, server enforced.
15
89
  * @param {string[]} [options.requiredClientFields] Required fields, client-side only.
16
90
  * @param {string[]} [options.requiredCreateFieldRows] Required fields, when creating a new user.
91
+ * @param {import('.').CustomColumnDefinition[][]} [options.progressiveProfileFieldRows] The
92
+ * questions the progressive-profile gate may ask an already-known visitor. Attributes only
93
+ * (`custom-select` / `custom-text`) — a `built-in` or `custom-boolean` column throws at boot;
94
+ * see `validateProgressiveProfileFieldRows` for why each is a user-facing trap.
17
95
  * @param {string[]} [options.activeCustomFieldIds] Limit displayed custom fields, if present.
18
96
  * @param {string[]} [options.hiddenFields] The fields to hide from the profile.
19
97
  * @param {function} [options.onHookError] Reporter for non-fatal errors. Called as
@@ -42,6 +120,7 @@ class IdentityXConfiguration {
42
120
  requiredServerFields = [],
43
121
  requiredClientFields = [],
44
122
  requiredCreateFieldRows = [],
123
+ progressiveProfileFieldRows = [],
45
124
  activeCustomFieldIds = [],
46
125
  additionalCustomFieldIds = [],
47
126
  defaultFieldLabels = {},
@@ -55,6 +134,11 @@ class IdentityXConfiguration {
55
134
  ...rest
56
135
  } = {}) {
57
136
  if (!appId) throw new Error('Unable to configure IdentityX: no Application ID was provided.');
137
+ validateProgressiveProfileFieldRows(
138
+ progressiveProfileFieldRows,
139
+ activeCustomFieldIds,
140
+ apiToken,
141
+ );
58
142
  this.appId = appId;
59
143
  this.apiToken = apiToken;
60
144
  this.options = {
@@ -62,6 +146,7 @@ class IdentityXConfiguration {
62
146
  requiredServerFields,
63
147
  requiredClientFields,
64
148
  requiredCreateFieldRows,
149
+ progressiveProfileFieldRows,
65
150
  activeCustomFieldIds,
66
151
  additionalCustomFieldIds,
67
152
  defaultFieldLabels,
@@ -159,6 +244,19 @@ class IdentityXConfiguration {
159
244
  return this.getAsArray('requiredCreateFieldRows');
160
245
  }
161
246
 
247
+ /**
248
+ * The per-site progressive-profile questions, in the same `CustomColumnDefinition[][]` shape as
249
+ * `requiredCreateFieldRows` — so per-site custom-select ids work and `browser/custom-column.vue`
250
+ * renders them unchanged.
251
+ *
252
+ * Empty by default: the whole feature is inert fleet-wide until a site declares fields *and*
253
+ * enables `progressiveProfile` in its site config. Validated at boot (see
254
+ * `validateProgressiveProfileFieldRows`).
255
+ */
256
+ getProgressiveProfileFieldRows() {
257
+ return this.getAsArray('progressiveProfileFieldRows');
258
+ }
259
+
162
260
  getHiddenFields() {
163
261
  return this.getAsArray('hiddenFields');
164
262
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindful-web/marko-web-identity-x",
3
- "version": "1.83.1",
3
+ "version": "1.85.2",
4
4
  "description": "Marko Wrapper for IdentityX",
5
5
  "repository": "https://github.com/parameter1/mindful-web/tree/main/packages/marko-web-identity-x",
6
6
  "author": "Josh Worden <josh@parameter1.com>",
@@ -14,6 +14,7 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "@mindful-web/marko-web-gtm": "^1.83.1",
17
+ "@mindful-web/marko-web-recaptcha": "^1.83.1",
17
18
  "@mindful-web/object-path": "^1.83.1",
18
19
  "@mindful-web/utils": "^1.83.1",
19
20
  "apollo-cache-inmemory": "^1.6.6",
@@ -38,5 +39,5 @@
38
39
  "publishConfig": {
39
40
  "access": "public"
40
41
  },
41
- "gitHead": "c9507129d0e30847cb2b2a941cb383746d1fd501"
42
+ "gitHead": "96764a47941171b3da9865ac201c891494926f37"
42
43
  }
package/routes/google.js CHANGED
@@ -105,6 +105,11 @@ module.exports = asyncRoute(async (req, res) => {
105
105
  // the request dies is the step that failed, so an error arrives already
106
106
  // localized instead of being a bare GraphQL message to reverse-engineer.
107
107
  //
108
+ // The sink itself defaults to `utils/add-custom-attributes.js` (New Relic,
109
+ // inert without the agent) and is overridable per site -- see the
110
+ // `addRequestAttributes` JSDoc in `config.js`. Everything below this line is
111
+ // about the *attributes*, not where they go.
112
+ //
108
113
  // Timing comes in two parts, because they answer different questions.
109
114
  //
110
115
  // `idxGoogleStepMs` is elapsed-at-last-step: last-write-wins, so on a completed
package/routes/index.js CHANGED
@@ -16,6 +16,7 @@ const login = require('./login');
16
16
  const loginFields = require('./login-fields');
17
17
  const logout = require('./logout');
18
18
  const profile = require('./profile');
19
+ const progressiveProfile = require('./progressive-profile');
19
20
  const regions = require('./regions');
20
21
 
21
22
  const router = Router();
@@ -37,6 +38,8 @@ router.post('/login-fields', loginFields);
37
38
  router.post('/login', login);
38
39
  router.post('/logout', logout);
39
40
  router.post('/profile', profile);
41
+ router.post('/progressive-profile/dismiss', progressiveProfile.dismiss);
42
+ router.post('/progressive-profile', progressiveProfile.submit);
40
43
  router.use(jsonErrorHandler());
41
44
 
42
45
  module.exports = router;