diff --git a/app/controllers/redirect/help_center_controller.rb b/app/controllers/redirect/help_center_controller.rb new file mode 100644 index 00000000000..0871ff15a2a --- /dev/null +++ b/app/controllers/redirect/help_center_controller.rb @@ -0,0 +1,25 @@ +module Redirect + class HelpCenterController < RedirectController + before_action :validate_help_center_article_params + + def show + redirect_to_and_log MarketingSite.help_center_article_url(**article_params) + end + + private + + def validate_help_center_article_params + begin + return if MarketingSite.valid_help_center_article?(**article_params) + rescue ActionController::ParameterMissing + end + + redirect_to root_url + end + + def article_params + category, article = params.require([:category, :article]) + { category: category, article: article } + end + end +end diff --git a/app/controllers/redirect/redirect_controller.rb b/app/controllers/redirect/redirect_controller.rb new file mode 100644 index 00000000000..5df85350acc --- /dev/null +++ b/app/controllers/redirect/redirect_controller.rb @@ -0,0 +1,16 @@ +module Redirect + class RedirectController < ApplicationController + PERMITTED_LOCATION_PARAMS = [:flow, :step, :location].freeze + + private + + def location_params + params.permit(*PERMITTED_LOCATION_PARAMS).to_h.symbolize_keys + end + + def redirect_to_and_log(url, event: Analytics::EXTERNAL_REDIRECT) + analytics.track_event(event, redirect_url: url, **location_params) + redirect_to(url) + end + end +end diff --git a/app/controllers/redirect/return_to_sp_controller.rb b/app/controllers/redirect/return_to_sp_controller.rb new file mode 100644 index 00000000000..c3b28e10ada --- /dev/null +++ b/app/controllers/redirect/return_to_sp_controller.rb @@ -0,0 +1,43 @@ +module Redirect + class ReturnToSpController < Redirect::RedirectController + before_action :validate_sp_exists + + def cancel + redirect_url = sp_return_url_resolver.return_to_sp_url + redirect_to_and_log redirect_url, event: Analytics::RETURN_TO_SP_CANCEL + end + + def failure_to_proof + redirect_url = sp_return_url_resolver.failure_to_proof_url + redirect_to_and_log redirect_url, event: Analytics::RETURN_TO_SP_FAILURE_TO_PROOF + end + + private + + def sp_return_url_resolver + @sp_return_url_resolver ||= SpReturnUrlResolver.new( + service_provider: current_sp, + oidc_state: sp_request_params[:state], + oidc_redirect_uri: sp_request_params[:redirect_uri], + ) + end + + def sp_request_params + @request_params ||= begin + if sp_request_url.present? + UriService.params(sp_request_url) + else + {} + end + end + end + + def sp_request_url + sp_session[:request_url] || service_provider_request&.url + end + + def validate_sp_exists + redirect_to account_url if current_sp.nil? + end + end +end diff --git a/app/controllers/return_to_sp_controller.rb b/app/controllers/return_to_sp_controller.rb deleted file mode 100644 index b0bb714ca12..00000000000 --- a/app/controllers/return_to_sp_controller.rb +++ /dev/null @@ -1,51 +0,0 @@ -class ReturnToSpController < ApplicationController - before_action :validate_sp_exists - - def cancel - redirect_url = sp_return_url_resolver.return_to_sp_url - analytics.track_event(Analytics::RETURN_TO_SP_CANCEL, redirect_url: redirect_url) - redirect_to redirect_url - end - - def failure_to_proof - redirect_url = sp_return_url_resolver.failure_to_proof_url - analytics.track_event( - Analytics::RETURN_TO_SP_FAILURE_TO_PROOF, - redirect_url: redirect_url, - **idv_location_params, - ) - redirect_to redirect_url - end - - private - - def idv_location_params - params.permit(:step, :location).to_h.symbolize_keys - end - - def sp_return_url_resolver - @sp_return_url_resolver ||= SpReturnUrlResolver.new( - service_provider: current_sp, - oidc_state: sp_request_params[:state], - oidc_redirect_uri: sp_request_params[:redirect_uri], - ) - end - - def sp_request_params - @request_params ||= begin - if sp_request_url.present? - UriService.params(sp_request_url) - else - {} - end - end - end - - def sp_request_url - sp_session[:request_url] || service_provider_request&.url - end - - def validate_sp_exists - redirect_to account_url if current_sp.nil? - end -end diff --git a/app/javascript/packages/document-capture/components/capture-advice.jsx b/app/javascript/packages/document-capture/components/capture-advice.jsx index 94791815724..e8337d296e6 100644 --- a/app/javascript/packages/document-capture/components/capture-advice.jsx +++ b/app/javascript/packages/document-capture/components/capture-advice.jsx @@ -1,7 +1,7 @@ import { useContext } from 'react'; import { useI18n } from '@18f/identity-react-i18n'; import ServiceProviderContext from '../context/service-provider'; -import MarketingSiteContext from '../context/marketing-site'; +import HelpCenterContext from '../context/help-center'; import useAsset from '../hooks/use-asset'; import Warning from './warning'; @@ -20,7 +20,7 @@ import Warning from './warning'; */ function CaptureAdvice({ onTryAgain, isAssessedAsGlare, isAssessedAsBlurry }) { const { name: spName, getFailureToProofURL } = useContext(ServiceProviderContext); - const { documentCaptureTipsURL } = useContext(MarketingSiteContext); + const { getHelpCenterURL } = useContext(HelpCenterContext); const { getAssetPath } = useAsset(); const { t } = useI18n(); @@ -33,7 +33,11 @@ function CaptureAdvice({ onTryAgain, isAssessedAsGlare, isAssessedAsBlurry }) { troubleshootingOptions={ /** @type {TroubleshootingOption[]} */ ([ { - url: documentCaptureTipsURL, + url: getHelpCenterURL({ + category: 'verify-your-identity', + article: 'how-to-add-images-of-your-state-issued-id', + location: 'capture_tips', + }), text: t('idv.troubleshooting.options.doc_capture_tips'), isExternal: true, }, diff --git a/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.jsx b/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.jsx index 7cdf98017cf..f19049ada37 100644 --- a/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.jsx +++ b/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.jsx @@ -2,13 +2,13 @@ import { useContext } from 'react'; import { TroubleshootingOptions } from '@18f/identity-components'; import { useI18n } from '@18f/identity-react-i18n'; import ServiceProviderContext from '../context/service-provider'; -import MarketingSiteContext from '../context/marketing-site'; +import HelpCenterContext from '../context/help-center'; /** @typedef {import('@18f/identity-components/troubleshooting-options').TroubleshootingOption} TroubleshootingOption */ function DocumentCaptureTroubleshootingOptions() { const { t } = useI18n(); - const { documentCaptureTipsURL, supportedDocumentsURL } = useContext(MarketingSiteContext); + const { getHelpCenterURL } = useContext(HelpCenterContext); const { name: spName, getFailureToProofURL } = useContext(ServiceProviderContext); return ( @@ -16,13 +16,21 @@ function DocumentCaptureTroubleshootingOptions() { heading={t('idv.troubleshooting.headings.having_trouble')} options={ /** @type {TroubleshootingOption[]} */ ([ - documentCaptureTipsURL && { - url: documentCaptureTipsURL, + { + url: getHelpCenterURL({ + category: 'verify-your-identity', + article: 'how-to-add-images-of-your-state-issued-id', + location: 'troubleshooting_options', + }), text: t('idv.troubleshooting.options.doc_capture_tips'), isExternal: true, }, - supportedDocumentsURL && { - url: supportedDocumentsURL, + { + url: getHelpCenterURL({ + category: 'verify-your-identity', + article: 'accepted-state-issued-identification', + location: 'troubleshooting_options', + }), text: t('idv.troubleshooting.options.supported_documents'), isExternal: true, }, diff --git a/app/javascript/packages/document-capture/components/review-issues-step.jsx b/app/javascript/packages/document-capture/components/review-issues-step.jsx index f4e4885f75a..83cf0ec332a 100644 --- a/app/javascript/packages/document-capture/components/review-issues-step.jsx +++ b/app/javascript/packages/document-capture/components/review-issues-step.jsx @@ -13,7 +13,7 @@ import DocumentCaptureTroubleshootingOptions from './document-capture-troublesho import PageHeading from './page-heading'; import StartOverOrCancel from './start-over-or-cancel'; import Warning from './warning'; -import MarketingSiteContext from '../context/marketing-site'; +import HelpCenterContext from '../context/help-center'; import AnalyticsContext from '../context/analytics'; import useDidUpdateEffect from '../hooks/use-did-update-effect'; import './review-issues-step.scss'; @@ -75,7 +75,7 @@ function ReviewIssuesStep({ const { t } = useI18n(); const { isMobile } = useContext(DeviceContext); const serviceProvider = useContext(ServiceProviderContext); - const { documentCaptureTipsURL } = useContext(MarketingSiteContext); + const { getHelpCenterURL } = useContext(HelpCenterContext); const { addPageAction } = useContext(AnalyticsContext); const selfieError = errors.find(({ field }) => field === 'selfie')?.error; const [hasDismissed, setHasDismissed] = useState(remainingAttempts === Infinity); @@ -164,7 +164,11 @@ function ReviewIssuesStep({ troubleshootingOptions={ /** @type {TroubleshootingOption[]} */ ([ { - url: documentCaptureTipsURL, + url: getHelpCenterURL({ + category: 'verify-your-identity', + article: 'how-to-add-images-of-your-state-issued-id', + location: 'post_submission_warning', + }), text: t('idv.troubleshooting.options.doc_capture_tips'), isExternal: true, }, diff --git a/app/javascript/packages/document-capture/context/help-center.jsx b/app/javascript/packages/document-capture/context/help-center.jsx new file mode 100644 index 00000000000..83f1a67048d --- /dev/null +++ b/app/javascript/packages/document-capture/context/help-center.jsx @@ -0,0 +1,56 @@ +import { createContext } from 'react'; +import { addSearchParams } from '@18f/identity-url'; + +/** @typedef {import('react').ReactNode} ReactNode */ + +/** + * @typedef HelpCenterURLParameters + * + * @prop {string} category + * @prop {string} article + * @prop {string} location + */ + +/** + * @typedef {(params: HelpCenterURLParameters) => string} GetHelpCenterURL + */ + +/** + * @typedef HelpCenterContext + * + * @prop {string} helpCenterRedirectURL + * @prop {GetHelpCenterURL} getHelpCenterURL + */ + +const HelpCenterContext = createContext( + /** @type {HelpCenterContext} */ ({ + helpCenterRedirectURL: '', + getHelpCenterURL: (params) => addSearchParams('', params), + }), +); + +HelpCenterContext.displayName = 'HelpCenterContext'; + +/** + * @typedef HelpCenterContextProviderProps + * + * @prop {Omit} value + * @prop {ReactNode} children + */ + +/** + * @param {HelpCenterContextProviderProps} props + */ +function HelpCenterContextProvider({ value, children }) { + /** @type {GetHelpCenterURL} */ + const getHelpCenterURL = (params) => addSearchParams(value.helpCenterRedirectURL, params); + + return ( + + {children} + + ); +} + +export default HelpCenterContext; +export { HelpCenterContextProvider as Provider }; diff --git a/app/javascript/packages/document-capture/context/index.js b/app/javascript/packages/document-capture/context/index.js index d937b6a60ad..8c3dca2d6ad 100644 --- a/app/javascript/packages/document-capture/context/index.js +++ b/app/javascript/packages/document-capture/context/index.js @@ -2,7 +2,7 @@ export { default as AppContext } from './app'; export { default as AssetContext } from './asset'; export { default as DeviceContext } from './device'; export { default as AcuantContext, Provider as AcuantContextProvider } from './acuant'; -export { default as MarketingSiteContext } from './marketing-site'; +export { default as HelpCenterContext, Provider as HelpCenterContextProvider } from './help-center'; export { default as UploadContext, Provider as UploadContextProvider } from './upload'; export { default as ServiceProviderContext, diff --git a/app/javascript/packages/document-capture/context/marketing-site.jsx b/app/javascript/packages/document-capture/context/marketing-site.jsx deleted file mode 100644 index 57c5f7eb2bd..00000000000 --- a/app/javascript/packages/document-capture/context/marketing-site.jsx +++ /dev/null @@ -1,16 +0,0 @@ -import { createContext } from 'react'; - -/** - * @typedef MarketingSiteContext - * - * @prop {string} documentCaptureTipsURL Link to Help Center article with tips for document capture. - * @prop {string} supportedDocumentsURL Link to Help Center article detailing supported documents. - */ - -const MarketingSiteContext = createContext( - /** @type {MarketingSiteContext} */ ({ documentCaptureTipsURL: '', supportedDocumentsURL: '' }), -); - -MarketingSiteContext.displayName = 'MarketingSiteContext'; - -export default MarketingSiteContext; diff --git a/app/javascript/packages/document-capture/context/service-provider.jsx b/app/javascript/packages/document-capture/context/service-provider.jsx index 086f299452b..370668acf25 100644 --- a/app/javascript/packages/document-capture/context/service-provider.jsx +++ b/app/javascript/packages/document-capture/context/service-provider.jsx @@ -1,4 +1,5 @@ import { createContext, useMemo } from 'react'; +import { addSearchParams } from '@18f/identity-url'; /** @typedef {import('react').ReactNode} ReactNode */ @@ -37,11 +38,7 @@ function ServiceProviderContextProvider({ value, children }) { const mergedValue = useMemo( () => ({ ...value, - getFailureToProofURL(location) { - const url = new URL(value.failureToProofURL); - url.searchParams.set('location', location); - return url.toString(); - }, + getFailureToProofURL: (location) => addSearchParams(value.failureToProofURL, { location }), }), [value], ); diff --git a/app/javascript/packages/url/index.js b/app/javascript/packages/url/index.js new file mode 100644 index 00000000000..1cd6071318c --- /dev/null +++ b/app/javascript/packages/url/index.js @@ -0,0 +1,29 @@ +/** + * Given a URL or a string fragment of search parameters and an object of parameters, returns a + * new URL or search parameters with the parameters added. + * + * @param {string} urlOrParams Original URL or search parameters. + * @param {Record} params Search parameters to add. + * + * @return {string} Modified URL or search parameters. + */ +export function addSearchParams(urlOrParams, params) { + /** @type {URL|URLSearchParams} */ + let parsedURLOrParams; + + /** @type {URLSearchParams} */ + let searchParams; + + try { + parsedURLOrParams = new URL(urlOrParams); + searchParams = parsedURLOrParams.searchParams; + } catch { + parsedURLOrParams = new URLSearchParams(urlOrParams); + searchParams = parsedURLOrParams; + } + + Object.entries(params).forEach(([key, value]) => searchParams.set(key, value)); + + const result = parsedURLOrParams.toString(); + return parsedURLOrParams instanceof URLSearchParams ? `?${result}` : result; +} diff --git a/app/javascript/packages/url/index.spec.js b/app/javascript/packages/url/index.spec.js new file mode 100644 index 00000000000..16b3d07a941 --- /dev/null +++ b/app/javascript/packages/url/index.spec.js @@ -0,0 +1,30 @@ +import { addSearchParams } from './index.js'; + +describe('addSearchParams', () => { + it('adds search params to an existing URL', () => { + const url = 'https://example.com/?a=1&b=1'; + const expected = 'https://example.com/?a=1&b=2&c=3'; + + const actual = addSearchParams(url, { b: 2, c: 3 }); + + expect(actual).to.equal(expected); + }); + + it('adds search params to an existing search fragment', () => { + const params = '?a=1&b=1'; + const expected = '?a=1&b=2&c=3'; + + const actual = addSearchParams(params, { b: 2, c: 3 }); + + expect(actual).to.equal(expected); + }); + + it('adds search params to an empty URL', () => { + const params = ''; + const expected = '?a=1&b=2&c=3'; + + const actual = addSearchParams(params, { a: 1, b: 2, c: 3 }); + + expect(actual).to.equal(expected); + }); +}); diff --git a/app/javascript/packages/url/package.json b/app/javascript/packages/url/package.json new file mode 100644 index 00000000000..38deefef5de --- /dev/null +++ b/app/javascript/packages/url/package.json @@ -0,0 +1,5 @@ +{ + "name": "@18f/identity-url", + "version": "1.0.0", + "private": true +} diff --git a/app/javascript/packs/document-capture.jsx b/app/javascript/packs/document-capture.jsx index e8ca33b0bfa..b03a2f4ef5e 100644 --- a/app/javascript/packs/document-capture.jsx +++ b/app/javascript/packs/document-capture.jsx @@ -10,7 +10,7 @@ import { ServiceProviderContextProvider, AnalyticsContext, FailedCaptureAttemptsContextProvider, - MarketingSiteContext, + HelpCenterContextProvider, } from '@18f/identity-document-capture'; import { loadPolyfills } from '@18f/identity-polyfill'; import { isCameraCapableMobile } from '@18f/identity-device'; @@ -53,8 +53,7 @@ import { I18nContext } from '@18f/identity-react-i18n'; /** * @typedef AppRootData * - * @prop {string} documentCaptureTipsUrl - * @prop {string} supportedDocumentsUrl + * @prop {string} helpCenterRedirectUrl * @prop {string} appName * @prop {string} maxCaptureAttemptsBeforeTips * @prop {FlowPath} flowPath @@ -62,7 +61,7 @@ import { I18nContext } from '@18f/identity-react-i18n'; * @prop {string} cancelUrl * * @see AppContext - * @see MarketingSiteContext + * @see HelpCenterContextProvider * @see FailedCaptureAttemptsContext * @see UploadContext */ @@ -156,8 +155,7 @@ loadPolyfills(['fetch', 'crypto', 'url']).then(async () => { window.fetch(keepAliveEndpoint, { method: 'POST', headers: { 'X-CSRF-Token': csrf } }); const { - documentCaptureTipsUrl: documentCaptureTipsURL, - supportedDocumentsUrl: supportedDocumentsURL, + helpCenterRedirectUrl: helpCenterRedirectURL, maxCaptureAttemptsBeforeTips, appName, flowPath, @@ -167,7 +165,7 @@ loadPolyfills(['fetch', 'crypto', 'url']).then(async () => { const App = composeComponents( [AppContext.Provider, { value: { appName } }], - [MarketingSiteContext.Provider, { value: { documentCaptureTipsURL, supportedDocumentsURL } }], + [HelpCenterContextProvider, { value: { helpCenterRedirectURL } }], [DeviceContext.Provider, { value: device }], [AnalyticsContext.Provider, { value: { addPageAction, noticeError } }], [ diff --git a/app/services/analytics.rb b/app/services/analytics.rb index 2f9d4409f75..903b5140b26 100644 --- a/app/services/analytics.rb +++ b/app/services/analytics.rb @@ -142,6 +142,7 @@ def browser_attributes EVENT_DISAVOWAL_PASSWORD_RESET = 'Event disavowal password reset'.freeze EVENT_DISAVOWAL_TOKEN_INVALID = 'Event disavowal token invalid'.freeze EVENTS_VISIT = 'Events Page Visited'.freeze + EXTERNAL_REDIRECT = 'External Redirect'.freeze FORGET_ALL_BROWSERS_SUBMITTED = 'Forget All Browsers Submitted'.freeze FORGET_ALL_BROWSERS_VISITED = 'Forget All Browsers Visited'.freeze IDV_ADDRESS_VISIT = 'IdV: address visited'.freeze diff --git a/app/services/marketing_site.rb b/app/services/marketing_site.rb index be2cbe80cbf..1ab759b2ba3 100644 --- a/app/services/marketing_site.rb +++ b/app/services/marketing_site.rb @@ -1,6 +1,18 @@ +require 'set' + class MarketingSite BASE_URL = URI('https://www.login.gov').freeze + HELP_CENTER_ARTICLES = %w[ + authentication-methods/which-authentication-method-should-i-use + creating-an-account/authentication-application + signing-in/what-is-a-hardware-security-key + verify-your-identity/accepted-state-issued-identification + verify-your-identity/how-to-add-images-of-your-state-issued-id + verify-your-identity/phone-number-and-phone-plan-in-your-name + verify-your-identity/verify-your-address-by-mail + ].to_set.freeze + def self.locale_segment active_locale = I18n.locale active_locale == I18n.default_locale ? '/' : "/#{active_locale}/" @@ -39,62 +51,39 @@ def self.help_url end def self.help_authentication_app_url - URI.join(BASE_URL, locale_segment, 'help/creating-an-account/authentication-application/').to_s - end - - def self.help_idv_supported_documents_url - URI.join( - BASE_URL, - locale_segment, - 'help/verify-your-identity/accepted-state-issued-identification/', - ).to_s - end - - def self.help_idv_verify_by_mail_url - URI.join( - BASE_URL, - locale_segment, - 'help/verify-your-identity/verify-your-address-by-mail/', - ).to_s - end - - def self.help_idv_verify_by_phone_url - URI.join( - BASE_URL, - locale_segment, - 'help/verify-your-identity/phone-number-and-phone-plan-in-your-name/', - ).to_s + help_center_article_url( + category: 'creating-an-account', + article: 'authentication-application', + ) end def self.help_which_authentication_method_url - URI.join( - BASE_URL, - locale_segment, - 'help/authentication-methods/which-authentication-method-should-i-use/', - ).to_s + help_center_article_url( + category: 'authentication-methods', + article: 'which-authentication-method-should-i-use', + ) end def self.help_hardware_security_key_url - URI.join(BASE_URL, locale_segment, 'help/signing-in/what-is-a-hardware-security-key/').to_s + help_center_article_url( + category: 'signing-in', + article: 'what-is-a-hardware-security-key', + ) end - def self.help_document_capture_tips_url - URI.join( - BASE_URL, - locale_segment, - 'help/verify-your-identity/how-to-add-images-of-your-state-issued-id/', - ).to_s + def self.security_url + URI.join(BASE_URL, locale_segment, 'security/').to_s end - def self.help_document_capture_supported_documents_url - URI.join( - BASE_URL, - locale_segment, - 'help/verify-your-identity/accepted-state-issued-identification/', - ).to_s + def self.help_center_article_url(category:, article:) + if !valid_help_center_article?(category: category, article: article) + raise ArgumentError.new("Unknown help center article category #{category}/#{article}") + end + + URI.join(BASE_URL, locale_segment, "help/#{category}/#{article}/").to_s end - def self.security_url - URI.join(BASE_URL, locale_segment, 'security/').to_s + def self.valid_help_center_article?(category:, article:) + HELP_CENTER_ARTICLES.include?("#{category}/#{article}") end end diff --git a/app/views/idv/doc_auth/welcome.html.erb b/app/views/idv/doc_auth/welcome.html.erb index f6d8cddcc8f..de3411d8bef 100644 --- a/app/views/idv/doc_auth/welcome.html.erb +++ b/app/views/idv/doc_auth/welcome.html.erb @@ -92,12 +92,22 @@ heading: t('idv.troubleshooting.headings.missing_required_items'), options: [ { - url: MarketingSite.help_idv_supported_documents_url, + url: help_center_redirect_path( + category: 'verify-your-identity', + article: 'accepted-state-issued-identification', + flow: :idv, + step: :welcome, + ), text: t('idv.troubleshooting.options.supported_documents'), new_tab: true, }, { - url: MarketingSite.help_idv_verify_by_phone_url, + url: help_center_redirect_path( + category: 'verify-your-identity', + article: 'phone-number-and-phone-plan-in-your-name', + flow: :idv, + step: :welcome, + ), text: t('idv.troubleshooting.options.learn_more_address_verification_options'), new_tab: true, }, diff --git a/app/views/idv/gpo/index.html.erb b/app/views/idv/gpo/index.html.erb index fe433961090..8c2bc4dd35d 100644 --- a/app/views/idv/gpo/index.html.erb +++ b/app/views/idv/gpo/index.html.erb @@ -32,7 +32,12 @@ heading: t('idv.troubleshooting.headings.having_trouble'), options: [ { - url: MarketingSite.help_idv_verify_by_mail_url, + url: help_center_redirect_url( + category: 'verify-your-identity', + article: 'verify-your-address-by-mail', + flow: :idv, + step: :gpo_send_letter, + ), text: t('idv.troubleshooting.options.learn_more_verify_by_mail'), new_tab: true, }, diff --git a/app/views/idv/phone/new.html.erb b/app/views/idv/phone/new.html.erb index ad00f8931bd..da2855ef879 100644 --- a/app/views/idv/phone/new.html.erb +++ b/app/views/idv/phone/new.html.erb @@ -75,7 +75,12 @@ heading: t('idv.troubleshooting.headings.having_trouble'), options: [ { - url: MarketingSite.help_idv_verify_by_phone_url, + url: help_center_redirect_url( + category: 'verify-your-identity', + article: 'phone-number-and-phone-plan-in-your-name', + flow: :idv, + step: :phone, + ), text: t('idv.troubleshooting.options.learn_more_verify_by_phone'), new_tab: true, }, diff --git a/app/views/idv/shared/_document_capture.html.erb b/app/views/idv/shared/_document_capture.html.erb index 166c70f2fe8..c34fafe1965 100644 --- a/app/views/idv/shared/_document_capture.html.erb +++ b/app/views/idv/shared/_document_capture.html.erb @@ -20,8 +20,10 @@ app_name: APP_NAME, liveness_required: liveness_checking_enabled?.presence, mock_client: (DocAuthRouter.doc_auth_vendor(discriminator: session_id) == 'mock').presence, - document_capture_tips_url: MarketingSite.help_document_capture_tips_url, - supported_documents_url: MarketingSite.help_document_capture_supported_documents_url, + help_center_redirect_url: help_center_redirect_url( + flow: :idv, + step: :document_capture, + ), document_capture_session_uuid: flow_session[:document_capture_session_uuid], endpoint: FeatureManagement.document_capture_async_uploads_enabled? ? send(@step_url, step: :verify_document) : diff --git a/config/routes.rb b/config/routes.rb index f5c6695b9eb..302f8d4a388 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -247,8 +247,11 @@ put '/user_authorization_confirmation/reset' => 'users/authorization_confirmation#update', as: :reset_user_authorization get '/sign_up/cancel/' => 'sign_up/cancellations#new', as: :sign_up_cancel - get '/return_to_sp/cancel' => 'return_to_sp#cancel' - get '/return_to_sp/failure_to_proof' => 'return_to_sp#failure_to_proof' + get '/redirect/return_to_sp/cancel' => 'redirect/return_to_sp#cancel', as: :return_to_sp_cancel + get '/redirect/return_to_sp/failure_to_proof' => 'redirect/return_to_sp#failure_to_proof', as: :return_to_sp_failure_to_proof + get '/redirect/help_center' => 'redirect/help_center#show', as: :help_center_redirect + get '/return_to_sp/cancel' => redirect('/redirect/return_to_sp/cancel') # Temporary: Remove after RC169 + get '/return_to_sp/failure_to_proof' => redirect('/redirect/return_to_sp/failure_to_proof') # Temporary: Remove after RC169 match '/sign_out' => 'sign_out#destroy', via: %i[get post delete] diff --git a/spec/controllers/redirect/help_center_controller_spec.rb b/spec/controllers/redirect/help_center_controller_spec.rb new file mode 100644 index 00000000000..ae752fa2d96 --- /dev/null +++ b/spec/controllers/redirect/help_center_controller_spec.rb @@ -0,0 +1,69 @@ +require 'rails_helper' + +describe Redirect::HelpCenterController do + before do + stub_analytics + end + + let(:category) { nil } + let(:article) { nil } + let(:location_params) { {} } + subject(:response) do + get :show, params: { category: category, article: article, **location_params } + end + + describe '#show' do + context 'without help center article' do + it 'redirects to the root url' do + expect(response).to redirect_to root_url + expect(@analytics).not_to have_logged_event(Analytics::EXTERNAL_REDIRECT) + end + end + + context 'with invalid help center article' do + let(:category) { 'foo' } + let(:article) { 'bar' } + + it 'redirects to the root url' do + expect(response).to redirect_to root_url + expect(@analytics).not_to have_logged_event(Analytics::EXTERNAL_REDIRECT) + end + end + + context 'with valid help center article' do + let(:category) { 'verify-your-identity' } + let(:article) { 'accepted-state-issued-identification' } + + it 'redirects to the help center article and logs' do + redirect_url = MarketingSite.help_center_article_url( + category: 'verify-your-identity', + article: 'accepted-state-issued-identification', + ) + expect(response).to redirect_to redirect_url + expect(@analytics).to have_logged_event( + Analytics::EXTERNAL_REDIRECT, + redirect_url: redirect_url, + ) + end + + context 'with location params' do + let(:location_params) { { flow: 'flow', step: 'step', location: 'location', foo: 'bar' } } + + it 'logs with location params' do + response + + expect(@analytics).to have_logged_event( + Analytics::EXTERNAL_REDIRECT, + redirect_url: MarketingSite.help_center_article_url( + category: 'verify-your-identity', + article: 'accepted-state-issued-identification', + ), + flow: 'flow', + step: 'step', + location: 'location', + ) + end + end + end + end +end diff --git a/spec/controllers/return_to_sp_controller_spec.rb b/spec/controllers/redirect/return_to_sp_controller_spec.rb similarity index 98% rename from spec/controllers/return_to_sp_controller_spec.rb rename to spec/controllers/redirect/return_to_sp_controller_spec.rb index 3ccc01dc807..ff2e0c15772 100644 --- a/spec/controllers/return_to_sp_controller_spec.rb +++ b/spec/controllers/redirect/return_to_sp_controller_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe ReturnToSpController do +describe Redirect::ReturnToSpController do let(:current_sp) { build(:service_provider) } before do diff --git a/spec/javascripts/packages/document-capture/components/document-capture-troubleshooting-options-spec.jsx b/spec/javascripts/packages/document-capture/components/document-capture-troubleshooting-options-spec.jsx index 3f35eb4cdc0..4024ae7cce7 100644 --- a/spec/javascripts/packages/document-capture/components/document-capture-troubleshooting-options-spec.jsx +++ b/spec/javascripts/packages/document-capture/components/document-capture-troubleshooting-options-spec.jsx @@ -1,21 +1,18 @@ import { render } from '@testing-library/react'; import { composeComponents } from '@18f/identity-compose-components'; import { - MarketingSiteContext, + HelpCenterContextProvider, ServiceProviderContextProvider, } from '@18f/identity-document-capture'; import DocumentCaptureTroubleshootingOptions from '@18f/identity-document-capture/components/document-capture-troubleshooting-options'; describe('DocumentCaptureTroubleshootingOptions', () => { - const documentCaptureTipsURL = '/url/to/document-capture-tips'; - const supportedDocumentsURL = '/url/to/supported-documents'; - function renderWithContext({ serviceProviderContext } = {}) { const Component = composeComponents( ...[ [ - MarketingSiteContext.Provider, - { value: { documentCaptureTipsURL, supportedDocumentsURL } }, + HelpCenterContextProvider, + { value: { helpCenterRedirectURL: 'https://example.com/redirect/' } }, ], serviceProviderContext && [ ServiceProviderContextProvider, @@ -37,12 +34,16 @@ describe('DocumentCaptureTroubleshootingOptions', () => { expect(links[0].textContent).to.equal( 'idv.troubleshooting.options.doc_capture_tips links.new_window', ); - expect(links[0].getAttribute('href')).to.equal('/url/to/document-capture-tips'); + expect(links[0].getAttribute('href')).to.equal( + 'https://example.com/redirect/?category=verify-your-identity&article=how-to-add-images-of-your-state-issued-id&location=troubleshooting_options', + ); expect(links[0].target).to.equal('_blank'); expect(links[1].textContent).to.equal( 'idv.troubleshooting.options.supported_documents links.new_window', ); - expect(links[1].getAttribute('href')).to.equal('/url/to/supported-documents'); + expect(links[1].getAttribute('href')).to.equal( + 'https://example.com/redirect/?category=verify-your-identity&article=accepted-state-issued-identification&location=troubleshooting_options', + ); expect(links[1].target).to.equal('_blank'); }); @@ -61,12 +62,16 @@ describe('DocumentCaptureTroubleshootingOptions', () => { expect(links[0].textContent).to.equal( 'idv.troubleshooting.options.doc_capture_tips links.new_window', ); - expect(links[0].getAttribute('href')).to.equal('/url/to/document-capture-tips'); + expect(links[0].getAttribute('href')).to.equal( + 'https://example.com/redirect/?category=verify-your-identity&article=how-to-add-images-of-your-state-issued-id&location=troubleshooting_options', + ); expect(links[0].target).to.equal('_blank'); expect(links[1].textContent).to.equal( 'idv.troubleshooting.options.supported_documents links.new_window', ); - expect(links[1].getAttribute('href')).to.equal('/url/to/supported-documents'); + expect(links[1].getAttribute('href')).to.equal( + 'https://example.com/redirect/?category=verify-your-identity&article=accepted-state-issued-identification&location=troubleshooting_options', + ); expect(links[1].target).to.equal('_blank'); expect(links[2].textContent).to.equal( 'idv.troubleshooting.options.get_help_at_sp links.new_window', diff --git a/spec/javascripts/packages/document-capture/context/help-center-spec.jsx b/spec/javascripts/packages/document-capture/context/help-center-spec.jsx new file mode 100644 index 00000000000..b8030bc1382 --- /dev/null +++ b/spec/javascripts/packages/document-capture/context/help-center-spec.jsx @@ -0,0 +1,53 @@ +import { useContext } from 'react'; +import { renderHook } from '@testing-library/react-hooks'; +import HelpCenterContext, { Provider } from '@18f/identity-document-capture/context/help-center'; + +describe('document-capture/context/help-center', () => { + it('assigns default context', () => { + const { result } = renderHook(() => useContext(HelpCenterContext)); + + expect(result.current).to.have.keys(['helpCenterRedirectURL', 'getHelpCenterURL']); + expect(result.current.helpCenterRedirectURL).to.be.a('string'); + expect(result.current.getHelpCenterURL).to.be.a('function'); + }); + + describe('getHelpCenterURL', () => { + it('parameterizes category, article, location', () => { + const { result } = renderHook(() => useContext(HelpCenterContext)); + + const failureToProofURL = result.current.getHelpCenterURL({ + category: 'category', + article: 'article', + location: 'location', + }); + + expect(failureToProofURL).to.equal('?category=category&article=article&location=location'); + }); + }); + + describe('Provider', () => { + describe('getHelpCenterURL', () => { + it('parameterizes category, article, location', () => { + const { result } = renderHook(() => useContext(HelpCenterContext), { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + const failureToProofURL = result.current.getHelpCenterURL({ + category: 'category', + article: 'article', + location: 'location', + }); + + expect(failureToProofURL).to.equal( + 'http://example.com/redirect/?flow=example&category=category&article=article&location=location', + ); + }); + }); + }); +}); diff --git a/spec/javascripts/packages/document-capture/context/marketing-site-spec.jsx b/spec/javascripts/packages/document-capture/context/marketing-site-spec.jsx deleted file mode 100644 index b7aa4731fb7..00000000000 --- a/spec/javascripts/packages/document-capture/context/marketing-site-spec.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import { useContext } from 'react'; -import { renderHook } from '@testing-library/react-hooks'; -import MarketingSiteContext from '@18f/identity-document-capture/context/marketing-site'; - -describe('document-capture/context/marketing-site', () => { - it('assigns default context', () => { - const { result } = renderHook(() => useContext(MarketingSiteContext)); - - expect(result.current).to.have.keys(['documentCaptureTipsURL', 'supportedDocumentsURL']); - expect(result.current.documentCaptureTipsURL).to.be.a('string'); - expect(result.current.supportedDocumentsURL).to.be.a('string'); - }); -}); diff --git a/spec/services/marketing_site_spec.rb b/spec/services/marketing_site_spec.rb index e7890ad16ea..2407d7d6666 100644 --- a/spec/services/marketing_site_spec.rb +++ b/spec/services/marketing_site_spec.rb @@ -109,75 +109,49 @@ end end - describe '.help_idv_supported_documents_url' do - it 'points to the authentication app section of the help page' do - expect(MarketingSite.help_idv_supported_documents_url).to eq( - 'https://www.login.gov/help/verify-your-identity/accepted-state-issued-identification/', - ) - end + describe '.help_center_article_url' do + let(:category) {} + let(:article) {} + let(:result) { MarketingSite.help_center_article_url(category: category, article: article ) } - context 'when the user has set their locale to :es' do - before { I18n.locale = :es } + context 'with invalid article' do + let(:category) { 'foo' } + let(:article) { 'bar' } - it 'points to the authentication app section of the help page with the locale appended' do - expect(MarketingSite.help_idv_supported_documents_url).to eq( - 'https://www.login.gov/es/help/verify-your-identity/accepted-state-issued-identification/', - ) + it 'raises ArgumentError' do + expect { result }.to raise_error ArgumentError end end - end - describe '.help_idv_verify_by_mail_url' do - it 'points to the authentication app section of the help page' do - expect(MarketingSite.help_idv_verify_by_mail_url).to eq( - 'https://www.login.gov/help/verify-your-identity/verify-your-address-by-mail/', - ) - end - - context 'when the user has set their locale to :es' do - before { I18n.locale = :es } + context 'with valid article' do + let(:category) { 'verify-your-identity' } + let(:article) { 'accepted-state-issued-identification' } - it 'points to the authentication app section of the help page with the locale appended' do - expect(MarketingSite.help_idv_verify_by_mail_url).to eq( - 'https://www.login.gov/es/help/verify-your-identity/verify-your-address-by-mail/', + it 'returns article URL' do + expect(result).to eq( + 'https://www.login.gov/help/verify-your-identity/accepted-state-issued-identification/', ) end end end - describe '.help_idv_verify_by_phone_url' do - it 'points to the authentication app section of the help page' do - expect(MarketingSite.help_idv_verify_by_phone_url).to eq( - 'https://www.login.gov/help/verify-your-identity/phone-number-and-phone-plan-in-your-name/', - ) - end - - context 'when the user has set their locale to :es' do - before { I18n.locale = :es } + describe '.valid_help_center_article?' do + let(:category) {} + let(:article) {} + let(:result) { MarketingSite.valid_help_center_article?(category: category, article: article ) } - it 'points to the authentication app section of the help page with the locale appended' do - expect(MarketingSite.help_idv_verify_by_phone_url).to eq( - 'https://www.login.gov/es/help/verify-your-identity/phone-number-and-phone-plan-in-your-name/', - ) - end - end - end + context 'with invalid article' do + let(:category) { 'foo' } + let(:article) { 'bar' } - describe '.help_document_capture_tips_url' do - it 'points to the authentication app section of the help page' do - expect(MarketingSite.help_document_capture_tips_url).to eq( - 'https://www.login.gov/help/verify-your-identity/how-to-add-images-of-your-state-issued-id/', - ) + it { expect(result).to eq(false) } end - context 'when the user has set their locale to :es' do - before { I18n.locale = :es } + context 'with valid article' do + let(:category) { 'verify-your-identity' } + let(:article) { 'accepted-state-issued-identification' } - it 'points to the authentication app section of the help page with the locale appended' do - expect(MarketingSite.help_document_capture_tips_url).to eq( - 'https://www.login.gov/es/help/verify-your-identity/how-to-add-images-of-your-state-issued-id/', - ) - end + it { expect(result).to eq(true) } end end end