diff --git a/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.tsx b/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.tsx index ead1a99541b..07a24ad943d 100644 --- a/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.tsx +++ b/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.tsx @@ -27,6 +27,10 @@ interface DocumentCaptureTroubleshootingOptionsProps { * Whether to display alternative options for verifying. */ showAlternativeProofingOptions?: boolean; + + altInPersonCta?: string; + altInPersonPrompt?: string; + altInPersonCtaButtonText?: string; } function DocumentCaptureTroubleshootingOptions({ @@ -34,6 +38,9 @@ function DocumentCaptureTroubleshootingOptions({ location = 'document_capture_troubleshooting_options', showDocumentTips = true, showAlternativeProofingOptions, + altInPersonCta, + altInPersonPrompt, + altInPersonCtaButtonText, }: DocumentCaptureTroubleshootingOptionsProps) { const { t } = useI18n(); const { inPersonURL } = useContext(InPersonContext); @@ -42,7 +49,13 @@ function DocumentCaptureTroubleshootingOptions({ return ( <> - {showAlternativeProofingOptions && inPersonURL && } + {showAlternativeProofingOptions && inPersonURL && ( + + )} { - it('renders a section with label and description', () => { + it('renders a section with an accessible heading', () => { const { getByRole } = render(); - - const section = getByRole('region', { name: 'in_person_proofing.headings.cta' }); - const description = computeAccessibleDescription(section); - - 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 () => { diff --git a/app/javascript/packages/document-capture/components/in-person-call-to-action.tsx b/app/javascript/packages/document-capture/components/in-person-call-to-action.tsx index e52019b773b..0bae659d864 100644 --- a/app/javascript/packages/document-capture/components/in-person-call-to-action.tsx +++ b/app/javascript/packages/document-capture/components/in-person-call-to-action.tsx @@ -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); @@ -14,13 +20,10 @@ function InPersonCallToAction() { aria-describedby={`in-person-cta-tag-${instanceId}`} >
- - {t('in_person_proofing.body.cta.new_feature')} -

- {t('in_person_proofing.headings.cta')} + {altHeading || t('in_person_proofing.headings.cta')}

-

{t('in_person_proofing.body.cta.prompt_detail')}

+

{altPrompt || t('in_person_proofing.body.cta.prompt_detail')}

); diff --git a/app/javascript/packages/document-capture/components/review-issues-step.tsx b/app/javascript/packages/document-capture/components/review-issues-step.tsx index a660d881d0b..6433a52ee68 100644 --- a/app/javascript/packages/document-capture/components/review-issues-step.tsx +++ b/app/javascript/packages/document-capture/components/review-issues-step.tsx @@ -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'; @@ -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 }) => {children}, + }); +} type DocumentSide = 'front' | 'back'; @@ -74,12 +81,16 @@ 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 @@ -87,38 +98,160 @@ function ReviewIssuesStep({ changeStepCanComplete(!!hasDismissed); }, [hasDismissed]); + useEffect(() => { + if (!inPersonURL || isFailedResult) { + 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 ; } - return ( - - } - > - {!!unknownFieldErrors && - unknownFieldErrors - .filter((error) => !['front', 'back'].includes(error.field!)) - .map(({ error }) =>

{error.message}

)} - - {remainingAttempts <= DISPLAY_ATTEMPTS && ( -

- {t('idv.failure.attempts', { count: remainingAttempts })} -

- )} -
- ); + if (!inPersonURL || isFailedResult) { + return ( + + } + > + {!!unknownFieldErrors && + unknownFieldErrors + .filter((error) => !['front', 'back'].includes(error.field!)) + .map(({ error }) =>

{error.message}

)} + + {remainingAttempts <= DISPLAY_ATTEMPTS && ( +

+ {t('idv.failure.attempts', { count: remainingAttempts })} +

+ )} +
+ ); + } + if (inPersonCtaVariantActive === 'in_person_variant_a') { + return ( + + } + > +

{t('errors.doc_auth.throttled_subheading')}

+ {!!unknownFieldErrors && + unknownFieldErrors + .filter((error) => !['front', 'back'].includes(error.field!)) + .map(({ error }) =>

{error.message}

)} + + {remainingAttempts <= DISPLAY_ATTEMPTS && ( +

+ {remainingAttempts === 1 + ? formatWithStrongNoWrap(t('idv.failure.attempts.one_variant_a_html')) + : formatWithStrongNoWrap( + t('idv.failure.attempts.other_variant_a_html', { count: remainingAttempts }), + )} +

+ )} +
+ ); + } + if (inPersonCtaVariantActive === 'in_person_variant_b') { + return ( + + } + > + {!!unknownFieldErrors && + unknownFieldErrors + .filter((error) => !['front', 'back'].includes(error.field!)) + .map(({ error }) =>

{error.message}

)} + + {remainingAttempts <= DISPLAY_ATTEMPTS && ( +

+ {remainingAttempts === 1 + ? formatWithStrongNoWrap(t('idv.failure.attempts.one_variant_b_html')) + : formatWithStrongNoWrap( + t('idv.failure.attempts.other_variant_b_html', { count: remainingAttempts }), + )} +

+ )} +

{t('in_person_proofing.body.cta.prompt_detail_b')}

+
+ ); + } + if (inPersonCtaVariantActive === 'in_person_variant_c') { + return ( + + } + > + {!!unknownFieldErrors && + unknownFieldErrors + .filter((error) => !['front', 'back'].includes(error.field!)) + .map(({ error }) =>

{error.message}

)} + + {remainingAttempts <= DISPLAY_ATTEMPTS && ( +

+ + {remainingAttempts === 1 + ? t('idv.failure.attempts.one') + : t('idv.failure.attempts.other', { count: remainingAttempts })} + +

+ )} +
+ ); + } } return ( diff --git a/app/javascript/packages/document-capture/components/warning.tsx b/app/javascript/packages/document-capture/components/warning.tsx index 60ae578d4af..e6128770e40 100644 --- a/app/javascript/packages/document-capture/components/warning.tsx +++ b/app/javascript/packages/document-capture/components/warning.tsx @@ -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. */ @@ -45,6 +60,9 @@ function Warning({ heading, actionText, actionOnClick, + altActionText, + altActionOnClick, + altHref, children, troubleshootingOptions, location, @@ -69,6 +87,22 @@ function Warning({ {actionText} , ]; + if (altActionText && altActionOnClick) { + actionButtons.push( + , + ); + } } return ( diff --git a/app/javascript/packages/document-capture/context/in-person.ts b/app/javascript/packages/document-capture/context/in-person.ts index 19d255cb63d..90e7fdd6ced 100644 --- a/app/javascript/packages/document-capture/context/in-person.ts +++ b/app/javascript/packages/document-capture/context/in-person.ts @@ -6,6 +6,16 @@ export interface InPersonContextProps { */ arcgisSearchEnabled?: boolean; + /** + * Whether or not A/B testing of the in-person proofing CTA is enabled. + */ + inPersonCtaVariantTestingEnabled?: boolean; + + /** + * The specific A/B testing variant that was activated for the current user session. + */ + inPersonCtaVariantActive?: string; + /** * URL to in-person proofing alternative flow, if enabled. */ @@ -14,6 +24,8 @@ export interface InPersonContextProps { const InPersonContext = createContext({ arcgisSearchEnabled: false, + inPersonCtaVariantTestingEnabled: false, + inPersonCtaVariantActive: '', }); InPersonContext.displayName = 'InPersonContext'; diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx index 5cfa0d640c3..e3f0ba01abd 100644 --- a/app/javascript/packs/document-capture.tsx +++ b/app/javascript/packs/document-capture.tsx @@ -32,6 +32,8 @@ interface AppRootData { cancelUrl: string; idvInPersonUrl?: string; securityAndPrivacyHowItWorksUrl: string; + inPersonCtaVariantTestingEnabled: boolean; + inPersonCtaVariantActive: string; } const appRoot = document.getElementById('document-capture-form')!; @@ -119,6 +121,8 @@ const trackEvent: typeof baseTrackEvent = (event, payload) => { idvInPersonUrl: inPersonURL, securityAndPrivacyHowItWorksUrl: securityAndPrivacyHowItWorksURL, arcgisSearchEnabled, + inPersonCtaVariantTestingEnabled, + inPersonCtaVariantActive, } = appRoot.dataset as DOMStringMap & AppRootData; const App = composeComponents( @@ -170,7 +174,14 @@ const trackEvent: typeof baseTrackEvent = (event, payload) => { ], [ InPersonContext.Provider, - { value: { arcgisSearchEnabled: arcgisSearchEnabled === 'true', inPersonURL } }, + { + value: { + arcgisSearchEnabled: arcgisSearchEnabled === 'true', + inPersonCtaVariantTestingEnabled: inPersonCtaVariantTestingEnabled === true, + inPersonCtaVariantActive, + inPersonURL, + }, + }, ], [DocumentCapture, { isAsyncForm, onStepChange: keepAlive }], ); diff --git a/app/services/idv/steps/document_capture_step.rb b/app/services/idv/steps/document_capture_step.rb index 7cd0cb343a2..e2df7019cdd 100644 --- a/app/services/idv/steps/document_capture_step.rb +++ b/app/services/idv/steps/document_capture_step.rb @@ -29,7 +29,11 @@ def extra_view_variables image_type: 'back', transaction_id: flow_session[:document_capture_session_uuid], ), - }.merge(native_camera_ab_testing_variables, acuant_sdk_upgrade_a_b_testing_variables) + }.merge( + native_camera_ab_testing_variables, + acuant_sdk_upgrade_a_b_testing_variables, + in_person_cta_variant_testing_variables, + ) end private @@ -58,6 +62,15 @@ def acuant_sdk_upgrade_a_b_testing_variables } end + def in_person_cta_variant_testing_variables + bucket = AbTests::IN_PERSON_CTA.bucket(flow_session[:document_capture_session_uuid]) + { + in_person_cta_variant_testing_enabled: + IdentityConfig.store.in_person_cta_variant_testing_enabled, + in_person_cta_variant_active: bucket, + } + end + def handle_stored_result if stored_result&.success? save_proofing_components diff --git a/app/views/idv/capture_doc/document_capture.html.erb b/app/views/idv/capture_doc/document_capture.html.erb index 558e540c1c3..8665d905e9f 100644 --- a/app/views/idv/capture_doc/document_capture.html.erb +++ b/app/views/idv/capture_doc/document_capture.html.erb @@ -9,4 +9,6 @@ acuant_sdk_upgrade_a_b_testing_enabled: acuant_sdk_upgrade_a_b_testing_enabled, use_alternate_sdk: use_alternate_sdk, acuant_version: acuant_version, + in_person_cta_variant_testing_enabled: in_person_cta_variant_testing_enabled, + in_person_cta_variant_active: in_person_cta_variant_active, ) %> diff --git a/app/views/idv/doc_auth/document_capture.html.erb b/app/views/idv/doc_auth/document_capture.html.erb index 83de02e8367..ac560e00ed2 100644 --- a/app/views/idv/doc_auth/document_capture.html.erb +++ b/app/views/idv/doc_auth/document_capture.html.erb @@ -9,4 +9,6 @@ acuant_sdk_upgrade_a_b_testing_enabled: acuant_sdk_upgrade_a_b_testing_enabled, use_alternate_sdk: use_alternate_sdk, acuant_version: acuant_version, + in_person_cta_variant_testing_enabled: in_person_cta_variant_testing_enabled, + in_person_cta_variant_active: in_person_cta_variant_active, ) %> diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index deca885ec3f..362654929d0 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -44,6 +44,8 @@ idv_in_person_url: Idv::InPersonConfig.enabled_for_issuer?(decorated_session.sp_issuer) ? idv_in_person_url : nil, security_and_privacy_how_it_works_url: MarketingSite.security_and_privacy_how_it_works_url, arcgis_search_enabled: IdentityConfig.store.arcgis_search_enabled, + in_person_cta_variant_testing_enabled: IdentityConfig.store.in_person_cta_variant_testing_enabled, + in_person_cta_variant_active: in_person_cta_variant_active, } %> <%= simple_form_for( :doc_auth, diff --git a/config/application.yml.default b/config/application.yml.default index e8a35c98e9a..cdfee001840 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -126,6 +126,8 @@ idv_send_link_attempt_window_in_minutes: 10 idv_send_link_max_attempts: 5 ie11_support_end_date: '2022-12-31' idv_sp_required: false +in_person_cta_variant_testing_enabled: true +in_person_cta_variant_testing_percents: '{"A":50, "B":50}' in_person_email_reminder_early_benchmark_in_days: 11 in_person_email_reminder_final_benchmark_in_days: 1 in_person_email_reminder_late_benchmark_in_days: 4 diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index 2ec1d10a032..7a0c2bee4c8 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -99,6 +99,12 @@ ignore_unused: - 'errors.messages.*' - 'simple_form.*' - 'time.*' + - 'idv.failure.attempts.one' + - 'idv.failure.attempts.one_variant_a_html' + - 'idv.failure.attempts.one_variant_b_html' + - 'idv.failure.attempts.other' + - 'idv.failure.attempts.other_variant_a_html' + - 'idv.failure.attempts.other_variant_b_html' ## Exclude these keys from the `i18n-tasks eq-base' report: # ignore_eq_base: # all: diff --git a/config/initializers/ab_tests.rb b/config/initializers/ab_tests.rb index 4df55b07756..361813ce4f3 100644 --- a/config/initializers/ab_tests.rb +++ b/config/initializers/ab_tests.rb @@ -18,4 +18,25 @@ module AbTests 0, }, ) + + def self.in_person_cta_variant_testing_buckets + buckets = Hash.new + percents = IdentityConfig.store.in_person_cta_variant_testing_percents + if IdentityConfig.store.in_person_cta_variant_testing_enabled + percents.each do |variant, rate| + bucket_name = 'in_person_variant_' + variant.to_s.downcase + buckets[bucket_name.to_sym] = rate + end + else + buckets['in_person_variant_a'] = 100 + end + + buckets + end + + IN_PERSON_CTA = AbTestBucket.new( + experiment_name: 'In-Person Proofing CTA', + buckets: in_person_cta_variant_testing_buckets, + default_bucket: 'in_person_variant_a', + ) end diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml index 20cbfbff03c..ae932baccf6 100644 --- a/config/locales/errors/en.yml +++ b/config/locales/errors/en.yml @@ -29,6 +29,7 @@ en: send_link_throttle: You tried too many times, please try again in %{timeout}. You can also go back and choose to use your computer instead. throttled_heading: We could not verify your ID + throttled_subheading: Try taking new pictures throttled_text_html: 'Please try again in %{timeout}. For your security, we limit the number of times you can attempt to verify a document online.' diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml index c458fd6bc4a..02eb05ece63 100644 --- a/config/locales/errors/es.yml +++ b/config/locales/errors/es.yml @@ -30,6 +30,7 @@ es: en %{timeout}. También puede retroceder y elegir utilizar su computadora como alternativa. throttled_heading: No pudimos verificar la identificación + throttled_subheading: Intente tomar nuevas fotografías. throttled_text_html: 'Por favor, inténtelo de nuevo en %{timeout}. Por su seguridad, limitamos el número de veces que puede intentar verificar un documento en línea.' diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml index 5fb22723b36..2b72b1761fe 100644 --- a/config/locales/errors/fr.yml +++ b/config/locales/errors/fr.yml @@ -34,6 +34,7 @@ fr: %{timeout}. Vous pouvez également revenir en arrière et choisir d’utiliser votre ordinateur à la place. throttled_heading: Nous n’avons pas pu vérifier votre identité + throttled_subheading: Essayez de prendre de nouvelles photos. throttled_text_html: 'Veuillez réessayer dans %{timeout}. Pour votre sécurité, nous limitons le nombre de fois où vous pouvez tenter de vérifier un document en ligne.' diff --git a/config/locales/idv/en.yml b/config/locales/idv/en.yml index bf28ed1981e..44c0fc3317b 100644 --- a/config/locales/idv/en.yml +++ b/config/locales/idv/en.yml @@ -55,10 +55,20 @@ en: zipcode: Enter a 5 or 9 digit ZIP Code failure: attempts: - one: For security reasons, you have one attempt remaining. - other: For security reasons, you have %{count} attempts remaining. + one: For security reasons, you have one attempt remaining to add your ID online. + one_variant_a_html: For security reasons, you have one attempt + remaining to add your ID online. + one_variant_b_html: Try taking new pictures. For security reasons, you have + one attempt remaining to add your ID online. + other: For security reasons, you have %{count} attempts remaining to add your ID + online. + other_variant_a_html: For security reasons, you have %{count} + attempts remaining to add your ID online. + other_variant_b_html: Try taking new pictures. For security reasons, you have + %{count} attempts remaining to add your ID online. button: warning: Try again + warning_variant: Try again online exceptions: internal_error: There was an internal error processing your request. Please try again. link: please contact us diff --git a/config/locales/idv/es.yml b/config/locales/idv/es.yml index ff78721e9bd..7209fd3b991 100644 --- a/config/locales/idv/es.yml +++ b/config/locales/idv/es.yml @@ -56,10 +56,23 @@ es: zipcode: Ingresa un código postal de 5 o 9 dígitos failure: attempts: - one: Por razones de seguridad, le queda un intento. - other: Por razones de seguridad, le queda %{count} intentos. + one: Por motivos de seguridad, le quedan un intento para añadir su documento de + identidad en línea. + one_variant_a_html: Por motivos de seguridad, le quedan un + intento para añadir su documento de identidad en línea. + one_variant_b_html: Intente tomar nuevas fotografías. Por motivos de seguridad, + le quedan un intento para añadir su documento de + identidad en línea. + other: Por motivos de seguridad, le quedan %{count} intentos para añadir su + documento de identidad en línea. + other_variant_a_html: Por motivos de seguridad, le quedan %{count} + intentos para añadir su documento de identidad en línea. + other_variant_b_html: Intente tomar nuevas fotografías. Por motivos de + seguridad, le quedan %{count} intentos para añadir su + documento de identidad en línea. button: warning: Inténtelo de nuevo + warning_variant: Vuelva a intentarlo en línea exceptions: internal_error: Se produjo un error interno al procesar tu solicitud. Por favor, inténtalo de nuevo. diff --git a/config/locales/idv/fr.yml b/config/locales/idv/fr.yml index 33ec4eab1ea..a930f99afad 100644 --- a/config/locales/idv/fr.yml +++ b/config/locales/idv/fr.yml @@ -59,10 +59,24 @@ fr: zipcode: Entrez un code postal à 5 ou 9 chiffres failure: attempts: - one: Pour des raisons de sécurité, il vous reste une tentative. - other: Pour des raisons de sécurité, il vous reste %{count} tentatives. + one: Pour des raisons de sécurité, il vous reste une tentative pour ajouter + votre pièce d’identité en ligne. + one_variant_a_html: Pour des raisons de sécurité, il vous reste une + tentative pour ajouter votre pièce d’identité en ligne. + one_variant_b_html: Essayez de prendre de nouvelles photos. Pour des raisons de + sécurité, il vous reste une tentative pour ajouter + votre pièce d’identité en ligne. + other: Pour des raisons de sécurité, il vous reste %{count} tentatives pour + ajouter votre pièce d’identité en ligne. + other_variant_a_html: Pour des raisons de sécurité, il vous reste + %{count} tentatives pour ajouter votre pièce + d’identité en ligne. + other_variant_b_html: Essayez de prendre de nouvelles photos. Pour des raisons + de sécurité, il vous reste %{count} tentatives pour + ajouter votre pièce d’identité en ligne. button: warning: Essayez à nouveau + warning_variant: Réessayer en ligne exceptions: internal_error: Une erreur interne s’est produite lors du traitement de votre demande. Veuillez réessayer. diff --git a/config/locales/in_person_proofing/en.yml b/config/locales/in_person_proofing/en.yml index 18906eedc46..f88f8fd2dd1 100644 --- a/config/locales/in_person_proofing/en.yml +++ b/config/locales/in_person_proofing/en.yml @@ -27,10 +27,14 @@ en: to verify your identity. No appointment is required. cta: button: Verify your ID in person - new_feature: New Feature + button_variant: Try in person prompt_detail: If you are located near Washington, D.C. or Baltimore, MD, you may be able to verify your ID in person at limited post office locations. + prompt_detail_a: You may be able to verify your ID in person at a participating + Post Office near you. + prompt_detail_b: Alternatively, if you are having trouble adding your ID online, + you may be able to try in person at limited post office locations. location: contact_info_heading: Contact Information distance: @@ -110,6 +114,7 @@ en: address: Enter your current address barcode: You’re ready to verify your identity in person cta: Having trouble verifying your ID online? + cta_variant: Try verifying your ID in person location: Select a location to verify your ID po_search: location: Find a participating Post Office diff --git a/config/locales/in_person_proofing/es.yml b/config/locales/in_person_proofing/es.yml index 26a7f9e1b5c..ace67f27ade 100644 --- a/config/locales/in_person_proofing/es.yml +++ b/config/locales/in_person_proofing/es.yml @@ -30,10 +30,14 @@ es: empleado en ventanilla. No es necesario pedir cita. cta: button: Verifique su identificación en persona - new_feature: Nuevo artículo + button_variant: Inténtelo en persona prompt_detail: Si usted está ubicado cerca de Washington, D.C. o Baltimore, MD, puede verificar su identificación en persona en las oficinas de correos seleccionadas. + prompt_detail_a: Es posible que pueda verificar su documento de identidad en + persona en una oficina de correos participante cercana. + prompt_detail_b: Si tiene problemas para añadir su documento de identidad en + línea, puede intentarlo en persona en algunas oficinas de correos. location: contact_info_heading: Información del contacto distance: @@ -120,6 +124,7 @@ es: address: Ingrese su dirección actual barcode: Está listo para verificar su identidad en persona cta: ¿Tiene problemas para verificar su documento de identidad en línea? + cta_variant: Intente verificar su ID en persona location: Seleccione un lugar para verificar su cédula po_search: location: Encuentre una oficina de correos participante diff --git a/config/locales/in_person_proofing/fr.yml b/config/locales/in_person_proofing/fr.yml index 30587fa3ae3..3464bbb6ad0 100644 --- a/config/locales/in_person_proofing/fr.yml +++ b/config/locales/in_person_proofing/fr.yml @@ -31,10 +31,15 @@ fr: au détail pour vérifier votre identité. Aucun rendez-vous n’est exigé. cta: button: Vérifiez votre identité en personne - new_feature: Nouvelle fonctionnalité + button_variant: Essayer en personne prompt_detail: Si vous êtes situé près de Washington, D.C. ou de Baltimore (Maryland), vous pourrez peut-être vérifier votre identité en personne dans certains bureaux de poste. + prompt_detail_a: Vous pourrez peut-être faire vérifier votre pièce d’identité en + personne dans un bureau de poste participant près de chez vous. + prompt_detail_b: Ou, si vous rencontrez des difficultés pour ajouter votre pièce + d’identité en ligne, vous pourrez peut-être essayer de le faire en + personne dans certains bureaux de poste. location: contact_info_heading: Information de contact distance: @@ -121,6 +126,7 @@ fr: address: Entrez votre adresse actuelle barcode: Vous êtes prêt à vérifier votre identité en personne cta: Vous avez des difficultés à vérifier votre identité en ligne? + cta_variant: Essayez de vérifier votre identité en personne location: Sélectionnez un lieu pour vérifier votre identité po_search: location: Trouver un bureau de poste participant diff --git a/lib/ab_test_bucket.rb b/lib/ab_test_bucket.rb index 28326d31d23..9746892d84b 100644 --- a/lib/ab_test_bucket.rb +++ b/lib/ab_test_bucket.rb @@ -1,16 +1,17 @@ class AbTestBucket - attr_reader :buckets, :experiment_name + attr_reader :buckets, :experiment_name, :default_bucket - def initialize(experiment_name:, buckets: {}) + def initialize(experiment_name:, buckets: {}, default_bucket: :default) @buckets = buckets @experiment_name = experiment_name + @default_bucket = default_bucket raise 'invalid bucket data structure' unless valid_bucket_data_structure? ensure_numeric_percentages raise 'bucket percentages exceed 100' unless within_100_percent? end def bucket(discriminator = nil) - return :default if discriminator.blank? + return @default_bucket if discriminator.blank? user_value = percent(discriminator) @@ -21,7 +22,7 @@ def bucket(discriminator = nil) min = max end - :default + @default_bucket end private diff --git a/lib/identity_config.rb b/lib/identity_config.rb index e0f59434b30..c06adb8dcb6 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -205,6 +205,8 @@ def self.build_store(config_map) config.add(:idv_send_link_max_attempts, type: :integer) config.add(:idv_sp_required, type: :boolean) config.add(:ie11_support_end_date, type: :timestamp) + config.add(:in_person_cta_variant_testing_enabled, type: :boolean) + config.add(:in_person_cta_variant_testing_percents, type: :json) config.add(:in_person_email_reminder_early_benchmark_in_days, type: :integer) config.add(:in_person_email_reminder_final_benchmark_in_days, type: :integer) config.add(:in_person_email_reminder_late_benchmark_in_days, type: :integer) diff --git a/spec/javascripts/packages/document-capture/components/document-capture-spec.jsx b/spec/javascripts/packages/document-capture/components/document-capture-spec.jsx index 702b5683153..452a7d163a9 100644 --- a/spec/javascripts/packages/document-capture/components/document-capture-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/document-capture-spec.jsx @@ -501,6 +501,7 @@ describe('document-capture/components/document-capture', () => { @@ -532,7 +533,7 @@ describe('document-capture/components/document-capture', () => { await userEvent.click(getByText('forms.buttons.submit.default')); - const verifyInPersonButton = await findByText('in_person_proofing.body.cta.button'); + const verifyInPersonButton = await findByText('in_person_proofing.body.cta.button_variant'); await userEvent.click(verifyInPersonButton); expect(console).to.have.loggedError(/^Error: Uncaught/); diff --git a/spec/services/idv/steps/document_capture_step_spec.rb b/spec/services/idv/steps/document_capture_step_spec.rb index 574d372998a..5f84c3d95d9 100644 --- a/spec/services/idv/steps/document_capture_step_spec.rb +++ b/spec/services/idv/steps/document_capture_step_spec.rb @@ -125,5 +125,78 @@ end end end + + context 'in-person CTA variant A/B testing' do + let(:session_uuid) { SecureRandom.uuid } + let(:testing_enabled) { nil } + let(:active_variant) { nil } + + before do + allow(IdentityConfig.store). + to receive(:in_person_cta_variant_testing_enabled). + and_return(testing_enabled) + + flow.flow_session[:document_capture_session_uuid] = session_uuid + + stub_const( + 'AbTests::IN_PERSON_CTA', + FakeAbTestBucket.new.tap { |ab| ab.assign(session_uuid => active_variant) }, + ) + end + + context 'with in-person CTA variant A/B testing disabled' do + let(:testing_enabled) { false } + + context 'and A/B test specifies variant a' do + let(:active_variant) { :in_person_variant_a } + + it 'passes the correct variables' do + expect( + subject.extra_view_variables[:in_person_cta_variant_testing_enabled], + ).to eq(false) + expect( + subject.extra_view_variables[:in_person_cta_variant_active], + ).to eq(:in_person_variant_a) + end + end + end + + context 'with in-person CTA variant A/B testing enabled' do + let(:testing_enabled) { true } + + context 'and A/B test specifies variant a' do + let(:active_variant) { :in_person_variant_a } + + it 'passes the expected variables' do + expect(subject.extra_view_variables[:in_person_cta_variant_testing_enabled]).to eq(true) + expect( + subject.extra_view_variables[:in_person_cta_variant_active], + ).to eq(:in_person_variant_a) + end + end + + context 'and A/B test specifies variant b' do + let(:active_variant) { :in_person_variant_b } + + it 'passes the expected variables' do + expect(subject.extra_view_variables[:in_person_cta_variant_testing_enabled]).to eq(true) + expect( + subject.extra_view_variables[:in_person_cta_variant_active], + ).to eq(:in_person_variant_b) + end + end + + context 'and A/B test specifies variant c' do + let(:active_variant) { :in_person_variant_c } + + it 'passes the expected variables' do + expect(subject.extra_view_variables[:in_person_cta_variant_testing_enabled]).to eq(true) + expect( + subject.extra_view_variables[:in_person_cta_variant_active], + ).to eq(:in_person_variant_c) + end + end + end + end end end diff --git a/spec/views/idv/shared/_document_capture.html.erb_spec.rb b/spec/views/idv/shared/_document_capture.html.erb_spec.rb index 7b60278850d..b9491e705c1 100644 --- a/spec/views/idv/shared/_document_capture.html.erb_spec.rb +++ b/spec/views/idv/shared/_document_capture.html.erb_spec.rb @@ -16,6 +16,8 @@ let(:acuant_sdk_upgrade_a_b_testing_enabled) { false } let(:use_alternate_sdk) { false } let(:acuant_version) { '11.8.0' } + let(:in_person_cta_variant_testing_enabled) { false } + let(:in_person_cta_variant_active) { '' } before do decorated_session = instance_double( @@ -50,6 +52,8 @@ acuant_sdk_upgrade_a_b_testing_enabled: acuant_sdk_upgrade_a_b_testing_enabled, use_alternate_sdk: use_alternate_sdk, acuant_version: acuant_version, + in_person_cta_variant_testing_enabled: in_person_cta_variant_testing_enabled, + in_person_cta_variant_active: in_person_cta_variant_active, } end