diff --git a/app/components/phone_input_component.rb b/app/components/phone_input_component.rb index 0d263ce91b2..53a5781f5f0 100644 --- a/app/components/phone_input_component.rb +++ b/app/components/phone_input_component.rb @@ -63,7 +63,8 @@ def international_phone_codes def strings { country_code_label: t('components.phone_input.country_code_label'), - invalid_phone: t('errors.messages.invalid_phone_number'), + invalid_phone_us: t('errors.messages.invalid_phone_number.us'), + invalid_phone_international: t('errors.messages.invalid_phone_number.international'), unsupported_country: unsupported_country_string, } end diff --git a/app/controllers/api/internal/sessions_controller.rb b/app/controllers/api/internal/sessions_controller.rb index 8a1d81a1767..e376c6aa8c8 100644 --- a/app/controllers/api/internal/sessions_controller.rb +++ b/app/controllers/api/internal/sessions_controller.rb @@ -11,13 +11,13 @@ class SessionsController < ApplicationController respond_to :json def show - render json: { live: live?, timeout: timeout } + render json: status_response end def update analytics.session_kept_alive if live? update_last_request_at - render json: { live: live?, timeout: timeout } + render json: status_response end def destroy @@ -29,21 +29,21 @@ def destroy private + def status_response + { live: live?, timeout: live?.presence && timeout } + end + def skip_devise_hooks request.env['devise.skip_timeout'] = true request.env['devise.skip_trackable'] = true end def live? - timeout.future? + timeout.present? && timeout.future? end def timeout - if last_request_at.present? - Time.zone.at(last_request_at + User.timeout_in) - else - Time.current - end + Time.zone.at(last_request_at + User.timeout_in) if last_request_at.present? end def last_request_at diff --git a/app/controllers/concerns/idv/outage_concern.rb b/app/controllers/concerns/idv/outage_concern.rb new file mode 100644 index 00000000000..73893f96ddb --- /dev/null +++ b/app/controllers/concerns/idv/outage_concern.rb @@ -0,0 +1,23 @@ +module Idv + module OutageConcern + extend ActiveSupport::Concern + + def check_for_outage + return if user_session.fetch('idv/doc_auth', {})[:skip_vendor_outage] + + return redirect_for_gpo_only if FeatureManagement.idv_gpo_only? + end + + def redirect_for_gpo_only + return redirect_to vendor_outage_url unless FeatureManagement.gpo_verification_enabled? + + # During a phone outage, skip the hybrid handoff + # step and go straight to document upload + unless FeatureManagement.idv_allow_hybrid_flow? + user_session.fetch('idv/doc_auth', {})[:skip_upload_step] = true + end + + redirect_to idv_mail_only_warning_url + end + end +end diff --git a/app/controllers/concerns/idv_step_concern.rb b/app/controllers/concerns/idv_step_concern.rb index fd879cddcbe..043dff1ca91 100644 --- a/app/controllers/concerns/idv_step_concern.rb +++ b/app/controllers/concerns/idv_step_concern.rb @@ -48,8 +48,8 @@ def confirm_document_capture_complete redirect_to idv_document_capture_url elsif flow_path == 'hybrid' redirect_to idv_link_sent_url - else # no flow_path, go to UploadStep via FSM - redirect_to idv_doc_auth_url + else # no flow_path + redirect_to idv_hybrid_handoff_path end end diff --git a/app/controllers/idv/doc_auth_controller.rb b/app/controllers/idv/doc_auth_controller.rb index 848180d6cee..4e41f637749 100644 --- a/app/controllers/idv/doc_auth_controller.rb +++ b/app/controllers/idv/doc_auth_controller.rb @@ -8,6 +8,7 @@ class DocAuthController < ApplicationController include Flow::FlowStateMachine include Idv::ThreatMetrixConcern include FraudReviewConcern + include Idv::OutageConcern before_action :redirect_if_flow_completed before_action :handle_fraud @@ -20,7 +21,7 @@ class DocAuthController < ApplicationController FLOW_STATE_MACHINE_SETTINGS = { step_url: :idv_doc_auth_step_url, - final_url: :idv_link_sent_url, + final_url: :idv_hybrid_handoff_url, flow: Idv::Flows::DocAuthFlow, analytics_id: 'Doc Auth', }.freeze @@ -56,24 +57,5 @@ def do_meta_refresh(meta_refresh_count) def flow_session user_session['idv/doc_auth'] end - - def check_for_outage - return if flow_session[:skip_vendor_outage] - - return redirect_for_gpo_only if FeatureManagement.idv_gpo_only? - end - - def redirect_for_gpo_only - return redirect_to vendor_outage_url unless FeatureManagement.gpo_verification_enabled? - - # During a phone outage, skip the hybrid handoff - # step and go straight to document upload - flow_session[:skip_upload_step] = true unless FeatureManagement.idv_allow_hybrid_flow? - - session[:vendor_outage_redirect] = current_step - session[:vendor_outage_redirect_from_idv] = true - - redirect_to idv_mail_only_warning_url - end end end diff --git a/app/controllers/idv/document_capture_controller.rb b/app/controllers/idv/document_capture_controller.rb index 044de86df5e..a5b14ba47ec 100644 --- a/app/controllers/idv/document_capture_controller.rb +++ b/app/controllers/idv/document_capture_controller.rb @@ -4,6 +4,7 @@ class DocumentCaptureController < ApplicationController include DocumentCaptureConcern include IdvSession include IdvStepConcern + include OutageConcern include StepIndicatorConcern include StepUtilitiesConcern include RateLimitConcern @@ -12,6 +13,7 @@ class DocumentCaptureController < ApplicationController before_action :confirm_upload_step_complete before_action :confirm_document_capture_needed before_action :override_csp_to_allow_acuant + before_action :check_for_outage, only: :show def show analytics.idv_doc_auth_document_capture_visited(**analytics_arguments) @@ -54,11 +56,7 @@ def extra_view_variables def confirm_upload_step_complete return if flow_session[:flow_path].present? - if IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled - redirect_to idv_hybrid_handoff_url - else - redirect_to idv_doc_auth_url - end + redirect_to idv_hybrid_handoff_url end def confirm_document_capture_needed diff --git a/app/controllers/idv/hybrid_handoff_controller.rb b/app/controllers/idv/hybrid_handoff_controller.rb index fdf07d77a04..b04d02375a9 100644 --- a/app/controllers/idv/hybrid_handoff_controller.rb +++ b/app/controllers/idv/hybrid_handoff_controller.rb @@ -3,12 +3,14 @@ class HybridHandoffController < ApplicationController include ActionView::Helpers::DateHelper include IdvSession include IdvStepConcern + include OutageConcern include StepIndicatorConcern include StepUtilitiesConcern before_action :confirm_two_factor_authenticated before_action :confirm_agreement_step_complete before_action :confirm_hybrid_handoff_needed, only: :show + before_action :check_for_outage, only: :show def show analytics.idv_doc_auth_upload_visited(**analytics_arguments) @@ -43,7 +45,6 @@ def handle_phone_submission throttle.increment! return throttled_failure if throttle.throttled? idv_session.phone_for_mobile_flow = params[:doc_auth][:phone] - flow_session[:phone_for_mobile_flow] = idv_session.phone_for_mobile_flow flow_session[:flow_path] = 'hybrid' telephony_result = send_link telephony_form_response = build_telephony_form_response(telephony_result) @@ -61,9 +62,6 @@ def handle_phone_submission if !failure_reason redirect_to idv_link_sent_url - - # for the 50/50 state - flow_session['Idv::Steps::UploadStep'] = true else redirect_to idv_hybrid_handoff_url flow_session[:flow_path] = nil @@ -122,9 +120,6 @@ def bypass_send_link_steps flow_session[:flow_path] = 'standard' redirect_to idv_document_capture_url - # for the 50/50 state - flow_session['Idv::Steps::UploadStep'] = true - analytics.idv_doc_auth_upload_submitted( **analytics_arguments.merge( form_response(destination: :document_capture).to_h, diff --git a/app/controllers/idv/in_person/verify_info_controller.rb b/app/controllers/idv/in_person/verify_info_controller.rb index 154ad7c5641..c09e2dc8cad 100644 --- a/app/controllers/idv/in_person/verify_info_controller.rb +++ b/app/controllers/idv/in_person/verify_info_controller.rb @@ -6,10 +6,12 @@ class VerifyInfoController < ApplicationController include StepUtilitiesConcern include Steps::ThreatMetrixStepHelper include VerifyInfoConcern + include OutageConcern before_action :renders_404_if_flag_not_set before_action :confirm_ssn_step_complete before_action :confirm_verify_info_step_needed + before_action :check_for_outage, only: :show def show @step_indicator_steps = step_indicator_steps diff --git a/app/controllers/idv/link_sent_controller.rb b/app/controllers/idv/link_sent_controller.rb index a225ce44cd1..a144f7aa38b 100644 --- a/app/controllers/idv/link_sent_controller.rb +++ b/app/controllers/idv/link_sent_controller.rb @@ -3,6 +3,7 @@ class LinkSentController < ApplicationController include DocumentCaptureConcern include IdvSession include IdvStepConcern + include OutageConcern include StepIndicatorConcern include StepUtilitiesConcern @@ -10,6 +11,7 @@ class LinkSentController < ApplicationController before_action :confirm_upload_step_complete before_action :confirm_document_capture_needed before_action :extend_timeout_using_meta_refresh + before_action :check_for_outage, only: :show def show analytics.idv_doc_auth_link_sent_visited(**analytics_arguments) @@ -34,7 +36,7 @@ def update end def extra_view_variables - { phone: flow_session[:phone_for_mobile_flow], + { phone: idv_session.phone_for_mobile_flow, flow_session: flow_session } end @@ -45,10 +47,8 @@ def confirm_upload_step_complete if flow_session[:flow_path] == 'standard' redirect_to idv_document_capture_url - elsif IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled - redirect_to idv_hybrid_handoff_url else - redirect_to idv_doc_auth_url + redirect_to idv_hybrid_handoff_url end end @@ -73,18 +73,12 @@ def analytics_arguments def handle_document_verification_success(get_results_response) save_proofing_components(current_user) extract_pii_from_doc(current_user, get_results_response, store_in_session: true) - mark_upload_step_complete flow_session[:flow_path] = 'hybrid' end def render_document_capture_cancelled - if IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled - redirect_to idv_hybrid_handoff_url - flow_session[:flow_path] = nil - else - mark_upload_step_incomplete - redirect_to idv_doc_auth_url # was idv_url, why? - end + redirect_to idv_hybrid_handoff_url + flow_session[:flow_path] = nil failure(I18n.t('errors.doc_auth.document_capture_cancelled')) end @@ -103,14 +97,6 @@ def document_capture_session_result end end - def mark_upload_step_complete - flow_session['Idv::Steps::UploadStep'] = true - end - - def mark_upload_step_incomplete - flow_session['Idv::Steps::UploadStep'] = nil - end - def extend_timeout_using_meta_refresh max_10min_refreshes = IdentityConfig.store.doc_auth_extend_timeout_by_minutes / 10 return if max_10min_refreshes <= 0 diff --git a/app/controllers/idv/phone_controller.rb b/app/controllers/idv/phone_controller.rb index b811b2ba8ae..7cc37046e70 100644 --- a/app/controllers/idv/phone_controller.rb +++ b/app/controllers/idv/phone_controller.rb @@ -2,6 +2,7 @@ module Idv class PhoneController < ApplicationController include IdvStepConcern include StepIndicatorConcern + include OutageConcern include PhoneOtpRateLimitable include PhoneOtpSendable @@ -10,6 +11,9 @@ class PhoneController < ApplicationController before_action :confirm_verify_info_step_complete before_action :confirm_step_needed before_action :set_idv_form + # rubocop:disable Rails/LexicallyScopedActionFilter + before_action :check_for_outage, only: :show + # rubocop:enable Rails/LexicallyScopedActionFilter def new analytics.idv_phone_use_different(step: params[:step]) if params[:step] diff --git a/app/controllers/idv/ssn_controller.rb b/app/controllers/idv/ssn_controller.rb index bc45e21e254..0f94fd9b63a 100644 --- a/app/controllers/idv/ssn_controller.rb +++ b/app/controllers/idv/ssn_controller.rb @@ -2,6 +2,7 @@ module Idv class SsnController < ApplicationController include IdvSession include IdvStepConcern + include OutageConcern include StepIndicatorConcern include StepUtilitiesConcern include Steps::ThreatMetrixStepHelper @@ -11,6 +12,7 @@ class SsnController < ApplicationController before_action :confirm_document_capture_complete before_action :confirm_repeat_ssn, only: :show before_action :override_csp_for_threat_metrix_no_fsm + before_action :check_for_outage, only: :show attr_accessor :error_message diff --git a/app/controllers/idv/verify_info_controller.rb b/app/controllers/idv/verify_info_controller.rb index caee85ef348..f88706d7c39 100644 --- a/app/controllers/idv/verify_info_controller.rb +++ b/app/controllers/idv/verify_info_controller.rb @@ -1,6 +1,7 @@ module Idv class VerifyInfoController < ApplicationController include IdvStepConcern + include OutageConcern include StepUtilitiesConcern include StepIndicatorConcern include VerifyInfoConcern @@ -8,6 +9,7 @@ class VerifyInfoController < ApplicationController before_action :confirm_ssn_step_complete before_action :confirm_verify_info_step_needed + before_action :check_for_outage, only: :show def show @step_indicator_steps = step_indicator_steps diff --git a/app/javascript/packages/document-capture/components/barcode-attention-warning.tsx b/app/javascript/packages/document-capture/components/barcode-attention-warning.tsx index 84b7519f07e..e6ff6ebfe4a 100644 --- a/app/javascript/packages/document-capture/components/barcode-attention-warning.tsx +++ b/app/javascript/packages/document-capture/components/barcode-attention-warning.tsx @@ -1,16 +1,10 @@ -import { useContext } from 'react'; import { Button, StatusPage } from '@18f/identity-components'; -import { SpinnerButton } from '@18f/identity-spinner-button'; import { t } from '@18f/identity-i18n'; import { trackEvent } from '@18f/identity-analytics'; import { removeUnloadProtection } from '@18f/identity-url'; -import UploadContext from '../context/upload'; -import { toFormData } from '../services/upload'; import type { PII } from '../services/upload'; import DocumentCaptureTroubleshootingOptions from './document-capture-troubleshooting-options'; -const DOCUMENT_CAPTURE_ERRORS_API_URL = '/api/verify/v2/document_capture_errors'; - interface BarcodeAttentionWarningProps { /** * Callback to trigger when user opts to try to take new photos rather than continue to next step. @@ -24,16 +18,8 @@ interface BarcodeAttentionWarningProps { } function BarcodeAttentionWarning({ onDismiss, pii }: BarcodeAttentionWarningProps) { - const { formData } = useContext(UploadContext); - - async function skipAttention() { - await Promise.all([ - trackEvent('IdV: barcode warning continue clicked'), - window.fetch(DOCUMENT_CAPTURE_ERRORS_API_URL, { - method: 'DELETE', - body: toFormData({ document_capture_session_uuid: formData.document_capture_session_uuid }), - }), - ]); + function skipAttention() { + trackEvent('IdV: barcode warning continue clicked'); removeUnloadProtection(); const form = document.querySelector('.js-document-capture-form'); form?.submit(); @@ -49,9 +35,9 @@ function BarcodeAttentionWarning({ onDismiss, pii }: BarcodeAttentionWarningProp header={t('doc_auth.errors.barcode_attention.heading')} status="warning" actionButtons={[ - + , , diff --git a/app/javascript/packages/phone-input/index.spec.ts b/app/javascript/packages/phone-input/index.spec.ts index 2e7411d4601..a49ee0bc557 100644 --- a/app/javascript/packages/phone-input/index.spec.ts +++ b/app/javascript/packages/phone-input/index.spec.ts @@ -32,15 +32,15 @@ describe('PhoneInput', () => { }); function createAndConnectElement({ - isSingleOption = false, - isNonUSSingleOption = false, + isUSSingleOption = false, + isInternationalSingleOption = false, deliveryMethods = ['sms', 'voice'], translatedCountryCodeNames = {}, captchaExemptCountries = undefined, phoneInputValue = undefined, }: { - isSingleOption?: boolean; - isNonUSSingleOption?: Boolean; + isUSSingleOption?: boolean; + isInternationalSingleOption?: Boolean; deliveryMethods?: string[]; translatedCountryCodeNames?: Record; captchaExemptCountries?: string[]; @@ -70,15 +70,16 @@ describe('PhoneInput', () => {
- ${isSingleOption ? SINGLE_OPTION_HTML : ''} - ${isNonUSSingleOption ? SINGLE_OPTION_SELECT_NON_US_HTML : ''} - ${!isSingleOption && !isNonUSSingleOption ? MULTIPLE_OPTIONS_HTML : ''} + ${isUSSingleOption ? SINGLE_OPTION_HTML : ''} + ${isInternationalSingleOption ? SINGLE_OPTION_SELECT_NON_US_HTML : ''} + ${!isUSSingleOption && !isInternationalSingleOption ? MULTIPLE_OPTIONS_HTML : ''}
@@ -102,18 +103,41 @@ describe('PhoneInput', () => { expect(input.querySelector('.iti.iti--allow-dropdown')).to.be.ok(); }); - it('validates input', async () => { - const input = createAndConnectElement(); + context('with US phone number', () => { + it('validates input', async () => { + const input = createAndConnectElement(); + const phoneNumber = getByLabelText(input, 'Phone number') as HTMLInputElement; - const phoneNumber = getByLabelText(input, 'Phone number') as HTMLInputElement; + expect(phoneNumber.validity.valueMissing).to.be.true(); + + await userEvent.type(phoneNumber, '5'); + expect(phoneNumber.validationMessage).to.equal('Enter a 10 digit phone number.'); + + await userEvent.type(phoneNumber, '13-555-1234'); + expect(phoneNumber.validity.valid).to.be.true(); + }); + }); + + context('with international phone number', () => { + it('validates input', async () => { + const input = createAndConnectElement(); + + const phoneNumber = getByLabelText(input, 'Phone number') as HTMLInputElement; + const countryCode = getByLabelText(input, 'Country code', { + selector: 'select', + }) as HTMLSelectElement; - expect(phoneNumber.validity.valueMissing).to.be.true(); + expect(phoneNumber.validity.valueMissing).to.be.true(); - await userEvent.type(phoneNumber, '5'); - expect(phoneNumber.validationMessage).to.equal('Phone number is not valid'); + await userEvent.type(phoneNumber, '647'); + expect(countryCode.value).to.eql('CA'); + expect(phoneNumber.validationMessage).to.equal( + 'Enter a phone number with the correct number of digits.', + ); - await userEvent.type(phoneNumber, '13-555-1234'); - expect(phoneNumber.validity.valid).to.be.true(); + await userEvent.type(phoneNumber, '555-1234'); + expect(phoneNumber.validity.valid).to.be.true(); + }); }); it('validates supported delivery method', async () => { @@ -197,18 +221,20 @@ describe('PhoneInput', () => { context('with single option', () => { it('initializes without dropdown', () => { - const input = createAndConnectElement({ isSingleOption: true }); + const input = createAndConnectElement({ isUSSingleOption: true }); expect(input.querySelector('.iti:not(.iti--allow-dropdown)')).to.be.ok(); }); it('validates phone from region', async () => { - const input = createAndConnectElement({ isNonUSSingleOption: true }); + const input = createAndConnectElement({ isInternationalSingleOption: true }); const phoneNumber = getByLabelText(input, 'Phone number') as HTMLInputElement; - await userEvent.type(phoneNumber, '513-555-1234'); - expect(phoneNumber.validationMessage).to.equal('Phone number is not valid'); + await userEvent.type(phoneNumber, '5135551234'); + expect(phoneNumber.validationMessage).to.equal( + 'Enter a phone number with the correct number of digits.', + ); }); }); diff --git a/app/javascript/packages/phone-input/index.ts b/app/javascript/packages/phone-input/index.ts index 651b54aae18..e5dd328c35c 100644 --- a/app/javascript/packages/phone-input/index.ts +++ b/app/javascript/packages/phone-input/index.ts @@ -1,4 +1,4 @@ -import { isValidNumber, isValidNumberForRegion } from 'libphonenumber-js'; +import { isValidNumberForRegion, isValidNumber } from 'libphonenumber-js'; import 'intl-tel-input/build/js/utils.js'; import intlTelInput from 'intl-tel-input'; import type { CountryCode } from 'libphonenumber-js'; @@ -9,7 +9,9 @@ import { CAPTCHA_EVENT_NAME } from '@18f/identity-captcha-submit-button/captcha- interface PhoneInputStrings { country_code_label: string; - invalid_phone: string; + invalid_phone_us: string; + + invalid_phone_international: string; unsupported_country: string; } @@ -22,14 +24,6 @@ interface IntlTelInput extends IntlTelInputPlugin { options: Options; } -const isPhoneValid = (phone, countryCode) => { - let phoneValid = isValidNumber(phone, countryCode); - if (!phoneValid && countryCode === 'US') { - phoneValid = isValidNumber(`+1 ${phone}`, countryCode); - } - return phoneValid; -}; - const updateInternationalCodeInPhone = (phone, newCode) => phone.replace(new RegExp(`^\\+?(\\d+\\s+|${newCode})?`), `+${newCode} `); @@ -204,12 +198,12 @@ export class PhoneInputElement extends HTMLElement { const isInvalidCountry = !isValidNumberForRegion(phoneNumber, countryCode); if (isInvalidCountry) { - textInput.setCustomValidity(this.strings.invalid_phone || ''); + textInput.setCustomValidity(this.getInvalidFormatMessage(countryCode)); } - const isInvalidPhoneNumber = !isPhoneValid(phoneNumber, countryCode); + const isInvalidPhoneNumber = !isValidNumber(phoneNumber, countryCode); if (isInvalidPhoneNumber) { - textInput.setCustomValidity(this.strings.invalid_phone || ''); + textInput.setCustomValidity(this.getInvalidFormatMessage(countryCode)); } if (!this.isSupportedCountry()) { @@ -225,6 +219,12 @@ export class PhoneInputElement extends HTMLElement { } } + getInvalidFormatMessage(countryCode: CountryCode): string { + return countryCode === 'US' + ? this.strings.invalid_phone_us || '' + : this.strings.invalid_phone_international || ''; + } + formatTextInput() { const { textInput, selectedOption } = this; if (!textInput || !selectedOption) { diff --git a/app/javascript/packages/request/index.spec.ts b/app/javascript/packages/request/index.spec.ts index c94404acea8..34b1455edad 100644 --- a/app/javascript/packages/request/index.spec.ts +++ b/app/javascript/packages/request/index.spec.ts @@ -1,7 +1,7 @@ import sinon from 'sinon'; import type { SinonStub } from 'sinon'; import { useSandbox } from '@18f/identity-test-helpers'; -import { request } from '.'; +import { request, ResponseError } from '.'; describe('request', () => { const sandbox = useSandbox(); @@ -241,13 +241,14 @@ describe('request', () => { }); it('throws an error', async () => { - await request('https://example.com', { read: false }) - .then(() => { - throw new Error('Unexpected promise resolution'); - }) - .catch((error) => { - expect(error).to.exist(); - }); + let didCatch = false; + await request('https://example.com').catch((error: ResponseError) => { + expect(error).to.exist(); + expect(error.status).to.equal(400); + didCatch = true; + }); + + expect(didCatch).to.be.true(); }); context('with read=false option', () => { diff --git a/app/javascript/packages/request/index.ts b/app/javascript/packages/request/index.ts index a5b88b97ebc..0a94fc7fc66 100644 --- a/app/javascript/packages/request/index.ts +++ b/app/javascript/packages/request/index.ts @@ -1,5 +1,9 @@ type CSRFGetter = () => string | undefined; +export class ResponseError extends Error { + status: number; +} + interface RequestOptions extends RequestInit { /** * Either boolean or unstringified POJO to send with the request as JSON. Defaults to true. @@ -102,7 +106,9 @@ export async function request(url: string, options: Partial = {} if (read) { if (!response.ok) { - throw new Error(); + const error = new ResponseError(); + error.status = response.status; + throw error; } return json ? response.json() : response.text(); diff --git a/app/javascript/packages/session/requests.spec.ts b/app/javascript/packages/session/requests.spec.ts index dec5362091d..169ca37ea6b 100644 --- a/app/javascript/packages/session/requests.spec.ts +++ b/app/javascript/packages/session/requests.spec.ts @@ -7,73 +7,152 @@ import { requestSessionStatus, extendSession, } from './requests'; -import type { SessionStatusResponse } from './requests'; +import type { SessionLiveStatusResponse, SessionTimedOutStatusResponse } from './requests'; describe('requestSessionStatus', () => { - let isLive: boolean; - let timeout: string; - let server: SetupServer; - before(() => { - server = setupServer( - rest.get<{}, {}, SessionStatusResponse>(STATUS_API_ENDPOINT, (_req, res, ctx) => - res(ctx.json({ live: isLive, timeout })), - ), - ); - server.listen(); - }); - - after(() => { - server.close(); - }); context('session inactive', () => { - beforeEach(() => { - isLive = false; - timeout = new Date().toISOString(); + before(() => { + server = setupServer( + rest.get<{}, {}, SessionTimedOutStatusResponse>(STATUS_API_ENDPOINT, (_req, res, ctx) => + res(ctx.json({ live: false, timeout: null })), + ), + ); + server.listen(); + }); + + after(() => { + server.close(); }); it('resolves to the status', async () => { const result = await requestSessionStatus(); - expect(result).to.deep.equal({ isLive: false, timeout }); + expect(result).to.deep.equal({ isLive: false }); }); }); context('session active', () => { - beforeEach(() => { - isLive = true; + let timeout: string; + + before(() => { timeout = new Date(Date.now() + 1000).toISOString(); + server = setupServer( + rest.get<{}, {}, SessionLiveStatusResponse>(STATUS_API_ENDPOINT, (_req, res, ctx) => + res(ctx.json({ live: true, timeout })), + ), + ); + server.listen(); + }); + + after(() => { + server.close(); + }); + + it('resolves to the status', async () => { + const result = await requestSessionStatus(); + + expect(result).to.deep.equal({ isLive: true, timeout: new Date(timeout) }); + }); + }); + + context('server responds with 401', () => { + before(() => { + server = setupServer( + rest.get<{}, {}>(STATUS_API_ENDPOINT, (_req, res, ctx) => res(ctx.status(401))), + ); + server.listen(); + }); + + after(() => { + server.close(); }); it('resolves to the status', async () => { const result = await requestSessionStatus(); - expect(result).to.deep.equal({ isLive: true, timeout }); + expect(result).to.deep.equal({ isLive: false }); + }); + }); + + context('server responds with 500', () => { + before(() => { + server = setupServer( + rest.get<{}, {}>(STATUS_API_ENDPOINT, (_req, res, ctx) => res(ctx.status(500))), + ); + server.listen(); + }); + + after(() => { + server.close(); + }); + + it('throws an error', async () => { + await expect(requestSessionStatus()).to.be.rejected(); }); }); }); describe('extendSession', () => { - const timeout = new Date(Date.now() + 1000).toISOString(); - let server: SetupServer; - before(() => { - server = setupServer( - rest.post<{}, {}, SessionStatusResponse>(KEEP_ALIVE_API_ENDPOINT, (_req, res, ctx) => - res(ctx.json({ live: true, timeout })), - ), - ); - server.listen(); + + context('session active', () => { + const timeout = new Date(Date.now() + 1000).toISOString(); + + before(() => { + server = setupServer( + rest.post<{}, {}, SessionLiveStatusResponse>(KEEP_ALIVE_API_ENDPOINT, (_req, res, ctx) => + res(ctx.json({ live: true, timeout })), + ), + ); + server.listen(); + }); + + after(() => { + server.close(); + }); + + it('resolves to the status', async () => { + const result = await extendSession(); + + expect(result).to.deep.equal({ isLive: true, timeout: new Date(timeout) }); + }); }); - after(() => { - server.close(); + context('server responds with 401', () => { + before(() => { + server = setupServer( + rest.post<{}, {}>(KEEP_ALIVE_API_ENDPOINT, (_req, res, ctx) => res(ctx.status(401))), + ); + server.listen(); + }); + + after(() => { + server.close(); + }); + + it('resolves to the status', async () => { + const result = await extendSession(); + + expect(result).to.deep.equal({ isLive: false }); + }); }); - it('resolves to the status', async () => { - const result = await extendSession(); + context('server responds with 500', () => { + before(() => { + server = setupServer( + rest.post<{}, {}>(KEEP_ALIVE_API_ENDPOINT, (_req, res, ctx) => res(ctx.status(500))), + ); + server.listen(); + }); + + after(() => { + server.close(); + }); - expect(result).to.deep.equal({ isLive: true, timeout }); + it('throws an error', async () => { + await expect(extendSession()).to.be.rejected(); + }); }); }); diff --git a/app/javascript/packages/session/requests.ts b/app/javascript/packages/session/requests.ts index afc9deb1089..8165ad24976 100644 --- a/app/javascript/packages/session/requests.ts +++ b/app/javascript/packages/session/requests.ts @@ -1,10 +1,10 @@ -import { request } from '@18f/identity-request'; +import { request, ResponseError } from '@18f/identity-request'; -export interface SessionStatusResponse { +export interface SessionLiveStatusResponse { /** * Whether the session is still active. */ - live: boolean; + live: true; /** * ISO8601-formatted date string for session timeout. @@ -12,25 +12,74 @@ export interface SessionStatusResponse { timeout: string; } -export interface SessionStatus { +export interface SessionTimedOutStatusResponse { /** * Whether the session is still active. */ - isLive: boolean; + live: false; /** * ISO8601-formatted date string for session timeout. */ - timeout: string; + timeout: null; +} + +type SessionStatusResponse = SessionLiveStatusResponse | SessionTimedOutStatusResponse; + +interface SessionLiveStatus { + /** + * Whether the session is still active. + */ + isLive: true; + + /** + * ISO8601-formatted date string for session timeout. + */ + timeout: Date; +} + +interface SessionTimedOutStatus { + /** + * Whether the session is still active. + */ + isLive: false; + + /** + * ISO8601-formatted date string for session timeout. + */ + timeout?: undefined; } +export type SessionStatus = SessionLiveStatus | SessionTimedOutStatus; + export const STATUS_API_ENDPOINT = '/active'; export const KEEP_ALIVE_API_ENDPOINT = '/sessions/keepalive'; -const mapSessionStatusResponse = ({ live, timeout }: SessionStatusResponse): SessionStatus => ({ - isLive: live, - timeout, -}); +function mapSessionStatusResponse( + response: R, +): SessionLiveStatus; +function mapSessionStatusResponse( + response: R, +): SessionTimedOutStatus; +function mapSessionStatusResponse< + R extends SessionLiveStatusResponse | SessionTimedOutStatusResponse, +>({ live, timeout }: R): SessionLiveStatus | SessionTimedOutStatus { + return live ? { isLive: true, timeout: new Date(timeout) } : { isLive: false }; +} + +/** + * Handles a thrown error from a session endpoint, interpreting an unauthorized request (401) as + * effectively an inactive session. Any other error is re-thrown as being unexpected. + * + * @param error Error thrown from request. + */ +function handleUnauthorizedStatusResponse(error: ResponseError) { + if (error.status === 401) { + return { live: false, timeout: null }; + } + + throw error; +} /** * Request the current session status. Returns a promise resolving to the current session status. @@ -38,7 +87,9 @@ const mapSessionStatusResponse = ({ live, timeout }: SessionStatusResponse): Ses * @return A promise resolving to the current session status */ export const requestSessionStatus = (): Promise => - request(STATUS_API_ENDPOINT).then(mapSessionStatusResponse); + request(STATUS_API_ENDPOINT) + .catch(handleUnauthorizedStatusResponse) + .then(mapSessionStatusResponse); /** * Request that the current session be kept alive. Returns a promise resolving to the updated @@ -47,6 +98,6 @@ export const requestSessionStatus = (): Promise => * @return A promise resolving to the updated session status. */ export const extendSession = (): Promise => - request(KEEP_ALIVE_API_ENDPOINT, { method: 'POST' }).then( - mapSessionStatusResponse, - ); + request(KEEP_ALIVE_API_ENDPOINT, { method: 'POST' }) + .catch(handleUnauthorizedStatusResponse) + .then(mapSessionStatusResponse); diff --git a/app/javascript/packs/session-timeout-ping.ts b/app/javascript/packs/session-timeout-ping.ts index ae699b7a8b7..7765be26f82 100644 --- a/app/javascript/packs/session-timeout-ping.ts +++ b/app/javascript/packs/session-timeout-ping.ts @@ -47,20 +47,20 @@ function handleTimeout(redirectURL: string) { forceRedirect(redirectURL); } -function success(data: SessionStatus) { - if (!data.isLive) { +function success({ isLive, timeout }: SessionStatus) { + if (!isLive) { if (timeoutUrl) { handleTimeout(timeoutUrl); } return; } - const timeRemaining = new Date(data.timeout).valueOf() - Date.now(); + const timeRemaining = timeout.valueOf() - Date.now(); const showWarning = timeRemaining < warning; if (showWarning) { modal.show(); countdownEls.forEach((countdownEl) => { - countdownEl.expiration = new Date(data.timeout); + countdownEl.expiration = timeout; countdownEl.start(); }); } diff --git a/app/models/profile.rb b/app/models/profile.rb index d113a180ec1..e0a408ada17 100644 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -46,18 +46,22 @@ def pending_reasons end # rubocop:disable Rails/SkipsModelValidations - def activate + def activate(reason_deactivated: nil) confirm_that_profile_can_be_activated! now = Time.zone.now is_reproof = Profile.find_by(user_id: user_id, active: true) + + attrs = { + active: true, + activated_at: now, + } + + attrs[:verified_at] = now unless reason_deactivated == :password_reset + transaction do Profile.where(user_id: user_id).update_all(active: false) - update!( - active: true, - activated_at: now, - verified_at: now, - ) + update!(attrs) end send_push_notifications if is_reproof end @@ -97,7 +101,7 @@ def activate_after_password_reset update!( deactivation_reason: nil, ) - activate + activate(reason_deactivated: :password_reset) end end diff --git a/app/services/idv/actions/cancel_link_sent_action.rb b/app/services/idv/actions/cancel_link_sent_action.rb index 60393229121..31977fb6b11 100644 --- a/app/services/idv/actions/cancel_link_sent_action.rb +++ b/app/services/idv/actions/cancel_link_sent_action.rb @@ -6,12 +6,8 @@ def self.analytics_submitted_event end def call - if IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled - redirect_to idv_hybrid_handoff_url - flow_session[:flow_path] = nil - else - mark_step_incomplete(:upload) - end + redirect_to idv_hybrid_handoff_url + flow_session[:flow_path] = nil end end end diff --git a/app/services/idv/actions/redo_document_capture_action.rb b/app/services/idv/actions/redo_document_capture_action.rb index 40ffbe4aabe..4c1678f9dbf 100644 --- a/app/services/idv/actions/redo_document_capture_action.rb +++ b/app/services/idv/actions/redo_document_capture_action.rb @@ -9,11 +9,9 @@ def call flow_session['redo_document_capture'] = true if flow_session[:skip_upload_step] redirect_to idv_document_capture_url - elsif IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled + else redirect_to idv_hybrid_handoff_url flow_session[:flow_path] = nil - else - mark_step_incomplete(:upload) end end end diff --git a/app/services/idv/flows/doc_auth_flow.rb b/app/services/idv/flows/doc_auth_flow.rb index 466cda4c614..bc63f0e95ac 100644 --- a/app/services/idv/flows/doc_auth_flow.rb +++ b/app/services/idv/flows/doc_auth_flow.rb @@ -4,7 +4,6 @@ class DocAuthFlow < Flow::BaseFlow STEPS = { welcome: Idv::Steps::WelcomeStep, agreement: Idv::Steps::AgreementStep, - upload: Idv::Steps::UploadStep, }.freeze STEP_INDICATOR_STEPS = [ diff --git a/app/services/idv/steps/agreement_step.rb b/app/services/idv/steps/agreement_step.rb index 21942060dd9..0c223319459 100644 --- a/app/services/idv/steps/agreement_step.rb +++ b/app/services/idv/steps/agreement_step.rb @@ -12,8 +12,6 @@ def self.analytics_submitted_event end def call - return if !IdentityConfig.store.doc_auth_hybrid_handoff_controller_enabled - if flow_session[:skip_upload_step] redirect_to idv_document_capture_url flow_session[:flow_path] = 'standard' diff --git a/app/services/idv/steps/upload_step.rb b/app/services/idv/steps/upload_step.rb deleted file mode 100644 index 21dc0ff7a8f..00000000000 --- a/app/services/idv/steps/upload_step.rb +++ /dev/null @@ -1,191 +0,0 @@ -module Idv - module Steps - class UploadStep < DocAuthBaseStep - include ActionView::Helpers::DateHelper - STEP_INDICATOR_STEP = :verify_id - - def self.analytics_visited_event - :idv_doc_auth_upload_visited - end - - def self.analytics_submitted_event - :idv_doc_auth_upload_submitted - end - - def call - @flow.irs_attempts_api_tracker.idv_document_upload_method_selected( - upload_method: params[:type], - ) - - # See the simple_form_for in - # app/views/idv/doc_auth/upload.html.erb - if hybrid_flow_chosen? - handle_phone_submission - else - bypass_send_link_steps - end - end - - def hybrid_flow_chosen? - params[:type] != 'desktop' && !mobile_device? - end - - def extra_view_variables - { idv_phone_form: build_form } - end - - def link_for_send_link(session_uuid) - idv_hybrid_mobile_entry_url( - 'document-capture-session': session_uuid, - request_id: sp_session[:request_id], - ) - end - - private - - def build_form - Idv::PhoneForm.new( - previous_params: {}, - user: current_user, - delivery_methods: [:sms], - ) - end - - def form_submit - return super unless params[:type] == 'mobile' - - params = permit(:phone) - params[:otp_delivery_preference] = 'sms' - build_form.submit(params) - end - - def handle_phone_submission - throttle.increment! - return throttled_failure if throttle.throttled? - idv_session[:phone_for_mobile_flow] = permit(:phone)[:phone] - flow_session[:phone_for_mobile_flow] = idv_session[:phone_for_mobile_flow] - telephony_result = send_link - failure_reason = nil - if !telephony_result.success? - failure_reason = { telephony: [telephony_result.error.class.name.demodulize] } - end - @flow.irs_attempts_api_tracker.idv_phone_upload_link_sent( - success: telephony_result.success?, - phone_number: formatted_destination_phone, - failure_reason: failure_reason, - ) - - if !failure_reason - flow_session[:flow_path] = 'hybrid' - redirect_to idv_link_sent_url - end - - build_telephony_form_response(telephony_result) - end - - def identity - current_user&.identities&.order('created_at DESC')&.first - end - - def link - identity&.return_to_sp_url || root_url - end - - def application - identity&.friendly_name || APP_NAME - end - - def bypass_send_link_steps - flow_session[:flow_path] = 'standard' - redirect_to idv_document_capture_url - - form_response(destination: :document_capture) - end - - def throttle - @throttle ||= Throttle.new( - user: current_user, - throttle_type: :idv_send_link, - ) - end - - def throttled_failure - @flow.analytics.throttler_rate_limit_triggered( - throttle_type: :idv_send_link, - ) - message = I18n.t( - 'errors.doc_auth.send_link_throttle', - timeout: distance_of_time_in_words( - Time.zone.now, - [throttle.expires_at, Time.zone.now].compact.max, - except: :seconds, - ), - ) - - @flow.irs_attempts_api_tracker.idv_phone_send_link_rate_limited( - phone_number: formatted_destination_phone, - ) - - failure(message) - end - - def formatted_destination_phone - raw_phone = permit(:phone)[:phone] - PhoneFormatter.format(raw_phone, country_code: 'US') - end - - def update_document_capture_session_requested_at(session_uuid) - document_capture_session = DocumentCaptureSession.find_by(uuid: session_uuid) - return unless document_capture_session - document_capture_session.update!( - requested_at: Time.zone.now, - cancelled_at: nil, - issuer: sp_session[:issuer], - ) - end - - def sp_or_app_name - current_sp&.friendly_name.presence || APP_NAME - end - - def send_link - session_uuid = flow_session[:document_capture_session_uuid] - update_document_capture_session_requested_at(session_uuid) - Telephony.send_doc_auth_link( - to: formatted_destination_phone, - link: link_for_send_link(session_uuid), - country_code: Phonelib.parse(formatted_destination_phone).country, - sp_or_app_name: sp_or_app_name, - ) - end - - def build_telephony_form_response(telephony_result) - FormResponse.new( - success: telephony_result.success?, - errors: { message: telephony_result.error&.friendly_message }, - extra: { - telephony_response: telephony_result.to_h, - destination: :link_sent, - }, - ) - end - - def mobile_device? - # See app/javascript/packs/document-capture-welcome.js - # And app/services/idv/steps/agreement_step.rb - !!flow_session[:skip_upload_step] - end - - def form_response(destination:) - FormResponse.new( - success: true, - errors: {}, - extra: { - destination: destination, - skip_upload_step: mobile_device?, - }, - ) - end - end - end -end diff --git a/app/views/idv/doc_auth/document_capture.html.erb b/app/views/idv/doc_auth/document_capture.html.erb deleted file mode 100644 index 3fa7953f15a..00000000000 --- a/app/views/idv/doc_auth/document_capture.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<%= render( - 'idv/shared/document_capture', - document_capture_session_uuid: document_capture_session_uuid, - flow_path: 'standard', - sp_name: decorated_session.sp_name, - failure_to_proof_url: idv_doc_auth_return_to_sp_url, - 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/link_sent.html.erb b/app/views/idv/doc_auth/link_sent.html.erb deleted file mode 100644 index 64a557bacae..00000000000 --- a/app/views/idv/doc_auth/link_sent.html.erb +++ /dev/null @@ -1,51 +0,0 @@ -<% title t('titles.doc_auth.link_sent') %> - - -<% if @meta_refresh && !FeatureManagement.doc_capture_polling_enabled? %> - <%= content_for(:meta_refresh) { @meta_refresh.to_s } %> -<% end %> -<% if flow_session[:error_message] %> - <%= render AlertComponent.new( - type: :error, - class: 'margin-bottom-4', - message: flow_session[:error_message], - ) %> -<% end %> - -<%= render AlertComponent.new(type: :warning, class: 'margin-bottom-4') do %> - <%= t('doc_auth.info.keep_window_open') %> - <% if FeatureManagement.doc_capture_polling_enabled? %> - <%= t('doc_auth.info.link_sent_complete_polling') %> - <% else %> - <%= t('doc_auth.info.link_sent_complete_no_polling') %> - <% end %> -<% end %> -<%= render PageHeadingComponent.new.with_content(t('doc_auth.headings.text_message')) %> -
-
- <%= image_tag asset_url('idv/phone-icon.svg'), width: 88, height: 88, alt: t('image_description.camera_mobile_phone') %> -
-
-

- <%= t('doc_auth.info.you_entered') %> - <%= local_assigns[:phone] %> -

-

<%= t('doc_auth.info.link_sent') %>

-
-
- -
- <%= button_to( - url_for, - method: :put, - class: 'usa-button usa-button--big usa-button--wide', - form_class: 'link-sent-continue-button-form', - ) { t('forms.buttons.continue') } %> -
- -<% if FeatureManagement.doc_capture_polling_enabled? %> - <%= content_tag 'script', '', data: { status_endpoint: idv_capture_doc_status_url } %> - <%= javascript_packs_tag_once 'doc-capture-polling' %> -<% end %> - -<%= render 'idv/shared/back', action: 'cancel_link_sent', class: 'link-sent-back-link' %> diff --git a/app/views/idv/doc_auth/upload.html.erb b/app/views/idv/doc_auth/upload.html.erb deleted file mode 100644 index f65d741af23..00000000000 --- a/app/views/idv/doc_auth/upload.html.erb +++ /dev/null @@ -1,74 +0,0 @@ -<% title t('titles.doc_auth.upload') %> - -<%= render 'idv/doc_auth/error_messages', flow_session: flow_session %> - -<%= render PageHeadingComponent.new do %> - <%= t('doc_auth.headings.upload') %> -<% end %> - -

- <%= t('doc_auth.info.upload') %> -

- -
-
- <%= image_tag( - asset_url('idv/phone-icon.svg'), - alt: t('image_description.camera_mobile_phone'), - width: 88, - height: 88, - ) %> -
-
-
- <%= t('doc_auth.info.tag') %> -
-

- <%= t('doc_auth.headings.upload_from_phone') %> -

- <%= t('doc_auth.info.upload_from_phone') %> - <%= simple_form_for( - idv_phone_form, - as: :doc_auth, - url: url_for(type: :mobile), - method: 'PUT', - html: { autocomplete: 'off' }, - ) do |f| %> - <%= render PhoneInputComponent.new( - form: f, - required: true, - delivery_methods: [:sms], - class: 'margin-bottom-4', - ) %> - <%= f.submit t('forms.buttons.send_link') %> - <% end %> -
-
- -
-
-
- <%= image_tag( - asset_url('idv/laptop-icon.svg'), - alt: t('image_description.laptop'), - width: 88, - height: 88, - ) %> -
-
-

- <%= t('doc_auth.headings.upload_from_computer') %> -

- <%= t('doc_auth.info.upload_from_computer') %>  - <%= simple_form_for( - :doc_auth, - url: url_for(type: :desktop), - method: 'PUT', - class: 'margin-bottom-4', - ) do |f| %> - <%= f.submit t('forms.buttons.upload_photos'), outline: true %> - <% end %> -
-
- -<%= render 'idv/doc_auth/cancel', step: 'upload' %> diff --git a/config/application.yml.default b/config/application.yml.default index e53c475b156..115f01794eb 100644 --- a/config/application.yml.default +++ b/config/application.yml.default @@ -90,7 +90,6 @@ doc_auth_s3_request_timeout: 5 doc_auth_error_dpi_threshold: 290 doc_auth_error_glare_threshold: 40 doc_auth_error_sharpness_threshold: 40 -doc_auth_hybrid_handoff_controller_enabled: false doc_auth_max_attempts: 5 doc_auth_max_capture_attempts_before_tips: 3 doc_auth_max_capture_attempts_before_native_camera: 2 diff --git a/config/locales/errors/en.yml b/config/locales/errors/en.yml index 2d993ccfdce..c4006df0b08 100644 --- a/config/locales/errors/en.yml +++ b/config/locales/errors/en.yml @@ -52,7 +52,9 @@ en: inclusion: is not included in the list invalid_calling_area: Calls to that phone number are not supported. Please try SMS if you have an SMS-capable phone. - invalid_phone_number: Phone number is not valid + invalid_phone_number: + international: Enter a phone number with the correct number of digits. + us: Enter a 10 digit phone number. invalid_recaptcha_token: You must complete the spam prevention challenge. invalid_sms_number: The phone number entered doesn’t support text messaging. Try the Phone call option. diff --git a/config/locales/errors/es.yml b/config/locales/errors/es.yml index 74c565d0bd1..a5bfc7a2cbd 100644 --- a/config/locales/errors/es.yml +++ b/config/locales/errors/es.yml @@ -54,7 +54,9 @@ es: inclusion: No se incluye en la lista. invalid_calling_area: No se admiten llamadas a ese número de teléfono. Intenta enviar un SMS si tienes un teléfono que permita enviar SMS. - invalid_phone_number: Número de teléfono no válido + invalid_phone_number: + international: Ingrese un número de teléfono con el número correcto de dígitos. + us: Ingrese un número de teléfono de 10 dígitos. invalid_recaptcha_token: Debes superar el desafío de prevención de spam. invalid_sms_number: El número de teléfono ingresado no admite mensajes de texto. Pruebe la opción de llamada telefónica. diff --git a/config/locales/errors/fr.yml b/config/locales/errors/fr.yml index 5b5ab3a727b..4c02c972559 100644 --- a/config/locales/errors/fr.yml +++ b/config/locales/errors/fr.yml @@ -61,7 +61,9 @@ fr: invalid_calling_area: Les appels vers ce numéro de téléphone ne sont pas pris en charge. Veuillez essayer par SMS si vous possédez un téléphone disposant de cette fonction. - invalid_phone_number: Le numéro de téléphone n’est pas valide + invalid_phone_number: + international: Saisissez un numéro de téléphone avec le nombre correct de chiffres. + us: Entrez un numéro de téléphone à 10 chiffres. invalid_recaptcha_token: Vous devez relever le défi de la prévention du pourriel. invalid_sms_number: Le numéro de téléphone saisi ne prend pas en charge les messages textuels. Veuillez essayer l’option d’appel téléphonique. diff --git a/lib/identity_config.rb b/lib/identity_config.rb index ca7e8011ed9..f0271b2ffd3 100644 --- a/lib/identity_config.rb +++ b/lib/identity_config.rb @@ -160,7 +160,6 @@ def self.build_store(config_map) config.add(:doc_auth_error_glare_threshold, type: :integer) config.add(:doc_auth_error_sharpness_threshold, type: :integer) config.add(:doc_auth_extend_timeout_by_minutes, type: :integer) - config.add(:doc_auth_hybrid_handoff_controller_enabled, type: :boolean) config.add(:doc_auth_max_attempts, type: :integer) config.add(:doc_auth_max_capture_attempts_before_native_camera, type: :integer) config.add(:doc_auth_max_submission_attempts_before_native_camera, type: :integer) diff --git a/lib/pinpoint_supported_countries.rb b/lib/pinpoint_supported_countries.rb index 1859c8a49fb..b3452fcaee1 100644 --- a/lib/pinpoint_supported_countries.rb +++ b/lib/pinpoint_supported_countries.rb @@ -15,7 +15,6 @@ class PinpointSupportedCountries EG FR JO - MX PH TH ].to_set.freeze diff --git a/package.json b/package.json index 3d1ccabcd78..3651b3978d6 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@types/intl-tel-input": "^17.0.6", "@types/mocha": "^10.0.0", "@types/newrelic": "^7.0.3", + "@types/node": "^20.2.5", "@types/react": "^17.0.39", "@types/react-dom": "^17.0.11", "@types/sinon": "^10.0.13", @@ -87,7 +88,7 @@ "stylelint": "^15.5.0", "svgo": "^2.8.0", "swr": "^2.0.0", - "typescript": "^4.8.4", + "typescript": "^5.0.4", "webpack-dev-server": "^4.11.1", "whatwg-fetch": "^3.4.0" }, diff --git a/spec/controllers/api/internal/sessions_controller_spec.rb b/spec/controllers/api/internal/sessions_controller_spec.rb index 32252b94c6b..0f2eaa581d8 100644 --- a/spec/controllers/api/internal/sessions_controller_spec.rb +++ b/spec/controllers/api/internal/sessions_controller_spec.rb @@ -15,7 +15,7 @@ subject(:response) { JSON.parse(get(:show).body, symbolize_names: true) } it 'responds with live and timeout properties' do - expect(response).to eq(live: false, timeout: Time.zone.now.as_json) + expect(response).to eq(live: false, timeout: nil) end context 'signed in' do @@ -45,10 +45,7 @@ let(:delay) { User.timeout_in + 1.second } it 'responds with live and timeout properties' do - expect(response).to eq( - live: false, - timeout: (User.timeout_in - delay).from_now.as_json, - ) + expect(response).to eq(live: false, timeout: nil) end end end diff --git a/spec/controllers/idv/doc_auth_controller_spec.rb b/spec/controllers/idv/doc_auth_controller_spec.rb index df667ed9004..0d536c5cac0 100644 --- a/spec/controllers/idv/doc_auth_controller_spec.rb +++ b/spec/controllers/idv/doc_auth_controller_spec.rb @@ -138,7 +138,7 @@ it 'finishes the flow' do get :show, params: { step: 'welcome' } - expect(response).to redirect_to idv_link_sent_url + expect(response).to redirect_to idv_hybrid_handoff_url end end end @@ -202,7 +202,7 @@ it 'finishes the flow' do put :update, params: { step: 'ssn' } - expect(response).to redirect_to idv_link_sent_url + expect(response).to redirect_to idv_hybrid_handoff_url end end end diff --git a/spec/controllers/idv/document_capture_controller_spec.rb b/spec/controllers/idv/document_capture_controller_spec.rb index 1d7b7d5eebb..e2f67c0faf0 100644 --- a/spec/controllers/idv/document_capture_controller_spec.rb +++ b/spec/controllers/idv/document_capture_controller_spec.rb @@ -79,13 +79,13 @@ ) end - context 'upload step is not complete' do - it 'redirects to idv_doc_auth_url' do + context 'hybrid handoff step is not complete' do + it 'redirects to hybrid handoff' do flow_session.delete(:flow_path) get :show - expect(response).to redirect_to(idv_doc_auth_url) + expect(response).to redirect_to(idv_hybrid_handoff_url) end end diff --git a/spec/controllers/idv/hybrid_handoff_controller_spec.rb b/spec/controllers/idv/hybrid_handoff_controller_spec.rb index 19e8b525529..eae8825963f 100644 --- a/spec/controllers/idv/hybrid_handoff_controller_spec.rb +++ b/spec/controllers/idv/hybrid_handoff_controller_spec.rb @@ -6,8 +6,6 @@ let(:user) { create(:user) } before do - allow(IdentityConfig.store).to receive(:doc_auth_hybrid_handoff_controller_enabled). - and_return(true) stub_sign_in(user) stub_analytics stub_attempts_tracker diff --git a/spec/controllers/idv/link_sent_controller_spec.rb b/spec/controllers/idv/link_sent_controller_spec.rb index 944cd7d6405..78d80fe2cf5 100644 --- a/spec/controllers/idv/link_sent_controller_spec.rb +++ b/spec/controllers/idv/link_sent_controller_spec.rb @@ -6,8 +6,7 @@ let(:flow_session) do { 'document_capture_session_uuid' => 'fd14e181-6fb1-4cdc-92e0-ef66dad0df4e', :threatmetrix_session_id => 'c90ae7a5-6629-4e77-b97c-f1987c2df7d0', - :flow_path => 'hybrid', - :phone_for_mobile_flow => '201-555-1212' } + :flow_path => 'hybrid' } end let(:user) { create(:user) } @@ -69,12 +68,12 @@ context '#confirm_upload_step_complete' do context 'no flow_path' do - it 'redirects to idv_doc_auth_url' do + it 'redirects to idv_hybrid_handoff_url' do flow_session[:flow_path] = nil get :show - expect(response).to redirect_to(idv_doc_auth_url) + expect(response).to redirect_to(idv_hybrid_handoff_url) end end @@ -157,23 +156,11 @@ ) end - it 'redirects to doc_auth page' do + it 'redirects to hybrid_handoff page' do put :update - expect(response).to redirect_to(idv_doc_auth_url) + expect(response).to redirect_to(idv_hybrid_handoff_url) expect(flow_session[:error_message]).to eq(error_message) - expect(flow_session['Idv::Steps::UploadStep']).to be_nil - end - - context 'doc_auth_hybrid_handoff_controller_enabled is true' do - it 'redirects to hybrid_handoff page' do - allow(IdentityConfig.store).to receive(:doc_auth_hybrid_handoff_controller_enabled). - and_return(true) - - put :update - - expect(response).to redirect_to(idv_hybrid_handoff_url) - end end end diff --git a/spec/controllers/idv/ssn_controller_spec.rb b/spec/controllers/idv/ssn_controller_spec.rb index 1ac5cfd11fb..5773c0d3b69 100644 --- a/spec/controllers/idv/ssn_controller_spec.rb +++ b/spec/controllers/idv/ssn_controller_spec.rb @@ -79,10 +79,10 @@ context 'without a flow session' do let(:flow_session) { nil } - it 'redirects to doc_auth' do + it 'redirects to hybrid_handoff' do get :show - expect(response).to redirect_to(idv_doc_auth_url) + expect(response).to redirect_to(idv_hybrid_handoff_url) end end @@ -244,11 +244,11 @@ expect(response).to redirect_to idv_link_sent_url end - it 'redirects to FSM UploadStep if there is no flow_path' do + it 'redirects to hybrid_handoff if there is no flow_path' do flow_session[:flow_path] = nil put :update expect(response.status).to eq 302 - expect(response).to redirect_to idv_doc_auth_url + expect(response).to redirect_to idv_hybrid_handoff_url end end end diff --git a/spec/features/idv/actions/cancel_link_sent_action_spec.rb b/spec/features/idv/actions/cancel_link_sent_action_spec.rb index 2cf54a9f177..dbeae7f3cb6 100644 --- a/spec/features/idv/actions/cancel_link_sent_action_spec.rb +++ b/spec/features/idv/actions/cancel_link_sent_action_spec.rb @@ -12,6 +12,6 @@ it 'returns to link sent step' do click_doc_auth_back_link - expect(page).to have_current_path(idv_doc_auth_upload_step) + expect(page).to have_current_path(idv_hybrid_handoff_path) end end diff --git a/spec/features/idv/actions/redo_document_capture_action_spec.rb b/spec/features/idv/actions/redo_document_capture_action_spec.rb index 77632d1602e..8b829a5d7ad 100644 --- a/spec/features/idv/actions/redo_document_capture_action_spec.rb +++ b/spec/features/idv/actions/redo_document_capture_action_spec.rb @@ -27,7 +27,7 @@ ) click_link warning_link_text - expect(current_path).to eq(idv_doc_auth_upload_step) + expect(current_path).to eq(idv_hybrid_handoff_path) complete_upload_step DocAuth::Mock::DocAuthMockClient.reset! attach_and_submit_images diff --git a/spec/features/idv/analytics_spec.rb b/spec/features/idv/analytics_spec.rb index def0bbac6f0..7eaa143066e 100644 --- a/spec/features/idv/analytics_spec.rb +++ b/spec/features/idv/analytics_spec.rb @@ -16,8 +16,8 @@ 'IdV: doc auth agreement visited' => { flow_path: 'standard', step: 'agreement', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: consent checkbox toggled' => { checked: true }, 'IdV: doc auth agreement submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'agreement', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth upload visited' => { flow_path: 'standard', step: 'upload', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth upload submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'upload', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false, skip_upload_step: false }, + 'IdV: doc auth upload visited' => { step: 'upload', acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth upload submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'upload', acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false, skip_upload_step: false }, 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything }, 'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything }, @@ -51,8 +51,8 @@ 'IdV: doc auth welcome submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'welcome', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth agreement visited' => { flow_path: 'standard', step: 'agreement', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth agreement submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'agreement', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth upload visited' => { flow_path: 'standard', step: 'upload', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth upload submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'upload', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false, skip_upload_step: false }, + 'IdV: doc auth upload visited' => { step: 'upload', acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth upload submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'upload', acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false, skip_upload_step: false }, 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything }, 'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything }, @@ -79,8 +79,8 @@ 'IdV: doc auth welcome submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'welcome', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth agreement visited' => { flow_path: 'standard', step: 'agreement', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'IdV: doc auth agreement submitted' => { success: true, errors: {}, flow_path: 'standard', step: 'agreement', step_count: 1, analytics_id: 'Doc Auth', irs_reproofing: false, acuant_sdk_upgrade_ab_test_bucket: :default }, - 'IdV: doc auth upload visited' => { flow_path: 'standard', step: 'upload', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, - 'IdV: doc auth upload submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'upload', step_count: 1, acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false, skip_upload_step: false }, + 'IdV: doc auth upload visited' => { step: 'upload', acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, + 'IdV: doc auth upload submitted' => { success: true, errors: {}, destination: :document_capture, flow_path: 'standard', step: 'upload', acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false, skip_upload_step: false }, 'IdV: doc auth document_capture visited' => { flow_path: 'standard', step: 'document_capture', acuant_sdk_upgrade_ab_test_bucket: :default, analytics_id: 'Doc Auth', irs_reproofing: false }, 'Frontend: IdV: front image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything }, 'Frontend: IdV: back image added' => { 'width' => 284, 'height' => 38, 'mimeType' => 'image/png', 'source' => 'upload', 'size' => 3694, 'attempt' => 1, 'flow_path' => 'standard', 'acuant_sdk_upgrade_a_b_testing_enabled' => 'false', 'use_alternate_sdk' => anything, 'acuant_version' => anything }, diff --git a/spec/features/idv/doc_auth/agreement_step_spec.rb b/spec/features/idv/doc_auth/agreement_step_spec.rb index c9c86a488b4..f73327bea8d 100644 --- a/spec/features/idv/doc_auth/agreement_step_spec.rb +++ b/spec/features/idv/doc_auth/agreement_step_spec.rb @@ -1,12 +1,8 @@ require 'rails_helper' -feature 'doc auth welcome step' do +feature 'doc auth agreement step' do include DocAuthHelper - def expect_doc_auth_upload_step - expect(page).to have_current_path(idv_doc_auth_upload_step) - end - def expect_doc_auth_first_step expect(page).to have_current_path(idv_doc_auth_agreement_step) end @@ -28,7 +24,7 @@ def expect_doc_auth_first_step check t('doc_auth.instructions.consent', app_name: APP_NAME) click_continue - expect_doc_auth_upload_step + expect(page).to have_current_path(idv_hybrid_handoff_path) end end @@ -49,7 +45,7 @@ def expect_doc_auth_first_step check t('doc_auth.instructions.consent', app_name: APP_NAME) click_continue - expect_doc_auth_upload_step + expect(page).to have_current_path(idv_hybrid_handoff_path) end end @@ -68,41 +64,6 @@ def expect_doc_auth_first_step it 'progresses to document capture' do expect(page).to have_current_path(idv_document_capture_url) end - - it 'logs analytics for upload step' do - log = DocAuthLog.last - expect(log.upload_view_count).to eq 1 - expect(log.upload_view_at).not_to be_nil - - expect(fake_analytics).to have_logged_event( - 'IdV: doc auth upload visited', - analytics_id: 'Doc Auth', - flow_path: 'standard', - step: 'upload', step_count: 1, - irs_reproofing: false, - acuant_sdk_upgrade_ab_test_bucket: :default - ) - expect(fake_analytics).to have_logged_event( - 'IdV: doc auth upload submitted', - hash_including(step: 'upload', step_count: 2, success: true), - ) - end - end - - context 'doc_auth_hybrid_handoff_controller_enabled flag is true' do - context 'skipping upload step', :js, driver: :headless_chrome_mobile do - before do - allow(IdentityConfig.store).to receive(:doc_auth_hybrid_handoff_controller_enabled). - and_return(true) - sign_in_and_2fa_user - complete_doc_auth_steps_before_agreement_step - complete_agreement_step - end - - it 'progresses to document capture' do - expect(page).to have_current_path(idv_document_capture_url) - end - end end context 'during the acuant maintenance window' do diff --git a/spec/features/idv/doc_auth/document_capture_spec.rb b/spec/features/idv/doc_auth/document_capture_spec.rb index 87c3d502eea..c8eee9ed589 100644 --- a/spec/features/idv/doc_auth/document_capture_spec.rb +++ b/spec/features/idv/doc_auth/document_capture_spec.rb @@ -26,7 +26,7 @@ expect(page).to have_current_path(idv_doc_auth_agreement_step) complete_agreement_step visit(idv_document_capture_url) - expect(page).to have_current_path(idv_doc_auth_upload_step) + expect(page).to have_current_path(idv_hybrid_handoff_path) end context 'standard desktop flow' do @@ -52,7 +52,7 @@ # it redirects here if trying to move earlier in the flow visit(idv_doc_auth_agreement_step) expect(page).to have_current_path(idv_document_capture_path) - visit(idv_doc_auth_upload_step) + visit(idv_hybrid_handoff_url) expect(page).to have_current_path(idv_document_capture_path) end diff --git a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb index 92583e99ed4..30c8cdc8f90 100644 --- a/spec/features/idv/doc_auth/hybrid_handoff_spec.rb +++ b/spec/features/idv/doc_auth/hybrid_handoff_spec.rb @@ -7,7 +7,6 @@ let(:fake_analytics) { FakeAnalytics.new } let(:fake_attempts_tracker) { IrsAttemptsApiTrackingHelper::FakeAttemptsTracker.new } - let(:new_controller_enabled) { true } let(:document_capture_session) { DocumentCaptureSession.create! } let(:idv_send_link_max_attempts) { 3 } let(:idv_send_link_attempt_window_in_minutes) do @@ -16,8 +15,6 @@ before do sign_in_and_2fa_user - allow(IdentityConfig.store).to receive(:doc_auth_hybrid_handoff_controller_enabled). - and_return(new_controller_enabled) allow_any_instance_of(Idv::HybridHandoffController).to receive(:mobile_device?).and_return(true) allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) allow_any_instance_of(ApplicationController).to receive(:irs_attempts_api_tracker). diff --git a/spec/features/idv/doc_auth/upload_step_spec.rb b/spec/features/idv/doc_auth/upload_step_spec.rb deleted file mode 100644 index 41f316f58fa..00000000000 --- a/spec/features/idv/doc_auth/upload_step_spec.rb +++ /dev/null @@ -1,231 +0,0 @@ -require 'rails_helper' - -feature 'doc auth upload step' do - include IdvStepHelper - include DocAuthHelper - include ActionView::Helpers::DateHelper - - let(:fake_analytics) { FakeAnalytics.new } - let(:fake_attempts_tracker) { IrsAttemptsApiTrackingHelper::FakeAttemptsTracker.new } - let(:document_capture_session) { DocumentCaptureSession.create! } - let(:idv_send_link_max_attempts) { 3 } - let(:idv_send_link_attempt_window_in_minutes) do - IdentityConfig.store.idv_send_link_attempt_window_in_minutes - end - - before do - sign_in_and_2fa_user - allow_any_instance_of(Idv::Steps::UploadStep).to receive(:mobile_device?).and_return(true) - complete_doc_auth_steps_before_upload_step - allow_any_instance_of(ApplicationController).to receive(:analytics).and_return(fake_analytics) - allow_any_instance_of(ApplicationController).to receive(:irs_attempts_api_tracker). - and_return(fake_attempts_tracker) - end - - context 'on a desktop device', js: true do - before do - allow_any_instance_of(Idv::Steps::UploadStep).to receive(:mobile_device?).and_return(false) - end - - it 'displays with the expected content' do - expect(page).to have_content(t('doc_auth.headings.upload_from_computer')) - expect(page).to have_content(t('doc_auth.info.upload_from_computer')) - expect(page).to have_content(t('doc_auth.headings.upload_from_phone')) - end - - it 'proceeds to document capture when user chooses to upload from computer' do - expect(fake_attempts_tracker).to receive( - :idv_document_upload_method_selected, - ).with({ upload_method: 'desktop' }) - - expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) - - click_upload_from_computer - - expect(page).to have_current_path(idv_document_capture_url) - expect(fake_analytics).to have_logged_event( - 'IdV: doc auth upload submitted', - hash_including(step: 'upload', destination: :document_capture), - ) - end - - it "defaults phone to user's 2fa phone number" do - field = page.find_field(t('two_factor_authentication.phone_label')) - expect(field.value).to eq('(202) 555-1212') - end - - it 'proceeds to link sent page when user chooses to use phone' do - expect(fake_attempts_tracker).to receive( - :idv_document_upload_method_selected, - ).with({ upload_method: 'mobile' }) - - click_send_link - - expect(page).to have_current_path(idv_link_sent_path) - expect(fake_analytics).to have_logged_event( - 'IdV: doc auth upload submitted', - hash_including(step: 'upload', destination: :link_sent), - ) - end - - it 'proceeds to the next page with valid info' do - expect(fake_attempts_tracker).to receive( - :idv_phone_upload_link_sent, - ).with( - success: true, - phone_number: '+1 415-555-0199', - failure_reason: nil, - ) - - expect(Telephony).to receive(:send_doc_auth_link). - with(hash_including(to: '+1 415-555-0199')). - and_call_original - - expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) - - fill_in :doc_auth_phone, with: '415-555-0199' - click_send_link - - expect(page).to have_current_path(idv_link_sent_path) - end - - it 'does not proceed to the next page with invalid info' do - fill_in :doc_auth_phone, with: '' - click_send_link - - expect(page).to have_current_path(idv_doc_auth_upload_step, ignore_query: true) - end - - it 'sends a link that does not contain any underscores' do - # because URLs with underscores sometimes get messed up by carriers - expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| - expect(config[:link]).to_not include('_') - - impl.call(**config) - end - - fill_in :doc_auth_phone, with: '415-555-0199' - click_send_link - - expect(page).to have_current_path(idv_link_sent_path) - end - - it 'does not proceed if Telephony raises an error' do - expect(fake_attempts_tracker).to receive(:idv_phone_upload_link_sent).with( - success: false, - phone_number: '+1 225-555-1000', - failure_reason: { telephony: ['TelephonyError'] }, - ) - fill_in :doc_auth_phone, with: '225-555-1000' - click_send_link - - expect(page).to have_current_path(idv_doc_auth_upload_step, ignore_query: true) - expect(page).to have_content I18n.t('telephony.error.friendly_message.generic') - end - - it 'displays error if user selects a country to which we cannot send SMS', js: true do - page.find('div[aria-label="Country code"]').click - within(page.find('.iti__flag-container', visible: :all)) do - find('span', text: 'Sri Lanka').click - end - focused_input = page.find('.phone-input__number:focus') - - error_message_id = focused_input[:'aria-describedby']&.split(' ')&.find do |id| - page.has_css?(".usa-error-message##{id}") - end - expect(error_message_id).to_not be_empty - - error_message = page.find_by_id(error_message_id) - expect(error_message).to have_content( - t( - 'two_factor_authentication.otp_delivery_preference.sms_unsupported', - location: 'Sri Lanka', - ), - ) - click_send_link - expect(page.find(':focus')).to match_css('.phone-input__number') - end - - it 'throttles sending the link' do - user = user_with_2fa - sign_in_and_2fa_user(user) - complete_doc_auth_steps_before_upload_step - timeout = distance_of_time_in_words( - Throttle.attempt_window_in_minutes(:idv_send_link).minutes, - ) - allow(IdentityConfig.store).to receive(:idv_send_link_max_attempts). - and_return(idv_send_link_max_attempts) - - expect(fake_attempts_tracker).to receive( - :idv_phone_send_link_rate_limited, - ).with({ phone_number: '+1 415-555-0199' }) - - freeze_time do - (idv_send_link_max_attempts - 1).times do - expect(page).to_not have_content( - I18n.t('errors.doc_auth.send_link_throttle', timeout: timeout), - ) - - fill_in :doc_auth_phone, with: '415-555-0199' - click_send_link - - expect(page).to have_current_path(idv_link_sent_path) - - click_doc_auth_back_link - end - - fill_in :doc_auth_phone, with: '415-555-0199' - - click_send_link - expect(page).to have_current_path(idv_doc_auth_upload_step, ignore_query: true) - expect(page).to have_content( - I18n.t( - 'errors.doc_auth.send_link_throttle', - timeout: timeout, - ), - ) - end - expect(fake_analytics).to have_logged_event( - 'Throttler Rate Limit Triggered', - throttle_type: :idv_send_link, - ) - - # Manual expiration is needed for now since the Throttle uses - # Redis ttl instead of expiretime - Throttle.new(throttle_type: :idv_send_link, user: user).reset! - travel_to(Time.zone.now + idv_send_link_attempt_window_in_minutes.minutes) do - fill_in :doc_auth_phone, with: '415-555-0199' - click_send_link - expect(page).to have_current_path(idv_link_sent_path) - end - end - - it 'includes expected URL parameters' do - allow_any_instance_of(Flow::BaseFlow).to receive(:flow_session).and_return( - document_capture_session_uuid: document_capture_session.uuid, - ) - - expect(Telephony).to receive(:send_doc_auth_link).and_wrap_original do |impl, config| - params = Rack::Utils.parse_nested_query URI(config[:link]).query - expect(params).to eq('document-capture-session' => document_capture_session.uuid) - - impl.call(**config) - end - - fill_in :doc_auth_phone, with: '415-555-0199' - click_send_link - end - - it 'sets requested_at on the capture session' do - allow_any_instance_of(Flow::BaseFlow).to receive(:flow_session).and_return( - document_capture_session_uuid: document_capture_session.uuid, - ) - - fill_in :doc_auth_phone, with: '415-555-0199' - click_send_link - - document_capture_session.reload - expect(document_capture_session).to have_attributes(requested_at: a_kind_of(Time)) - end - end -end diff --git a/spec/features/idv/doc_auth/welcome_step_spec.rb b/spec/features/idv/doc_auth/welcome_step_spec.rb index 8ea2be143d1..3c3d4910213 100644 --- a/spec/features/idv/doc_auth/welcome_step_spec.rb +++ b/spec/features/idv/doc_auth/welcome_step_spec.rb @@ -4,10 +4,6 @@ include IdvHelper include DocAuthHelper - def expect_doc_auth_upload_step - expect(page).to have_current_path(idv_doc_auth_upload_step) - end - let(:fake_analytics) { FakeAnalytics.new } let(:maintenance_window) { [] } let(:sp_name) { 'Test SP' } diff --git a/spec/features/idv/outage_spec.rb b/spec/features/idv/outage_spec.rb index 38e5b9b5478..5a5e128543b 100644 --- a/spec/features/idv/outage_spec.rb +++ b/spec/features/idv/outage_spec.rb @@ -105,7 +105,7 @@ def sign_in_with_idv_required(user:, sms_or_totp: :sms) complete_agreement_step # Still offer the option for hybrid flow - expect(current_path).to eq idv_doc_auth_step_path(step: :upload) + expect(current_path).to eq idv_hybrid_handoff_path complete_upload_step complete_document_capture_step @@ -214,7 +214,7 @@ def sign_in_with_idv_required(user:, sms_or_totp: :sms) click_idv_continue complete_agreement_step - expect(current_path).to eq idv_doc_auth_step_path(step: :upload) + expect(current_path).to eq idv_hybrid_handoff_path end end diff --git a/spec/features/idv/steps/phone_step_spec.rb b/spec/features/idv/steps/phone_step_spec.rb index 9078db80975..a7df7a5d88e 100644 --- a/spec/features/idv/steps/phone_step_spec.rb +++ b/spec/features/idv/steps/phone_step_spec.rb @@ -49,7 +49,7 @@ fill_in :idv_phone_form_phone, with: '578190' click_idv_send_security_code expect(page).to have_current_path(idv_phone_path) - expect(page).to have_content(t('errors.messages.invalid_phone_number')) + expect(page).to have_content(t('errors.messages.invalid_phone_number.us')) end end diff --git a/spec/features/phone/add_phone_spec.rb b/spec/features/phone/add_phone_spec.rb index c68c3e12033..d450db03a8e 100644 --- a/spec/features/phone/add_phone_spec.rb +++ b/spec/features/phone/add_phone_spec.rb @@ -77,7 +77,7 @@ expect(error_message_id).to_not be_empty error_message = page.find_by_id(error_message_id) - expect(error_message).to have_content(t('errors.messages.invalid_phone_number')) + expect(error_message).to have_content(t('errors.messages.invalid_phone_number.us')) # Unsupported country should prompt as invalid and hide delivery options immediately page.find('div[aria-label="Country code"]').click @@ -115,7 +115,7 @@ expect(hidden_select.value).to eq('US') click_continue expect(page.find(':focus')).to match_css('.phone-input__number') - expect(page).to have_content(t('errors.messages.invalid_phone_number')) + expect(page).to have_content(t('errors.messages.invalid_phone_number.us')) # Entering valid number should allow submission input = fill_in :new_phone_form_phone, with: '+81543543643' diff --git a/spec/features/saml/ial1_sso_spec.rb b/spec/features/saml/ial1_sso_spec.rb index 92a689b415e..b5d4919b669 100644 --- a/spec/features/saml/ial1_sso_spec.rb +++ b/spec/features/saml/ial1_sso_spec.rb @@ -70,22 +70,34 @@ ) end - it 'after session timeout, signing in takes user back to SP' do + it 'after session timeout, signing in takes user back to SP', :js, allow_browser_log: true do + allow(IdentityConfig.store).to receive(:session_check_delay).and_return(0) + user = create(:user, :fully_registered) request_url = saml_authn_request_url visit request_url sp_request_id = ServiceProviderRequestProxy.last.uuid + fill_in_credentials_and_submit(user.email, user.password) + + Warden.on_next_request do |proxy| + proxy.env['devise.skip_trackable'] = true + session = proxy.env['rack.session'] + session['session_expires_at'] = Time.zone.now - 1 + end - visit timeout_path - expect(current_url).to eq root_url(request_id: sp_request_id) + expect(page).to have_current_path(new_user_session_path(request_id: sp_request_id), wait: 5) + allow(IdentityConfig.store).to receive(:session_check_delay).and_call_original fill_in_credentials_and_submit(user.email, user.password) fill_in_code_with_last_phone_otp - click_submit_default_twice + click_submit_default + + # SAML does internal redirect using JavaScript prior to showing consent screen + expect(page).to have_current_path(sign_up_completed_path, wait: 5) click_agree_and_continue - expect(current_url).to eq complete_saml_url + expect(page).to have_current_path(test_saml_decode_assertion_path) end end diff --git a/spec/features/two_factor_authentication/sign_in_spec.rb b/spec/features/two_factor_authentication/sign_in_spec.rb index 49b4aaa76f0..d8526e7dc10 100644 --- a/spec/features/two_factor_authentication/sign_in_spec.rb +++ b/spec/features/two_factor_authentication/sign_in_spec.rb @@ -75,7 +75,7 @@ click_send_one_time_code expect(page.find(':focus')).to match_css('.phone-input__number') - expect(page).to have_content(t('errors.messages.invalid_phone_number')) + expect(page).to have_content(t('errors.messages.invalid_phone_number.international')) fill_in 'new_phone_form_phone', with: '' @@ -87,7 +87,7 @@ click_send_one_time_code expect(page.find(':focus')).to match_css('.phone-input__number') - expect(page).to have_content(t('errors.messages.invalid_phone_number')) + expect(page).to have_content(t('errors.messages.invalid_phone_number.international')) expect(page.find('#new_phone_form_international_code', visible: false).value).to eq 'IE' fill_in 'new_phone_form_phone', with: '' @@ -99,7 +99,7 @@ click_send_one_time_code expect(page.find(':focus')).to match_css('.phone-input__number') - expect(page).to have_content(t('errors.messages.invalid_phone_number')) + expect(page).to have_content(t('errors.messages.invalid_phone_number.international')) expect(page.find('#new_phone_form_international_code', visible: false).value).to eq 'JP' end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb index 17af2a7c199..7d87abdaa2d 100644 --- a/spec/models/profile_spec.rb +++ b/spec/models/profile_spec.rb @@ -332,6 +332,24 @@ expect(profile.active).to eq true expect(profile.deactivation_reason).to eq nil + expect(profile.verified_at).to eq nil + end + + it 'activates a previously verified profile after password reset' do + verified_at = Time.zone.now - 1.year + profile = create( + :profile, + user: user, + active: false, + deactivation_reason: :password_reset, + verified_at: verified_at, + ) + + profile.activate_after_password_reset + + expect(profile.active).to eq true + expect(profile.deactivation_reason).to eq nil + expect(profile.verified_at).to eq verified_at end it 'does not activate a profile if it has a pending reason' do diff --git a/spec/services/idv/steps/upload_step_spec.rb b/spec/services/idv/steps/upload_step_spec.rb deleted file mode 100644 index 34556385cf1..00000000000 --- a/spec/services/idv/steps/upload_step_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -require 'rails_helper' - -describe Idv::Steps::UploadStep do - context 'with combined hybrid handoff enabled' do - let(:user) { build(:user) } - - let(:service_provider) do - create( - :service_provider, - issuer: 'http://sp.example.com', - app_id: '123', - ) - end - - let(:request) do - double( - 'request', - remote_ip: Faker::Internet.ip_v4_address, - headers: { 'X-Amzn-Trace-Id' => amzn_trace_id }, - ) - end - - let(:params) do - ActionController::Parameters.new( - { - doc_auth: { phone: '(201) 555-1212' }, - }, - ) - end - - let(:controller) do - instance_double( - 'controller', - session: { sp: { issuer: service_provider.issuer } }, - params: params, - current_user: user, - current_sp: service_provider, - analytics: FakeAnalytics.new, - url_options: {}, - request: request, - ) - end - - let(:amzn_trace_id) { SecureRandom.uuid } - - let(:pii_from_doc) do - { - ssn: '123-45-6789', - first_name: 'bob', - } - end - - let(:flow) do - Idv::Flows::DocAuthFlow.new(controller, {}, 'idv/doc_auth').tap do |flow| - flow.flow_session = { pii_from_doc: pii_from_doc } - end - end - - let(:irs_attempts_api_tracker) do - IrsAttemptsApiTrackingHelper::FakeAttemptsTracker.new - end - - subject(:step) do - Idv::Steps::UploadStep.new(flow) - end - - before do - allow(controller).to receive(:irs_attempts_api_tracker). - and_return(irs_attempts_api_tracker) - end - - describe '#extra_view_variables' do - it 'includes form' do - expect(step.extra_view_variables).to include( - { - idv_phone_form: be_an_instance_of(Idv::PhoneForm), - }, - ) - end - end - - describe 'the return value from #call' do - let(:response) { step.call } - - it 'includes the telephony response' do - expect(response.extra[:telephony_response]).to eq( - { - errors: {}, - message_id: 'fake-message-id', - request_id: 'fake-message-request-id', - success: true, - }, - ) - end - end - - describe '#link_for_send_link' do - subject(:link) { step.link_for_send_link document_capture_session_uuid } - let(:document_capture_session_uuid) { SecureRandom.uuid } - - it 'generates a link to the hybrid mobile doc auth entry controller' do - expect(link).to eql( - Rails.application.routes.url_helpers.idv_hybrid_mobile_entry_url( - params: { - 'document-capture-session': document_capture_session_uuid, - }, - ), - ) - end - end - end -end diff --git a/spec/support/features/doc_auth_helper.rb b/spec/support/features/doc_auth_helper.rb index 1d54ffe2caa..5c1c4cad2df 100644 --- a/spec/support/features/doc_auth_helper.rb +++ b/spec/support/features/doc_auth_helper.rb @@ -61,10 +61,6 @@ def idv_doc_auth_agreement_step idv_doc_auth_step_path(step: :agreement) end - def idv_doc_auth_upload_step - idv_doc_auth_step_path(step: :upload) - end - def complete_doc_auth_steps_before_welcome_step(expect_accessible: false) visit idv_doc_auth_welcome_step unless current_path == idv_doc_auth_welcome_step click_idv_continue if current_path == idv_mail_only_warning_path diff --git a/yarn.lock b/yarn.lock index d6f693931cc..7d9ba847366 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1483,6 +1483,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.6.tgz#0ba49ac517ad69abe7a1508bc9b3a5483df9d5d7" integrity sha512-/xUq6H2aQm261exT6iZTMifUySEt4GR5KX8eYyY+C4MSNPqSh9oNIP7tz2GLKTlFaiBbgZNxffoR3CVRG+cljw== +"@types/node@^20.2.5": + version "20.2.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.5.tgz#26d295f3570323b2837d322180dfbf1ba156fefb" + integrity sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ== + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -6632,10 +6637,10 @@ typed-array-length@^1.0.4: for-each "^0.3.3" is-typed-array "^1.1.9" -typescript@^4.8.4: - version "4.8.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" - integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== +typescript@^5.0.4: + version "5.0.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b" + integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw== unbox-primitive@^1.0.2: version "1.0.2"