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
5 changes: 3 additions & 2 deletions app/assets/javascripts/app/form-field-format.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PhoneFormatter, SocialSecurityNumberFormatter, TextField } from 'field-kit';
import { SocialSecurityNumberFormatter, TextField } from 'field-kit';
import DateFormatter from './modules/date-formatter';
import InternationalPhoneFormatter from './modules/international-phone-formatter';
import NumericFormatter from './modules/numeric-formatter';
import PersonalKeyFormatter from './modules/personal-key-formatter';
import ZipCodeFormatter from './modules/zip-code-formatter';
Expand All @@ -13,7 +14,7 @@ function formatForm() {
['.home_equity_line', new NumericFormatter()],
['.mfa', new NumericFormatter()],
['.mortgage', new NumericFormatter()],
['.phone', new PhoneFormatter()],
['.phone', new InternationalPhoneFormatter()],
['.personal-key', new PersonalKeyFormatter()],
['.ssn', new SocialSecurityNumberFormatter()],
['.zipcode', new ZipCodeFormatter()],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Formatter } from 'field-kit';
import { asYouType as AsYouType } from 'libphonenumber-js';

const fixCountryCodeSpacing = (text, countryCode) => {
// If the text is `+123456`, make it `+123 456`
if (text[countryCode.length + 1] !== ' ') {
return text.replace(`+${countryCode}`, `+${countryCode} `);
}
return text;
};

const getFormattedTextData = (text) => {
if (text === '1') {
text = '+1';
}

const asYouType = new AsYouType('US');
let formattedText = asYouType.input(text);
const countryCode = asYouType.country_phone_code;

if (asYouType.country_phone_code) {
formattedText = fixCountryCodeSpacing(formattedText, countryCode);
}

return {
text: formattedText,
template: asYouType.template,
countryCode,
};
};

const cursorPosition = (formattedTextData) => {
// If the text is `(23 )` the cursor goes after the 3
const match = formattedTextData.text.match(/\d[^\d]*$/);
if (match) {
return match.index + 1;
}
return formattedTextData.text.length + 1;
};

class InternationalPhoneFormatter extends Formatter {
format(text) {
const formattedTextData = getFormattedTextData(text);
return super.format(formattedTextData.text);
}

// eslint-disable-next-line class-methods-use-this
parse(text) {
return text.replace(/[^\d+]/g, '');
}

isChangeValid(change, error) {
const formattedTextData = getFormattedTextData(change.proposed.text);
const previousFormattedTextData = getFormattedTextData(change.current.text);

if (previousFormattedTextData.template &&
!formattedTextData.template &&
change.inserted.text.length === 1
) {
return false;
}

change.proposed.text = formattedTextData.text;
change.proposed.selectedRange.start = cursorPosition(formattedTextData);
return super.isChangeValid(change, error);
}
}

export default InternationalPhoneFormatter;
57 changes: 53 additions & 4 deletions app/assets/javascripts/app/phone-internationalization.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { PhoneFormatter } from 'field-kit';

const INTERNATIONAL_CODE_REGEX = /^\+(\d+) |^1 /;

const I18n = window.LoginGov.I18n;
const phoneFormatter = new PhoneFormatter();

Expand All @@ -18,7 +20,7 @@ const areaCodeFromUSPhone = (phone) => {
};

const selectedInternationCodeOption = () => {
const dropdown = document.querySelector('#two_factor_setup_form_international_code');
const dropdown = document.querySelector('[data-international-phone-form] .international-code');
return dropdown.item(dropdown.selectedIndex);
};

Expand Down Expand Up @@ -50,9 +52,14 @@ const unsupportedPhoneOTPDeliveryWarningMessage = (phone) => {
};

