• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / fx-private-relay / b4f21704-cf8f-4dbc-b7b2-8d24d2c4b9eb

05 Aug 2024 07:01PM CUT coverage: 85.416% (-0.02%) from 85.431%
b4f21704-cf8f-4dbc-b7b2-8d24d2c4b9eb

push

circleci

web-flow
Merge pull request #4928 from mozilla/dependabot/npm_and_yarn/eslint-cc3139cf36

Bump the eslint group with 2 updates

4101 of 5252 branches covered (78.08%)

Branch coverage included in aggregate %.

2 of 8 new or added lines in 3 files covered. (25.0%)

1 existing line in 1 file now uncovered.

15971 of 18247 relevant lines covered (87.53%)

10.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

23.81
/frontend/src/components/dashboard/aliases/CustomAddressGenerationModal.tsx
1
import {
2
  ButtonHTMLAttributes,
3
  ChangeEventHandler,
4
  FormEvent,
5
  FormEventHandler,
6
  ReactElement,
7
  ReactNode,
8
  RefObject,
9
  useRef,
10
  useState,
11
} from "react";
3✔
12
import Link from "next/link";
3✔
13
import Congratulations from "../images/free-onboarding-congratulations.svg";
3✔
14
import {
15
  OverlayContainer,
16
  FocusScope,
17
  useDialog,
18
  useModal,
19
  useOverlay,
20
  useButton,
21
  AriaOverlayProps,
22
  ButtonAria,
23
} from "react-aria";
3✔
24
import styles from "./CustomAddressGenerationModal.module.scss";
3✔
25
import {
26
  BulletPointIcon,
27
  CheckIcon,
28
  CloseIconNormal,
29
  CopyIcon,
30
  ErrorTriangleIcon,
31
  InfoBulbIcon,
32
  InvalidIcon,
33
} from "../../Icons";
3✔
34
import { Button } from "../../Button";
3✔
35
import Image from "../../Image";
3✔
36
import { InfoTooltip } from "../../InfoTooltip";
3✔
37
import { useL10n } from "../../../hooks/l10n";
3✔
38
import { ProfileData, useProfiles } from "../../../hooks/api/profile";
3✔
39
import { getRuntimeConfig } from "../../../config";
3✔
40
import { MenuTriggerState, useMenuTriggerState } from "react-stately";
3✔
41
import { AliasData } from "../../../hooks/api/aliases";
42
import { ReactLocalization } from "@fluent/react";
43

44
export type Props = {
45
  isOpen: boolean;
46
  onClose: () => void;
47
  onUpdate: (
48
    aliasToUpdate: AliasData | undefined,
49
    blockPromotions: boolean,
50
    copyToClipboard: boolean | undefined,
51
  ) => void;
52
  onPick: (address: string, setErrorState: (flag: boolean) => void) => void;
53
  subdomain: string;
54
  aliasGeneratedState: boolean;
55
  findAliasDataFromPrefix: (aliasPrefix: string) => AliasData | undefined;
56
};
57

58
/**
59
 * Modal in which the user can create a new custom alias,
60
 * while also being educated on why they don't need to do that.
61
 */
62
export const CustomAddressGenerationModal = (props: Props) => {
3✔
63
  const profileData = useProfiles();
×
64
  const l10n = useL10n();
×
65
  const cancelButtonRef = useRef<HTMLButtonElement>(null);
×
66
  const cancelButton = useButton(
×
67
    { onPress: () => props.onClose() },
×
68
    cancelButtonRef,
69
  );
70
  const [address, setAddress] = useState("");
×
71
  const errorExplainerState = useMenuTriggerState({});
×
72
  const profile = profileData.data?.[0];
×
73
  if (!profile) {
×
74
    return null;
×
75
  }
76

77
  return (
78
    <>
79
      <OverlayContainer>
80
        <PickerDialog
81
          title={
82
            !props.aliasGeneratedState
×
83
              ? l10n.getString("modal-custom-alias-picker-heading-2")
84
              : l10n.getString("modal-domain-register-success-title")
85
          }
86
          onClose={() => props.onClose()}
×
87
          isOpen={props.isOpen}
88
          isDismissable={true}
89
          errorStateIsOpen={errorExplainerState.isOpen}
90
          errorStateOnClose={errorExplainerState.close}
91
          aliasGeneratedState={props.aliasGeneratedState}
92
        >
93
          {!props.aliasGeneratedState ? (
94
            <CustomMaskCreator
×
95
              l10n={l10n}
96
              address={address}
97
              profile={profile}
98
              errorExplainerState={errorExplainerState}
99
              cancelButton={cancelButton}
100
              cancelButtonRef={cancelButtonRef}
101
              onPick={props.onPick}
102
              setAddress={setAddress}
103
            />
104
          ) : (
105
            <CustomMaskSuccess
106
              l10n={l10n}
107
              address={address}
108
              profile={profile}
109
              findAliasDataFromPrefix={props.findAliasDataFromPrefix}
110
              onUpdate={props.onUpdate}
111
            />
112
          )}
113
        </PickerDialog>
114
      </OverlayContainer>
115
    </>
116
  );
117
};
118

