@mx-cartographer/experiences 9.7.98 → 9.7.99

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/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [9.7.99] - 11-04-2026
2
+
3
+ - **CHANGED** - The `transaction_list_block` drilldown title is driven by the CTA variant (via a variant-to-title map): the `generic_list` variant drawer is titled "Transactions" (`copy.transactions.transactions_title`), while `summary_header`, `standard` and variant-less drawers keep the beat's own title. Weekly New Merchants V3's drawer (CTA variant `generic_list`) now reads "Transactions" instead of the card's "Purchases for Review" (WEC-2189)
4
+
1
5
  ## [9.7.98] - 11-04-2026
2
6
 
3
7
  - **FIXED** - Celebrate Credit Card Payoff card spacing matches Figma (WEC-2170)
@@ -1,13 +1,15 @@
1
1
  import { Beat, GlobalCopy, InDescriptionCTA, PrimaryCTA, SecondaryCTA } from '../../../core';
2
2
  type DrilldownCta = PrimaryCTA | SecondaryCTA | InDescriptionCTA;
3
- /** The CTA variant that opens the summary-header drilldown. */
4
- export declare const SUMMARY_HEADER_VARIANT = "summary_header";
3
+ /** The transaction_list_block CTA variants. */
4
+ export declare const TRANSACTION_LIST_VARIANTS: {
5
+ readonly SUMMARY_HEADER: "summary_header";
6
+ readonly GENERIC_LIST: "generic_list";
7
+ };
5
8
  /**
6
- * Drawer title: the beat's own localized title, whatever the variant. The drawer is shared across
7
- * beats (Replenish Savings' "Savings Withdrawal" summary-header drawer, Weekly New Merchants' plain
8
- * "Purchases for review" list), so the title comes from the beat, not the variant.
9
+ * Drawer title: the variant's title when it has one (`generic_list` -> "Transactions"), otherwise the
10
+ * beat's own localized title (`summary_header`, `standard`, or no variant).
9
11
  */