const updateOTPDeliveryMethods = () => {
const phoneInput = document.querySelector('#two_factor_setup_form_phone');
const phoneRadio = document.querySelector('#two_factor_setup_form_otp_delivery_preference_voice');
const smsRadio = document.querySelector('#two_factor_setup_form_otp_delivery_preference_sms');

if (!phoneRadio || !smsRadio) {
return;
}

const phoneInput = document.querySelector('[data-international-phone-form] .phone');
const phoneLabel = phoneRadio.parentNode.parentNode;
const deliveryMethodHint = document.querySelector('#otp_delivery_preference_instruction');
const optPhoneLabelInfo = document.querySelector('#otp_phone_label_info');
Expand All @@ -74,14 +81,56 @@ const updateOTPDeliveryMethods = () => {
}
};

const internationalCodeFromPhone = (phone) => {
const match = phone.match(INTERNATIONAL_CODE_REGEX);
if (match) {
return match[1] || match[2];
}
return '1';
};

const updateInternationalCodeSelection = () => {
const phoneInput = document.querySelector('[data-international-phone-form] .phone');
const phone = phoneInput.value;
const internationalCode = internationalCodeFromPhone(phone);
const option = document.querySelector(`[data-country-code='${internationalCode}']`);
if (option) {
const dropdown = document.querySelector('[data-international-phone-form] .international-code');
dropdown.value = option.value;
}
};

const updateInternationalCodeInPhone = (phone, newCode) => {
if (phone.match(/^\+[^d+]$/)) {
phone = phone.replace(/^\+[^d+]$/, '');
}
if (phone.match(INTERNATIONAL_CODE_REGEX)) {
return phone.replace(INTERNATIONAL_CODE_REGEX, `+${newCode} `);
}
return `+${newCode} ${phone}`;
};

const updateInternationalCodeInput = () => {
const phoneInput = document.querySelector('[data-international-phone-form] .phone');
const phone = phoneInput.value;
const inputInternationalCode = internationalCodeFromPhone(phone);
const selectedInternationalCode = selectedInternationCodeOption().dataset.countryCode;

if (inputInternationalCode !== selectedInternationalCode) {
phoneInput.value = updateInternationalCodeInPhone(phone, selectedInternationalCode);
}
};

document.addEventListener('DOMContentLoaded', () => {
const phoneInput = document.querySelector('#two_factor_setup_form_phone');
const codeInput = document.querySelector('#two_factor_setup_form_international_code');
const phoneInput = document.querySelector('[data-international-phone-form] .phone');
const codeInput = document.querySelector('[data-international-phone-form] .international-code');
if (phoneInput) {
phoneInput.addEventListener('keyup', updateOTPDeliveryMethods);
phoneInput.addEventListener('keyup', updateInternationalCodeSelection);
}
if (codeInput) {
codeInput.addEventListener('change', updateOTPDeliveryMethods);
codeInput.addEventListener('change', updateInternationalCodeInput);
updateOTPDeliveryMethods();
}
});
8 changes: 6 additions & 2 deletions app/views/users/phones/edit.html.slim
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@