119
type CustomMaskCreatorProps = {
120
  l10n: ReactLocalization;
121
  address: string;
122
  profile: ProfileData;
123
  errorExplainerState: MenuTriggerState;
124
  cancelButton: ButtonAria<ButtonHTMLAttributes<HTMLButtonElement>>;
125
  cancelButtonRef: RefObject<HTMLButtonElement>;
126
  onPick: (address: string, setErrorState: (flag: boolean) => void) => void;
127
  setAddress: (address: string) => void;
128
};
129

130
const CustomMaskCreator = (props: CustomMaskCreatorProps) => {
3✔
131
  const {
132
    l10n,
133
    profile,
134
    errorExplainerState,
135
    cancelButton,
136
    cancelButtonRef,
137
    onPick,
138
    address,
139
    setAddress,
140
  } = props;
×
141
  /**
142
   * We need a Ref to the address picker field so that we can call the browser's
143
   * native validation APIs.
144
   * See:
145
   * - https://beta.reactjs.org/learn/manipulating-the-dom-with-refs
146
   * - https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setCustomValidity
147
   */
148
  const addressFieldRef = useRef<HTMLInputElement>(null);
×
149
  const containsUppercase = /[A-Z]/.test(address);
×
150
  const containsSymbols = !/^[A-Za-z0-9.-]*$/.test(address);
×
151

152
  const onSubmit: FormEventHandler = (event) => {
×
153
    event.preventDefault();
×
154

155
    const isValid = isAddressValid(address);
×
NEW
156
    if (isValid) {
×
NEW
157
      onPick(address.toLowerCase(), errorExplainerState.setOpen);
×
158
    } else {
NEW
159
      errorExplainerState.setOpen(!isValid);
×
160
    }
161
  };
162
  const onChange: ChangeEventHandler<HTMLInputElement> = (event) => {
×
163
    setAddress(event.target.value);
×
164
    if (errorExplainerState.isOpen) {
×
165
      errorExplainerState.close();
×
166
    }
167
  };
168

169
  return (
170
    <form onSubmit={onSubmit}>
171
      <div className={styles["form-wrapper"]}>
172
        <div className={styles.prefix}>
173
          <label htmlFor="address">
174
            {l10n.getString("modal-custom-alias-picker-form-prefix-label-3")}
175
          </label>
176
          <input
177
            id="address"
178
            type="text"
179
            value={address}
180
            onChange={onChange}
181
            ref={addressFieldRef}
182
            placeholder={l10n.getString(
183
              "modal-custom-alias-picker-form-prefix-placeholder-redesign",
184
            )}
185
            autoCapitalize="none"
186
            className={`${
187
              errorExplainerState.isOpen || containsUppercase || containsSymbols
×
188
                ? styles["invalid-prefix"]
189
                : null
190
            }`}
191
          />
192
          <ErrorTooltip
193
            containsUppercase={containsUppercase}
194
            containsSymbols={containsSymbols}
195
            address={address}
196
          />
197
          <label
198
            htmlFor="address"
199
            className={styles["profile-registered-domain-value"]}
200
          >
201
            @{profile.subdomain}.{getRuntimeConfig().mozmailDomain}
202
          </label>
203
        </div>
204
      </div>
205

206
      <hr />
207
      <div className={styles.buttons}>
208
        <button
209
          {...cancelButton.buttonProps}
210
          ref={cancelButtonRef}
211
          className={styles["end-button"]}
212
        >
213
          {l10n.getString("profile-label-cancel")}
214
        </button>
215
        <Button
216
          type="submit"
217
          disabled={
218
            address.length === 0 || containsUppercase || containsSymbols
×
219
          }
220
        >
221
          {l10n.getString("modal-custom-alias-picker-form-submit-label-2")}
222
        </Button>
223
      </div>
224
    </form>
225
  );
226
};
227

