Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4150aa1
Implement dynamic bucket support for AB(C) testing
jess-fortier Jan 12, 2023
0a4050a
Remove New Feature tag from CTA (all variants)
jess-fortier Jan 13, 2023
f9b03ab
Add variant button text for doc auth error
jess-fortier Jan 13, 2023
0b26483
Remove unused import
jess-fortier Jan 13, 2023
d71117f
Add variant button text for IPP CTA
jess-fortier Jan 13, 2023
b406cb2
Add variant copy and translations, add to VariantA
jess-fortier Jan 13, 2023
272ec5a
Allow overriding the IPP CTA heading
Jan 13, 2023
0a033ab
Add alternative IPP CTA button text
Jan 13, 2023
7305923
More work on variation A of AB test
jess-fortier Jan 13, 2023
1cf93da
Prevent wrapping of attempts count
jess-fortier Jan 13, 2023
bb03445
Appease the linter
jess-fortier Jan 14, 2023
96a82fe
Prevent IPP AB test views when IPP would not otherwise be shown. Rout…
jess-fortier Jan 18, 2023
d3ab1ff
Fix linting issues
jess-fortier Jan 18, 2023
7d4a49e
Fix more linting issues
jess-fortier Jan 18, 2023
7b2641e
Fix linting in translation files and include es translation of ipp.ct…
jess-fortier Jan 19, 2023
9834cbf
Update remaining attempts message in variants A and B with nationwide…
jess-fortier Jan 19, 2023
cbf5f3d
Update non-variant remaining attempts message with nationwide copy in…
jess-fortier Jan 19, 2023
38f3c71
Include variant A IPP CTA subheading spanish translation
jess-fortier Jan 19, 2023
a473396
changelog: User-Facing Improvements, In-person proofing, Update PO Se…
jess-fortier Jan 19, 2023
6d9702a
Define a/b values when not defined in specs
Jan 19, 2023
95b3778
Merge branch 'jess/LG-8274-IPP-CTA-AB-test-higher-level-switching' of…
jess-fortier Jan 19, 2023
746791d
Merge remote-tracking branch 'origin/main' into jess/LG-8274-IPP-CTA-…
Jan 20, 2023
ffc3675
Merge branch 'main' into jess/LG-8274-IPP-CTA-AB-test-higher-level-sw…
jess-fortier Jan 20, 2023
c42f0d5
Make A/B test props optional
Jan 20, 2023
5c6fe33
changelog: User-Facing Improvements, In-person proofing, Add a/b test…
Jan 20, 2023
5736bec
Merge branch 'jess/LG-8274-IPP-CTA-AB-test-higher-level-switching' of…
jess-fortier Jan 20, 2023
cf1be06
Add a/b test variant strings to i18n ignore_unused
jess-fortier Jan 20, 2023
e5866a9
Remove unused copy and revise related test
jess-fortier Jan 20, 2023
ac84f4f
Update for linter
jess-fortier Jan 23, 2023
e7af181
Merge branch 'main' into jess/LG-8274-IPP-CTA-AB-test-higher-level-sw…
jess-fortier Jan 23, 2023
ff93953
Fix indentation'
jess-fortier Jan 23, 2023
f80bf84
Update app/javascript/packages/document-capture/components/review-iss…
jess-fortier Jan 23, 2023
720bb08
Merge branch 'main' into jess/LG-8274-IPP-CTA-AB-test-higher-level-sw…
jess-fortier Jan 24, 2023
09327aa
Update CTA test to check for accessible heading
jess-fortier Jan 24, 2023
67718f8
Move variant logging into useEffect hook
jess-fortier Jan 24, 2023
bf5c4a2
Default AB test bucketing to variant A instead of standard so that th…
jess-fortier Jan 25, 2023
4ac9186
Update document capture spec to reflect new default view of IPP CTA
jess-fortier Jan 25, 2023
24ed7f3
Fix a linting issue
jess-fortier Jan 25, 2023
c41d007
Fix incorrect typing of inPersonCtaVariantTestingEnabled context field
jess-fortier Jan 26, 2023
859b1f3
Include tests for the document capture step written by Tomas Apodaca
jess-fortier Jan 26, 2023
3e45804
Make default AB test bucket configurable and set the default for IPP …
jess-fortier Jan 26, 2023
54f6d48
Merge branch 'main' into jess/LG-8274-IPP-CTA-AB-test-higher-level-sw…
jess-fortier Jan 26, 2023
2061018
Move CTA variant testing bucket generation into a method as written b…
jess-fortier Jan 26, 2023
b1e2e48
Merge branch 'jess/LG-8274-IPP-CTA-AB-test-higher-level-switching' of…
jess-fortier Jan 26, 2023
afab0db
Remove trailing space from empty line
jess-fortier Jan 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,20 @@ interface DocumentCaptureTroubleshootingOptionsProps {
* Whether to display alternative options for verifying.
*/
showAlternativeProofingOptions?: boolean;
Comment thread
jess-fortier marked this conversation as resolved.

altInPersonCta?: string;
altInPersonPrompt?: string;
altInPersonCtaButtonText?: string;
}

