@windstream/react-shared-components 0.2.68 → 0.2.69

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.
@@ -11,12 +11,17 @@ import { SelectPlanButton } from "@shared/components/select-plan-button";
11
11
  import { Text } from "@shared/components/text";
12
12
  import { cx } from "@shared/utils";
13
13
 
14
- /** Renders text where `|...|`-wrapped segments are bold (pipes are stripped). */
14
+ /**
15
+ * Renders text where `|...|`-wrapped segments (e.g. the dollar value in a gift
16
+ * badge) use the heavier `label3` weight (900), while the surrounding text keeps
17
+ * the parent `body3` weight (500) — matching residential's gift-reward styling.
18
+ * Pipes are stripped.
19
+ */
15
20
  const renderPipeBold = (value: React.ReactNode): React.ReactNode => {
16
21
  if (typeof value !== "string" || !value.includes("|")) return value;
17
22
  return value.split("|").map((segment, index) =>
18
23
  index % 2 === 1 ? (
19
- <span key={index} className="font-bold">
24
+ <span key={index} className="label3">
20
25
  {segment}
21
26
  </span>
22
27
  ) : (
@@ -72,7 +77,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
72
77
  const priceIntegerClass = twoColumn
73
78
  ? "heading2 md:subheading5"
74
79
  : "subheading5";
75
- const priceUnitClass = twoColumn ? "label3" : "label1";
80
+ const priceUnitClass = twoColumn ? "label3 md:label1" : "label1";
76
81
 
77
82
  return (
78
83
  <article className="relative flex h-full w-full max-w-[392px] flex-col">
@@ -304,27 +309,21 @@ export const ProductCard: React.FC<ProductCardProps> = ({
304
309
  height={24}
305
310
  className="ml-1 h-auto pr-2"
306
311
  />
307
- <span
308
- className={cx(
309
- "body3",
310
- isDark ? "text-text-inverse" : "text-text"
311
- )}
312
- >
313
- {innerBadge.badgeText.includes("|") ? (
314
- <>
315
- <span className="font-bold">
316
- {innerBadge.badgeText.split("|")[0]}
317
- </span>
318
- <span className="font-normal">
319
- {innerBadge.badgeText.split("|")[1]}
320
- </span>
321
- </>
322
- ) : (
323
- <Text className="font-bold">
324
- {innerBadge.badgeText}
325
- </Text>
326
- )}
327
- </span>
312
+ {(innerBadge.badgeText.includes("|")
313
+ ? innerBadge.badgeText.split("|")
314
+ : [innerBadge.badgeText]
315
+ ).map((badge, index) => (
316
+ <Text
317
+ key={index}
318
+ as="span"
319
+ className={cx(
320
+ index === 0 ? "label3" : "body3",
321
+ isDark ? "text-text-inverse" : "text-text"
322
+ )}
323
+ >
324
+ {badge.trim()}
325
+ </Text>
326
+ ))}
328
327
  </div>
329
328
  ) : null}
330
329
  </div>
@@ -75,7 +75,7 @@ export const Carousel: React.FC<CarouselProps> = ({
75
75
  {disclaimerText && (
76
76
  <Text
77
77
  as="div"
78
- className="footnote mt-8 px-4 text-center text-text-secondary"
78
+ className="body3 mt-8 px-4 text-center text-text-secondary"
79
79
  >
80
80
  {disclaimerText}
81
81
  </Text>
@@ -308,7 +308,7 @@ const RadioButtonInputField: React.FC<FieldRenderProps> = args => {
308
308
  hasError={hasError}
309
309
  onChange={handleInputChange}
310
310
  required={requiredField}
311
- labelClassName="footnote"
311
+ labelClassName="footnote text-left"
312
312
  />
313
313
  </div>
314
314
  ))}
@@ -119,6 +119,102 @@ describe("DynamicForm", () => {
119
119
  const submit = () =>
120
120
  fireEvent.click(screen.getByRole("button", { name: /submit form/i }));
121
121
 
122
+ describe("non-prod email rerouting", () => {
123
+ const fillAndSubmit = () => {
124
+ fireEvent.change(screen.getByLabelText("First Name"), {
125
+ target: { value: "Jane", name: "firstName" },
126
+ });
127
+ fireEvent.click(screen.getByRole("checkbox"));
128
+ fireEvent.click(screen.getByRole("button", { name: /submit form/i }));
129
+ };
130
+
131
+ it("reroutes email to multiple comma-separated test recipients when isProduction is false", async () => {
132
+ const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
133
+ render(
134
+ <DynamicForm
135
+ fields={fields}
136
+ formSubmitOptions={["email"]}
137
+ emailTemplateName="welcome"
138
+ toEmail="real@customer.com"
139
+ testEmail=" a@test.com , b@test.com "
140
+ isProduction={false}
141
+ onEmailSubmit={onEmailSubmit}
142
+ successMessage={<div>Done</div>}
143
+ />
144
+ );
145
+
146
+ fillAndSubmit();
147
+
148
+ await waitFor(() => expect(onEmailSubmit).toHaveBeenCalledTimes(1));
149
+ const body = onEmailSubmit.mock.calls[0][0];
150
+ expect(body.toEmail).toBe("a@test.com,b@test.com");
151
+ });
152
+
153
+ it("skips the email action when isProduction is false and testEmail is empty", async () => {
154
+ const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
155
+ render(
156
+ <DynamicForm
157
+ fields={fields}
158
+ formSubmitOptions={["email"]}
159
+ emailTemplateName="welcome"
160
+ toEmail="real@customer.com"
161
+ testEmail=""
162
+ isProduction={false}
163
+ onEmailSubmit={onEmailSubmit}
164
+ successMessage={<div>Done</div>}
165
+ />
166
+ );
167
+
168
+ fillAndSubmit();
169
+
170
+ await waitFor(() => expect(screen.getByText("Done")).toBeInTheDocument());
171
+ expect(onEmailSubmit).not.toHaveBeenCalled();
172
+ });
173
+
174
+ it("does not reroute when isProduction is not set (preserves prior behavior)", async () => {
175
+ const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
176
+ render(
177
+ <DynamicForm
178
+ fields={fields}
179
+ formSubmitOptions={["email"]}
180
+ emailTemplateName="welcome"
181
+ toEmail="real@customer.com"
182
+ testEmail="test@test.com"
183
+ onEmailSubmit={onEmailSubmit}
184
+ successMessage={<div>Done</div>}
185
+ />
186
+ );
187
+
188
+ fillAndSubmit();
189
+
190
+ await waitFor(() => expect(onEmailSubmit).toHaveBeenCalledTimes(1));
191
+ const body = onEmailSubmit.mock.calls[0][0];
192
+ expect(body.toEmail).toBe("real@customer.com");
193
+ });
194
+
195
+ it("does not reroute when isProduction is true", async () => {
196
+ const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
197
+ render(
198
+ <DynamicForm
199
+ fields={fields}
200
+ formSubmitOptions={["email"]}
201
+ emailTemplateName="welcome"
202
+ toEmail="real@customer.com"
203
+ testEmail="test@test.com"
204
+ isProduction={true}
205
+ onEmailSubmit={onEmailSubmit}
206
+ successMessage={<div>Done</div>}
207
+ />
208
+ );
209
+
210
+ fillAndSubmit();
211
+
212
+ await waitFor(() => expect(onEmailSubmit).toHaveBeenCalledTimes(1));
213
+ const body = onEmailSubmit.mock.calls[0][0];
214
+ expect(body.toEmail).toBe("real@customer.com");
215
+ });
216
+ });
217
+
122
218
  it("applies TN/IP preset validation before submitting", async () => {
123
219
  const onEmailSubmit = jest.fn().mockResolvedValue(undefined);
124
220
  const presetFields: DynamicFormItem[] = [
@@ -108,6 +108,8 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
108
108
  emailSubject,
109
109
  emailTemplateName,
110
110
  toEmail,
111
+ testEmail,
112
+ isProduction,
111
113
  segmentEventId,
112
114
  onEmailSubmit,
113
115
  onSmtpSubmit,
@@ -380,7 +382,28 @@ export const DynamicForm: React.FC<DynamicFormProps> = props => {
380
382
 
381
383
  onTrack?.(segmentEventId, payload);
382
384
 
383
- const actions = payload.formSubmitOptions || [];
385
+ // Non-prod safety: when the app flags a non-production environment,
386
+ // reroute the `email` action to the configured test recipient(s) — which
387
+ // may be multiple comma-separated addresses — or skip `email` entirely
388
+ // when no test recipient is set. Gated on an explicit `isProduction ===
389
+ // false` so prod/undefined behavior is unchanged.
390
+ let effectiveActions = payload.formSubmitOptions || [];
391
+ if (isProduction === false && effectiveActions.includes("email")) {
392
+ const normalizedTestEmail = (testEmail || "")
393
+ .split(",")
394
+ .map(email => email.trim())
395
+ .filter(Boolean)
396
+ .join(",");
397
+ if (normalizedTestEmail) {
398
+ payload.toEmail = normalizedTestEmail;
399
+ } else {
400
+ effectiveActions = effectiveActions.filter(
401
+ action => action !== "email"
402
+ );
403
+ }
404
+ }
405
+
406
+ const actions = effectiveActions;
384
407
  const hasPost = actions.includes("post");
385
408
  const serverActions = actions.filter(a => a !== "post");
386
409
 
@@ -180,6 +180,15 @@ export interface DynamicFormProps {
180
180
  emailSubject?: string;
181
181
  emailTemplateName?: string;
182
182
  toEmail?: string;
183
+ /**
184
+ * Test recipient(s) for the `email` action in non-production environments.
185
+ * When `isProduction === false` and the `email` action runs, the payload's
186
+ * `toEmail` is rerouted here so real recipients never receive non-prod
187
+ * submissions. Supports multiple comma-separated addresses (each is trimmed);
188
+ * when empty, the `email` action is skipped. Mirrors the residential form's
189
+ * `testEmail`.
190
+ */
191
+ testEmail?: string;
183
192
  segmentEventId?: string;
184
193
 
185
194
  /** Injected per-action async submit handlers. */
@@ -212,8 +221,10 @@ export interface DynamicFormProps {
212
221
  metadata?: Record<string, any>;
213
222
  }) => void;
214
223
 
215
- /** Whether the current environment is production (affects nothing in the
216
- * base component today but reserved for parity with app behavior). */
224
+ /** Whether the current environment is production. When explicitly `false`,
225
+ * the `email` action is rerouted to `testEmail` (or skipped when `testEmail`
226
+ * is empty) so non-prod submissions never reach real recipients. Leaving it
227
+ * `undefined` preserves the prior behavior (no rerouting). */
217
228
  isProduction?: boolean;
218
229
 
219
230
  /** Field customization + async data. */
@@ -130,7 +130,10 @@ export const Navigation: React.FC<NavigationProps> = props => {
130
130
  )}
131
131
  >
132
132
  <div className="mx-auto flex max-w-120 justify-between">
133
- <ul className="flex gap-5" aria-label="Utility Navigation">
133
+ <ul
134
+ className="flex min-h-11 gap-5"
135
+ aria-label="Utility Navigation"
136
+ >
134
137
  {utilityNavigationLinks?.map((links, index) => {
135
138
  return (
136
139
  <li key={`main-menu-items-${index}`}>
@@ -4,8 +4,8 @@ import { PrimaryHero } from "./index";
4
4
  import { fireEvent, render, screen } from "@testing-library/react";
5
5
 
6
6
  jest.mock("@shared/components/checklist", () => ({
7
- Checklist: ({ items }: any) => (
8
- <ul data-testid="checklist">
7
+ Checklist: ({ items, iconSize }: any) => (
8
+ <ul data-testid="checklist" data-icon-size={iconSize}>
9
9
  {items?.map((i: string, idx: number) => (
10
10
  <li key={idx}>{i}</li>
11
11
  ))}
@@ -128,6 +128,15 @@ describe("PrimaryHero", () => {
128
128
  expect(screen.getByText("Free install")).toBeInTheDocument();
129
129
  });
130
130
 
131
+ it("passes iconSize of 24 to the checklist", () => {
132
+ const checklist = [{ title: "No data caps" }, { title: "Free install" }];
133
+ render(<PrimaryHero {...baseProps} checklist={checklist} />);
134
+ expect(screen.getByTestId("checklist")).toHaveAttribute(
135
+ "data-icon-size",
136
+ "24"
137
+ );
138
+ });
139
+
131
140
  it("does not render checklist when not provided", () => {
132
141
  render(<PrimaryHero {...baseProps} />);
133
142
  expect(screen.queryByTestId("checklist")).not.toBeInTheDocument();
@@ -171,6 +171,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
171
171
  {/* checklist */}
172
172
  {checklist && (
173
173
  <Checklist
174
+ iconSize={24}
174
175
  listItemClassName={`body2 text-left ${textColorClass}`}
175
176
  items={checklist.map(item =>
176
177
  item.iconUrl
@@ -230,7 +231,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
230
231
  {...primaryCta1}
231
232
  renderCheckPlans={renderCheckPlans}
232
233
  onModalButtonClick={onModalButtonClick}
233
- fullWidth={true}
234
+ // fullWidth={true}
234
235
  buttonClassName="lg:w-auto text-center"
235
236
  />
236
237
  </div>
@@ -243,7 +244,7 @@ export const PrimaryHero: React.FC<PrimaryHeroProps> = props => {
243
244
  {...primaryCta2}
244
245
  renderCheckPlans={renderCheckPlans}
245
246
  onModalButtonClick={onModalButtonClick}
246
- fullWidth={true}
247
+ // fullWidth={true}
247
248
  buttonClassName="lg:w-auto text-center"
248
249
  />
249
250
  </div>