@mindful-web/marko-web-identity-x 1.85.3 → 1.85.4

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.
package/CLAUDE.md CHANGED
@@ -348,7 +348,7 @@ configurations that are each a user-facing trap:
348
348
  | Boot check | Why the runtime failure is worse than a failed deploy |
349
349
  | --- | --- |
350
350
  | `type: 'built-in'` | Unwritable ⇒ permanent gate loop. **Silent** — the only one of these with no visible symptom until a user is stuck. |
351
- | `type: 'custom-boolean'` | `custom-column.vue` binds the checkbox to `customField.answer` but its handler writes `customField.value`, so a *required* checkbox can never be checked. The diff util, payload builder and mutation all handle booleans correctly — only the shared renderer does not. Re-allow once that is fixed. |
351
+ | `type: 'custom-boolean'` | The renderer bug that once made it unsatisfiable is fixed, but the gate forces every column `required: true`, and a *required* HTML5 checkbox can only ever be submitted checked — a progressive boolean could only ever record `true`, a toll rather than an answer. Re-allow only if the gate ever supports optional columns or explicit yes/no booleans. |
352
352
  | any other type | Falls through to `custom-column.vue`'s `<pre v-else>` debug block, printing raw field metadata to a real visitor. |
353
353
  | id missing from a non-empty `activeCustomFieldIds` | `routes/profile.js:88-104` silently discards the answer ⇒ gate loop. |
354
354
  | rows configured with no `apiToken` | Every write uses `getOrgUserApiToken()` and the form token is signed from it. |
@@ -358,11 +358,11 @@ No coverage is lost: every field the fleet-wide impact analysis measured is a se
358
358
  ### Files
359
359
 
360
360
  - `utils/get-missing-progressive-fields.js` — the diff. Matches on **`answer.field.id`**, never
361
- `answer.id`. That distinction is the whole reason this feature does not reuse
362
- `get-form-custom-fields.js` (or `config.getGTMUserData`): an idx answer's `id` is the *answer's*
363
- id, so comparing it to a configured field id never matches, and the truthy-empty-array bug that
364
- follows returns an answer object with no `field`. Returns `missingRows` with every column forced
365
- `required: true`, so `custom-column.vue` needs no changes.
361
+ `answer.id`. That distinction is why this feature did not reuse `get-form-custom-fields.js`
362
+ (or `config.getGTMUserData`) at ship time: that util keyed on the answer's own `id` and its
363
+ truthy-empty-array check returned answer objects with no `field` — both fixed in
364
+ mindful-web#324, but the dedicated util stays (it also forces every column `required: true` in
365
+ the `missingRows` it returns, so `custom-column.vue` needs no changes).
366
366
  - `utils/get-progressive-form-fields.js` — correct, feature-scoped replacement for
367
367
  `get-form-custom-fields.js`. Stamps `id` with the **field** id, which is the contract
368
368
  `custom-column.vue` reads and what `get-create-user-custom-fields.js` already emits.
@@ -612,18 +612,19 @@ reaches the bridge. The content entity is passed as `emit`'s third argument inst
612
612
  Submitted **answer values are never tracked** anywhere — `fieldKeys` and the member-event's
613
613
  `fieldIds` record which questions were asked, nothing more.
614
614
 
615
- ### Two live follow-ups, neither in this package
616
-
617
- 1. 🔴 **Index drop/recreate, production.** `completed_auth_date_website_channel` and
618
- `completed_auth_email_website_channel` had their `partialFilterExpression` widened; a changed
619
- partial filter does not alter an index in place, so both must be dropped and recreated. Until
620
- then progressive-profiling `member-events` queries collection-scan. Owner: audience/infra.
621
- 2. **The shared-code PR.** `get-form-custom-fields.js`, the commented-out `custom-boolean` branches
622
- in `generate-required-field-payload.js`, the `routes/profile.js` allowlist, `custom-column.vue`'s
623
- boolean binding, and the p1events bridge's `contentGatingType`/`contentGateType` mismatch (every
624
- producer in the fleet sets the latter, so gate type has **never** reached p1events for any
625
- gate). All shared with live flows across 18 repos — deliberately not fixed alongside a new
626
- feature.
615
+ ### Follow-ups — both resolved 2026-08-15
616
+
617
+ 1. ✅ **Index drop/recreate, production** — done. Both `completed_auth_*` indexes in production
618
+ carry `PROGRESSIVE_PROFILE_SUBMIT` in their partial filters (verified on `audience:abmedia`
619
+ and `audience:transpire`).
620
+ 2. ✅ **The shared-code PR** — landed as mindful-web#324 (this package: `get-form-custom-fields.js`
621
+ field-id keying, `custom-column.vue` boolean binding, `routes/profile.js` allowlist,
622
+ `generate-required-field-payload.js` boolean branches + `hasAnswered` false-boolean fix) and
623
+ mindful-web#325 (the p1events bridge's `contentGatingType`→`contentGateType` rename — the gate
624
+ type had never reached p1events on any conversion event). The allured copied-meter frozen-clock
625
+ bug went separately as allured-business-media-websites#461. Progressive-profile field rows
626
+ still exclude `custom-boolean` — no longer for the renderer bug, but because the gate forces
627
+ required columns and a required checkbox can only submit `true` (see the boot-check table).
627
628
 