10
- export declare const getTransactionListDrawerTitle: (_variant: string | null | undefined, _copy: GlobalCopy, beat?: Beat) => string;
12
+ export declare const getTransactionListDrawerTitle: (variant: string | null | undefined, copy: GlobalCopy, beat?: Beat) => string;
11
13
  export interface TransactionListSummary {
12
14
  /** Headline figure (e.g. total withdrawn). */
13
15
  amount: number;
@@ -1,38 +1,44 @@
1
- import { parseISO as s } from "date-fns/parseISO";
2
- var c = "summary_header", p = (t, r, e) => e?.insights_payload?.title || "", m = (t) => typeof t == "number" && Number.isFinite(t) ? t : void 0, f = (t) => typeof t == "number" && Number.isInteger(t) && t > 0 ? t : void 0, _ = Date.UTC(2e3, 0, 1), u = Date.UTC(2100, 0, 1), i = /* @__PURE__ */ new WeakSet(), d = (t, r) => {
1
+ import { parseISO as m } from "date-fns/parseISO";
2
+ var s = {
3
+ SUMMARY_HEADER: "summary_header",
4
+ GENERIC_LIST: "generic_list"
5
+ }, _ = (t) => ({ [s.GENERIC_LIST]: t.transactions?.transactions_title ?? "" }), y = (t, r, n) => {
6
+ const e = _(r);
7
+ return t && Object.hasOwn(e, t) ? e[t] ?? "" : n?.insights_payload?.title ?? "";
8
+ }, f = (t) => typeof t == "number" && Number.isFinite(t) ? t : void 0, c = (t) => typeof t == "number" && Number.isInteger(t) && t > 0 ? t : void 0, u = Date.UTC(2e3, 0, 1), T = Date.UTC(2100, 0, 1), i = /* @__PURE__ */ new WeakSet(), d = (t, r) => {
3
9
  i.has(t) || (i.add(t), console.warn(r));
4
- }, o = (t, r, e) => {
10
+ }, o = (t, r, n) => {
5
11
  if (t == null || t === "") return;
6
- let a;
7
- if (typeof t == "number" && Number.isFinite(t)) a = t < 1e12 ? t * 1e3 : t;
12
+ let e;
13
+ if (typeof t == "number" && Number.isFinite(t)) e = t < 1e12 ? t * 1e3 : t;
8
14
  else if (typeof t == "string") {
9
- const n = s(t).getTime();
10
- a = Number.isNaN(n) ? void 0 : n;
15
+ const a = m(t).getTime();
16
+ e = Number.isNaN(a) ? void 0 : a;
11
17
  }
12
- if (a === void 0 || a < _ || a > u) {
13
- d(e, `[TransactionListBlock] drilldown_data.${r} is unparseable or out of range (${typeof t})`);
18
+ if (e === void 0 || e < u || e > T) {
19
+ d(n, `[TransactionListBlock] drilldown_data.${r} is unparseable or out of range (${typeof t})`);
14
20
  return;
15
21
  }
16
- return new Date(a);
22
+ return new Date(e);
17
23
  }, b = (t) => {
18
- if (t?.variant !== "summary_header") return;
24
+ if (t?.variant !== s.SUMMARY_HEADER) return;
19
25
  const r = t?.drilldown_data;
20
26
  if (!r) return;
21
- const e = m(r.amount);
22
- if (e === void 0) {
27
+ const n = f(r.amount);
28
+ if (n === void 0) {
23
29
  Object.keys(r).length > 0 && d(r, "[TransactionListBlock] drilldown_data has no finite `amount` — no summary header.");
24
30
  return;
25
31
  }
26
32
  return {
27
- amount: e,
33
+ amount: n,
28
34
  startDate: o(r.start_date, "start_date", r),
29
35
  endDate: o(r.end_date, "end_date", r),
30
- count: f(r.count)
36
+ count: c(r.count)
31
37
  };
32
38
  };
33
39
  export {
34
- c as SUMMARY_HEADER_VARIANT,
35
- p as getTransactionListDrawerTitle,
40
+ s as TRANSACTION_LIST_VARIANTS,
41
+ y as getTransactionListDrawerTitle,
36
42
  b as readSummary
37
43
  };
38
44
 
@@ -1 +1 @@
1
- {"version":3,"file":"layout.es.js","names":[],"sources":["../../../../src/insights-v2/actionBlocks/TransactionListBlock/layout.ts"],"sourcesContent":["import { parseISO } from 'date-fns/parseISO'\n\nimport type { Beat, GlobalCopy, InDescriptionCTA, PrimaryCTA, SecondaryCTA } from '../../../core'\n\ntype DrilldownCta = PrimaryCTA | SecondaryCTA | InDescriptionCTA\n\n/** The CTA variant that opens the summary-header drilldown. */\nexport const SUMMARY_HEADER_VARIANT = 'summary_header'\n\n/**\n * Drawer title: the beat's own localized title, whatever the variant. The drawer is shared across\n * beats (Replenish Savings' \"Savings Withdrawal\" summary-header drawer, Weekly New Merchants' plain\n * \"Purchases for review\" list), so the title comes from the beat, not the variant.\n */\nexport const getTransactionListDrawerTitle = (\n _variant: string | null | undefined,\n _copy: GlobalCopy,\n beat?: Beat,\n): string => beat?.insights_payload?.title || ''\n\nexport interface TransactionListSummary {\n /** Headline figure (e.g. total withdrawn). */\n amount: number\n /** Validated range endpoints; rendered as `{start} – {end}`. Omitted when the payload value is\n * missing or unparseable (so the drawer never crashes on an Invalid Date). */\n startDate?: Date\n endDate?: Date\n /** Number of transactions for the pill, from the CTA's `drilldown_data.count`. */\n count?: number\n}\n\nconst toFiniteNumber = (value: unknown): number | undefined =>\n typeof value === 'number' && Number.isFinite(value) ? value : undefined\n\n// `> 0`: the card only shows with >= 1 guid, so a `count` of 0 is dropped and falls back to the\n// guid count rather than rendering \"0 transactions\".\nconst toCount = (value: unknown): number | undefined =>\n typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined\n\n// Sane epoch window so a bad/typo payload can't render a far-past/future date.\nconst EPOCH_MIN = Date.UTC(2000, 0, 1)\nconst EPOCH_MAX = Date.UTC(2100, 0, 1)\n\n// `readSummary` runs on every observer re-render (no `useMemo` by design), so warn at most once per\n// distinct payload object instead of on every render. Keyed on the `drilldown_data` object, which is\n// a stable reference across re-renders of the same beat.\nconst warnedPayloads = new WeakSet<object>()\nconst warnOncePerPayload = (source: object, message: string): void => {\n if (warnedPayloads.has(source)) return\n warnedPayloads.add(source)\n console.warn(message)\n}\n\n/**\n * Parse a payload date value to a validated `Date`, or `undefined`. Accepts unix seconds or\n * milliseconds (auto-detected) and ISO strings. Fails closed (no throw): an unparseable or\n * out-of-range value returns undefined and warns, so `formatDate`/`intlFormat` never sees an Invalid\n * Date (which throws `RangeError` and takes down the whole drawer render).\n */\nconst toDate = (value: unknown, field: string, source: object): Date | undefined => {\n // Missing/empty is a normal \"no range\" case — return quietly, don't warn.\n if (value === undefined || value === null || value === '') return undefined\n\n let ms: number | undefined\n if (typeof value === 'number' && Number.isFinite(value)) {\n // < 1e12 is treated as seconds (1e12 ms is year 33658). Trade-off: genuine ms timestamps from\n // ~2000-2001 would be misread as seconds and fall outside the window below (dropped, not shown).\n ms = value < 1e12 ? value * 1000 : value\n } else if (typeof value === 'string') {\n const parsed = parseISO(value).getTime()\n ms = Number.isNaN(parsed) ? undefined : parsed\n }\n\n if (ms === undefined || ms < EPOCH_MIN || ms > EPOCH_MAX) {\n warnOncePerPayload(\n source,\n `[TransactionListBlock] drilldown_data.${field} is unparseable or out of range (${typeof value})`,\n )\n return undefined\n }\n return new Date(ms)\n}\n\n/**\n * Summary-header figures for the drawer, read from the clicked CTA's `drilldown_data` (the backend\n * hangs a drilldown's own data off the CTA that opens it). Fails closed throughout: only the\n * `summary_header` variant renders a header (so figures from a different CTA that `getDynamicCta…`\n * fell back to can't be misattributed), then without a finite `amount` there is still no header, and\n * each date is validated to a real `Date` or dropped — an unparseable value never reaches\n * `formatDate` (which throws on an Invalid Date).\n *\n * `drilldown_data` fields: `amount` (number), `start_date`/`end_date` (unix seconds|ms or ISO),\n * `count` (number of transactions).\n */\nexport const readSummary = (cta: DrilldownCta | undefined): TransactionListSummary | undefined => {\n // Defense-in-depth on financial figures: only the summary-header variant shows the card.\n if (cta?.variant !== SUMMARY_HEADER_VARIANT) return undefined\n\n const source = cta?.drilldown_data\n if (!source) return undefined\n\n const amount = toFiniteNumber(source.amount)\n if (amount === undefined) {\n // `drilldown_data` present but no usable amount -> surface the likely contract drift (field\n // rename) instead of a silently missing header.\n if (Object.keys(source).length > 0) {\n warnOncePerPayload(\n source,\n '[TransactionListBlock] drilldown_data has no finite `amount` — no summary header.',\n )\n }\n return undefined\n }\n\n return {\n amount,\n startDate: toDate(source.start_date, 'start_date', source),\n endDate: toDate(source.end_date, 'end_date', source),\n count: toCount(source.count),\n }\n}\n"],"mappings":";AAOA,IAAa,IAAyB,kBAOzB,IAAA,CACX,GACA,GACA,MACW,GAAM,kBAAkB,SAAS,IAaxC,IAAA,CAAkB,MACtB,OAAO,KAAU,YAAY,OAAO,SAAS,CAAK,IAAI,IAAQ,QAI1D,IAAA,CAAW,MACf,OAAO,KAAU,YAAY,OAAO,UAAU,CAAK,KAAK,IAAQ,IAAI,IAAQ,QAGxE,IAAY,KAAK,IAAI,KAAM,GAAG,CAAC,GAC/B,IAAY,KAAK,IAAI,MAAM,GAAG,CAAC,GAK/B,IAAiB,oBAAI,QAAgB,GACrC,IAAA,CAAsB,GAAgB,MAA0B;AACpE,EAAI,EAAe,IAAI,CAAM,MAC7B,EAAe,IAAI,CAAM,GACzB,QAAQ,KAAK,CAAO;AACtB,GAQM,IAAA,CAAU,GAAgB,GAAe,MAAqC;AAElF,MAA2B,KAAU,QAAQ,MAAU,GAAI;AAE3D,MAAI;AACJ,MAAI,OAAO,KAAU,YAAY,OAAO,SAAS,CAAK,EAGpD,CAAA,IAAK,IAAQ,OAAO,IAAQ,MAAO;AAAA,WAC1B,OAAO,KAAU,UAAU;AACpC,UAAM,IAAS,EAAS,CAAK,EAAE,QAAQ;AACvC,IAAA,IAAK,OAAO,MAAM,CAAM,IAAI,SAAY;AAAA,EAC1C;AAEA,MAAI,MAAO,UAAa,IAAK,KAAa,IAAK,GAAW;AACxD,IAAA,EACE,GACA,yCAAyC,CAAA,oCAAyC,OAAO,CAAA,GAC3F;AACA;AAAA,EACF;AACA,SAAO,IAAI,KAAK,CAAE;AACpB,GAaa,IAAA,CAAe,MAAsE;AAEhG,MAAI,GAAK,YAAA,iBAAoC;AAE7C,QAAM,IAAS,GAAK;AACpB,MAAI,CAAC,EAAQ;AAEb,QAAM,IAAS,EAAe,EAAO,MAAM;AAC3C,MAAI,MAAW,QAAW;AAGxB,IAAI,OAAO,KAAK,CAAM,EAAE,SAAS,KAC/B,EACE,GACA,mFACF;AAEF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,QAAA;AAAA,IACA,WAAW,EAAO,EAAO,YAAY,cAAc,CAAM;AAAA,IACzD,SAAS,EAAO,EAAO,UAAU,YAAY,CAAM;AAAA,IACnD,OAAO,EAAQ,EAAO,KAAK;AAAA,EAC7B;AACF"}
1
+ {"version":3,"file":"layout.es.js","names":[],"sources":["../../../../src/insights-v2/actionBlocks/TransactionListBlock/layout.ts"],"sourcesContent":["import { parseISO } from 'date-fns/parseISO'\n\nimport type { Beat, GlobalCopy, InDescriptionCTA, PrimaryCTA, SecondaryCTA } from '../../../core'\n\ntype DrilldownCta = PrimaryCTA | SecondaryCTA | InDescriptionCTA\n\n/** The transaction_list_block CTA variants. */\nexport const TRANSACTION_LIST_VARIANTS = {\n SUMMARY_HEADER: 'summary_header',\n GENERIC_LIST: 'generic_list',\n} as const\n\n/** Drawer titles for variants that don't use the beat's own title. Add new variants here. */\nconst variantTitles = (copy: GlobalCopy): Record<string, string> => ({\n [TRANSACTION_LIST_VARIANTS.GENERIC_LIST]: copy.transactions?.transactions_title ?? '',\n})\n\n/**\n * Drawer title: the variant's title when it has one (`generic_list` -> \"Transactions\"), otherwise the\n * beat's own localized title (`summary_header`, `standard`, or no variant).\n */\nexport const getTransactionListDrawerTitle = (\n variant: string | null | undefined,\n copy: GlobalCopy,\n beat?: Beat,\n): string => {\n const titles = variantTitles(copy)\n if (variant && Object.hasOwn(titles, variant)) return titles[variant] ?? ''\n\n return beat?.insights_payload?.title ?? ''\n}\n\nexport interface TransactionListSummary {\n /** Headline figure (e.g. total withdrawn). */\n amount: number\n /** Validated range endpoints; rendered as `{start} – {end}`. Omitted when the payload value is\n * missing or unparseable (so the drawer never crashes on an Invalid Date). */\n startDate?: Date\n endDate?: Date\n /** Number of transactions for the pill, from the CTA's `drilldown_data.count`. */\n count?: number\n}\n\nconst toFiniteNumber = (value: unknown): number | undefined =>\n typeof value === 'number' && Number.isFinite(value) ? value : undefined\n\n// `> 0`: the card only shows with >= 1 guid, so a `count` of 0 is dropped and falls back to the\n// guid count rather than rendering \"0 transactions\".\nconst toCount = (value: unknown): number | undefined =>\n typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : undefined\n\n// Sane epoch window so a bad/typo payload can't render a far-past/future date.\nconst EPOCH_MIN = Date.UTC(2000, 0, 1)\nconst EPOCH_MAX = Date.UTC(2100, 0, 1)\n\n// `readSummary` runs on every observer re-render (no `useMemo` by design), so warn at most once per\n// distinct payload object instead of on every render. Keyed on the `drilldown_data` object, which is\n// a stable reference across re-renders of the same beat.\nconst warnedPayloads = new WeakSet<object>()\nconst warnOncePerPayload = (source: object, message: string): void => {\n if (warnedPayloads.has(source)) return\n warnedPayloads.add(source)\n console.warn(message)\n}\n\n/**\n * Parse a payload date value to a validated `Date`, or `undefined`. Accepts unix seconds or\n * milliseconds (auto-detected) and ISO strings. Fails closed (no throw): an unparseable or\n * out-of-range value returns undefined and warns, so `formatDate`/`intlFormat` never sees an Invalid\n * Date (which throws `RangeError` and takes down the whole drawer render).\n */\nconst toDate = (value: unknown, field: string, source: object): Date | undefined => {\n // Missing/empty is a normal \"no range\" case — return quietly, don't warn.\n if (value === undefined || value === null || value === '') return undefined\n\n let ms: number | undefined\n if (typeof value === 'number' && Number.isFinite(value)) {\n // < 1e12 is treated as seconds (1e12 ms is year 33658). Trade-off: genuine ms timestamps from\n // ~2000-2001 would be misread as seconds and fall outside the window below (dropped, not shown).\n ms = value < 1e12 ? value * 1000 : value\n } else if (typeof value === 'string') {\n const parsed = parseISO(value).getTime()\n ms = Number.isNaN(parsed) ? undefined : parsed\n }\n\n if (ms === undefined || ms < EPOCH_MIN || ms > EPOCH_MAX) {\n warnOncePerPayload(\n source,\n `[TransactionListBlock] drilldown_data.${field} is unparseable or out of range (${typeof value})`,\n )\n return undefined\n }\n return new Date(ms)\n}\n\n/**\n * Summary-header figures for the drawer, read from the clicked CTA's `drilldown_data` (the backend\n * hangs a drilldown's own data off the CTA that opens it). Fails closed throughout: only the\n * `summary_header` variant renders a header (so figures from a different CTA that `getDynamicCta…`\n * fell back to can't be misattributed), then without a finite `amount` there is still no header, and\n * each date is validated to a real `Date` or dropped — an unparseable value never reaches\n * `formatDate` (which throws on an Invalid Date).\n *\n * `drilldown_data` fields: `amount` (number), `start_date`/`end_date` (unix seconds|ms or ISO),\n * `count` (number of transactions).\n */\nexport const readSummary = (cta: DrilldownCta | undefined): TransactionListSummary | undefined => {\n // Defense-in-depth on financial figures: only the summary-header variant shows the card.\n if (cta?.variant !== TRANSACTION_LIST_VARIANTS.SUMMARY_HEADER) return undefined\n\n const source = cta?.drilldown_data\n if (!source) return undefined\n\n const amount = toFiniteNumber(source.amount)\n if (amount === undefined) {\n // `drilldown_data` present but no usable amount -> surface the likely contract drift (field\n // rename) instead of a silently missing header.\n if (Object.keys(source).length > 0) {\n warnOncePerPayload(\n source,\n '[TransactionListBlock] drilldown_data has no finite `amount` — no summary header.',\n )\n }\n return undefined\n }\n\n return {\n amount,\n startDate: toDate(source.start_date, 'start_date', source),\n endDate: toDate(source.end_date, 'end_date', source),\n count: toCount(source.count),\n }\n}\n"],"mappings":";AAOA,IAAa,IAA4B;AAAA,EACvC,gBAAgB;AAAA,EAChB,cAAc;AAChB,GAGM,IAAA,CAAiB,OAA8C,EAAA,CAClE,EAA0B,YAAA,GAAe,EAAK,cAAc,sBAAsB,GACrF,IAMa,IAAA,CACX,GACA,GACA,MACW;AACX,QAAM,IAAS,EAAc,CAAI;AACjC,SAAI,KAAW,OAAO,OAAO,GAAQ,CAAO,IAAU,EAAO,CAAA,KAAY,KAElE,GAAM,kBAAkB,SAAS;AAC1C,GAaM,IAAA,CAAkB,MACtB,OAAO,KAAU,YAAY,OAAO,SAAS,CAAK,IAAI,IAAQ,QAI1D,IAAA,CAAW,MACf,OAAO,KAAU,YAAY,OAAO,UAAU,CAAK,KAAK,IAAQ,IAAI,IAAQ,QAGxE,IAAY,KAAK,IAAI,KAAM,GAAG,CAAC,GAC/B,IAAY,KAAK,IAAI,MAAM,GAAG,CAAC,GAK/B,IAAiB,oBAAI,QAAgB,GACrC,IAAA,CAAsB,GAAgB,MAA0B;AACpE,EAAI,EAAe,IAAI,CAAM,MAC7B,EAAe,IAAI,CAAM,GACzB,QAAQ,KAAK,CAAO;AACtB,GAQM,IAAA,CAAU,GAAgB,GAAe,MAAqC;AAElF,MAA2B,KAAU,QAAQ,MAAU,GAAI;AAE3D,MAAI;AACJ,MAAI,OAAO,KAAU,YAAY,OAAO,SAAS,CAAK,EAGpD,CAAA,IAAK,IAAQ,OAAO,IAAQ,MAAO;AAAA,WAC1B,OAAO,KAAU,UAAU;AACpC,UAAM,IAAS,EAAS,CAAK,EAAE,QAAQ;AACvC,IAAA,IAAK,OAAO,MAAM,CAAM,IAAI,SAAY;AAAA,EAC1C;AAEA,MAAI,MAAO,UAAa,IAAK,KAAa,IAAK,GAAW;AACxD,IAAA,EACE,GACA,yCAAyC,CAAA,oCAAyC,OAAO,CAAA,GAC3F;AACA;AAAA,EACF;AACA,SAAO,IAAI,KAAK,CAAE;AACpB,GAaa,IAAA,CAAe,MAAsE;AAEhG,MAAI,GAAK,YAAY,EAA0B,eAAgB;AAE/D,QAAM,IAAS,GAAK;AACpB,MAAI,CAAC,EAAQ;AAEb,QAAM,IAAS,EAAe,EAAO,MAAM;AAC3C,MAAI,MAAW,QAAW;AAGxB,IAAI,OAAO,KAAK,CAAM,EAAE,SAAS,KAC/B,EACE,GACA,mFACF;AAEF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,QAAA;AAAA,IACA,WAAW,EAAO,EAAO,YAAY,cAAc,CAAM;AAAA,IACzD,SAAS,EAAO,EAAO,UAAU,YAAY,CAAM;AAAA,IACnD,OAAO,EAAQ,EAAO,KAAK;AAAA,EAC7B;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mx-cartographer/experiences",
3
- "version": "9.7.98",
3
+ "version": "9.7.99",
4
4
  "description": "Library containing experience widgets",
5
5
  "author": "MX",
6
6
  "license": "MIT",