Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/components/spinner_button_component.html.erb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<%= content_tag(:'lg-spinner-button', class: css_class) do %>
<%= content_tag(:'lg-spinner-button', class: css_class, 'spin-on-click': spin_on_click) do %>
<div class="spinner-button__content">
<%= render ButtonComponent.new(type: :button, **button_options).with_content(content) %>
<span class="spinner-dots spinner-dots--centered" aria-hidden="true">
Expand Down
5 changes: 3 additions & 2 deletions app/components/spinner_button_component.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
class SpinnerButtonComponent < BaseComponent
attr_reader :action_message, :button_options, :outline
attr_reader :action_message, :button_options, :outline, :spin_on_click

# @param [String] action_message Message describing the action being performed, shown visually to
# users when the animation has been active for a long time, and
# immediately to users of assistive technology.
def initialize(action_message: nil, **button_options)
def initialize(action_message: nil, spin_on_click: nil, **button_options)
@action_message = action_message
@button_options = button_options
@outline = button_options[:outline]
@spin_on_click = spin_on_click
end

def css_class
Expand Down
1 change: 1 addition & 0 deletions app/controllers/frontend_log_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class FrontendLogController < ApplicationController
'IdV: verify in person troubleshooting option clicked' => :idv_verify_in_person_troubleshooting_option_clicked,
'IdV: location visited' => :idv_in_person_location_visited,
'IdV: location submitted' => :idv_in_person_location_submitted,
'IdV: Mobile device and camera check' => :idv_mobile_device_and_camera_check,
'IdV: prepare visited' => :idv_in_person_prepare_visited,
'IdV: prepare submitted' => :idv_in_person_prepare_submitted,
'IdV: switch_back visited' => :idv_in_person_switch_back_visited,
Expand Down
19 changes: 0 additions & 19 deletions app/javascript/packs/document-capture-welcome.js

This file was deleted.

73 changes: 73 additions & 0 deletions app/javascript/packs/document-capture-welcome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { trackEvent } from '@18f/identity-analytics';
import { hasCamera, isCameraCapableMobile } from '@18f/identity-device';
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(You'll want to compare this file with the .js version above, as I converted it to Typescript)


const GRACE_TIME_FOR_CAMERA_CHECK_MS = 2000;
const DEVICE_CHECK_EVENT = 'IdV: Mobile device and camera check';

function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function measure<T>(func: () => Promise<T>): Promise<{ result: T; duration: number }> {
const start = performance.now();
const result = await func();
const duration = performance.now() - start;
return { result, duration };
}

function addFormInputsForMobileDeviceCapabilities() {
const form = document.querySelector<HTMLFormElement>('.js-consent-continue-form');

if (!form) {
return;
}

if (!isCameraCapableMobile()) {
trackEvent(DEVICE_CHECK_EVENT, {
is_camera_capable_mobile: false,
});
return;
}

// The check for a camera on the device is async -- kick it off here and intercept
// submit() to ensure that it completes in time.
const cameraCheckPromise = measure(hasCamera).then(
async ({ result: cameraPresent, duration }) => {
if (!cameraPresent) {
// Signal to the backend that this is a mobile device, but no camera is present
const ncInput = document.createElement('input');
ncInput.type = 'hidden';
ncInput.name = 'no_camera';
form.appendChild(ncInput);
}

// Signal to the backend that this is a mobile device, and this user should skip the
// "hybrid handoff" step.
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'skip_upload';
form.appendChild(input);

await trackEvent(DEVICE_CHECK_EVENT, {
is_camera_capable_mobile: true,
camera_present: !!cameraPresent,
grace_time: GRACE_TIME_FOR_CAMERA_CHECK_MS,
duration: Math.floor(duration),
});
},
);

form.addEventListener('submit', (event) => {
event.preventDefault();

for (const spinner of form.querySelectorAll('lg-spinner-button')) {
spinner.toggleSpinner(true);
}

Promise.race([delay(GRACE_TIME_FOR_CAMERA_CHECK_MS), cameraCheckPromise]).then(() =>
form.submit(),
);
});
}

addFormInputsForMobileDeviceCapabilities();
22 changes: 22 additions & 0 deletions app/services/analytics_events.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3089,6 +3089,28 @@ def idv_arcgis_request_failure(
)
end

# Tracks whether the user's device appears to be mobile device with a camera attached.
# @param [Boolean] is_camera_capable_mobile Whether we think the device _could_ have a camera.
# @param [Boolean,nil] camera_present Whether the user's device _actually_ has a camera available.
# @param [Integer,nil] grace_time Extra time allowed for browser to report camera availability.
# @param [Integer,nil] duration Time taken for browser to report camera availability.
def idv_mobile_device_and_camera_check(
is_camera_capable_mobile:,
camera_present: nil,
grace_time: nil,
duration: nil,
**extra
)
track_event(
'IdV: Mobile device and camera check',
is_camera_capable_mobile: is_camera_capable_mobile,
camera_present: camera_present,
grace_time: grace_time,
duration: duration,
**extra,
)
end

# Tracks when the user visits one of the the session error pages.
# @param [String] type
# @param [Integer,nil] attempts_remaining
Expand Down
11 changes: 10 additions & 1 deletion app/views/idv/doc_auth/agreement.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,16 @@
MarketingSite.security_and_privacy_practices_url,
) %>
</p>
<%= f.submit t('doc_auth.buttons.continue'), class: 'margin-top-4' %>
<div class="margin-top-4">
<%= render(
SpinnerButtonComponent.new(
type: :submit,
big: true,
wide: true,
spin_on_click: false,
).with_content(t('doc_auth.buttons.continue')),
) %>
</div>
<% end %>

<%= render 'idv/doc_auth/cancel', step: 'agreement' %>
Expand Down
8 changes: 7 additions & 1 deletion app/views/idv/doc_auth/welcome.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,13 @@
url: url_for,
method: 'put',
html: { autocomplete: 'off', class: 'margin-y-5 js-consent-continue-form' } do |f| %>
<%= f.submit t('doc_auth.buttons.continue') %>
<%= render(
SpinnerButtonComponent.new(
type: :submit,
big: true,
wide: true,
).with_content(t('doc_auth.buttons.continue')),
) %>
<% end %>

<%= render(
Expand Down
12 changes: 12 additions & 0 deletions spec/components/spinner_button_component_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
expect(rendered).to_not have_selector('.spinner-button__action-message')
end

it 'renders without spin-on-click attribute by default' do
render_inline SpinnerButtonComponent.new.with_content('hi')
expect(page).to_not have_selector('[spin-on-click]')
end

context 'with action message' do
it 'renders with action message' do
rendered = render_inline SpinnerButtonComponent.new(
Expand All @@ -34,4 +39,11 @@
expect(rendered).to have_css('lg-spinner-button.spinner-button--outline')
end
end

context 'with spin_on_click' do
it 'renders spin-on-click attribute' do
render_inline SpinnerButtonComponent.new(spin_on_click: true).with_content('')
expect(page).to have_selector('[spin-on-click=true]')
end
end
end