diff --git a/app/javascript/packages/device/index.js b/app/javascript/packages/device/index.js index 53de2ecd1ac..bdd835e797e 100644 --- a/app/javascript/packages/device/index.js +++ b/app/javascript/packages/device/index.js @@ -1,3 +1,16 @@ +/** + * Returns true if the device is an iPad, or false otherwise. + * + * iPadOS devices no longer list the correct user agent. As a proxy, we check for the incorrect + * one (Macintosh) then test the number of touchpoints, which for iPads will be 5. + * + * @return {boolean} + */ +export function isIPad() { + const { userAgent, maxTouchPoints } = window.navigator; + return /ipad/i.test(userAgent) || (/macintosh/i.test(userAgent) && maxTouchPoints === 5); +} + /** * Returns true if the device is likely a mobile device, or false otherwise. This is a rough * approximation, using device user agent sniffing. @@ -5,7 +18,7 @@ * @return {boolean} */ export function isLikelyMobile() { - return /ip(hone|ad|od)|android/i.test(window.navigator.userAgent); + return isIPad() || /iphone|android/i.test(window.navigator.userAgent); } /** diff --git a/app/services/idv/steps/upload_step.rb b/app/services/idv/steps/upload_step.rb index b473984701d..9caac138b77 100644 --- a/app/services/idv/steps/upload_step.rb +++ b/app/services/idv/steps/upload_step.rb @@ -6,6 +6,8 @@ class UploadStep < DocAuthBaseStep def call @flow.irs_attempts_api_tracker.document_upload_method_selected(upload_method: params[:type]) + # See the simple_form_for in + # app/views/idv/doc_auth/upload.html.erb if params[:type] == 'desktop' handle_desktop_selection else @@ -65,7 +67,9 @@ def bypass_send_link_steps end def mobile_device? - BrowserCache.parse(request.user_agent).mobile? + # 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:) diff --git a/spec/features/idv/doc_auth/email_sent_step_spec.rb b/spec/features/idv/doc_auth/email_sent_step_spec.rb index 8ebba47251d..99b07e8231a 100644 --- a/spec/features/idv/doc_auth/email_sent_step_spec.rb +++ b/spec/features/idv/doc_auth/email_sent_step_spec.rb @@ -5,6 +5,7 @@ include DocAuthHelper before do + allow_any_instance_of(Idv::Steps::UploadStep).to receive(:mobile_device?).and_return(true) sign_in_and_2fa_user complete_doc_auth_steps_before_email_sent_step end diff --git a/spec/features/idv/doc_auth/upload_step_spec.rb b/spec/features/idv/doc_auth/upload_step_spec.rb index 05b9676ef7d..bf4c4e50b9f 100644 --- a/spec/features/idv/doc_auth/upload_step_spec.rb +++ b/spec/features/idv/doc_auth/upload_step_spec.rb @@ -9,6 +9,7 @@ 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). @@ -55,6 +56,10 @@ end context 'on a desktop device' do + before do + allow_any_instance_of(Idv::Steps::UploadStep).to receive(:mobile_device?).and_return(false) + end + it 'is on the correct page' do expect(page).to have_current_path(idv_doc_auth_upload_step) expect_step_indicator_current_step(t('step_indicator.flows.idv.verify_id')) diff --git a/spec/javascripts/packages/device/index-spec.js b/spec/javascripts/packages/device/index-spec.js index ff68f8f8346..7df25cf03dc 100644 --- a/spec/javascripts/packages/device/index-spec.js +++ b/spec/javascripts/packages/device/index-spec.js @@ -1,26 +1,101 @@ -import { isLikelyMobile, hasMediaAccess, isCameraCapableMobile } from '@18f/identity-device'; +import { + isLikelyMobile, + hasMediaAccess, + isCameraCapableMobile, + isIPad, +} from '@18f/identity-device'; + +describe('isIPad', () => { + let originalUserAgent; + let originalTouchPoints; + + beforeEach(() => { + originalUserAgent = navigator.userAgent; + originalTouchPoints = navigator.maxTouchPoints; + navigator.maxTouchPoints = 0; + Object.defineProperty(navigator, 'userAgent', { + configurable: true, + writable: true, + }); + Object.defineProperty(navigator, 'maxTouchPoints', { + writable: true, + }); + }); + + afterEach(() => { + navigator.userAgent = originalUserAgent; + navigator.maxTouchPoints = originalTouchPoints; + }); + + it('returns true if ipad is in the user agent string (old format)', () => { + navigator.userAgent = + 'Mozilla/5.0(iPad; U; CPU iPhone OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 (KHTML, like Gecko) Version/4.0.4 Mobile/7B314 Safari/531.21.10'; + + expect(isIPad()).to.be.true(); + }); + + it('returns false if the user agent is Macintosh but with 0 maxTouchPoints', () => { + navigator.userAgent = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36'; + + expect(isIPad()).to.be.false(); + }); + + it('returns true if the user agent is Macintosh but with 5 maxTouchPoints', () => { + navigator.userAgent = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36'; + navigator.maxTouchPoints = 5; + + expect(isIPad()).to.be.true(); + }); + + it('returns false for non-Apple userAgent, even with 5 macTouchPoints', () => { + navigator.userAgent = + 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.58 Mobile Safari/537.36'; + navigator.maxTouchPoints = 5; + + expect(isIPad()).to.be.false(); + }); +}); describe('isLikelyMobile', () => { let originalUserAgent; + let originalTouchPoints; + beforeEach(() => { originalUserAgent = navigator.userAgent; + originalTouchPoints = navigator.maxTouchPoints; + navigator.maxTouchPoints = 0; Object.defineProperty(navigator, 'userAgent', { configurable: true, writable: true, }); + Object.defineProperty(navigator, 'maxTouchPoints', { + writable: true, + }); }); afterEach(() => { navigator.userAgent = originalUserAgent; + navigator.maxTouchPoints = originalTouchPoints; }); - it('returns false if not mobile', () => { + it('returns false if not mobile and has no touchpoints', () => { navigator.userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36'; + navigator.maxTouchPoints = 0; expect(isLikelyMobile()).to.be.false(); }); + it('returns true if there is an Apple user agent and 5 maxTouchPoints', () => { + navigator.userAgent = + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36'; + navigator.maxTouchPoints = 5; + + expect(isLikelyMobile()).to.be.true(); + }); + it('returns true if likely mobile', () => { navigator.userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148';