228
type CustomMaskSuccessProps = {
229
  l10n: ReactLocalization;
230
  address: string;
231
  profile: ProfileData;
232
  findAliasDataFromPrefix: (aliasPrefix: string) => AliasData | undefined;
233
  onUpdate: (
234
    aliasToUpdate: AliasData | undefined,
235
    blockPromotions: boolean,
236
    copyToClipboard: boolean | undefined,
237
  ) => void;
238
};
239

240
const CustomMaskSuccess = (props: CustomMaskSuccessProps) => {
3✔
241
  const { l10n, address, profile, findAliasDataFromPrefix, onUpdate } = props;
×
242

243
  const [promotionalsBlocking, setPromotionalsBlocking] = useState(false);
×
244

245
  const onFinished = (event: FormEvent, copyToClipboard?: boolean) => {
×
246
    event.preventDefault();
×
247
    const newlyCreatedAliasData = findAliasDataFromPrefix(address);
×
248
    onUpdate(newlyCreatedAliasData, promotionalsBlocking, copyToClipboard);
×
249
  };
250

251
  const updatePromotionsCheckbox: ChangeEventHandler<HTMLInputElement> = (
×
252
    event,
253
  ) => {
254
    setPromotionalsBlocking(event.target.checked);
×
255
  };
256

257
  return (
258
    <form
259
      onSubmit={(e) => {
260
        onFinished(e, true);
×
261
      }}
262
    >
263
      <div className={styles["newly-created-mask"]}>
264
        <Image
265
          src={Congratulations}
266
          alt={l10n.getString("modal-domain-register-success-title")}
267
        />
268
        <p>
269
          {address}@{profile.subdomain}.{getRuntimeConfig().mozmailDomain}
270
        </p>
271
      </div>
272
      <div className={styles["promotionals-blocking-control"]}>
273
        <input
274
          type="checkbox"
275
          id="promotionalsBlocking"
276
          onChange={updatePromotionsCheckbox}
277
        />
278
        <label htmlFor="promotionalsBlocking">
279
          {l10n.getString(
280
            "popover-custom-alias-explainer-promotional-block-checkbox-label",
281
          )}
282
        </label>
283
        <InfoTooltip
284
          alt={l10n.getString(
285
            "popover-custom-alias-explainer-promotional-block-tooltip-trigger",
286
          )}
287
          iconColor="black"
288
        >
289
          <h3>
290
            {l10n.getString(
291
              "popover-custom-alias-explainer-promotional-block-checkbox",
292
            )}
293
          </h3>
294
          <p className={styles["promotionals-blocking-description"]}>
295
            {l10n.getString(
296
              "popover-custom-alias-explainer-promotional-block-tooltip-2",
297
            )}
298
            <Link href="/faq#faq-promotional-email-blocking">
299
              {l10n.getString("banner-label-data-notification-body-cta")}
300
            </Link>
301
          </p>
302
        </InfoTooltip>
303
      </div>
304
      <div className={styles.tip}>
305
        <span className={styles["tip-icon"]}>
306
          <InfoBulbIcon alt="" />
307
        </span>
308
        <p>{l10n.getString("modal-custom-alias-picker-tip-redesign")}</p>
309
      </div>
310
      <hr />
311
      <div className={styles.buttons}>
312
        <button className={styles["end-button"]} onClick={onFinished}>
313
          {l10n.getString("done-msg")}
314
        </button>
315
        <Button type="submit">
316
          {l10n.getString("copy-mask")}
317
          <CopyIcon alt="" />
318
        </Button>
319
      </div>
320
    </form>
321
  );
322
};
323

324
type PickerDialogProps = {
325
  title: string | ReactElement;
326
  errorStateIsOpen: boolean;
327
  errorStateOnClose: () => void;
328
  children: ReactNode;
329
  isOpen: boolean;
330
  onClose?: () => void;
331
  aliasGeneratedState: boolean;
332
};
333
const PickerDialog = (props: PickerDialogProps & AriaOverlayProps) => {
3✔
334
  const wrapperRef = useRef<HTMLDivElement>(null);
×
335
  const { overlayProps, underlayProps } = useOverlay(props, wrapperRef);
×
336
  const { modalProps } = useModal();
×
337
  const { dialogProps, titleProps } = useDialog({}, wrapperRef);
×
338

339
  return (
340
    <div className={styles.underlay} {...underlayProps}>
341
      <FocusScope contain restoreFocus>
342
        <div
343
          className={styles["dialog-wrapper"]}
344
          {...overlayProps}
345
          {...dialogProps}
346
          {...modalProps}
347
          ref={wrapperRef}
348
        >
349
          <InvalidPrefixBanner
350
            isOpen={props.errorStateIsOpen}
351
            onClose={props.errorStateOnClose}
352
          />
353
          <div className={styles.hero}>
354
            <h3 {...titleProps}>{props.title}</h3>
355
          </div>
356
          {props.children}
357
        </div>
358
      </FocusScope>
359
    </div>
360
  );
361
};
362

