From 81f19883b2a85ab9e5b4ccce099c3f10cb88077a Mon Sep 17 00:00:00 2001 From: Dave Peck Date: Mon, 18 Mar 2024 12:56:54 -0700 Subject: [PATCH] Add "verify registration" buttons + forms to the site (#4) We now always display two buttons, one for "Verify registration" and one for "Register to vote". In cases where either state will do, we just have generic buttons; in cases where we're explicitly suggesting a state, the buttons name the state by abbreviation. This adds two new events that go to both Facebook and Google: VerifyStart and VerifyFinish. VerifyStart happens after the user submits the initial intake form. VerifyFinish generally speaking happens right after (see https://docs.voteamerica.com/software/events/#action-finish-event-4) * Separate out the form and verification pages. * Plumb new 'verify' events through. * About as good as I can make it, I suppose. --- src/components/More.tsx | 118 +++++++++++++++++------- src/components/VoteAmericaAnalytics.tsx | 81 ++++++++++++++-- src/layouts/va-form.astro | 94 +++++++++++++++++++ src/pages/va-register.astro | 88 ++---------------- src/pages/va-verify.astro | 11 +++ src/utils/analytics.ts | 78 +++++++++++++++- 6 files changed, 348 insertions(+), 122 deletions(-) create mode 100644 src/layouts/va-form.astro create mode 100644 src/pages/va-verify.astro diff --git a/src/components/More.tsx b/src/components/More.tsx index 9eee87f..a1cba59 100644 --- a/src/components/More.tsx +++ b/src/components/More.tsx @@ -208,11 +208,18 @@ const SelectStates: React.FC<{ /** Props to the register-to-vote button. */ interface RegisterToVoteButtonProps { - state: State; handler: RegistrationHandler; + behavior: "register" | "verify"; + + /** The specific state to register in. */ + state: State; /** If true, this was a preferred state. */ chosen?: boolean; + + /** If true, hide the state name in the button. */ + hideState?: boolean; + className?: string; } @@ -220,24 +227,27 @@ interface RegisterToVoteButtonProps { const RegisterToVoteButton: React.FC = ({ state, handler, + behavior, chosen, + hideState, className, }) => { - const url = bestRegistrationUrl(state); - const handleRegistrationClick = useCallback( - (e: React.MouseEvent, state: State, url: string, chosen?: boolean) => { + (e: React.MouseEvent, state: State, chosen?: boolean) => { e.preventDefault(); fireClickRegisterEvent({ state, handler }); switch (handler) { case "direct": + const url = bestRegistrationUrl(state); + if (!url) return; window.open(url, "_blank"); break; case "voteamerica": + const path = behavior === "register" ? "/va-register" : "/va-verify"; window.document.location.href = - "/va-register/?state=" + + `${path}/?state=` + state + (chosen ? `&chosen=${STATE_NAMES[state]}` : ""); break; @@ -253,23 +263,27 @@ const RegisterToVoteButton: React.FC = ({ [handler] ); - if (!url) return null; - return ( handleRegistrationClick(e, state, url, chosen)} + onClick={(e) => handleRegistrationClick(e, state, chosen)} aria-label={`Follow this link to register to vote in ${STATE_NAMES[state]}`} > - Register to vote in{" "} - - {state.toUpperCase()} - + {behavior === "register" ? "Register to vote" : "Verify registration"} + {!hideState && ( + <> + {" "} + in{" "} + + {state.toUpperCase()} + + + )} ); }; @@ -422,7 +436,65 @@ const DescribeSelection: React.FC<{ result: StateSelectionResult; handler: RegistrationHandler; }> = ({ result, handler }) => { - const { selection, homeState, schoolState } = result; + const { selection, homeState } = result; + + let buttons; + if (selection === "toss-up") { + // Show one generic register button + one generic verify button + buttons = ( + <> + + + + ); + } else if (selection === "same") { + // Show one state-specific register button and one state-specific verify + buttons = ( + <> + + + + ); + } else { + // Show one state-specific register button and one state-specific verify + buttons = ( + <> + + + + ); + } return (
@@ -435,23 +507,7 @@ const DescribeSelection: React.FC<{
- {selection !== "school" && bestRegistrationUrl(homeState) && ( - - )} - {selection !== "home" && - selection !== "same" && - bestRegistrationUrl(schoolState) && ( - - )} + {buttons}
diff --git a/src/components/VoteAmericaAnalytics.tsx b/src/components/VoteAmericaAnalytics.tsx index a19883c..2d7dc2a 100644 --- a/src/components/VoteAmericaAnalytics.tsx +++ b/src/components/VoteAmericaAnalytics.tsx @@ -4,9 +4,11 @@ import { fireRegisterFinishEvent, fireRegisterFollowUpEvent, fireRegisterStartEvent, + fireVerifyFinishEvent, + fireVerifyStartEvent, type RegisterFinishMethod, type RegisterFollowUpMethod, - type RegisterUser, + type VoterUser, } from "../utils/analytics"; import { assertNever } from "../utils/asserts"; @@ -16,11 +18,22 @@ import { assertNever } from "../utils/asserts"; * * See https://docs.voteamerica.com/software/events/#register-tool */ -interface RegisterStartEvent extends RegisterUser { +interface RegisterStartEvent extends VoterUser { event: "action-start"; tool: "register"; } +/** + * VoteAmerica "start verify" event, when a user submits the Verify tool's + * intake form. + * + * See https://docs.voteamerica.com/software/events/#verify-tool + */ +interface VerifyStartEvent extends VoterUser { + event: "action-start"; + tool: "verify"; +} + /** Details about what took place during the VA+ form flow. */ type FinishMethod = /** The user clicked a link to visit a state-hosted online voter site. */ @@ -50,7 +63,7 @@ type FinishMethod = * * See https://docs.voteamerica.com/software/events/#register-tool */ -interface RegisterFinishEvent extends RegisterUser { +interface RegisterFinishEvent extends VoterUser { event: "action-finish"; tool: "register"; @@ -61,6 +74,17 @@ interface RegisterFinishEvent extends RegisterUser { url?: string; } +/** + * VoteAmerica "finish verify" event, when the user has completed the + * full VoteAmerica form flow. + * + * See https://docs.voteamerica.com/software/events/#verify-tool + */ +interface VerifyFinishEvent extends VoterUser { + event: "action-finish"; + tool: "verify"; +} + /** Details about user follow-up actions, well after the VA+ form flow. */ type FollowUpMethod = /** @@ -81,7 +105,7 @@ type FollowUpMethod = * * See https://docs.voteamerica.com/software/events/#register-tool */ -interface RegisterFollowUpEvent extends RegisterUser { +interface RegisterFollowUpEvent extends VoterUser { event: "action-follow-up"; tool: "register"; @@ -98,9 +122,12 @@ type RegisterEvent = | RegisterFinishEvent | RegisterFollowUpEvent; +/** All verification event types. */ +type VerifyEvent = VerifyStartEvent | VerifyFinishEvent; + /** The VoteAmerica event type. */ type VoteAmericaEvent = CustomEvent<{ - data: RegisterEvent; + data: RegisterEvent | VerifyEvent; embedEl: HTMLDivElement; iFrame: HTMLIFrameElement; }>; @@ -138,9 +165,8 @@ const followUpMethodToCountMore = ( } }; -/** Listen to VoteAmerica events and manage them. */ -const listener = (event: VoteAmericaEvent, intended: State) => { - const { data } = event.detail; +/** Listen to VoteAmerica reigster tool events. */ +const registerListener = (data: RegisterEvent, intended: State) => { switch (data.event) { case "action-start": console.log("RegisterStartEvent", data); @@ -173,6 +199,45 @@ const listener = (event: VoteAmericaEvent, intended: State) => { } }; +/** Listen to VoteAmerica verify tool events. */ +const verifyListener = (data: VerifyEvent, intended: State) => { + switch (data.event) { + case "action-start": + console.log("VerifyStartEvent", data); + fireVerifyStartEvent({ + state: data.state, + intended, + handler: "voteamerica", + }); + break; + case "action-finish": + console.log("VerifyFinishEvent", data); + fireVerifyFinishEvent({ + state: data.state, + intended, + handler: "voteamerica", + }); + break; + default: + assertNever(data); + } +}; + +/** Listen to VoteAmerica events and manage them. */ +const listener = (event: VoteAmericaEvent, intended: State) => { + const { data } = event.detail; + switch (data.tool) { + case "register": + registerListener(data, intended); + break; + case "verify": + verifyListener(data, intended); + break; + default: + assertNever(data); + } +}; + const useVoteAmericaAnalytics = (intended: State) => { const wrapListener = useCallback( (event: VoteAmericaEvent) => listener(event, intended), diff --git a/src/layouts/va-form.astro b/src/layouts/va-form.astro new file mode 100644 index 0000000..f713447 --- /dev/null +++ b/src/layouts/va-form.astro @@ -0,0 +1,94 @@ +--- +import Layout from "./Layout.astro"; +import VoteAmericaAnalytics from "../components/VoteAmericaAnalytics"; + +// define the props type +interface Props { + /** Page title. */ + title: string; + + /** Action description for the generic banner. */ + action: string; +} + +const { title, action } = Astro.props; +--- + + + {/* Add VoteAmerica+ Embed Tools To section of page. */} + + +
+ {/* Header for above the vote america form. */} +
+
+
+
+

+ Make your vote +

+

+ Count
More +

+
+

+ {action} below with + our trusted partner VoteAmerica. +

+
+
+
+ + {/* Embed the primary voteamerica area */} +
+ +
+
+ + {/* Implement Vote America analytics watching. */} + +
+ + diff --git a/src/pages/va-register.astro b/src/pages/va-register.astro index 65f009a..3dc7ac8 100644 --- a/src/pages/va-register.astro +++ b/src/pages/va-register.astro @@ -1,82 +1,12 @@ --- -import Layout from "../layouts/Layout.astro"; -import VoteAmericaAnalytics from "../components/VoteAmericaAnalytics"; +import VAForm from "../layouts/va-form.astro"; --- - - {/* Add VoteAmerica+ Embed Tools To section of page. */} - - -
-
-
-
-
-

- Make your vote -

-

- Count
More -

-
-

- Register to vote - below with our trusted partner VoteAmerica. -

-
-
-
- - {/* Embed the primary voteamerica area */} -
-
-
-
-
- - {/* Implement Vote America analytics watching. */} - -
- - + +
+
+
diff --git a/src/pages/va-verify.astro b/src/pages/va-verify.astro new file mode 100644 index 0000000..3367dd5 --- /dev/null +++ b/src/pages/va-verify.astro @@ -0,0 +1,11 @@ +--- +import VAForm from "../layouts/va-form.astro"; +--- + + +
+
+
diff --git a/src/utils/analytics.ts b/src/utils/analytics.ts index f3cee32..e42f6a0 100644 --- a/src/utils/analytics.ts +++ b/src/utils/analytics.ts @@ -96,8 +96,34 @@ export const fireClickRegisterEvent = (event: ClickRegisterEvent) => { }); }; +/** + * The "click to verify" event type. This event is fired when a user clicks a + * 'verify registration' button. + */ +export interface ClickVerifyEvent { + /** The state the user clicked to verify in. */ + state: State; + + /** Where we plan to take them next. */ + handler: RegistrationHandler; +} + +/** Fire the click to verify event. */ +export const fireClickVerifyEvent = (event: ClickVerifyEvent) => { + const battleground = isBattleground(event.state); + fireGoogle("event", "click_verify", { + event_category: "engagement", + battleground, + ...event, + }); + fireMetaCustom("ClickVerify", { + battleground, + ...event, + }); +}; + /** Generic information about the registering user. */ -export interface RegisterUser { +export interface VoterUser { /** The user's state of registration. */ state: State; @@ -109,7 +135,7 @@ export interface RegisterUser { * The "start registration form" event type. This event is fired when a user * starts filling out a registration form -- any form. */ -export interface RegisterStartEvent extends RegisterUser { +export interface RegisterStartEvent extends VoterUser { handler: RegistrationHandler; } @@ -127,6 +153,28 @@ export const fireRegisterStartEvent = (event: RegisterStartEvent) => { }); }; +/** + * The "start verify form" event type. This event is fired when a user + * starts filling out a verify form -- any form. + */ +export interface VerifyStartEvent extends VoterUser { + handler: RegistrationHandler; +} + +/** Fire the start verify form event. */ +export const fireVerifyStartEvent = (event: VerifyStartEvent) => { + const battleground = isBattleground(event.state); + fireGoogle("event", "verify_start", { + event_category: "engagement", + battleground, + ...event, + }); + fireMetaCustom("VerifyStart", { + battleground, + ...event, + }); +}; + /** The three common outcomes in registration. */ export type RegisterFinishMethod = "online" | "paper" | "ineligible"; @@ -134,7 +182,7 @@ export type RegisterFinishMethod = "online" | "paper" | "ineligible"; * The "finish registration form" event type. This event is fired when a user * completes a registration form -- any form. */ -export interface RegisterFinishEvent extends RegisterUser { +export interface RegisterFinishEvent extends VoterUser { handler: RegistrationHandler; method: RegisterFinishMethod; url?: string; @@ -154,6 +202,28 @@ export const fireRegisterFinishEvent = (event: RegisterFinishEvent) => { }); }; +/** + * The "finish verify form" event type. This event is fired when a user + * completes a verify form -- any form. + */ +export interface VerifyFinishEvent extends VoterUser { + handler: RegistrationHandler; +} + +/** Fire the finish verify form event. */ +export const fireVerifyFinishEvent = (event: VerifyFinishEvent) => { + const battleground = isBattleground(event.state); + fireGoogle("event", "verify_finish", { + event_category: "engagement", + battleground, + ...event, + }); + fireMetaCustom("VerifyFinish", { + battleground, + ...event, + }); +}; + /** The two common follow-up actions. */ export type RegisterFollowUpMethod = "confirm-online" | "request-paper"; @@ -161,7 +231,7 @@ export type RegisterFollowUpMethod = "confirm-online" | "request-paper"; * The "follow up registration" event type. This event is fired when a user * takes some kind of follow up action. */ -export interface RegisterFollowUpEvent extends RegisterUser { +export interface RegisterFollowUpEvent extends VoterUser { handler: RegistrationHandler; method: RegisterFollowUpMethod; url?: string;