diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5ee9aa3037a..0a14d738825 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -105,7 +105,7 @@ install: specs: stage: test - parallel: 7 + parallel: 11 cache: - <<: *ruby_cache - <<: *yarn_cache diff --git a/app/assets/images/email/README.md b/app/assets/images/email/README.md new file mode 100644 index 00000000000..9796d3c69ac --- /dev/null +++ b/app/assets/images/email/README.md @@ -0,0 +1,3 @@ +# Email Images + +This folder contains images for exclusive use by mailer templates. This includes email-specific imagery, and also variants of existing assets. For example, since [SVG images are not well-supported](https://www.caniemail.com/features/image-svg/) in all email clients, this folder may include rasterized versions of common SVG images. diff --git a/app/assets/images/email/info.png b/app/assets/images/email/info.png new file mode 100644 index 00000000000..67da418b193 Binary files /dev/null and b/app/assets/images/email/info.png differ diff --git a/app/assets/stylesheets/components/_barcode.scss b/app/assets/stylesheets/components/_barcode.scss new file mode 100644 index 00000000000..e842cb94627 --- /dev/null +++ b/app/assets/stylesheets/components/_barcode.scss @@ -0,0 +1,14 @@ +.barcode.barby-barcode { + width: auto; + table-layout: fixed; + border-spacing: 0; +} + +.barcode .barby-cell { + width: 2px; + height: 96px; + + &.on { + background-color: #000; + } +} diff --git a/app/assets/stylesheets/components/all.scss b/app/assets/stylesheets/components/all.scss index ee295a189d1..3db8dedb6fb 100644 --- a/app/assets/stylesheets/components/all.scss +++ b/app/assets/stylesheets/components/all.scss @@ -1,5 +1,6 @@ @import 'account-header'; @import 'banner'; +@import 'barcode'; @import 'block-link'; @import 'block-submit-button'; @import 'btn'; diff --git a/app/assets/stylesheets/email.css.scss b/app/assets/stylesheets/email.css.scss index f1ae38d61e7..0c13b1ef748 100644 --- a/app/assets/stylesheets/email.css.scss +++ b/app/assets/stylesheets/email.css.scss @@ -1,5 +1,9 @@ +@import 'required'; @import 'variables/email'; @import 'foundation-emails/scss/foundation-emails'; +@import 'identity-style-guide/dist/assets/scss/packages/required'; +@import 'identity-style-guide/dist/assets/scss/packages/utilities'; +@import './components/barcode'; .gray { &:active, @@ -118,3 +122,37 @@ h4 { padding-right: $global-gutter-small !important; } } + +.info-alert { + background-color: color('info-lighter'); + padding: 0 units(0.5); + + td { + padding: units(1.5); + padding-right: units(1); + + & + td { + padding-left: 0; + padding-right: units(1.5); + } + } +} + +.process-list td { + padding-bottom: units(4); +} + +.process-list__circle { + border-radius: 50%; + width: units(3); + height: units(2.5); + background-color: color($theme-process-list-counter-background-color); + border: units($theme-process-list-counter-border-width) solid + color($theme-process-list-counter-border-color); + color: color('white'); + font-size: units(2); + font-weight: 700; + text-align: center; + padding-top: units(0.5); + margin-right: units(1.5); +} diff --git a/app/components/barcode_component.html.erb b/app/components/barcode_component.html.erb new file mode 100644 index 00000000000..c41cb0b82b7 --- /dev/null +++ b/app/components/barcode_component.html.erb @@ -0,0 +1,16 @@ +<%# Beware: This component is used in mailer content, so be mindful of email markup compatibility %> +<%= content_tag( + :div, + role: 'figure', + 'aria-labelledby': barcode_caption_id, + class: css_class, + **tag_options, + ) do %> + <%= barcode_html.html_safe %> +
<%= t('in_person_proofing.body.barcode.deadline', deadline: @presenter.formatted_due_date) %>
+<%= t('in_person_proofing.body.barcode.deadline', deadline: @presenter.formatted_due_date) %>
<%= t('in_person_proofing.body.barcode.deadline_restart') %>
<% end %> @@ -31,18 +31,11 @@ <%= render ProcessListComponent.new(heading_level: :h3, class: 'margin-y-3') do |c| %> <% c.item(heading: t('in_person_proofing.process.barcode.heading')) do %><%= t('in_person_proofing.process.barcode.info') %>
-<%= t('in_person_proofing.process.state_id.info') %>
@@ -85,7 +78,7 @@ <%= @presenter.selected_location_details['state'] %> <%= @presenter.selected_location_details['zip5'] %>-<%= @presenter.selected_location_details['zip4'] %> -
+ <%= t('user_mailer.in_person_ready_to_verify.greeting', name: @first_name) %>
+ <%= t('user_mailer.in_person_ready_to_verify.intro') %>
+
| + <%= image_tag('email/info.png', width: 16, height: 16, alt: '') %> + | +
+ <%= t('in_person_proofing.body.barcode.deadline', deadline: @presenter.formatted_due_date) %> +<%= t('in_person_proofing.body.barcode.deadline_restart') %> + |
+
<%= t('in_person_proofing.body.barcode.emailed_info') %>
+1 |
+
+ <%= t('in_person_proofing.process.barcode.heading') %>+<%= t('in_person_proofing.process.barcode.info') %> + <%= render BarcodeComponent.new( + barcode_data: @presenter.enrollment_code, + label: nil, + label_formatter: Idv::InPerson::EnrollmentCodeFormatter.method(:format), + ) %> + |
+
2 |
+
+ <%= t('in_person_proofing.process.state_id.heading') %>+<%= t('in_person_proofing.process.state_id.info') %> +
<%= t('in_person_proofing.process.state_id.no_other_documents') %> + |
+
4 |
+
+ <%= t('in_person_proofing.process.proof_of_address.heading') %>+<%= t('in_person_proofing.process.proof_of_address.info') %> +
|
+
+ <%= t('in_person_proofing.body.barcode.items_to_bring_questions') %> + <%= link_to( + t('in_person_proofing.body.barcode.learn_more'), + MarketingSite.help_center_article_url( + category: 'verify-your-identity', + article: 'how-to-verify-in-person', + ), + ) %> +
+<%= t('in_person_proofing.body.barcode.speak_to_associate') %>
+ ++ <%= t('in_person_proofing.body.barcode.deadline', deadline: @presenter.formatted_due_date) %> + <%= t('in_person_proofing.body.barcode.no_appointment_required') %> +
diff --git a/config/initializers/secure_headers.rb b/config/initializers/secure_headers.rb index 8ce946d7589..8f9769790f4 100644 --- a/config/initializers/secure_headers.rb +++ b/config/initializers/secure_headers.rb @@ -5,7 +5,7 @@ } config.action_dispatch.default_headers.merge!( - 'X-Frame-Options' => 'DENY', + 'X-Frame-Options' => IdentityConfig.store.rails_mailer_previews_enabled ? 'SAMEORIGIN' : 'DENY', 'X-XSS-Protection' => '1; mode=block', 'X-Download-Options' => 'noopen', ) diff --git a/config/locales/components/en.yml b/config/locales/components/en.yml index d197121dd90..94915725810 100644 --- a/config/locales/components/en.yml +++ b/config/locales/components/en.yml @@ -1,6 +1,8 @@ --- en: components: + barcode: + table_label: Barcode clipboard_button: label: Copy javascript_required: diff --git a/config/locales/components/es.yml b/config/locales/components/es.yml index 11f564c2f78..cec7ec4d61c 100644 --- a/config/locales/components/es.yml +++ b/config/locales/components/es.yml @@ -1,6 +1,8 @@ --- es: components: + barcode: + table_label: Código de barras clipboard_button: label: Copiar javascript_required: diff --git a/config/locales/components/fr.yml b/config/locales/components/fr.yml index 5011d86f66f..62f05681bf4 100644 --- a/config/locales/components/fr.yml +++ b/config/locales/components/fr.yml @@ -1,6 +1,8 @@ --- fr: components: + barcode: + table_label: Code-barres clipboard_button: label: Copier javascript_required: diff --git a/config/locales/in_person_proofing/en.yml b/config/locales/in_person_proofing/en.yml index faf2ce8e0e4..02fcb91e258 100644 --- a/config/locales/in_person_proofing/en.yml +++ b/config/locales/in_person_proofing/en.yml @@ -83,7 +83,6 @@ en: barcode: caption_label: Enrollment code heading: A copy of your barcode - image_alt: Barcode info: Print or scan from your mobile device. proof_of_address: acceptable_proof: diff --git a/config/locales/in_person_proofing/es.yml b/config/locales/in_person_proofing/es.yml index 2546488f7e5..8c463aea8c1 100644 --- a/config/locales/in_person_proofing/es.yml +++ b/config/locales/in_person_proofing/es.yml @@ -87,7 +87,6 @@ es: barcode: caption_label: Código de registro heading: Una copia de su código de barras - image_alt: Código de barras info: Imprima o escanee desde su dispositivo móvil. proof_of_address: acceptable_proof: diff --git a/config/locales/in_person_proofing/fr.yml b/config/locales/in_person_proofing/fr.yml index 0e333ff59f4..7767c7520f4 100644 --- a/config/locales/in_person_proofing/fr.yml +++ b/config/locales/in_person_proofing/fr.yml @@ -91,7 +91,6 @@ fr: barcode: caption_label: Code d’inscription heading: Une copie de votre code-barres - image_alt: Code-barres info: Imprimez ou numérisez depuis votre appareil mobile. proof_of_address: acceptable_proof: diff --git a/config/locales/user_mailer/en.yml b/config/locales/user_mailer/en.yml index 7b6f7f33011..587cc7c4aa0 100644 --- a/config/locales/user_mailer/en.yml +++ b/config/locales/user_mailer/en.yml @@ -100,6 +100,11 @@ en: %{app_name} %{help_link} or %{contact_link}. subject: Email address deleted help_link_text: Help Center + in_person_ready_to_verify: + greeting: Hi %{name}, + intro: Here are the details to verify your identity in person at a United States + Post Office near you. + subject: You’re ready to verify your identity with %{app_name} in person letter_reminder: info_html: The letter you are about to receive will contain a confirmation code that helps us verify your address. You can complete the identity diff --git a/config/locales/user_mailer/es.yml b/config/locales/user_mailer/es.yml index 9bcc9d62f61..7ac1e5820c8 100644 --- a/config/locales/user_mailer/es.yml +++ b/config/locales/user_mailer/es.yml @@ -106,6 +106,11 @@ es: %{app_name} %{help_link} o el %{contact_link}. subject: Dirección de correo electrónico eliminada help_link_text: Centro de Ayuda + in_person_ready_to_verify: + greeting: 'Hola, %{name}:' + intro: Estos son los detalles para verificar su identidad en persona en una + oficina de correos de los Estados Unidos cercana a usted. + subject: Está listo para verificar su identidad con %{app_name} en persona letter_reminder: info_html: La carta que está a punto de recibir contendrá un código de confirmación que nos ayudará a verificar su dirección. Puede completar diff --git a/config/locales/user_mailer/fr.yml b/config/locales/user_mailer/fr.yml index 616a7302c7f..f1a56ba36e1 100644 --- a/config/locales/user_mailer/fr.yml +++ b/config/locales/user_mailer/fr.yml @@ -109,6 +109,11 @@ fr: veuillez visiter le %{help_link} de %{app_name} ou %{contact_link}. subject: Adresse email supprimée help_link_text: Centre d’aide + in_person_ready_to_verify: + greeting: Bonjour %{name}, + intro: Voici les détails pour vérifier votre identité en personne dans un bureau + de poste des États-Unis près de chez vous. + subject: Vous êtes prêt à vérifier votre identité avec %{app_name} en personne letter_reminder: info_html: La lettre que vous êtes sur le point de recevoir contiendra un code de confirmation nous permettant de vérifier votre adresse. Vous pouvez diff --git a/spec/components/barcode_component_spec.rb b/spec/components/barcode_component_spec.rb new file mode 100644 index 00000000000..44e171fc434 --- /dev/null +++ b/spec/components/barcode_component_spec.rb @@ -0,0 +1,51 @@ +require 'rails_helper' + +RSpec.describe BarcodeComponent, type: :component do + it 'renders expected content' do + rendered = render_inline BarcodeComponent.new(barcode_data: '1234', label: 'Code') + + caption = page.find_css('table + div', text: 'Code: 1234').first + + expect(rendered).to have_css("table.barcode[aria-label=#{t('components.barcode.table_label')}]") + expect(rendered).to have_css("[role=figure][aria-labelledby=#{caption.attr(:id)}]") + expect(rendered).to have_css('table tbody[aria-hidden=true]') + end + + context 'with tag options' do + it 'renders with attributes' do + rendered = render_inline( + BarcodeComponent.new( + barcode_data: '1234', + label: '', + data: { foo: 'bar' }, + aria: { hidden: 'false' }, + class: 'example', + ), + ) + + expect(rendered).to have_css( + '.example[role=figure][aria-labelledby][data-foo=bar][aria-hidden=false]', + ) + end + end + + context 'with empty label' do + it 'renders label without prefix' do + rendered = render_inline BarcodeComponent.new(barcode_data: '1234', label: '') + + expect(rendered).to have_css('table + div', text: '1234') + end + end + + context 'with label formatter' do + it 'renders formatted label' do + rendered = render_inline BarcodeComponent.new( + barcode_data: '1234', + label: '', + label_formatter: ->(barcode_data) { barcode_data + '5678' }, + ) + + expect(rendered).to have_css('table + div', text: '12345678') + end + end +end diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb index 461a28822b7..2ea771354ab 100644 --- a/spec/mailers/previews/user_mailer_preview.rb +++ b/spec/mailers/previews/user_mailer_preview.rb @@ -134,6 +134,15 @@ def account_verified ) end + def in_person_ready_to_verify + UserMailer.in_person_ready_to_verify( + user, + email_address_record, + first_name: 'Michael', + enrollment: in_person_enrollment, + ) + end + private def user @@ -148,6 +157,38 @@ def email_address_record unsaveable(EmailAddress.new(email: email_address)) end + def in_person_enrollment + unsaveable( + InPersonEnrollment.new( + user: user, + profile: unsaveable(Profile.new(user: user)), + enrollment_code: '2048702198804358', + created_at: Time.zone.now, + current_address_matches_id: true, + selected_location_details: { + 'name' => 'BALTIMORE — Post Office™', + 'streetAddress' => '900 E FAYETTE ST RM 118', + 'city' => 'BALTIMORE', + 'state' => 'MD', + 'zip5' => '21233', + 'zip4' => '9715', + 'phone' => '555-123-6409', + 'hours' => [ + { + 'weekdayHours' => '8:30 AM - 4:30 PM', + }, + { + 'saturdayHours' => '9:00 AM - 12:00 PM', + }, + { + 'sundayHours' => 'Closed', + }, + ], + }, + ), + ) + end + # Remove #save and #save! to make sure we can't write these made-up records def unsaveable(record) class << record diff --git a/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb b/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb index f0951f55415..ba3e8bc71dc 100644 --- a/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb +++ b/spec/presenters/idv/in_person/ready_to_verify_presenter_spec.rb @@ -20,14 +20,6 @@ subject(:presenter) { described_class.new(enrollment: enrollment) } - describe '#barcode_data_url' do - subject(:barcode_data_url) { presenter.barcode_data_url } - - it 'returns a valid data URL' do - expect(barcode_data_url).to match URI::DEFAULT_PARSER.make_regexp('data') - end - end - describe '#formatted_due_date' do subject(:formatted_due_date) { presenter.formatted_due_date } @@ -40,16 +32,6 @@ end end - describe '#formatted_enrollment_code' do - subject(:formatted_enrollment_code) { presenter.formatted_enrollment_code } - - it 'returns a formatted enrollment code' do - expect(formatted_enrollment_code).to eq( - Idv::InPerson::EnrollmentCodeFormatter.format(enrollment_code), - ) - end - end - describe '#selected_location_details' do subject(:selected_location_details) { presenter.selected_location_details }