@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.
- package/dist/contentful/index.d.ts +13 -2
- package/dist/contentful/index.esm.js +4 -4
- package/dist/contentful/index.esm.js.map +1 -1
- package/dist/contentful/index.js +2 -2
- package/dist/contentful/index.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.esm.js +1 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/components/select-plan-button/index.test.tsx +27 -0
- package/src/components/select-plan-button/index.tsx +6 -1
- package/src/contentful/blocks/cards/product-card/index.test.tsx +36 -0
- package/src/contentful/blocks/cards/product-card/index.tsx +23 -24
- package/src/contentful/blocks/carousel/index.tsx +1 -1
- package/src/contentful/blocks/dynamic-form/field-map.tsx +1 -1
- package/src/contentful/blocks/dynamic-form/index.test.tsx +96 -0
- package/src/contentful/blocks/dynamic-form/index.tsx +24 -1
- package/src/contentful/blocks/dynamic-form/types.ts +13 -2
- package/src/contentful/blocks/navigation/index.tsx +4 -1
- package/src/contentful/blocks/primary-hero/index.test.tsx +11 -2
- package/src/contentful/blocks/primary-hero/index.tsx +3 -2
|
@@ -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
|
-
/**
|
|
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="
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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="
|
|
78
|
+
className="body3 mt-8 px-4 text-center text-text-secondary"
|
|
79
79
|
>
|
|
80
80
|
{disclaimerText}
|
|
81
81
|
</Text>
|
|
@@ -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
|
-
|
|
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
|
|
216
|
-
*
|
|
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
|
|
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>
|