in src/app/(proper_react)/(redesign)/(authenticated)/user/(dashboard)/dashboard/fix/leaked-passwords/LeakedPasswordsLayout.tsx [42:264]
export function LeakedPasswordsLayout(props: LeakedPasswordsLayoutProps) {
const l10n = useL10n();
const router = useRouter();
const recordTelemetry = useTelemetry();
const [isResolving, setIsResolving] = useState(false);
const [subscriberBreaches, setSubscriberBreaches] = useState(
props.data.subscriberBreaches,
);
const stepMap: Record<LeakedPasswordsTypes, StepLink["id"]> = {
passwords: "LeakedPasswordsPassword",
"passwords-done": "LeakedPasswordsPassword",
"security-questions": "LeakedPasswordsSecurityQuestion",
"security-questions-done": "LeakedPasswordsSecurityQuestion",
none: "LeakedPasswordsSecurityQuestion",
};
const isStepDone =
props.type === "passwords-done" || props.type === "security-questions-done";
const guidedExperienceBreaches = getGuidedExperienceBreaches(
subscriberBreaches,
props.subscriberEmails,
);
const unresolvedPasswordBreach = findFirstUnresolvedBreach(
guidedExperienceBreaches,
props.type,
);
// TODO: Write unit tests MNTOR-2560
/* c8 ignore start */
const emailsAffected = unresolvedPasswordBreach?.emailsAffected ?? [];
const nextStep = getNextGuidedStep(
props.data,
props.enabledFeatureFlags,
stepMap[props.type],
);
// If there are no unresolved breaches for the ”leaked passwords” step:
// Go to the next step in the guided resolution or back to the dashboard.
useEffect(() => {
// The check for `isResolving` is a bit of a workaround; when we're done resolving
// all leaked passwords/security questions, we call `router.push()` with a `-done`
// route, and then call `router.refresh()`. However, that call to `router.refresh()`
// results in a new instance of `unresolvedPasswordBreach`, resulting in this effect
// trigger and redirecting to the next step.
// To avoid this, we also check `isResolving`, which is still `true` when marking
// this step as done.
if (!unresolvedPasswordBreach && !isStepDone && !isResolving) {
router.push(nextStep.href);
}
}, [
nextStep.href,
router,
unresolvedPasswordBreach,
isStepDone,
isResolving,
]);
const pageData = getLeakedPasswords({
dataType: props.type,
breaches: guidedExperienceBreaches,
l10n,
emailsAffected,
nextStep,
});
// The non-null assertion here should be safe since we already did this check
// in `./[type]/page.tsx`:
const { title, illustration, content } = pageData!;
const handleUpdateBreachStatus = async () => {
const leakedPasswordsBreachClasses: Record<
LeakedPasswordsTypes,
| (typeof LeakedPasswordsDataTypes)[keyof typeof LeakedPasswordsDataTypes]
| null
> = {
passwords: LeakedPasswordsDataTypes.Passwords,
"passwords-done": null,
"security-questions": LeakedPasswordsDataTypes.SecurityQuestions,
"security-questions-done": null,
none: null,
};
const dataType = leakedPasswordsBreachClasses[props.type];
if (
!dataType ||
!unresolvedPasswordBreach ||
emailsAffected?.length === 0 ||
isResolving
) {
return;
}
setIsResolving(true);
try {
unresolvedPasswordBreach.resolvedDataClasses.push(dataType);
// FIXME/BUG: MNTOR-2562 Remove empty [""] string
const formattedDataClasses =
unresolvedPasswordBreach.resolvedDataClasses.filter(Boolean);
await updatePasswordsBreachStatus(
emailsAffected,
unresolvedPasswordBreach.id,
formattedDataClasses,
);
// Manually move to the next step when breach has been marked as fixed.
const updatedSubscriberBreaches = subscriberBreaches.map(
(subscriberBreach) => {
if (subscriberBreach.id === unresolvedPasswordBreach.id) {
subscriberBreach.resolvedDataClasses.push(dataType);
}
return subscriberBreach;
},
);
const isComplete = hasCompletedStep(
{ ...props.data, subscriberBreaches: updatedSubscriberBreaches },
stepMap[props.type],
);
if (!isComplete) {
setSubscriberBreaches(updatedSubscriberBreaches);
setIsResolving(false);
return;
}
// If all breaches in the step are fully resolved,
// take users to the celebration view.
const doneSlug: LeakedPasswordsTypes =
props.type === "passwords"
? "passwords-done"
: "security-questions-done";
router.push(`/user/dashboard/fix/leaked-passwords/${doneSlug}`);
// Make sure the dashboard re-fetches the breaches on the next visit,
// in order to make resolved breaches move to the "Fixed" tab.
// If we had used server actions, we could've called
// `revalidatePath("/user/dashboard")` there, but the API doesn't appear
// to necessarily share a cache with the client.
router.refresh();
} catch {
// TODO: MNTOR-2563: Capture client error with @next/sentry
setIsResolving(false);
}
};
/* c8 ignore stop */
useEffect(() => {
recordTelemetry("page", "view", {
utm_campaign:
props.type === "passwords"
? "password_exposed"
: "security_question_exposed",
utm_source: "guided_experience",
});
}, [props.type, recordTelemetry]);
return (
<FixView
subscriberEmails={props.subscriberEmails}
data={props.data}
nextStep={nextStep}
currentSection="leaked-passwords"
hideProgressIndicator={isStepDone}
showConfetti={isStepDone}
enabledFeatureFlags={props.enabledFeatureFlags}
>
<ResolutionContainer
type="leakedPasswords"
title={title}
illustration={illustration}
isPremiumUser={hasPremium(props.data.user)}
enabledFeatureFlags={props.enabledFeatureFlags}
cta={
!isStepDone && (
<>
<Button
variant="primary"
small
onPress={() => {
void handleUpdateBreachStatus();
recordTelemetry("ctaButton", "click", {
button_id: "marked_fixed",
// TODO: Enable after the parameter has been added to metrics.yaml.
// button_name:
// props.type === "passwords"
// ? "mark_as_fixed_password_${breachName}"
// : `mark_as_fixed_security_question_${breachName}`,
});
}}
autoFocus={true}
disabled={isResolving}
>
{l10n.getString("leaked-passwords-mark-as-fixed")}
</Button>
<Link
href={nextStep.href}
onClick={() => {
recordTelemetry("button", "click", {
button_id: "skipped_resolution",
// TODO: Enable after the parameter has been added to metrics.yaml.
// button_name:
// props.type === "passwords"
// ? `skip_for_now_password_${breachName}`
// : `skip_for_now_security_question_${breachName}`,
});
}}
>
{l10n.getString("leaked-passwords-skip")}
</Link>
</>
)
}
estimatedTime={!isStepDone ? 4 : undefined}
isStepDone={isStepDone}
data={props.data}
isEligibleForPremium={props.isEligibleForPremium}
>
<ResolutionContent content={content} locale={getLocale(l10n)} />
</ResolutionContainer>
</FixView>
);
}