h1.h3.my0 = t('headings.edit_info.phone')
= simple_form_for(@update_user_phone_form, url: manage_phone_path,
= simple_form_for(@update_user_phone_form,
data: { unsupported_area_codes: @unsupported_area_codes,
international_phone_form: true },
url: manage_phone_path,
html: { autocomplete: 'off', method: :put, role: 'form' }) do |f|
= f.input :international_code,
collection: international_phone_codes,
include_blank: false
include_blank: false,
input_html: { class: 'international-code' }
= f.input :phone, as: :tel, required: true, input_html: { class: 'phone', value: nil },
label: t('account.index.phone')
= f.button :submit, t('forms.buttons.submit.confirm_change'), class: 'mt2'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ p.mt-tiny.mb0
= t('devise.two_factor_authentication.otp_setup_html')
= simple_form_for(@two_factor_setup_form,
html: { autocomplete: 'off', role: 'form' },
data: { unsupported_area_codes: @unsupported_area_codes },
data: { unsupported_area_codes: @unsupported_area_codes,
international_phone_form: true },
method: :patch,
url: phone_setup_path) do |f|
.clearfix
.sm-col.sm-col-8
= f.input :international_code,
collection: international_phone_codes,
include_blank: false
include_blank: false,
input_html: { class: 'international-code' }
= f.label :phone, class: 'block'
strong.left = t('devise.two_factor_authentication.otp_phone_label')
span#otp_phone_label_info.ml1.italic
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"field-kit": "^2.1.0",
"focus-trap": "^2.3.0",
"hint.css": "^2.3.2",
"libphonenumber-js": "^0.4.23",
"normalize.css": "^4.2.0",
"sinon": "^1.17.7",
"zxcvbn": "^4.4.2"
Expand Down
2 changes: 1 addition & 1 deletion spec/features/idv/phone_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
fill_in 'Phone', with: ''
find('#idv_phone_form_phone').native.send_keys('abcd1234')

expect(find('#idv_phone_form_phone').value).to eq '1 (234) '
expect(find('#idv_phone_form_phone').value).to eq '+1 234'
end

def complete_idv_profile_with_phone(phone)
Expand Down
19 changes: 18 additions & 1 deletion spec/features/two_factor_authentication/sign_in_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
scenario 'disables the phone option and displays a warning with js', :js do
sign_in_before_2fa
select 'Turkey +90', from: 'International code'
fill_in 'Phone', with: '555-555-5000'
fill_in 'Phone', with: '+90 312 213 29 65'
phone_radio_button = page.find(
'#two_factor_setup_form_otp_delivery_preference_voice',
visible: :all
Expand All @@ -112,6 +112,23 @@
)
expect(phone_radio_button).to_not be_disabled
end

scenario 'updates international code as user types', :js do
sign_in_before_2fa
fill_in 'Phone', with: '+81 54 354 3643'

expect(page.find('#two_factor_setup_form_international_code').value).to eq 'JP'

fill_in 'Phone', with: '5376'
select 'Morocco +212', from: 'International code'

expect(find('#two_factor_setup_form_phone').value).to eq '+212 5376'

fill_in 'Phone', with: '54354'
select 'Japan +81', from: 'International code'

expect(find('#two_factor_setup_form_phone').value).to include '+81'
end
end
end

Expand Down
48 changes: 36 additions & 12 deletions spec/features/users/user_edit_spec.rb
Original file line number Diff line number Diff line change
@@ -1,23 +1,47 @@
require 'rails_helper'

feature 'User edit' do
scenario 'user sees error message if form is submitted without email', js: true, idv_job: true do
sign_in_and_2fa_user
context 'editing email' do
before do
sign_in_and_2fa_user
visit manage_email_path
end

visit manage_email_path
fill_in 'Email', with: ''
click_button 'Update'
scenario 'user sees error message if form is submitted without email', :js, idv_job: true do
fill_in 'Email', with: ''
click_button 'Update'

expect(page).to have_content t('valid_email.validations.email.invalid')
expect(page).to have_content t('valid_email.validations.email.invalid')
end
end

scenario 'user sees error message if form is submitted without phone number', js: true do
sign_in_and_2fa_user
context 'editing 2FA phone number' do
before do
sign_in_and_2fa_user
visit manage_phone_path
end

visit manage_phone_path
fill_in 'Phone', with: ''
click_button t('forms.buttons.submit.confirm_change')
scenario 'user sees error message if form is submitted without phone number', js: true do
fill_in 'Phone', with: ''
click_button t('forms.buttons.submit.confirm_change')

expect(page).to have_content t('errors.messages.improbable_phone')
expect(page).to have_content t('errors.messages.improbable_phone')
end

scenario 'updates international code as user types', :js do
fill_in 'Phone', with: '+81 54 354 3643'

expect(page.find('#update_user_phone_form_international_code').value).to eq 'JP'

fill_in 'Phone', with: '5376'
select 'Morocco +212', from: 'International code'

expect(find('#update_user_phone_form_phone').value).to eq '+212 5376'

fill_in 'Phone', with: '54354'
select 'Japan +81', from: 'International code'

expect(find('#update_user_phone_form_phone').value).to include '+81'
end
end
end