diff --git a/app/controllers/verify_controller.rb b/app/controllers/verify_controller.rb index 02b44a5d800..6717a7851ad 100644 --- a/app/controllers/verify_controller.rb +++ b/app/controllers/verify_controller.rb @@ -24,7 +24,6 @@ def app_data { base_path: idv_app_path, - app_name: APP_NAME, completion_url: completion_url, initial_values: initial_values, enabled_step_names: IdentityConfig.store.idv_api_enabled_steps, diff --git a/app/javascript/packages/analytics/index.js b/app/javascript/packages/analytics/index.js deleted file mode 100644 index 1383cb14f3d..00000000000 --- a/app/javascript/packages/analytics/index.js +++ /dev/null @@ -1,20 +0,0 @@ -import { getPageData } from '@18f/identity-page-data'; - -/** - * Logs an event. - * - * @param {string} event Event name. - * @param {Record=} payload Payload object. - * - * @return {Promise} - */ -export async function trackEvent(event, payload = {}) { - const endpoint = getPageData('analyticsEndpoint'); - if (endpoint) { - await window.fetch(endpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ event, payload }), - }); - } -} diff --git a/app/javascript/packages/analytics/index.ts b/app/javascript/packages/analytics/index.ts new file mode 100644 index 00000000000..99fd2efb7a8 --- /dev/null +++ b/app/javascript/packages/analytics/index.ts @@ -0,0 +1,20 @@ +import { getConfigValue } from '@18f/identity-config'; + +/** + * Logs an event. + * + * @param event Event name. + * @param payload Payload object. + * + * @return Promise resolving once event has been logged. + */ +export async function trackEvent(event: string, payload: object = {}): Promise { + const endpoint = getConfigValue('analyticsEndpoint'); + if (endpoint) { + await window.fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ event, payload }), + }); + } +} diff --git a/app/javascript/packages/config/get-config-value.spec.ts b/app/javascript/packages/config/get-config-value.spec.ts new file mode 100644 index 00000000000..a746ce7a391 --- /dev/null +++ b/app/javascript/packages/config/get-config-value.spec.ts @@ -0,0 +1,32 @@ +import getConfigValue from './get-config-value'; +import type { Config } from './get-config-value'; + +describe('getConfigValue', () => { + context('with page config element absent', () => { + it('returns undefined', () => { + expect(getConfigValue('appName')).to.be.undefined(); + expect(getConfigValue('analyticsEndpoint')).to.be.undefined(); + }); + }); + + context('with page config element present', () => { + const APP_NAME = 'app'; + const ANALYTICS_ENDPOINT = 'url'; + + beforeEach(() => { + const config = document.createElement('script'); + config.type = 'application/json'; + config.setAttribute('data-config', ''); + config.textContent = JSON.stringify({ + appName: APP_NAME, + analyticsEndpoint: ANALYTICS_ENDPOINT, + } as Config); + document.body.appendChild(config); + }); + + it('returns the config value corresponding to the given key', () => { + expect(getConfigValue('appName')).to.equal(APP_NAME); + expect(getConfigValue('analyticsEndpoint')).to.equal(ANALYTICS_ENDPOINT); + }); + }); +}); diff --git a/app/javascript/packages/config/get-config-value.ts b/app/javascript/packages/config/get-config-value.ts new file mode 100644 index 00000000000..a93082ed723 --- /dev/null +++ b/app/javascript/packages/config/get-config-value.ts @@ -0,0 +1,45 @@ +/** + * Supported page configuration values. + */ +export interface Config { + /** + * Application name. + */ + appName: string; + + /** + * URL for analytics logging endpoint. + */ + analyticsEndpoint: string; +} + +/** + * Cached configuration. + */ +let cache: Partial; + +/** + * Whether configuration should be cached in this environment. + */ +const isCacheEnvironment = process.env.NODE_ENV !== 'test'; + +/** + * Returns the value associated as initialized through page configuration, if available. + * + * @param key Key for which to return value. + * + * @return Value, if exists. + */ +function getConfigValue(key: K): Config[K] | undefined { + if (cache === undefined || !isCacheEnvironment) { + try { + cache = JSON.parse(document.querySelector('[data-config]')?.textContent || ''); + } catch { + cache = {}; + } + } + + return cache[key]; +} + +export default getConfigValue; diff --git a/app/javascript/packages/config/index.ts b/app/javascript/packages/config/index.ts new file mode 100644 index 00000000000..289bdfcf4fb --- /dev/null +++ b/app/javascript/packages/config/index.ts @@ -0,0 +1 @@ +export { default as getConfigValue } from './get-config-value'; diff --git a/app/javascript/packages/page-data/package.json b/app/javascript/packages/config/package.json similarity index 54% rename from app/javascript/packages/page-data/package.json rename to app/javascript/packages/config/package.json index 76a02212b54..c9af12df0b3 100644 --- a/app/javascript/packages/page-data/package.json +++ b/app/javascript/packages/config/package.json @@ -1,5 +1,5 @@ { - "name": "@18f/identity-page-data", + "name": "@18f/identity-config", "private": true, "version": "1.0.0" } diff --git a/app/javascript/packages/page-data/index.js b/app/javascript/packages/page-data/index.js deleted file mode 100644 index 235703adfed..00000000000 --- a/app/javascript/packages/page-data/index.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Naive implementation of converting string to dash-delimited string, targeting support only for - * alphanumerical strings. - * - * @example - * ``` - * kebabCase('HelloWorld'); - * // 'hello-world' - * ``` - * - * @param {string} string - * - * @return {string} - */ -const kebabCase = (string) => string.replace(/(.)([A-Z])/g, '$1-$2').toLowerCase(); - -/** - * Returns data- attribute selector associated with a given dataset key. - * - * @param {string} key Dataset key. - * - * @return {string} Data attribute. - */ -const getDataAttributeSelector = (key) => `[data-${kebabCase(key)}]`; - -/** - * Returns the value associated with a page element with the given dataset property, or undefined if - * the element does not exist. - * - * @param {string} key Key for which to return value. - * - * @return {string=} Value, if exists. - */ -export function getPageData(key) { - const element = document.querySelector(getDataAttributeSelector(key)); - return /** @type {HTMLElement=} */ (element)?.dataset[key]; -} diff --git a/app/javascript/packages/verify-flow/steps/password-confirm/index.ts b/app/javascript/packages/verify-flow/steps/password-confirm/index.ts index 3763448bbdb..b06eba50311 100644 --- a/app/javascript/packages/verify-flow/steps/password-confirm/index.ts +++ b/app/javascript/packages/verify-flow/steps/password-confirm/index.ts @@ -1,4 +1,5 @@ import { t } from '@18f/identity-i18n'; +import { getConfigValue } from '@18f/identity-config'; import type { FormStep } from '@18f/identity-form-steps'; import type { VerifyFlowValues } from '../../verify-flow'; import form from './password-confirm-step'; @@ -6,7 +7,7 @@ import submit from './submit'; export default { name: 'password_confirm', - title: t('idv.titles.session.review'), + title: t('idv.titles.session.review', { app_name: getConfigValue('appName') }), form, submit, } as FormStep; diff --git a/app/javascript/packages/verify-flow/verify-flow.spec.tsx b/app/javascript/packages/verify-flow/verify-flow.spec.tsx index f314f3b35f8..0d5270c32b1 100644 --- a/app/javascript/packages/verify-flow/verify-flow.spec.tsx +++ b/app/javascript/packages/verify-flow/verify-flow.spec.tsx @@ -14,18 +14,14 @@ describe('VerifyFlow', () => { sandbox.stub(window, 'fetch').resolves({ json: () => Promise.resolve({ personal_key: personalKey }), } as Response); + document.body.innerHTML = ``; }); it('advances through flow to completion', async () => { const onComplete = sandbox.spy(); const { getByText, findByText, getByLabelText } = render( - , + , ); // Password confirm @@ -59,7 +55,6 @@ describe('VerifyFlow', () => { it('sets details according to the first enabled steps', () => { render( {}} enabledStepNames={[STEPS[1].name]} diff --git a/app/javascript/packages/verify-flow/verify-flow.tsx b/app/javascript/packages/verify-flow/verify-flow.tsx index ddc5e565c9e..f116ed1017c 100644 --- a/app/javascript/packages/verify-flow/verify-flow.tsx +++ b/app/javascript/packages/verify-flow/verify-flow.tsx @@ -1,6 +1,7 @@ import { useEffect, useState } from 'react'; import { FormSteps } from '@18f/identity-form-steps'; import { trackEvent } from '@18f/identity-analytics'; +import { getConfigValue } from '@18f/identity-config'; import { STEPS } from './steps'; import VerifyFlowStepIndicator from './verify-flow-step-indicator'; import VerifyFlowAlert from './verify-flow-alert'; @@ -51,11 +52,6 @@ interface VerifyFlowProps { */ basePath: string; - /** - * Application name, used in generating page titles for current step. - */ - appName: string; - /** * Callback invoked after completing the form. */ @@ -87,7 +83,6 @@ function VerifyFlow({ initialValues = {}, enabledStepNames, basePath, - appName, onComplete, }: VerifyFlowProps) { let steps = STEPS; @@ -117,7 +112,7 @@ function VerifyFlow({ initialStep={initialStep} promptOnNavigate={false} basePath={basePath} - titleFormat={`%{step} - ${appName}`} + titleFormat={`%{step} - ${getConfigValue('appName')}`} onChange={setSyncedValues} onStepSubmit={onStepSubmit} onStepChange={setCurrentStep} diff --git a/app/javascript/packs/doc-capture-polling.js b/app/javascript/packs/doc-capture-polling.js deleted file mode 100644 index f70da30e557..00000000000 --- a/app/javascript/packs/doc-capture-polling.js +++ /dev/null @@ -1,12 +0,0 @@ -import { DocumentCapturePolling } from '@18f/identity-document-capture-polling'; -import { getPageData } from '@18f/identity-page-data'; - -new DocumentCapturePolling({ - statusEndpoint: /** @type {string} */ (getPageData('docCaptureStatusEndpoint')), - elements: { - backLink: /** @type {HTMLAnchorElement} */ (document.querySelector('.link-sent-back-link')), - form: /** @type {HTMLFormElement} */ ( - document.querySelector('.link-sent-continue-button-form') - ), - }, -}).bind(); diff --git a/app/javascript/packs/doc-capture-polling.ts b/app/javascript/packs/doc-capture-polling.ts new file mode 100644 index 00000000000..b53647e05bd --- /dev/null +++ b/app/javascript/packs/doc-capture-polling.ts @@ -0,0 +1,11 @@ +import { DocumentCapturePolling } from '@18f/identity-document-capture-polling'; + +new DocumentCapturePolling({ + statusEndpoint: document + .querySelector('[data-status-endpoint]') + ?.getAttribute('data-status-endpoint') as string, + elements: { + backLink: document.querySelector('.link-sent-back-link') as HTMLAnchorElement, + form: document.querySelector('.link-sent-continue-button-form') as HTMLFormElement, + }, +}).bind(); diff --git a/app/javascript/packs/verify-flow.tsx b/app/javascript/packs/verify-flow.tsx index 89da765213a..9de0dfb0c80 100644 --- a/app/javascript/packs/verify-flow.tsx +++ b/app/javascript/packs/verify-flow.tsx @@ -19,11 +19,6 @@ interface AppRootValues { */ basePath: string; - /** - * Application name. - */ - appName: string; - /** * URL to which user should be redirected after completing the form. */ @@ -49,7 +44,6 @@ const { initialValues: initialValuesJSON, enabledStepNames: enabledStepNamesJSON, basePath, - appName, completionUrl: completionURL, storeKey: storeKeyBase64, } = appRoot.dataset; @@ -93,7 +87,6 @@ const storage = new SecretSessionStorage('verify'); initialValues={initialValues} enabledStepNames={enabledStepNames} basePath={basePath} - appName={appName} onComplete={onComplete} /> , diff --git a/app/views/idv/doc_auth/link_sent.html.erb b/app/views/idv/doc_auth/link_sent.html.erb index 1ed880bfc93..4fce17076e7 100644 --- a/app/views/idv/doc_auth/link_sent.html.erb +++ b/app/views/idv/doc_auth/link_sent.html.erb @@ -37,7 +37,7 @@ <% if FeatureManagement.doc_capture_polling_enabled? %> - <%= content_tag 'script', '', data: { doc_capture_status_endpoint: idv_capture_doc_status_url } %> + <%= content_tag 'script', '', data: { status_endpoint: idv_capture_doc_status_url } %> <%= javascript_packs_tag_once 'doc-capture-polling' %> <% end %> diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index 18b3f91f379..d814df410bd 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -84,7 +84,15 @@ } %> <% end %> - <%= content_tag 'script', '', data: { analytics_endpoint: api_logger_path } %> + <%= content_tag( + :script, + { + 'appName' => APP_NAME, + 'analyticsEndpoint' => api_logger_path, + }.to_json, + { type: 'application/json', data: { config: '' } }, + false, + ) %> <%= javascript_packs_tag_once('application', prepend: true) %> <%= render_javascript_pack_once_tags %> diff --git a/spec/controllers/verify_controller_spec.rb b/spec/controllers/verify_controller_spec.rb index 137034c5e3b..d7bb409c4e3 100644 --- a/spec/controllers/verify_controller_spec.rb +++ b/spec/controllers/verify_controller_spec.rb @@ -67,7 +67,6 @@ response expect(assigns[:app_data]).to include( - app_name: APP_NAME, base_path: idv_app_path, completion_url: idv_gpo_verify_url, enabled_step_names: idv_api_enabled_steps, @@ -97,7 +96,6 @@ response expect(assigns[:app_data]).to include( - app_name: APP_NAME, base_path: idv_app_path, completion_url: account_url, enabled_step_names: idv_api_enabled_steps, diff --git a/spec/features/visitors/i18n_spec.rb b/spec/features/visitors/i18n_spec.rb index acce94392da..460db930b48 100644 --- a/spec/features/visitors/i18n_spec.rb +++ b/spec/features/visitors/i18n_spec.rb @@ -22,10 +22,9 @@ end it 'initializes front-end logger with default locale' do - expect(page).to have_selector( - "[data-analytics-endpoint='#{api_logger_path(locale: nil)}']", - visible: :all, - ) + config = JSON.parse(page.find('[data-config]', visible: :all).text(:all)) + + expect(config['analyticsEndpoint']).to eq api_logger_path(locale: nil) end end @@ -37,10 +36,9 @@ end it 'initializes front-end logger with locale parameter' do - expect(page).to have_selector( - "[data-analytics-endpoint='#{api_logger_path(locale: 'es')}']", - visible: :all, - ) + config = JSON.parse(page.find('[data-config]', visible: :all).text(:all)) + + expect(config['analyticsEndpoint']).to eq api_logger_path(locale: 'es') end end diff --git a/spec/javascripts/packages/analytics/index-spec.js b/spec/javascripts/packages/analytics/index-spec.js index ff7e9ead970..1778ec39178 100644 --- a/spec/javascripts/packages/analytics/index-spec.js +++ b/spec/javascripts/packages/analytics/index-spec.js @@ -21,7 +21,7 @@ describe('trackEvent', () => { const endpoint = '/log'; beforeEach(() => { - document.body.innerHTML = ``; + document.body.innerHTML = ``; }); context('no payload', () => { diff --git a/spec/javascripts/packages/page-data/index-spec.js b/spec/javascripts/packages/page-data/index-spec.js deleted file mode 100644 index 7313b6816c6..00000000000 --- a/spec/javascripts/packages/page-data/index-spec.js +++ /dev/null @@ -1,23 +0,0 @@ -import { getPageData } from '@18f/identity-page-data'; - -describe('getPageData', () => { - context('page data exists', () => { - beforeEach(() => { - document.body.innerHTML = ''; - }); - - it('returns value', () => { - const result = getPageData('fooBarBaz'); - - expect(result).to.equal('value'); - }); - }); - - context('page data does not exist', () => { - it('returns undefined', () => { - const result = getPageData('fooBarBaz'); - - expect(result).to.be.undefined(); - }); - }); -});