-
Notifications
You must be signed in to change notification settings - Fork 224
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4787 from mozilla/MNTOR-3358
Free scan CTA experiment (MNTOR-3341)
- Loading branch information
Showing
9 changed files
with
400 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ landing-all-hero-lead = We scan data breaches to see if your data has been leake | |
landing-all-hero-emailform-input-placeholder = [email protected] | ||
landing-all-hero-emailform-input-label = Enter your email address to check for data breach exposures. | ||
landing-all-hero-emailform-submit-label = Get free scan | ||
landing-all-hero-emailform-submit-sign-in-label = Sign in to get free scan | ||
# This is a label underneath a big number "14" - it's an image that demos Monitor. | ||
landing-all-hero-image-chart-label = exposures | ||
|
120 changes: 120 additions & 0 deletions
120
src/app/(proper_react)/(redesign)/(public)/FreeScanCta.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
"use client"; | ||
|
||
import { signIn } from "next-auth/react"; | ||
import { useCookies } from "react-cookie"; | ||
import { SignUpForm, Props as SignUpFormProps } from "./SignUpForm"; | ||
import { TelemetryButton } from "../../../components/client/TelemetryButton"; | ||
import { modifyAttributionsForUrlSearchParams } from "../../../functions/universal/attributions"; | ||
import { ExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; | ||
import { useL10n } from "../../../hooks/l10n"; | ||
import { WaitlistCta } from "./ScanLimit"; | ||
|
||
export type Props = { | ||
eligibleForPremium: boolean; | ||
signUpCallbackUrl: string; | ||
isHero?: boolean; | ||
eventId: { | ||
cta: string; | ||
field?: string; | ||
}; | ||
scanLimitReached: boolean; | ||
placeholder?: string; | ||
}; | ||
|
||
export function getAttributionSearchParams({ | ||
cookies, | ||
emailInput, | ||
experimentData, | ||
}: { | ||
cookies: { | ||
attributionsFirstTouch?: string; | ||
}; | ||
emailInput?: string; | ||
experimentData?: ExperimentData; | ||
}) { | ||
const attributionSearchParams = modifyAttributionsForUrlSearchParams( | ||
new URLSearchParams(cookies.attributionsFirstTouch), | ||
{ | ||
entrypoint: "monitor.mozilla.org-monitor-product-page", | ||
form_type: typeof emailInput === "string" ? "email" : "button", | ||
service: process.env.OAUTH_CLIENT_ID as string, | ||
...(emailInput && { email: emailInput }), | ||
...(experimentData && | ||
experimentData["landing-page-free-scan-cta"].enabled && { | ||
entrypoint_experiment: "landing-page-free-scan-cta", | ||
entrypoint_variation: | ||
experimentData["landing-page-free-scan-cta"].variant, | ||
}), | ||
}, | ||
{ | ||
utm_source: "product", | ||
utm_medium: "monitor", | ||
utm_campaign: "get_free_scan", | ||
}, | ||
); | ||
|
||
return attributionSearchParams.toString(); | ||
} | ||
|
||
export const FreeScanCta = ( | ||
props: SignUpFormProps & { | ||
experimentData: ExperimentData; | ||
}, | ||
) => { | ||
const l10n = useL10n(); | ||
const [cookies] = useCookies(["attributionsFirstTouch"]); | ||
if ( | ||
!props.experimentData["landing-page-free-scan-cta"].enabled || | ||
props.experimentData["landing-page-free-scan-cta"].variant === | ||
"ctaWithEmail" | ||
) { | ||
return ( | ||
<SignUpForm | ||
scanLimitReached={props.scanLimitReached} | ||
isHero={props.isHero} | ||
eligibleForPremium={props.eligibleForPremium} | ||
signUpCallbackUrl={props.signUpCallbackUrl} | ||
eventId={props.eventId} | ||
experimentData={props.experimentData} | ||
/> | ||
); | ||
} | ||
|
||
return props.scanLimitReached ? ( | ||
<WaitlistCta /> | ||
) : ( | ||
<div> | ||
<TelemetryButton | ||
variant="primary" | ||
event={{ | ||
module: "ctaButton", | ||
name: "click", | ||
data: { | ||
button_id: `${props.eventId.cta}-${props.experimentData["landing-page-free-scan-cta"].variant}`, | ||
}, | ||
}} | ||
onPress={() => { | ||
void signIn( | ||
"fxa", | ||
{ callbackUrl: props.signUpCallbackUrl }, | ||
getAttributionSearchParams({ | ||
cookies, | ||
experimentData: props.experimentData, | ||
}), | ||
); | ||
}} | ||
> | ||
{l10n.getString( | ||
props.experimentData["landing-page-free-scan-cta"].variant === | ||
"ctaOnly" | ||
? "landing-all-hero-emailform-submit-label" | ||
: "landing-all-hero-emailform-submit-sign-in-label", | ||
)} | ||
</TelemetryButton> | ||
</div> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ import Meta, { | |
LandingUsScanLimit, | ||
} from "./LandingView.stories"; | ||
import { deleteAllCookies } from "../../../functions/client/deleteAllCookies"; | ||
import { defaultExperimentData } from "../../../../telemetry/generated/nimbus/experiments"; | ||
|
||
jest.mock("next-auth/react", () => { | ||
return { | ||
|
@@ -833,6 +834,200 @@ describe("When Premium is available", () => { | |
}); | ||
}); | ||
|
||
describe("Free scan CTA experiment", () => { | ||
it("shows the CTA button with email input if the experiment disabled", () => { | ||
const ComposedDashboard = composeStory(LandingUs, Meta); | ||
render( | ||
<ComposedDashboard | ||
experimentData={{ | ||
...defaultExperimentData, | ||
"landing-page-free-scan-cta": { | ||
...defaultExperimentData["landing-page-free-scan-cta"], | ||
enabled: false, | ||
}, | ||
}} | ||
/>, | ||
); | ||
|
||
const inputField = screen.getAllByLabelText( | ||
"Enter your email address to check for data breach exposures and sites selling your info.", | ||
); | ||
expect(inputField[0]).toBeInTheDocument(); | ||
|
||
const submitButton = screen.getAllByRole("button", { | ||
name: "Get free scan", | ||
}); | ||
expect(submitButton[0]).toBeInTheDocument(); | ||
}); | ||
|
||
it("shows the CTA button with email input for the variant `ctaWithEmail` if the experiment is enabled", () => { | ||
const ComposedDashboard = composeStory(LandingUs, Meta); | ||
render( | ||
<ComposedDashboard | ||
experimentData={{ | ||
...defaultExperimentData, | ||
"landing-page-free-scan-cta": { | ||
enabled: true, | ||
variant: "ctaWithEmail", | ||
}, | ||
}} | ||
/>, | ||
); | ||
|
||
const inputField = screen.getAllByLabelText( | ||
"Enter your email address to check for data breach exposures and sites selling your info.", | ||
); | ||
expect(inputField[0]).toBeInTheDocument(); | ||
|
||
const submitButton = screen.getAllByRole("button", { | ||
name: "Get free scan", | ||
}); | ||
expect(submitButton[0]).toBeInTheDocument(); | ||
}); | ||
|
||
it("shows the CTA button only for the variant `ctaOnly` if the experiment is enabled", () => { | ||
const ComposedDashboard = composeStory(LandingUs, Meta); | ||
render( | ||
<ComposedDashboard | ||
experimentData={{ | ||
...defaultExperimentData, | ||
"landing-page-free-scan-cta": { | ||
enabled: true, | ||
variant: "ctaOnly", | ||
}, | ||
}} | ||
/>, | ||
); | ||
|
||
const inputField = screen.queryAllByTestId("signup-form-input"); | ||
expect(inputField.length).toBe(0); | ||
|
||
const submitButton = screen.getAllByRole("button", { | ||
name: "Get free scan", | ||
}); | ||
expect(submitButton[0]).toBeInTheDocument(); | ||
}); | ||
|
||
it("shows the CTA button only with an alternative label for the variant `ctaOnlyAlternativeLabel` if the experiment is enabled", () => { | ||
const ComposedDashboard = composeStory(LandingUs, Meta); | ||
render( | ||
<ComposedDashboard | ||
experimentData={{ | ||
...defaultExperimentData, | ||
"landing-page-free-scan-cta": { | ||
enabled: true, | ||
variant: "ctaOnlyAlternativeLabel", | ||
}, | ||
}} | ||
/>, | ||
); | ||
|
||
const inputField = screen.queryAllByTestId("signup-form-input"); | ||
expect(inputField.length).toBe(0); | ||
|
||
const submitButton = screen.getAllByRole("button", { | ||
name: "Sign in to get free scan", | ||
}); | ||
expect(submitButton[0]).toBeInTheDocument(); | ||
}); | ||
|
||
it("shows the waitlist CTA when the scan limit is reached", () => { | ||
const ComposedDashboard = composeStory(LandingUsScanLimit, Meta); | ||
render( | ||
<ComposedDashboard | ||
experimentData={{ | ||
...defaultExperimentData, | ||
"landing-page-free-scan-cta": { | ||
enabled: true, | ||
variant: "ctaOnly", | ||
}, | ||
}} | ||
/>, | ||
); | ||
const waitlistCta = screen.getAllByRole("link", { | ||
name: "Join waitlist", | ||
}); | ||
expect(waitlistCta[0]).toBeInTheDocument(); | ||
}); | ||
|
||
it("sends telemetry for the different experiment variants", async () => { | ||
const mockedRecord = useTelemetry(); | ||
const user = userEvent.setup(); | ||
const ComposedDashboard = composeStory(LandingUs, Meta); | ||
render( | ||
<ComposedDashboard | ||
experimentData={{ | ||
...defaultExperimentData, | ||
"landing-page-free-scan-cta": { | ||
enabled: true, | ||
variant: "ctaOnly", | ||
}, | ||
}} | ||
/>, | ||
); | ||
|
||
// jsdom will complain about not being able to navigate to a different page | ||
// after clicking the link; suppress that error, as it's not relevant to the | ||
// test: | ||
jest.spyOn(console, "error").mockImplementation(() => undefined); | ||
|
||
const submitButton = screen.getAllByRole("button", { | ||
name: "Get free scan", | ||
}); | ||
await user.click(submitButton[0]); | ||
|
||
expect(mockedRecord).toHaveBeenCalledWith( | ||
"ctaButton", | ||
"click", | ||
expect.objectContaining({ button_id: "clicked_get_scan_header-ctaOnly" }), | ||
); | ||
}); | ||
|
||
it("passes the expected URL to the identity provider", async () => { | ||
const user = userEvent.setup(); | ||
const ComposedDashboard = composeStory(LandingUs, Meta); | ||
render( | ||
<ComposedDashboard | ||
experimentData={{ | ||
...defaultExperimentData, | ||
"landing-page-free-scan-cta": { | ||
enabled: true, | ||
variant: "ctaWithEmail", | ||
}, | ||
}} | ||
/>, | ||
); | ||
|
||
const inputField = screen.getAllByLabelText( | ||
"Enter your email address to check for data breach exposures and sites selling your info.", | ||
); | ||
await user.type(inputField[0], "[email protected]"); | ||
|
||
const submitButton = screen.getAllByRole("button", { | ||
name: "Get free scan", | ||
}); | ||
await user.click(submitButton[0]); | ||
|
||
expect(signIn).toHaveBeenCalledWith( | ||
"fxa", | ||
expect.any(Object), | ||
expect.stringContaining( | ||
[ | ||
"entrypoint=monitor.mozilla.org-monitor-product-page", | ||
"form_type=email", | ||
"service=edd29a80019d61a1", | ||
"email=mail%40example.com", | ||
"entrypoint_experiment=landing-page-free-scan-cta", | ||
"entrypoint_variation=ctaWithEmail", | ||
"utm_source=product", | ||
"utm_medium=monitor", | ||
"utm_campaign=get_free_scan", | ||
].join("&"), | ||
), | ||
); | ||
}); | ||
}); | ||
|
||
it("does not show a confirmaton message if the user has just deleted their account", () => { | ||
document.cookie = "justDeletedAccount=justDeletedAccount; max-age=0"; | ||
|
||
|
Oops, something went wrong.