function DocumentCaptureTroubleshootingOptions({
heading,
location = 'document_capture_troubleshooting_options',
showDocumentTips = true,
showAlternativeProofingOptions,
altInPersonCta,
altInPersonPrompt,
altInPersonCtaButtonText,
}: DocumentCaptureTroubleshootingOptionsProps) {
const { t } = useI18n();
const { inPersonURL } = useContext(InPersonContext);
Expand All @@ -42,7 +49,13 @@ function DocumentCaptureTroubleshootingOptions({

return (
<>
{showAlternativeProofingOptions && inPersonURL && <InPersonCallToAction />}
{showAlternativeProofingOptions && inPersonURL && (
<InPersonCallToAction
altHeading={altInPersonCta}
altPrompt={altInPersonPrompt}
altButtonText={altInPersonCtaButtonText}
/>
)}
<TroubleshootingOptions
heading={heading}
options={
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import sinon from 'sinon';
import { computeAccessibleName } from 'dom-accessibility-api';
Comment thread
jess-fortier marked this conversation as resolved.
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { computeAccessibleDescription } from 'dom-accessibility-api';
import { AnalyticsContextProvider } from '../context/analytics';
import InPersonCallToAction from './in-person-call-to-action';

describe('InPersonCallToAction', () => {
it('renders a section with label and description', () => {
it('renders a section with an accessible heading', () => {
const { getByRole } = render(<InPersonCallToAction />);

const section = getByRole('region', { name: 'in_person_proofing.headings.cta' });
const description = computeAccessibleDescription(section);
Comment thread
jess-fortier marked this conversation as resolved.

expect(description).to.equal('in_person_proofing.body.cta.new_feature');
const heading = getByRole('heading');
expect(computeAccessibleName(heading)).to.equals('in_person_proofing.headings.cta');
});

it('logs an event when clicking the call to action button', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import { useContext } from 'react';
import { Button, Tag } from '@18f/identity-components';
import { Button } from '@18f/identity-components';
import { useInstanceId } from '@18f/identity-react-hooks';
import { t } from '@18f/identity-i18n';
import AnalyticsContext from '../context/analytics';

function InPersonCallToAction() {
interface InPersonCallToActionProps {
altHeading?: string;
altPrompt?: string;
altButtonText?: string;
}

function InPersonCallToAction({ altHeading, altPrompt, altButtonText }: InPersonCallToActionProps) {
const instanceId = useInstanceId();
const { trackEvent } = useContext(AnalyticsContext);

Expand All @@ -14,13 +20,10 @@ function InPersonCallToAction() {
aria-describedby={`in-person-cta-tag-${instanceId}`}
>
<hr className="margin-y-5" />
<Tag id={`in-person-cta-tag-${instanceId}`} isInformative>
{t('in_person_proofing.body.cta.new_feature')}
</Tag>
<h2 id={`in-person-cta-heading-${instanceId}`} className="margin-y-2">
{t('in_person_proofing.headings.cta')}
{altHeading || t('in_person_proofing.headings.cta')}
</h2>
<p>{t('in_person_proofing.body.cta.prompt_detail')}</p>
<p>{altPrompt || t('in_person_proofing.body.cta.prompt_detail')}</p>
<Button
isBig
isOutline
Expand All @@ -29,7 +32,7 @@ function InPersonCallToAction() {
className="margin-top-3 margin-bottom-1"
onClick={() => trackEvent('IdV: verify in person troubleshooting option clicked')}
>
{t('in_person_proofing.body.cta.button')}
{altButtonText || t('in_person_proofing.body.cta.button')}
</Button>
</section>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useContext, useEffect, useState } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import { useContext, useEffect, useState, ReactNode } from 'react';
import { useI18n, formatHTML } from '@18f/identity-react-i18n';
import { useDidUpdateEffect } from '@18f/identity-react-hooks';
import { FormStepsContext, FormStepsButton } from '@18f/identity-form-steps';
import { PageHeading } from '@18f/identity-components';
Expand All @@ -13,6 +13,13 @@ import Warning from './warning';
import AnalyticsContext from '../context/analytics';
import BarcodeAttentionWarning from './barcode-attention-warning';
import FailedCaptureAttemptsContext from '../context/failed-capture-attempts';
import { InPersonContext } from '../context';

function formatWithStrongNoWrap(text: string): ReactNode {
return formatHTML(text, {
strong: ({ children }) => <strong className="text-no-wrap">{children}</strong>,
});
}

type DocumentSide = 'front' | 'back';

Expand Down Expand Up @@ -74,51 +81,177 @@ function ReviewIssuesStep({
useDidUpdateEffect(onPageTransition, [hasDismissed]);

const { onFailedSubmissionAttempt } = useContext(FailedCaptureAttemptsContext);
const { inPersonURL, inPersonCtaVariantActive } = useContext(InPersonContext);
useEffect(() => onFailedSubmissionAttempt(), []);
function onWarningPageDismissed() {
trackEvent('IdV: Capture troubleshooting dismissed');

setHasDismissed(true);
}
function onInPersonSelected() {
trackEvent('IdV: verify in person troubleshooting option clicked');
}

// let FormSteps know, via FormStepsContext, whether this page
// is ready to submit form values
useEffect(() => {
changeStepCanComplete(!!hasDismissed);
}, [hasDismissed]);

useEffect(() => {
if (!inPersonURL || isFailedResult) {
Comment thread
jess-fortier marked this conversation as resolved.
return;
}
if (inPersonCtaVariantActive === 'in_person_variant_a') {
trackEvent('IdV: IPP CTA Variant A');
} else if (inPersonCtaVariantActive === 'in_person_variant_b') {
trackEvent('IdV: IPP CTA Variant B');
} else if (inPersonCtaVariantActive === 'in_person_variant_c') {
trackEvent('IdV: IPP CTA Variant C');
}
}, []);

if (!hasDismissed) {
if (pii) {
return <BarcodeAttentionWarning onDismiss={onWarningPageDismissed} pii={pii} />;
}

return (
<Warning
heading={t('errors.doc_auth.throttled_heading')}
actionText={t('idv.failure.button.warning')}
actionOnClick={onWarningPageDismissed}
location="doc_auth_review_issues"
remainingAttempts={remainingAttempts}
troubleshootingOptions={
<DocumentCaptureTroubleshootingOptions
location="post_submission_warning"
showAlternativeProofingOptions={!isFailedResult}
heading={t('components.troubleshooting_options.ipp_heading')}
/>
}
>
{!!unknownFieldErrors &&
unknownFieldErrors
.filter((error) => !['front', 'back'].includes(error.field!))
.map(({ error }) => <p key={error.message}>{error.message}</p>)}

{remainingAttempts <= DISPLAY_ATTEMPTS && (
<p>
<strong>{t('idv.failure.attempts', { count: remainingAttempts })}</strong>
</p>
)}
</Warning>
);
if (!inPersonURL || isFailedResult) {
return (
<Warning
heading={t('errors.doc_auth.throttled_heading')}
actionText={t('idv.failure.button.warning')}
actionOnClick={onWarningPageDismissed}
location="doc_auth_review_issues"
remainingAttempts={remainingAttempts}
troubleshootingOptions={
<DocumentCaptureTroubleshootingOptions
location="post_submission_warning"
showAlternativeProofingOptions={!isFailedResult}
heading={t('components.troubleshooting_options.ipp_heading')}
/>
}
>
{!!unknownFieldErrors &&
unknownFieldErrors
.filter((error) => !['front', 'back'].includes(error.field!))
.map(({ error }) => <p key={error.message}>{error.message}</p>)}

{remainingAttempts <= DISPLAY_ATTEMPTS && (
<p>
<strong>{t('idv.failure.attempts', { count: remainingAttempts })}</strong>
</p>
)}
</Warning>
);
}
if (inPersonCtaVariantActive === 'in_person_variant_a') {
Comment thread
jess-fortier marked this conversation as resolved.
return (
<Warning
heading={t('errors.doc_auth.throttled_heading')}
actionText={t('idv.failure.button.warning_variant')}
actionOnClick={onWarningPageDismissed}
location="doc_auth_review_issues"
remainingAttempts={remainingAttempts}
troubleshootingOptions={
<DocumentCaptureTroubleshootingOptions
location="post_submission_warning"
showAlternativeProofingOptions={!isFailedResult}
heading={t('components.troubleshooting_options.ipp_heading')}
altInPersonCta={t('in_person_proofing.headings.cta_variant')}
altInPersonPrompt={t('in_person_proofing.body.cta.prompt_detail_a')}
altInPersonCtaButtonText={t('in_person_proofing.body.cta.button_variant')}
/>
}
>
<h2>{t('errors.doc_auth.throttled_subheading')}</h2>
{!!unknownFieldErrors &&
unknownFieldErrors
.filter((error) => !['front', 'back'].includes(error.field!))
.map(({ error }) => <p key={error.message}>{error.message}</p>)}

{remainingAttempts <= DISPLAY_ATTEMPTS && (
<p>
{remainingAttempts === 1
? formatWithStrongNoWrap(t('idv.failure.attempts.one_variant_a_html'))
: formatWithStrongNoWrap(
t('idv.failure.attempts.other_variant_a_html', { count: remainingAttempts }),
)}
Comment thread
jess-fortier marked this conversation as resolved.
</p>
)}
</Warning>
);
}
if (inPersonCtaVariantActive === 'in_person_variant_b') {
return (
<Warning
heading={t('errors.doc_auth.throttled_heading')}
actionText={t('idv.failure.button.warning_variant')}
actionOnClick={onWarningPageDismissed}
altActionText={t('in_person_proofing.body.cta.button_variant')}
altActionOnClick={onInPersonSelected}
altHref="#location"
location="doc_auth_review_issues"
remainingAttempts={remainingAttempts}
troubleshootingOptions={
<DocumentCaptureTroubleshootingOptions
location="post_submission_warning"
showAlternativeProofingOptions={false}
heading={t('components.troubleshooting_options.ipp_heading')}
/>
}
>
{!!unknownFieldErrors &&
unknownFieldErrors
.filter((error) => !['front', 'back'].includes(error.field!))
.map(({ error }) => <p key={error.message}>{error.message}</p>)}

{remainingAttempts <= DISPLAY_ATTEMPTS && (
<p>
{remainingAttempts === 1
? formatWithStrongNoWrap(t('idv.failure.attempts.one_variant_b_html'))
: formatWithStrongNoWrap(
t('idv.failure.attempts.other_variant_b_html', { count: remainingAttempts }),
)}
</p>
)}
<p>{t('in_person_proofing.body.cta.prompt_detail_b')}</p>
</Warning>
);
}
if (inPersonCtaVariantActive === 'in_person_variant_c') {
return (
<Warning
heading={t('errors.doc_auth.throttled_heading')}
actionText={t('idv.failure.button.warning')}
actionOnClick={onWarningPageDismissed}
location="doc_auth_review_issues"
remainingAttempts={remainingAttempts}
troubleshootingOptions={
<DocumentCaptureTroubleshootingOptions
location="post_submission_warning"
showAlternativeProofingOptions={false}
heading={t('components.troubleshooting_options.ipp_heading')}
/>
}
>
{!!unknownFieldErrors &&
unknownFieldErrors
.filter((error) => !['front', 'back'].includes(error.field!))
.map(({ error }) => <p key={error.message}>{error.message}</p>)}

{remainingAttempts <= DISPLAY_ATTEMPTS && (
<p>
<strong>
{remainingAttempts === 1
? t('idv.failure.attempts.one')
: t('idv.failure.attempts.other', { count: remainingAttempts })}
</strong>
</p>
)}
</Warning>
Comment thread
jess-fortier marked this conversation as resolved.
);
}
}

return (
Expand Down
34 changes: 34 additions & 0 deletions app/javascript/packages/document-capture/components/warning.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ interface WarningProps {
*/
actionOnClick?: () => void;

/**
* Secondary action button text.
*/
altActionText?: string;

/**
* Secondary action button text.
*/
altActionOnClick?: () => void;

/**
* Secondary action button location.
*/
altHref?: string;

/**
* Component children.
*/
Expand All @@ -45,6 +60,9 @@ function Warning({
heading,
actionText,
actionOnClick,
altActionText,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just want to make sure i understand - altAction is variant-b's cta?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Speaking first generally, we augmented this component to allow for two actions in the same paradigm as you might see an "OK" and a "Cancel". The primary user action is as before: Blue button, white text. We've added an alternative user action option that is presented as a white button with blue outline.

You are correct about the way this is used in the variant B Warning component.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh ok so then i should think of this as a [stylistic] secondary version of the button.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm I might be misunderstanding you here - "a [stylistic] secondary version of the button" makes it sound like a different (stylistic) way to present a sole intended user option, like you would be using the white-with-blue-outline style instead of the blue. While the implementation would technically allow for that (I think...), that isn't the intended use. Rather than a different way to display the same button, we wanted the Warning to display two different buttons.

altActionOnClick,
altHref,
children,
troubleshootingOptions,
location,
Expand All @@ -69,6 +87,22 @@ function Warning({
{actionText}
</Button>,
];
if (altActionText && altActionOnClick) {
actionButtons.push(
<Button
isBig
isOutline
isWide
href={altHref}
onClick={() => {
trackEvent('IdV: warning action triggered', { location });
altActionOnClick();
}}
>
{altActionText}
</Button>,
);
}
}

return (
Expand Down
Loading