628
629
  ## Fleet / rollout
629
630
 
@@ -353,9 +353,14 @@ export default {
353
353
  /**
354
354
  *
355
355
  */
356
- onCustomBooleanChange(field) {
356
+ onCustomBooleanChange(field, checked) {
357
+ // `.answer` is canonical (it's what the template binds and what `routes/profile.js`
358
+ // reads); `.value` is kept in sync because the payload builders read it. Assign from the
359
+ // emitted event rather than toggling, so the model can never invert against the checkbox.
357
360
  // eslint-disable-next-line no-param-reassign
358
- field.value = !field.value;
361
+ field.answer = checked;
362
+ // eslint-disable-next-line no-param-reassign
363
+ field.value = checked;
359
364
  // eslint-disable-next-line no-param-reassign
360
365
  field.hasAnswered = true;
361
366
  this.user.customBooleanFieldAnswers = this.customBooleanFieldAnswers;
@@ -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.85.3/components/access.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/access.marko",
6
6
  marko_component = require("./access.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -53,7 +53,7 @@ marko_template._ = marko_renderer(render, {
53
53
  }, marko_component);
54
54
 
55
55
  marko_template.meta = {
56
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/access.marko",
56
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/access.marko",
57
57
  component: "./access.marko",
58
58
  tags: [
59
59
  "@mindful-web/marko-core/components/resolve.marko"
@@ -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.85.3/components/comment-stream.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/comment-stream.marko",
6
6
  marko_component = require("./comment-stream.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -91,7 +91,7 @@ marko_template._ = marko_renderer(render, {
91
91
  }, marko_component);
92
92
 
93
93
  marko_template.meta = {
94
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/comment-stream.marko",
94
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/comment-stream.marko",
95
95
  component: "./comment-stream.marko",
96
96
  tags: [
97
97
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -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.85.3/components/context.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/context.marko",
6
6
  marko_component = require("./context.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  marko_dynamicTag = require("marko/dist/runtime/helpers/dynamic-tag"),
@@ -49,7 +49,7 @@ marko_template._ = marko_renderer(render, {
49
49
  }, marko_component);
50
50
 
51
51
  marko_template.meta = {
52
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/context.marko",
52
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/context.marko",
53
53
  component: "./context.marko",
54
54
  tags: [
55
55
  "@mindful-web/marko-core/components/resolve.marko"
@@ -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.85.3/components/form-access.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/form-access.marko",
6
6
  marko_component = require("./form-access.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_defaultValue = require("@mindful-web/marko-core/utils/default-value"),
@@ -179,7 +179,7 @@ marko_template._ = marko_renderer(render, {
179
179
  }, marko_component);
180
180
 
181
181
  marko_template.meta = {
182
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/form-access.marko",
182
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/form-access.marko",
183
183
  component: "./form-access.marko",
184
184
  tags: [
185
185
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -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.85.3/components/form-authenticate.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/form-authenticate.marko",
6
6
  marko_component = require("./form-authenticate.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -81,7 +81,7 @@ marko_template._ = marko_renderer(render, {
81
81
  }, marko_component);
82
82
 
83
83
  marko_template.meta = {
84
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/form-authenticate.marko",
84
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/form-authenticate.marko",
85
85
  component: "./form-authenticate.marko",
86
86
  tags: [
87
87
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -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.85.3/components/form-change-email.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/form-change-email.marko",
6
6
  marko_component = require("./form-change-email.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -59,7 +59,7 @@ marko_template._ = marko_renderer(render, {
59
59
  }, marko_component);
60
60
 
61
61
  marko_template.meta = {
62
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/form-change-email.marko",
62
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/form-change-email.marko",
63
63
  component: "./form-change-email.marko",
64
64
  tags: [
65
65
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -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.85.3/components/form-login.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/form-login.marko",
6
6
  marko_component = require("./form-login.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -92,7 +92,7 @@ marko_template._ = marko_renderer(render, {
92
92
  }, marko_component);
93
93
 
94
94
  marko_template.meta = {
95
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/form-login.marko",
95
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/form-login.marko",
96
96
  component: "./form-login.marko",
97
97
  tags: [
98
98
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -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.85.3/components/form-logout.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/form-logout.marko",
6
6
  marko_renderer = require("marko/dist/runtime/components/renderer"),
7
7
  module_defaultValue = require("@mindful-web/marko-core/utils/default-value"),
8
8
  defaultValue = module_defaultValue.default || module_defaultValue,
@@ -29,7 +29,7 @@ marko_template._ = marko_renderer(render, {
29
29
  });
30
30
 
31
31
  marko_template.meta = {
32
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/form-logout.marko",
32
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/form-logout.marko",
33
33
  tags: [
34
34
  "@mindful-web/marko-web/components/browser-component.marko"
35
35
  ]
@@ -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.85.3/components/form-profile.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/form-profile.marko",
6
6
  marko_component = require("./form-profile.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -84,7 +84,7 @@ marko_template._ = marko_renderer(render, {
84
84
  }, marko_component);
85
85
 
86
86
  marko_template.meta = {
87
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/form-profile.marko",
87
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/form-profile.marko",
88
88
  component: "./form-profile.marko",
89
89
  tags: [
90
90
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -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.85.3/components/form-register.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/form-register.marko",
6
6
  marko_renderer = require("marko/dist/runtime/components/renderer"),
7
7
  marko_assign = require("marko/dist/runtime/helpers/assign"),
8
8
  marko_web_identity_x_form_login_template = require("./form-login.marko"),
@@ -25,7 +25,7 @@ marko_template._ = marko_renderer(render, {
25
25
  });
26
26
 
27
27
  marko_template.meta = {
28
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/form-register.marko",
28
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/form-register.marko",
29
29
  tags: [
30
30
  "./form-login.marko"
31
31
  ]
@@ -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.85.3/components/google-init.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/google-init.marko",
6
6
  marko_renderer = require("marko/dist/runtime/components/renderer"),
7
7
  marko_attr = require("marko/dist/runtime/html/helpers/attr"),
8
8
  marko_str = require("marko/dist/runtime/helpers/to-string");
@@ -239,5 +239,5 @@ marko_template._ = marko_renderer(render, {
239
239
  });
240
240
 
241
241
  marko_template.meta = {
242
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/google-init.marko"
242
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/google-init.marko"
243
243
  };
@@ -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.85.3/components/google-sign-in-button.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/google-sign-in-button.marko",
6
6
  marko_renderer = require("marko/dist/runtime/components/renderer"),
7
7
  helpers_escape_xml = require("marko/dist/runtime/html/helpers/escape-xml"),
8
8
  marko_escapeXml = helpers_escape_xml.x,
@@ -40,5 +40,5 @@ marko_template._ = marko_renderer(render, {
40
40
  });
41
41
 
42
42
  marko_template.meta = {
43
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/google-sign-in-button.marko"
43
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/google-sign-in-button.marko"
44
44
  };
@@ -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.85.3/components/identify.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/identify.marko",
6
6
  marko_component = require("./identify.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_objectPath_module = require("@mindful-web/object-path"),
@@ -50,7 +50,7 @@ marko_template._ = marko_renderer(render, {
50
50
  }, marko_component);
51
51
 
52
52
  marko_template.meta = {
53
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/identify.marko",
53
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/identify.marko",
54
54
  component: "./identify.marko",
55
55
  tags: [
56
56
  "@mindful-web/marko-web-gtm/components/push.marko",
@@ -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.85.3/components/non-auth-identify.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/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"),
@@ -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.85.3/components/non-auth-identify.marko",
48
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/non-auth-identify.marko",
49
49
  component: "./non-auth-identify.marko",
50
50
  tags: [
51
51
  "@mindful-web/marko-core/components/resolve.marko"
@@ -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.85.3/components/progressive-profile-form.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/components/progressive-profile-form.marko",
6
6
  marko_component = require("./progressive-profile-form.marko"),
7
7
  marko_renderer = require("marko/dist/runtime/components/renderer"),
8
8
  module_defaultValue = require("@mindful-web/marko-core/utils/default-value"),
@@ -137,7 +137,7 @@ marko_template._ = marko_renderer(render, {
137
137
  }, marko_component);
138
138
 
139
139
  marko_template.meta = {
140
- id: "/@mindful-web/marko-web-identity-x$1.85.3/components/progressive-profile-form.marko",
140
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/components/progressive-profile-form.marko",
141
141
  component: "./progressive-profile-form.marko",
142
142
  tags: [
143
143
  "@mindful-web/marko-web/components/browser-component.marko",
@@ -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.85.3/components/subscribe.marko",
5
+ marko_componentType = "/@mindful-web/marko-web-identity-x$1.85.4/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.85.3/components/subscribe.marko",
76
+ id: "/@mindful-web/marko-web-identity-x$1.85.4/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
@@ -15,12 +15,12 @@ const { log } = console;
15
15
  * attributes-only (core/root member fields were cut on review), so a built-in column renders,
16
16
  * accepts a value, and is then silently dropped: the user submits, gets `changed: false`, and is
17
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.
18
+ * - **`custom-boolean` is semantically useless here.** The renderer bug that once made it
19
+ * unsatisfiable (custom-column.vue read `.answer` but wrote `.value`) is fixed, and the diff
20
+ * util, payload builder, and mutation all handle booleans — but the gate forces every column
21
+ * `required: true`, and a *required* HTML5 checkbox can only ever be submitted checked. A
22
+ * progressive boolean could therefore only ever record `true`, which is not an answer, it's a
23
+ * toll. Re-allow only if the gate ever supports optional columns or explicit yes/no booleans.
24
24
  *
25
25
  * No coverage is lost: every field the fleet-wide impact analysis measured is a select attribute.
26
26
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindful-web/marko-web-identity-x",
3
- "version": "1.85.3",
3
+ "version": "1.85.4",
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>",
@@ -10,7 +10,7 @@
10
10
  "lint": "eslint --ext .js --ext .vue --max-warnings 5 ./",
11
11
  "compile": "mindful-web-marko-compile compile",
12
12
  "prepublish": "yarn compile --silent",
13
- "test": "yarn compile --no-clean && yarn lint"
13
+ "test": "yarn compile --no-clean && yarn lint && mocha --reporter spec"
14
14
  },
15
15
  "dependencies": {
16
16
  "@mindful-web/marko-web-gtm": "^1.83.1",
@@ -39,5 +39,9 @@
39
39
  "publishConfig": {
40
40
  "access": "public"
41
41
  },
42
- "gitHead": "34418dcc1061957e9782f403c17093281f08c07d"
42
+ "devDependencies": {
43
+ "chai": "^4.3.7",
44
+ "mocha": "^6.2.3"
45
+ },
46
+ "gitHead": "e793692becf12f650680a93a12f36f227aaace30"
43
47
  }
package/routes/profile.js CHANGED
@@ -85,13 +85,17 @@ module.exports = asyncRoute(async (req, res) => {
85
85
  receiveEmail,
86
86
  };
87
87
 
88
- // get identity-x.forms custom-select question ids to append to ActiveCustomQuestionIds
88
+ // get identity-x.forms custom question ids to append to ActiveCustomQuestionIds
89
89
  const customFormFieldIds = Object.values(getAsObject(identityX, 'config.options.forms')).reduce((questions, form) => {
90
90
  // loop over the fieldRows
91
91
  getAsArray(form, 'fieldRows').forEach((row) => row.forEach((question) => {
92
- // push the custom-select form being used with those forms to acceptable profile inputs
93
- // @todo figure out if other types should also make it... custom-boolean specifically.
94
- if (question.type === 'custom-select') questions.push(question.id);
92
+ // Every custom question a form renders must be writable on submit. This list filters all
93
+ // three answer mutations below, so omitting a type here (custom-select-only, previously)
94
+ // silently discarded that form's boolean/text answers unless the site also listed the id
95
+ // in `activeCustomFieldIds`.
96
+ if (question.id && typeof question.type === 'string' && question.type.startsWith('custom-')) {
97
+ questions.push(question.id);
98
+ }
95
99
  }));
96
100
  return questions;
97
101
  }, []);
@@ -0,0 +1,2 @@
1
+ --recursive
2
+ --timeout 2000
@@ -0,0 +1,92 @@
1
+ const { describe, it } = require('mocha');
2
+ const { expect } = require('chai');
3
+ const generate = require('../../utils/generate-required-field-payload');
4
+
5
+ const boolDef = {
6
+ type: 'custom-boolean', id: 'bool-1', key: 'bool-1', required: true,
7
+ };
8
+ const selectDef = {
9
+ type: 'custom-select', id: 'select-1', key: 'select-1', required: true,
10
+ };
11
+
12
+ const boolAnswer = (answer, { fieldId = 'bool-1', hasAnswered = true } = {}) => ({
13
+ id: fieldId, hasAnswered, answer, value: answer, field: { id: fieldId },
14
+ });
15
+ const selectAnswer = (optionIds, { fieldId = 'select-1' } = {}) => ({
16
+ id: fieldId,
17
+ hasAnswered: Boolean(optionIds.length),
18
+ answers: optionIds.map((id) => ({ id })),
19
+ field: { id: fieldId },
20
+ });
21
+
22
+ describe('utils/generate-required-field-payload', () => {
23
+ describe('required custom-boolean fields', () => {
24
+ it('should treat an answered `false` as answered', () => {
25
+ const out = generate({
26
+ appUser: { customBooleanFieldAnswers: [boolAnswer(false)] },
27
+ body: {},
28
+ definitions: [boolDef],
29
+ });
30
+ expect(out.appUserHasAllRequiredFields).to.equal(true);
31
+ expect(out.hasAllRequiredFields).to.equal(true);
32
+ });
33
+
34
+ it('should treat an unanswered boolean as missing', () => {
35
+ const out = generate({
36
+ appUser: { customBooleanFieldAnswers: [boolAnswer(null, { hasAnswered: false })] },
37
+ body: {},
38
+ definitions: [boolDef],
39
+ });
40
+ expect(out.appUserHasAllRequiredFields).to.equal(false);
41
+ expect(out.hasAllRequiredFields).to.equal(false);
42
+ });
43
+
44
+ it('should satisfy requirements from the submitted body and build the payload', () => {
45
+ const out = generate({
46
+ appUser: {},
47
+ body: { customBooleanFieldAnswers: [boolAnswer(true)] },
48
+ definitions: [boolDef],
49
+ });
50
+ expect(out.appUserHasAllRequiredFields).to.equal(false);
51
+ expect(out.hasAllRequiredFields).to.equal(true);
52
+ expect(out.payload.customBooleanFieldAnswers).to.deep.equal([
53
+ { fieldId: 'bool-1', value: true },
54
+ ]);
55
+ });
56
+
57
+ it('should carry a submitted `false` into the payload as false', () => {
58
+ const out = generate({
59
+ appUser: {},
60
+ body: { customBooleanFieldAnswers: [boolAnswer(false)] },
61
+ definitions: [boolDef],
62
+ });
63
+ expect(out.hasAllRequiredFields).to.equal(true);
64
+ expect(out.payload.customBooleanFieldAnswers).to.deep.equal([
65
+ { fieldId: 'bool-1', value: false },
66
+ ]);
67
+ });
68
+ });
69
+
70
+ describe('required custom-select fields (existing behavior preserved)', () => {
71
+ it('should treat an empty answers array as unanswered', () => {
72
+ const out = generate({
73
+ appUser: { customSelectFieldAnswers: [selectAnswer([])] },
74
+ body: {},
75
+ definitions: [selectDef],
76
+ });
77
+ expect(out.appUserHasAllRequiredFields).to.equal(false);
78
+ });
79
+
80
+ it('should satisfy from the app user and exclude satisfied fields from the body payload', () => {
81
+ const out = generate({
82
+ appUser: { customSelectFieldAnswers: [selectAnswer(['opt-1'])] },
83
+ body: {},
84
+ definitions: [selectDef],
85
+ });
86
+ expect(out.appUserHasAllRequiredFields).to.equal(true);
87
+ expect(out.payload.customSelectFieldAnswers).to.deep.equal([
88
+ { fieldId: 'select-1', optionIds: ['opt-1'], writeInValues: [] },
89
+ ]);
90
+ });
91
+ });
92
+ });
@@ -0,0 +1,76 @@
1
+ const { describe, it } = require('mocha');
2
+ const { expect } = require('chai');
3
+ const getFormCustomFields = require('../../utils/get-form-custom-fields');
4
+
5
+ const fields = [
6
+ { id: 'bool-field-1', __typename: 'BooleanField', label: 'Opt in?' },
7
+ { id: 'select-field-1', __typename: 'SelectField', label: 'Title' },
8
+ { id: 'text-field-1', __typename: 'TextField', label: 'Nickname' },
9
+ ];
10
+ const fieldRows = [[{ id: 'bool-field-1' }, { id: 'select-field-1' }, { id: 'text-field-1' }]];
11
+
12
+ describe('utils/get-form-custom-fields', () => {
13
+ it('should return unanswered stubs carrying the field id when there is no user', () => {
14
+ const out = getFormCustomFields({ fields, fieldRows, user: null });
15
+ expect(out.customBooleanFieldAnswers).to.deep.equal([
16
+ {
17
+ id: 'bool-field-1', hasAnswered: false, answer: null, value: null, field: fields[0],
18
+ },
19
+ ]);
20
+ expect(out.customSelectFieldAnswers[0]).to.include({ id: 'select-field-1', hasAnswered: false });
21
+ expect(out.customTextFieldAnswers[0]).to.include({ id: 'text-field-1', hasAnswered: false });
22
+ });
23
+
24
+ it('should key user answers by field.id, not the answer document id', () => {
25
+ const user = {
26
+ customBooleanFieldAnswers: [],
27
+ customSelectFieldAnswers: [
28
+ // `id` here is an answer-document id that does NOT equal the field id.
29
+ {
30
+ id: 'answer-doc-9', hasAnswered: true, answers: [{ id: 'opt-1' }], field: { id: 'select-field-1' },
31
+ },
32
+ ],
33
+ customTextFieldAnswers: [],
34
+ };
35
+ const out = getFormCustomFields({ fields, fieldRows, user });
36
+ const [select] = out.customSelectFieldAnswers;
37
+ expect(select.hasAnswered).to.equal(true);
38
+ expect(select.answers).to.deep.equal([{ id: 'opt-1' }]);
39
+ // Normalized for custom-column.vue, which finds answers via `ans.id === fieldId`.
40
+ expect(select.id).to.equal('select-field-1');
41
+ expect(select.field).to.equal(fields[1]);
42
+ });
43
+
44
+ it('should return an unanswered stub (not an empty object) when the user has no matching answer', () => {
45
+ const user = {
46
+ customBooleanFieldAnswers: [
47
+ {
48
+ id: 'other', hasAnswered: true, answer: true, field: { id: 'some-other-field' },
49
+ },
50
+ ],
51
+ customSelectFieldAnswers: [],
52
+ customTextFieldAnswers: [],
53
+ };
54
+ const out = getFormCustomFields({ fields, fieldRows, user });
55
+ const [bool] = out.customBooleanFieldAnswers;
56
+ expect(bool.id).to.equal('bool-field-1');
57
+ expect(bool.hasAnswered).to.equal(false);
58
+ expect(bool.field).to.equal(fields[0]);
59
+ });
60
+
61
+ it('should preserve a false boolean answer', () => {
62
+ const user = {
63
+ customBooleanFieldAnswers: [
64
+ {
65
+ id: 'bool-field-1', hasAnswered: true, answer: false, value: false, field: { id: 'bool-field-1' },
66
+ },
67
+ ],
68
+ customSelectFieldAnswers: [],
69
+ customTextFieldAnswers: [],
70
+ };
71
+ const out = getFormCustomFields({ fields, fieldRows, user });
72
+ const [bool] = out.customBooleanFieldAnswers;
73
+ expect(bool.hasAnswered).to.equal(true);
74
+ expect(bool.answer).to.equal(false);
75
+ });
76
+ });
@@ -1,7 +1,13 @@
1
1
  const { get } = require('@mindful-web/object-path');
2
2
 
3
- // eslint-disable-next-line max-len
4
- const hasAnswered = (ans) => ans && ans.hasAnswered && (ans.value || (ans.answers && ans.answers.length));
3
+ const hasAnswered = (ans) => {
4
+ if (!ans || !ans.hasAnswered) return false;
5
+ // A boolean field is answered by having been answered — `false` is a legitimate value that
6
+ // must not read as missing, so never truth-test it. Boolean answers carry a boolean
7
+ // `answer`/`value`; selects carry `answers`, texts a string `value`.
8
+ if (typeof ans.answer === 'boolean' || typeof ans.value === 'boolean') return true;
9
+ return Boolean(ans.value || (ans.answers && ans.answers.length));
10
+ };
5
11
 
6
12
  const hasMappedAnswers = (map) => (id) => {
7
13
  const ans = map.get(id);
@@ -40,10 +46,10 @@ module.exports = ({ appUser, body, definitions }) => {
40
46
 
41
47
  const requiredFieldKeys = [...requiredFieldKeySet];
42
48
 
43
- // const requiredCustomBooleans = [...definitions.reduce((set, def) => {
44
- // if (def.type === 'custom-boolean' && def.required) set.add(def.id);
45
- // return set;
46
- // }, new Set())];
49
+ const requiredCustomBooleans = [...definitions.reduce((set, def) => {
50
+ if (def.type === 'custom-boolean' && def.required) set.add(def.id);
51
+ return set;
52
+ }, new Set())];
47
53
 
48
54
  const requiredCustomSelects = [...definitions.reduce((set, def) => {
49
55
  if (def.type === 'custom-select' && def.required) set.add(def.id);
@@ -56,15 +62,15 @@ module.exports = ({ appUser, body, definitions }) => {
56
62
  }, new Set())];
57
63
 
58
64
  const userCustomFieldAnswers = new Map([
59
- // ...(get(appUser, 'customBooleanFieldAnswers') || []).map((a) => [a.field.id, a]),
65
+ ...(get(appUser, 'customBooleanFieldAnswers') || []).map((a) => [a.field.id, a]),
60
66
  ...(get(appUser, 'customSelectFieldAnswers') || []).map((a) => [a.field.id, a]),
61
67
  ...(get(appUser, 'customTextFieldAnswers') || []).map((a) => [a.field.id, a]),
62
68
  ]);
63
69
 
64
70
  const customFieldAnswers = new Map([
65
71
  ...userCustomFieldAnswers,
66
- // ...(get(body, 'customBooleanFieldAnswers') || [])
67
- // .filter(hasAnswered).map((a) => [a.field.id, a]),
72
+ ...(get(body, 'customBooleanFieldAnswers') || [])
73
+ .filter(hasAnswered).map((a) => [a.field.id, a]),
68
74
  ...(get(body, 'customSelectFieldAnswers') || []).filter(hasAnswered).map((a) => [a.field.id, a]),
69
75
  ...(get(body, 'customTextFieldAnswers') || [])
70
76
  .filter(hasAnswered).map((a) => [a.field.id, a]),
@@ -74,9 +80,7 @@ module.exports = ({ appUser, body, definitions }) => {
74
80
  requiredFieldKeys.every(
75
81
  (key) => get(appUser, key),
76
82
  ),
77
- // requiredCustomBooleans.every(
78
- // (id) => userCustomFieldAnswers.has(id) && userCustomFieldAnswers.get(id).hasAnswered,
79
- // ),
83
+ requiredCustomBooleans.every(hasMappedAnswers(userCustomFieldAnswers)),
80
84
  requiredCustomSelects.every(hasMappedAnswers(userCustomFieldAnswers)),
81
85
  requiredCustomTexts.every(
82
86
  (id) => userCustomFieldAnswers.has(id) && userCustomFieldAnswers.get(id).hasAnswered,
@@ -87,14 +91,23 @@ module.exports = ({ appUser, body, definitions }) => {
87
91
  requiredFieldKeys.every(
88
92
  (key) => get(appUser, key) || get(body, key),
89
93
  ),
90
- // requiredCustomBooleans.every(hasMappedAnswers(customFieldAnswers)),
94
+ requiredCustomBooleans.every(hasMappedAnswers(customFieldAnswers)),
91
95
  requiredCustomSelects.every(hasMappedAnswers(customFieldAnswers)),
92
96
  requiredCustomTexts.every(hasMappedAnswers(customFieldAnswers)),
93
97
  ].every((v) => v);
94
98
 
95
99
  const payload = {
96
100
  ...(requiredFieldKeys.reduce((obj, key) => ({ ...obj, [key]: body[key] }), {})),
97
- // customBooleanFieldAnswers: [], // @todo
101
+ customBooleanFieldAnswers: requiredCustomBooleans
102
+ .filter(hasMappedAnswers(customFieldAnswers))
103
+ .map((id) => {
104
+ const answer = customFieldAnswers.get(id);
105
+ return {
106
+ fieldId: answer.field.id,
107
+ // `.answer` is canonical (what the profile route reads); fall back to `.value`.
108
+ value: Boolean(typeof answer.answer === 'boolean' ? answer.answer : answer.value),
109
+ };
110
+ }),
98
111
  customSelectFieldAnswers: requiredCustomSelects
99
112
  .filter(hasMappedAnswers(customFieldAnswers))
100
113
  .map((id) => {
@@ -11,44 +11,35 @@ module.exports = ({
11
11
  });
12
12
  });
13
13
 
14
+ // A user answer is keyed by its `field.id` — never the answer's own `id`, which some backends
15
+ // set to the answer document id. The returned object always carries `id` = the field id and
16
+ // `field` = the form's field definition, because `custom-column.vue` finds answers via
17
+ // `ans.id === fieldId` and renders from `customField.field`.
18
+ const findUserAnswer = (answers, f) => {
19
+ const found = (answers || []).find((a) => a && a.field && a.field.id === f.id);
20
+ return found ? { ...found, id: f.id, field: f } : null;
21
+ };
22
+
14
23
  // eslint-disable-next-line no-underscore-dangle
15
24
  const customBooleanFieldAnswers = fieldRowFields.filter((f) => f.__typename === 'BooleanField').map((f) => {
16
- if (user) {
17
- const userAnswer = user.customBooleanFieldAnswers.filter(({ id }) => id === f.id);
18
- if (userAnswer) {
19
- return ({
20
- ...userAnswer[0],
21
- });
22
- }
23
- }
25
+ const userAnswer = user ? findUserAnswer(user.customBooleanFieldAnswers, f) : null;
26
+ if (userAnswer) return userAnswer;
24
27
  return ({
25
28
  id: f.id, hasAnswered: false, answer: null, value: null, field: f,
26
29
  });
27
30
  });
28
- // eslint-disable-next-line no-underscore-dangle, eqeqeq
29
- const customSelectFieldAnswers = fieldRowFields.filter((f) => f.__typename == 'SelectField').map((f) => {
30
- if (user) {
31
- const userAnswer = user.customSelectFieldAnswers.filter(({ id }) => id === f.id);
32
- if (userAnswer) {
33
- return ({
34
- ...userAnswer[0],
35
- });
36
- }
37
- }
31
+ // eslint-disable-next-line no-underscore-dangle
32
+ const customSelectFieldAnswers = fieldRowFields.filter((f) => f.__typename === 'SelectField').map((f) => {
33
+ const userAnswer = user ? findUserAnswer(user.customSelectFieldAnswers, f) : null;
34
+ if (userAnswer) return userAnswer;
38
35
  return ({
39
36
  id: f.id, hasAnswered: false, answers: [], field: f,
40
37
  });
41
38
  });
42
- // eslint-disable-next-line no-underscore-dangle, eqeqeq
43
- const customTextFieldAnswers = fieldRowFields.filter((f) => f.__typename == 'TextField').map((f) => {
44
- if (user) {
45
- const userAnswer = user.customTextFieldAnswers.filter(({ id }) => id === f.id);
46
- if (userAnswer) {
47
- return ({
48
- ...userAnswer[0],
49
- });
50
- }
51
- }
39
+ // eslint-disable-next-line no-underscore-dangle
40
+ const customTextFieldAnswers = fieldRowFields.filter((f) => f.__typename === 'TextField').map((f) => {
41
+ const userAnswer = user ? findUserAnswer(user.customTextFieldAnswers, f) : null;
42
+ if (userAnswer) return userAnswer;
52
43
  return ({
53
44
  id: f.id, hasAnswered: false, value: null, field: f,
54
45
  });