363
type ErrorTooltipProps = {
364
  containsUppercase: boolean;
365
  containsSymbols: boolean;
366
  address: string;
367
};
368

369
const ErrorTooltip = (props: ErrorTooltipProps) => {
3✔
370
  const l10n = useL10n();
×
371
  const { containsUppercase, containsSymbols, address } = props;
×
372

373
  return (
374
    <span className={styles.wrapper}>
375
      <span className={styles.tooltip}>
376
        <div className={styles.errors}>
377
          <ErrorStateIcons address={address} errorState={containsUppercase} />
378
          <p>
379
            {l10n.getString(
380
              "error-alias-picker-prefix-invalid-uppercase-letters",
381
            )}
382
          </p>
383
        </div>
384
        <div className={styles.errors}>
385
          <ErrorStateIcons address={address} errorState={containsSymbols} />
386
          <p>{l10n.getString("error-alias-picker-prefix-invalid-symbols")}</p>
387
        </div>
388
      </span>
389
    </span>
390
  );
391
};
392

393
type ErrorStateProps = {
394
  errorState: boolean;
395
  address: string;
396
};
397

398
const ErrorStateIcons = (props: ErrorStateProps) => {
3✔
399
  const { errorState, address } = props;
×
400
  const l10n = useL10n();
×
401

402
  return (
403
    <>
404
      <div className={styles["error-icons"]}>
405
        {address === "" ? (
406
          <BulletPointIcon alt="" className={styles["bullet-icon"]} />
×
407
        ) : !errorState ? (
408
          <CheckIcon
×
409
            alt={l10n.getString("error-state-valid-alt")}
410
            className={styles["check-icon"]}
411
          />
412
        ) : (
413
          <InvalidIcon
414
            alt={l10n.getString("error-state-invalid-alt")}
415
            className={styles["close-icon"]}
416
          />
417
        )}
418
      </div>
419
    </>
420
  );
421
};
422

423
type InvalidPrefixProps = {
424
  isOpen: boolean;
425
  onClose: () => void;
426
};
427

428
const InvalidPrefixBanner = (props: InvalidPrefixProps) => {
3✔
429
  const l10n = useL10n();
×
430

431
  return (
432
    <div
433
      className={`${styles["invalid-address-wrapper"]} ${
434
        props.isOpen ? styles["active"] : null
×
435
      }`}
436
    >
437
      <div className={styles["invalid-address-msg"]}>
438
        <div className={styles["left-content"]}>
439
          <ErrorTriangleIcon alt="" className={styles["prefix-error-icon"]} />
440
          <p>{l10n.getString("error-alias-picker-prefix-invalid")}</p>
441
        </div>
442
        <button
443
          onClick={props.onClose}
444
          className={`${styles["prefix-error-icon"]}  ${styles["close-button"]}`}
445
        >
446
          <CloseIconNormal alt={l10n.getString("close-button-label-alt")} />
447
        </button>
448
      </div>
449
    </div>
450
  );
451
};
452

453
export function isAddressValid(address: string): boolean {
×
454
  // Regular expression:
455
  //
456
  //   ^[a-z0-9]  Starts with a lowercase letter or number;
457
  //
458
  //   (...)?     followed by zero or one of:
459
  //
460
  //              [a-z0-9-.]{0,61} zero up to 61 lowercase letters, numbers, hyphens, or periods, and
461
  //              [a-z0-9]         a lowercase letter or number (but not a hyphen),
462
  //
463
  //   $          and nothing following that.
464
  //
465
  // All that combines to 1-63 lowercase characters, numbers, or hyphens,
466
  // but not starting or ending with a hyphen, aligned with the backend's
467
  // validation (`valid_address_pattern` in emails/models.py).
468
  return /^[a-z0-9]([a-z0-9-.]{0,61}[a-z0-9])?$/.test(address);
×
469
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc