From 8c1991a5a58ed15e391b9564c9f8e724323391de Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Wed, 4 Mar 2026 16:32:00 -0500 Subject: [PATCH 1/7] INSR-8688: Add address autocomplete, fix edit location navigation --- .../contact-details/contact-details.vue | 96 ++++++++++++++++++- src/layouts/payment-method/payment-method.vue | 6 +- .../router-constants/navigation-scenarios.js | 3 +- src/router/router-constants/routing-table.js | 6 +- 4 files changed, 105 insertions(+), 6 deletions(-) diff --git a/src/layouts/contact-details/contact-details.vue b/src/layouts/contact-details/contact-details.vue index b864ff19..8222f36a 100644 --- a/src/layouts/contact-details/contact-details.vue +++ b/src/layouts/contact-details/contact-details.vue @@ -133,6 +133,7 @@ import errorMessages from '@/constants/error-messages'; import { required } from '@/helpers/validation-rules'; import widgetFields from '@/constants/cms-widget-fields'; import states from '@/constants/states'; +import applicationConfig from '@/constants/application-config'; // DEFINE VALIDATION RULES defineRule('street-address-required', required(errorMessages.SERVICE_ADDRESS_REQUIRED)); @@ -159,6 +160,7 @@ export default { next((vm) => { vm.setCmsContent(cmsContent); + vm.setupAddressLookup(); }); }, data() { @@ -239,7 +241,7 @@ export default { /** * @summary Steps to perform when forward button clicked. */ - forwardButtonAction() { + async forwardButtonAction() { const contactInfo = { notesForTechnician: this.notesForTechnician }; @@ -247,6 +249,8 @@ export default { const provider = useMainStore().serviceLocation.provider; + await this.geocodeAddress(); + useMainStore().updateServiceLocation({ address: this.address, address2: this.address2, @@ -267,7 +271,95 @@ export default { this.address = ''; this.address2 = ''; this.city = ''; - } + }, + setupAddressLookup() { + const apiKey = applicationConfig.GOOGLE_PLACES_API_KEY; + + this.$loadScript(`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=Function.prototype`) + .catch(() => { + // Failed to fetch script + window.console.warn('Unable to load Google Places API script'); + }); + }, + geocodeAddress() { + const autoCompletePromise = new Promise((resolve, reject) => { + // Get Autocomplete Service + const acService = new window.google.maps.places.AutocompleteService(); + // Get Places Service, needs psuedo element (or a map) + const placeService = new window.google.maps.places.PlacesService(document.createElement('div')); + // Create Autocomplete Session token, multiple requests one pricing hit + const acSessionToken = new window.google.maps.places.AutocompleteSessionToken(); + + const addressValue = `${this.address}, ${this.city}, ${this.state} ${this.zipCode}`; + acService.getPlacePredictions( + { + input: addressValue, + type: ['geocode'], + componentRestrictions: { country: ['us'] }, + sessionToken: acSessionToken + }, + (predictions) => { + if (predictions && predictions.length > 0) { + const firstPrediction = predictions[0]; + if (firstPrediction.place_id) { + placeService.getDetails( + { + placeId: firstPrediction.place_id, + fields: ['address_components'], + sessionToken: acSessionToken + }, + (details) => { + this.fillInAddress(details); + resolve(); + } + ); + } else { + resolve(); + } + } else { + resolve(); + } + } + ); + }); + + return autoCompletePromise; + }, + async fillInAddress(googlePlace) { + let processedStreetAddress = false; + let processedRoute = false; + for (const component of googlePlace.address_components) { + const componentType = component.types[0]; + + switch (componentType) { + case 'street_number': { + if (processedRoute) { + this.address = `${component.long_name} ${this.address}`; + } else { + this.address = component.long_name; + } + + processedStreetAddress = true; + break; + } + case 'route': { + if (processedStreetAddress) { + this.address += ` ${component.short_name}`; + } else { + this.address = component.short_name; + } + + processedRoute = true; + break; + } + case 'locality': { + this.city = component.long_name; + break; + } + default: + } + } + }, } }; diff --git a/src/layouts/payment-method/payment-method.vue b/src/layouts/payment-method/payment-method.vue index 90144474..41eee5ee 100644 --- a/src/layouts/payment-method/payment-method.vue +++ b/src/layouts/payment-method/payment-method.vue @@ -389,7 +389,6 @@ export default { } this.smsPhoneNumber = this.getSMSPhoneFromStore(); if (this.isPayInAdvanceDisabled) { - console.log('Pay in Advance is disabled, defaulting to Pay at Time of Service'); this.paymentMethod = paymentMethods.PAY_AT_TIME_OF_SERVICE; } }, @@ -442,8 +441,11 @@ export default { handleEditClicked(section) { switch (section) { case 'location': + const scenario = this.mainStore.isMobileAppointment + ? this.navigationScenarios.EDIT_SERVICE_LOCATION_MOBILE + : this.navigationScenarios.EDIT_SERVICE_LOCATION_INSHOP; this.$router.navigateWithSpinner( - this.navigationScenarios.EDIT_SERVICE_LOCATION, + scenario, this.$route ); break; diff --git a/src/router/router-constants/navigation-scenarios.js b/src/router/router-constants/navigation-scenarios.js index bc9146fb..2c9768bf 100644 --- a/src/router/router-constants/navigation-scenarios.js +++ b/src/router/router-constants/navigation-scenarios.js @@ -105,7 +105,8 @@ const navigationScenarios = Object.freeze({ PAY_IN_ADVANCE_ERROR: 'PAY_IN_ADVANCE_ERROR', PAY_IN_ADVANCE_CREDIT_CARD_ERROR: 'PAY_IN_ADVANCE_CREDIT_CARD_ERROR', PAY_IN_ADVANCE_SUCCESS: 'PAY_IN_ADVANCE_SUCCESS', - EDIT_SERVICE_LOCATION: 'EDIT_SERVICE_LOCATION', + EDIT_SERVICE_LOCATION_INSHOP: 'EDIT_SERVICE_LOCATION_INSHOP', + EDIT_SERVICE_LOCATION_MOBILE: 'EDIT_SERVICE_LOCATION_MOBILE', EDIT_SCHEDULE: 'EDIT_SCHEDULE', EDIT_WIPERS: 'EDIT_WIPERS', diff --git a/src/router/router-constants/routing-table.js b/src/router/router-constants/routing-table.js index af06b4c6..8a45d37b 100644 --- a/src/router/router-constants/routing-table.js +++ b/src/router/router-constants/routing-table.js @@ -638,9 +638,13 @@ const routingTable = () => [ destinationIssPageValue: issPageValues.PAYMENT_PAGE }, { - scenario: navigationScenarios.EDIT_SERVICE_LOCATION, + scenario: navigationScenarios.EDIT_SERVICE_LOCATION_INSHOP, destinationIssPageValue: issPageValues.SCHEDULE_PAGE }, + { + scenario: navigationScenarios.EDIT_SERVICE_LOCATION_MOBILE, + destinationIssPageValue: issPageValues.CONTACT_DETAILS + }, { scenario: navigationScenarios.EDIT_SCHEDULE, destinationIssPageValue: issPageValues.SCHEDULE_PAGE From 21b76532b01a7c4e42b48b11797d71bd8da00a3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 00:12:20 +0000 Subject: [PATCH 2/7] Bump svgo from 2.8.0 to 2.8.2 Bumps [svgo](https://github.com/svg/svgo) from 2.8.0 to 2.8.2. - [Release notes](https://github.com/svg/svgo/releases) - [Commits](https://github.com/svg/svgo/compare/v2.8.0...v2.8.2) --- updated-dependencies: - dependency-name: svgo dependency-version: 2.8.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index cf98e150..d474da42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4125,14 +4125,6 @@ "node": ">= 10" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/aria-query": { "version": "5.0.1", "dev": true, @@ -18309,6 +18301,16 @@ "node": ">=16.13.2" } }, + "node_modules/sax": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/saxes": { "version": "6.0.0", "dev": true, @@ -19295,16 +19297,18 @@ "dev": true }, "node_modules/svgo": { - "version": "2.8.0", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.2.tgz", + "integrity": "sha512-TyzE4NVGLUFy+H/Uy4N6c3G0HEeprsVfge6Lmq+0FdQQ/zqoVYB62IsBZORsiL+o96s6ff/V6/3UQo/C0cgCAA==", "dev": true, "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^4.1.3", "css-tree": "^1.1.3", "csso": "^4.2.0", "picocolors": "^1.0.0", + "sax": "^1.5.0", "stable": "^0.1.8" }, "bin": { From 3a652a9c7518aee9874ac2de2706899ca45bc0fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Mar 2026 04:52:16 +0000 Subject: [PATCH 3/7] Bump immutable from 4.1.0 to 4.3.8 Bumps [immutable](https://github.com/immutable-js/immutable-js) from 4.1.0 to 4.3.8. - [Release notes](https://github.com/immutable-js/immutable-js/releases) - [Changelog](https://github.com/immutable-js/immutable-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/immutable-js/immutable-js/compare/v4.1.0...v4.3.8) --- updated-dependencies: - dependency-name: immutable dependency-version: 4.3.8 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index cf98e150..c9050e48 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11372,7 +11372,9 @@ } }, "node_modules/immutable": { - "version": "4.1.0", + "version": "4.3.8", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.8.tgz", + "integrity": "sha512-d/Ld9aLbKpNwyl0KiM2CT1WYvkitQ1TSvmRtkcV8FKStiDoA7Slzgjmb/1G2yhKM1p0XeNOieaTbFZmU1d3Xuw==", "dev": true, "license": "MIT" }, From a1cdf7b630bb7c148073b0a9422e1a7d6fb415eb Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Thu, 5 Mar 2026 10:05:29 -0500 Subject: [PATCH 4/7] Update unit tests to account for mocking out/ignoring google maps script --- .../contact-details/contact-details.spec.js | 174 +++++++++--------- .../contact-details/contact-details.vue | 57 +++--- 2 files changed, 123 insertions(+), 108 deletions(-) diff --git a/src/layouts/contact-details/contact-details.spec.js b/src/layouts/contact-details/contact-details.spec.js index 8a4d4d7b..b56a929b 100644 --- a/src/layouts/contact-details/contact-details.spec.js +++ b/src/layouts/contact-details/contact-details.spec.js @@ -10,11 +10,72 @@ import { getRandomString, getRandomInt, getRandomBoolean } from '@/helpers/data- import navigationScenarios from '@/router/router-constants/navigation-scenarios.js'; import { useMainStore } from '@/store/index.js'; +/** @ignore */ +function setupMocks({ + storeData, + props, + isShallowMount = true, + querySelectorFunction, + geocoderResult = ['1234 Test Street'] +}) { + const mountOptions = getMountOptions({ + router: { + navigate: jest.fn(), + navigateWithSpinner: jest.fn() + }, + loadScript: jest.fn().mockResolvedValue() + }); + + if (storeData) mountOptions.global.plugins = [createTestingPinia({ + initialState: { + main: storeData + } + })]; + + window.google = { + maps: { + event: { + addListener: jest + .fn() + .mockImplementation((element, eventName, callbackFunction) => { + /** @ignore */ + function interceptedCallbackFunction(e) { + callbackFunction(e.detail); + } + // selectedPlace = "Woogly"; + element.addEventListener(eventName, interceptedCallbackFunction); + }), + removeListener: jest.fn(), + clearInstanceListeners: jest.fn() + }, + places: { + Autocomplete: jest.fn().mockImplementation((el) => el), + AutocompleteService: jest.fn().mockImplementation(() => { + return { + getPlacePredictions: (request, callback) => { + // Throwing an error here guarantees that the function that calls it finishes + throw new Error(); + } + }; + }), + PlacesService: jest.fn().mockImplementation(() => {}), + AutocompleteSessionToken: jest.fn().mockImplementation(() => {}) + } + } + }; + + if (props) mountOptions.propsData = props; + + const wrapper = shallowMount(contactDetails, mountOptions); + + return { wrapper }; +} + describe('contactDetails.vue', () => { describe('Rendering', () => { test('Should render site header', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions()); + const { wrapper } = setupMocks({}); // Act const siteHeader = wrapper.findComponent({ ref: 'siteHeader' }); @@ -24,7 +85,7 @@ describe('contactDetails.vue', () => { }); test('Should render sub title', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions()); + const { wrapper } = setupMocks({}); // Act const siteSubHeader = wrapper.findComponent({ ref: 'siteSubHeader' }); @@ -34,7 +95,7 @@ describe('contactDetails.vue', () => { }); test('Should render appointment information alert', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions()); + const { wrapper } = setupMocks({}); // Act const appointmentInformationAlert = wrapper.findComponent({ ref: 'appointmentInformationAlert' }); @@ -44,8 +105,6 @@ describe('contactDetails.vue', () => { }); test('Should render same as policy address question subcomponent if service zip is same as customer zip', async () => { // Arrange - const mountOptions = getMountOptions(); - const customerStreetAddress = getRandomString(10, 50); const customerStreetAddress2 = getRandomString(0, 50); const customerCity = getRandomString(4, 20); @@ -55,7 +114,7 @@ describe('contactDetails.vue', () => { const serviceCity = getRandomString(4, 20); const serviceState = getRandomString(2, 2); const zipCode = getRandomInt(10000, 99999).toString(); - const mainInitialState = { + const storeData = { order: { customer: { address: { @@ -75,12 +134,7 @@ describe('contactDetails.vue', () => { } } }; - mountOptions.global.plugins = [createTestingPinia({ - initialState: { - main: mainInitialState - } - })]; - const wrapper = shallowMount(contactDetails, mountOptions); + const { wrapper } = setupMocks({ storeData }); await wrapper.vm.$nextTick(); @@ -92,8 +146,6 @@ describe('contactDetails.vue', () => { }); test('Should not render same as policy address question subcomponent if service zip is different from customer zip', () => { // Arrange - const mountOptions = getMountOptions(); - const firstName = getRandomString(4, 15); const lastName = getRandomString(4, 15); const emailAddress = getRandomString(10, 20); @@ -101,7 +153,7 @@ describe('contactDetails.vue', () => { const testZipCode = getRandomInt(10000, 99999); const customerZipCode = testZipCode.toString(); const serviceZipCode = (testZipCode + 1).toString(); - const mainInitialState = { + const storeData = { order: { customer: { firstName, @@ -119,12 +171,7 @@ describe('contactDetails.vue', () => { } } }; - mountOptions.global.plugins = [createTestingPinia({ - initialState: { - main: mainInitialState - } - })]; - const wrapper = shallowMount(contactDetails, mountOptions); + const { wrapper } = setupMocks({ storeData }); // Act const sameAsPolicyAddressQuestion = wrapper.findComponent({ ref: 'sameAsPolicyAddressQuestion' }); @@ -134,7 +181,7 @@ describe('contactDetails.vue', () => { }); test('Should render address question subcomponent', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions()); + const { wrapper } = setupMocks({}); // Act const addressQuestion = wrapper.findComponent({ ref: 'addressQuestion' }); @@ -144,7 +191,7 @@ describe('contactDetails.vue', () => { }); test('Should render apartment question subcomponent', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions()); + const { wrapper } = setupMocks({}); // Act const address2Question = wrapper.findComponent({ ref: 'address2Question' }); @@ -154,7 +201,7 @@ describe('contactDetails.vue', () => { }); test('Should render city question subcomponent', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions()); + const { wrapper } = setupMocks({}); // Act const cityQuestion = wrapper.findComponent({ ref: 'cityQuestion' }); @@ -164,7 +211,7 @@ describe('contactDetails.vue', () => { }); test('Should render state question subcomponent', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions()); + const { wrapper } = setupMocks({}); // Act const stateQuestion = wrapper.findComponent({ ref: 'stateQuestion' }); @@ -174,7 +221,7 @@ describe('contactDetails.vue', () => { }); test('Should render zip code question subcomponent', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions()); + const { wrapper } = setupMocks({}); // Act const zipCodeQuestion = wrapper.findComponent({ ref: 'zipCodeQuestion' }); @@ -184,7 +231,7 @@ describe('contactDetails.vue', () => { }); test('Should render change zip code alert', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions()); + const { wrapper } = setupMocks({}); // Act const changeZipCodeAlert = wrapper.findComponent({ ref: 'changeZipCodeAlert' }); @@ -194,7 +241,7 @@ describe('contactDetails.vue', () => { }); test('Should render vehicle protected question subcomponent', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions()); + const { wrapper } = setupMocks({}); // Act const vehicleProtectedQuestion = wrapper.findComponent({ ref: 'vehicleProtectedQuestion' }); @@ -204,7 +251,7 @@ describe('contactDetails.vue', () => { }); test('Should render technician notes textarea question subcomponent', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions()); + const { wrapper } = setupMocks({}); // Act const notesQuestion = wrapper.findComponent({ ref: 'notesQuestion' }); @@ -214,7 +261,7 @@ describe('contactDetails.vue', () => { }); test('Should render clearance text subcomponent', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions()); + const { wrapper } = setupMocks({}); // Act const clearanceText = wrapper.findComponent({ ref: 'clearanceText' }); @@ -224,7 +271,7 @@ describe('contactDetails.vue', () => { }); test('Should render site footer', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions()); + const { wrapper } = setupMocks({}); // Act const footer = wrapper.findComponent({ ref: 'siteFooter' }); @@ -234,8 +281,6 @@ describe('contactDetails.vue', () => { }); test('Mocked store yields expected data', () => { // Arrange - const mountOptions = getMountOptions(); - const address = getRandomString(10, 50); const address2 = getRandomString(0, 50); const city = getRandomString(4, 20); @@ -243,7 +288,7 @@ describe('contactDetails.vue', () => { const zipCode = getRandomInt(10000, 99999).toString(); const notesForTechnician = getRandomString(1, 100); const isVehicleProtected = getRandomBoolean(); - const mainInitialState = { + const storeData = { order: { serviceLocation: { address, @@ -258,13 +303,8 @@ describe('contactDetails.vue', () => { } } }; - mountOptions.global.plugins = [createTestingPinia({ - initialState: { - main: mainInitialState - } - })]; - const wrapper = shallowMount(contactDetails, mountOptions); + const { wrapper } = setupMocks({ storeData }); // Assert expect(wrapper.vm.address).toBe(address); @@ -280,11 +320,7 @@ describe('contactDetails.vue', () => { describe('Navigation', () => { test('Back button clicked triggers navigation', () => { // Arrange - const wrapper = shallowMount(contactDetails, getMountOptions({ - router: { - navigateWithSpinner: jest.fn() - } - })); + const { wrapper } = setupMocks({}); wrapper.vm.navigateBack = baseMixin.methods.navigateBack; // Act @@ -295,41 +331,25 @@ describe('contactDetails.vue', () => { expect(wrapper.vm.$router.navigateWithSpinner) .toHaveBeenCalledWith(navigationScenarios.CLICKED_BACK, undefined); }); - test('Forward button clicked triggers appropriate navigation', () => { + test('Forward button clicked triggers appropriate navigation', async () => { // Arrange - const mountOptions = getMountOptions({ - router: { - navigate: jest.fn() - }, - navigationScenarios - }); - const mainInitialState = { + const storeData = { order: { serviceLocation: { IsSafeliteProvider: true } } }; - mountOptions.global.plugins = [createTestingPinia({ - initialState: { - main: mainInitialState - } - })]; - const wrapper = shallowMount(contactDetails, mountOptions); + const { wrapper } = setupMocks({ storeData }); // Act - wrapper.vm.forwardButtonAction(); + await wrapper.vm.forwardButtonAction(); // Assert expect(wrapper.vm.$router.navigate).toHaveBeenCalled(); expect(wrapper.vm.$router.navigate) .toHaveBeenCalledWith(navigationScenarios.CLICKED_FORWARD, undefined); }); - test('Forward button click updates service location info', () => { + test('Forward button click updates service location info', async () => { // Arrange - const mountOptions = getMountOptions({ - router: { - navigate: jest.fn() - } - }); const provider = { address: { city: getRandomString(4, 20), @@ -342,19 +362,14 @@ describe('contactDetails.vue', () => { phoneNumber: getRandomInt(1000000000, 9999999999).toString(), providerNumber: getRandomInt(100000, 999999).toString() } - const mainInitialState = { + const storeData = { order: { serviceLocation: { provider } } }; - mountOptions.global.plugins = [createTestingPinia({ - initialState: { - main: mainInitialState - } - })]; - const wrapper = shallowMount(contactDetails, mountOptions); + const { wrapper } = setupMocks({ storeData }); const address = getRandomString(10, 50); const address2 = getRandomString(0, 50); const city = getRandomString(4, 20); @@ -371,7 +386,7 @@ describe('contactDetails.vue', () => { }); // Act - wrapper.vm.forwardButtonAction(); + await wrapper.vm.forwardButtonAction(); // Assert expect(useMainStore().updateServiceLocation).toHaveBeenCalledWith({ @@ -383,21 +398,16 @@ describe('contactDetails.vue', () => { }); }); - test('Forward button click updates notes for technician', () => { + test('Forward button click updates notes for technician', async () => { // Arrange - const mountOptions = getMountOptions({ - router: { - navigate: jest.fn() - } - }); - const wrapper = shallowMount(contactDetails, mountOptions); + const { wrapper } = setupMocks({}); const notesForTechnician = getRandomString(1, 100); wrapper.setData({ notesForTechnician }); // Act - wrapper.vm.forwardButtonAction(); + await wrapper.vm.forwardButtonAction(); // Assert expect(useMainStore().updateContactInfo).toHaveBeenCalledWith({ diff --git a/src/layouts/contact-details/contact-details.vue b/src/layouts/contact-details/contact-details.vue index 8222f36a..8fff6681 100644 --- a/src/layouts/contact-details/contact-details.vue +++ b/src/layouts/contact-details/contact-details.vue @@ -291,36 +291,41 @@ export default { const acSessionToken = new window.google.maps.places.AutocompleteSessionToken(); const addressValue = `${this.address}, ${this.city}, ${this.state} ${this.zipCode}`; - acService.getPlacePredictions( - { - input: addressValue, - type: ['geocode'], - componentRestrictions: { country: ['us'] }, - sessionToken: acSessionToken - }, - (predictions) => { - if (predictions && predictions.length > 0) { - const firstPrediction = predictions[0]; - if (firstPrediction.place_id) { - placeService.getDetails( - { - placeId: firstPrediction.place_id, - fields: ['address_components'], - sessionToken: acSessionToken - }, - (details) => { - this.fillInAddress(details); - resolve(); - } - ); + try { + acService.getPlacePredictions( + { + input: addressValue, + type: ['geocode'], + componentRestrictions: { country: ['us'] }, + sessionToken: acSessionToken + }, + (predictions) => { + if (predictions && predictions.length > 0) { + const firstPrediction = predictions[0]; + if (firstPrediction.place_id) { + placeService.getDetails( + { + placeId: firstPrediction.place_id, + fields: ['address_components'], + sessionToken: acSessionToken + }, + (details) => { + this.fillInAddress(details); + resolve(); + } + ); + } else { + resolve(); + } } else { resolve(); } - } else { - resolve(); } - } - ); + ); + } catch (error) { + window.console.warn('Error initializing Google Places API services', error); + resolve(); + } }); return autoCompletePromise; From 52645df56c7c340e93ce2380689d3b878c5db92a Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Thu, 5 Mar 2026 10:31:41 -0500 Subject: [PATCH 5/7] INSR-8688: More awaits and broader try/catch for google places API calls --- .../contact-details/contact-details.spec.js | 5 +--- .../contact-details/contact-details.vue | 24 +++++++++---------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/layouts/contact-details/contact-details.spec.js b/src/layouts/contact-details/contact-details.spec.js index b56a929b..6d1be304 100644 --- a/src/layouts/contact-details/contact-details.spec.js +++ b/src/layouts/contact-details/contact-details.spec.js @@ -13,10 +13,7 @@ import { useMainStore } from '@/store/index.js'; /** @ignore */ function setupMocks({ storeData, - props, - isShallowMount = true, - querySelectorFunction, - geocoderResult = ['1234 Test Street'] + props }) { const mountOptions = getMountOptions({ router: { diff --git a/src/layouts/contact-details/contact-details.vue b/src/layouts/contact-details/contact-details.vue index 8fff6681..b73d3fc4 100644 --- a/src/layouts/contact-details/contact-details.vue +++ b/src/layouts/contact-details/contact-details.vue @@ -158,9 +158,9 @@ export default { async beforeRouteEnter(to, from, next) { const cmsContent = await fetchCmsContentForPage(to.query.issPage); - next((vm) => { + next(async (vm) => { vm.setCmsContent(cmsContent); - vm.setupAddressLookup(); + await vm.setupAddressLookup(); }); }, data() { @@ -272,10 +272,10 @@ export default { this.address2 = ''; this.city = ''; }, - setupAddressLookup() { + async setupAddressLookup() { const apiKey = applicationConfig.GOOGLE_PLACES_API_KEY; - this.$loadScript(`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=Function.prototype`) + await this.$loadScript(`https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=Function.prototype`) .catch(() => { // Failed to fetch script window.console.warn('Unable to load Google Places API script'); @@ -283,15 +283,15 @@ export default { }, geocodeAddress() { const autoCompletePromise = new Promise((resolve, reject) => { - // Get Autocomplete Service - const acService = new window.google.maps.places.AutocompleteService(); - // Get Places Service, needs psuedo element (or a map) - const placeService = new window.google.maps.places.PlacesService(document.createElement('div')); - // Create Autocomplete Session token, multiple requests one pricing hit - const acSessionToken = new window.google.maps.places.AutocompleteSessionToken(); - - const addressValue = `${this.address}, ${this.city}, ${this.state} ${this.zipCode}`; try { + // Get Autocomplete Service + const acService = new window.google.maps.places.AutocompleteService(); + // Get Places Service, needs pseudo element (or a map) + const placeService = new window.google.maps.places.PlacesService(document.createElement('div')); + // Create Autocomplete Session token, multiple requests one pricing hit + const acSessionToken = new window.google.maps.places.AutocompleteSessionToken(); + + const addressValue = `${this.address}, ${this.city}, ${this.state} ${this.zipCode}`; acService.getPlacePredictions( { input: addressValue, From 10f4469ebbbca100cff3c74a7e4d8bb68ebc7363 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Fri, 6 Mar 2026 09:57:01 -0500 Subject: [PATCH 6/7] INSR-8720: Fix payment-page defects - Adjust error message font size - Hide redundant loading modal for PayPal - Adjust iframe resize behavior for when Afterpay opens in the iframe instead of as a pop-up --- public/css/hop-styling.css | 45 ++++++------ public/css/site-2020Styling.css | 83 ---------------------- public/scss/hop-styling.scss | 47 ++++++++----- public/scss/site-2020Styling.scss | 85 +---------------------- src/layouts/payment-page/payment-page.vue | 11 ++- 5 files changed, 63 insertions(+), 208 deletions(-) diff --git a/public/css/hop-styling.css b/public/css/hop-styling.css index f6d8b552..bfe1f5f3 100644 --- a/public/css/hop-styling.css +++ b/public/css/hop-styling.css @@ -171,23 +171,38 @@ body .buttonContainer .btn-success:focus, body .buttonContainer .btn-success:act background-color: #0c7e47; box-shadow: 0 0 0 3px #ffffff, 0 0 0 5.5px #0c7e47; } -body .has-error input, -body .has-error select { +body .has-error input.form-control, +body .has-error select.form-control { border-color: #db0020; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +body .has-error input.form-control:focus, +body .has-error select.form-control:focus { + box-shadow: none; } body .has-error select { background-image: url(/icons/select-error.png); } body .has-error small { + font-size: 1rem; + line-height: 1.56; margin-bottom: 0.3125rem; color: #db0020; } -body .has-success input, -body .has-success select { +body .has-success .control-label { + color: #4d4e53; +} +body .has-success input.form-control, +body .has-success select.form-control { border-color: #0c7e47; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +body .has-success input.form-control:focus, +body .has-success select.form-control:focus { + box-shadow: none; } body .has-success select { - background-image: url(/icons/select-error.png); + background-image: url(/icons/select-success.png); } body .cc-error-container #alert-message { background-color: #fcbfbb; @@ -337,17 +352,6 @@ body .cc-error-container #alert-message { } } -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: Glyphicons Halflings; - font-style: normal; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; -} - .form-control-feedback { position: absolute; top: 0; @@ -361,11 +365,12 @@ body .cc-error-container #alert-message { pointer-events: none; top: 27px; } - -.glyphicon-remove:before { +.form-control-feedback.glyphicon { + display: none; +} +.form-control-feedback.glyphicon-remove:before { content: "\e014"; } - -.glyphicon-ok:before { +.form-control-feedback.glyphicon-ok:before { content: "\e013"; } diff --git a/public/css/site-2020Styling.css b/public/css/site-2020Styling.css index 7327505d..e235fc85 100644 --- a/public/css/site-2020Styling.css +++ b/public/css/site-2020Styling.css @@ -4,95 +4,12 @@ /* IMPORTANT: READ README.md in this folder! Do not edit .css file directly! */ /* Style the modal when PayPal button is selected */ #pageLoadingModal { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.4); - display: flex; -} -#pageLoadingModal .modal-content { - margin: auto auto; -} -#pageLoadingModal p { - margin: 0; - text-transform: uppercase; - font-size: 14px; -} -#pageLoadingModal .modal { - display: none; - overflow: hidden; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - -webkit-overflow-scrolling: touch; - outline: 0; -} -#pageLoadingModal .modal-content { - position: relative; - background-color: #fff; - border-radius: 8px; - background-clip: padding-box; - outline: 0; - width: 176px; - height: 136px; - display: flex; - justify-content: center; - align-items: center; - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='none' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 84 32'%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' fill='%23fff'/%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' fill='%23fff'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06.8c8.08 0 11.89.935 11.89.935 3.58.576 5.696 7.207 5.696 7.207h.727c0-2.427 1.302-2.397 2.341-2 .723.292 1.363.76 1.86 1.361 1.319 1.547.465 1.674.465 1.674h-5.067l3.846 3.01v12.016c0 2.41-2.029 2.31-2.029 2.31H22.338s-2.029.1-2.029-2.31V12.996l3.84-3.009H19.07s-.853-.127.466-1.674a4.693 4.693 0 0 1 1.86-1.361c1.032-.397 2.341-.427 2.341 2h.736s2.117-6.64 5.696-7.21c0 0 3.81-.942 11.89-.942Z' fill='%23fff' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06 8.847c7.924 0 14.685.511 14.685.511 0-2.585-2.38-6.011-2.38-6.011S50.4 2.519 42.06 2.519s-12.303.828-12.303.828-2.38 3.426-2.38 6.011c0 0 6.76-.51 14.683-.51Z' fill='%23DA291C' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M35.277 16.523a62.558 62.558 0 0 1 13.277 0m3.276-.439s2.384-2.257 8.608-2.257c0 0 1.179 2.657-2.54 3.37m-25.894-1.113s-2.387-2.257-8.611-2.257c0 0-1.16 2.579 2.543 3.37m-3.546 5.856s21.52 3.647 39.123 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: center center; - background-size: 96px; -} -#pageLoadingModal .modal-content:after { - content: ""; - border-radius: 50%; - position: absolute; - width: 5.5rem; - height: 5.5rem; - border: 0.45em solid #d4281c; - border-right-color: transparent; - animation: spinnerrotate 0.75s linear infinite; -} -@keyframes spinnerrotate { - 0% { - transform: rotate(0); - } - 100% { - transform: rotate(360deg); - } -} -#pageLoadingModal .modal-content p { - display: none; -} -#pageLoadingModal .modal-backdrop { - position: absolute; - top: 0; - right: 0; - left: 0; - background-color: #000000; -} -#pageLoadingModal .modal-backdrop.fade { - opacity: 0; - filter: alpha(opacity=0); -} -#pageLoadingModal.hide { display: none; } .buy-backdrop { background-color: rgba(0, 0, 0, 0.8) !important; } -.buy-backdrop #afterpay__iframe-checkout-container { - width: 100%; -} .buy-backdrop #afterpay__iframe-checkout-container .style_closeWrapper__lIHJe { display: none; } diff --git a/public/scss/hop-styling.scss b/public/scss/hop-styling.scss index ace72aa6..7044fdd4 100644 --- a/public/scss/hop-styling.scss +++ b/public/scss/hop-styling.scss @@ -198,12 +198,20 @@ body { .has-error { input, select { - border-color: #db0020; + &.form-control { + border-color: #db0020; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + &:focus { + box-shadow: none; + } + } } select { background-image: url(/icons/select-error.png); } small { + font-size: 1rem; + line-height: 1.56; margin-bottom: .3125rem; color: #db0020; } @@ -211,9 +219,18 @@ body { .has-success { + .control-label { + color: #4d4e53 + } input, select { - border-color: #0c7e47; + &.form-control { + border-color: #0c7e47; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + &:focus { + box-shadow: none; + } + } } select { background-image: url(/icons/select-success.png); @@ -442,17 +459,6 @@ body { } } -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: Glyphicons Halflings; - font-style: normal; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; -} - .form-control-feedback { position: absolute; top: 0; @@ -465,10 +471,13 @@ body { text-align: center; pointer-events: none; top: 27px; -} -.glyphicon-remove:before { - content: "\e014"; -} -.glyphicon-ok:before { - content: "\e013"; + &.glyphicon { + display: none; + } + &.glyphicon-remove:before { + content: "\e014"; + } + &.glyphicon-ok:before { + content: "\e013"; + } } \ No newline at end of file diff --git a/public/scss/site-2020Styling.scss b/public/scss/site-2020Styling.scss index 6ddde5dc..fe1fff81 100644 --- a/public/scss/site-2020Styling.scss +++ b/public/scss/site-2020Styling.scss @@ -5,89 +5,7 @@ /* Style the modal when PayPal button is selected */ #pageLoadingModal { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - width: 100%; - height: 100%; - background-color: rgba(0,0,0,0.40); - display: flex; - - .modal-content { - margin: auto auto; - } - - p { - margin: 0; - text-transform: uppercase; - font-size: 14px; - } - - .modal { - display: none; - overflow: hidden; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - -webkit-overflow-scrolling: touch; - outline: 0; - } - - .modal-content { - position: relative; - background-color: #fff; - border-radius: 8px; - background-clip: padding-box; - outline: 0; - width: 176px; - height: 136px; - display: flex; - justify-content: center; - align-items: center; - background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg fill='none' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 84 32'%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' fill='%23fff'/%3e%3cpath d='M22.39 27.313a3.53 3.53 0 0 0 7.058 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' fill='%23fff'/%3e%3cpath d='M54.459 27.313a3.527 3.527 0 0 0 7.054 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06.8c8.08 0 11.89.935 11.89.935 3.58.576 5.696 7.207 5.696 7.207h.727c0-2.427 1.302-2.397 2.341-2 .723.292 1.363.76 1.86 1.361 1.319 1.547.465 1.674.465 1.674h-5.067l3.846 3.01v12.016c0 2.41-2.029 2.31-2.029 2.31H22.338s-2.029.1-2.029-2.31V12.996l3.84-3.009H19.07s-.853-.127.466-1.674a4.693 4.693 0 0 1 1.86-1.361c1.032-.397 2.341-.427 2.341 2h.736s2.117-6.64 5.696-7.21c0 0 3.81-.942 11.89-.942Z' fill='%23fff' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M42.06 8.847c7.924 0 14.685.511 14.685.511 0-2.585-2.38-6.011-2.38-6.011S50.4 2.519 42.06 2.519s-12.303.828-12.303.828-2.38 3.426-2.38 6.011c0 0 6.76-.51 14.683-.51Z' fill='%23DA291C' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3cpath d='M35.277 16.523a62.558 62.558 0 0 1 13.277 0m3.276-.439s2.384-2.257 8.608-2.257c0 0 1.179 2.657-2.54 3.37m-25.894-1.113s-2.387-2.257-8.611-2.257c0 0-1.16 2.579 2.543 3.37m-3.546 5.856s21.52 3.647 39.123 0' stroke='%23000' stroke-width='.75' stroke-linecap='round' stroke-linejoin='round'/%3e%3c/svg%3e"); - background-repeat: no-repeat; - background-position: center center; - background-size: 96px; - &:after { - content: ''; - border-radius: 50%; - position: absolute; - width: 5.5rem; - height: 5.5rem; - border: 0.45em solid #d4281c; - border-right-color: transparent; - animation: spinnerrotate 0.75s linear infinite; - } - @keyframes spinnerrotate { - 0% { transform: rotate(0)} - 100% { transform: rotate(360deg)} - } - p { - display: none; - } - } - - .modal-backdrop { - position: absolute; - top: 0; - right: 0; - left: 0; - background-color: #000000; - &.fade { - opacity: 0; - filter: alpha(opacity=0); - } - } - - &.hide { - display: none; - } + display: none; } // AfterPay Styles @@ -95,7 +13,6 @@ .buy-backdrop { background-color: rgba(0,0,0,.8)!important; #afterpay__iframe-checkout-container { - width: 100%; .style_closeWrapper__lIHJe { display: none; } diff --git a/src/layouts/payment-page/payment-page.vue b/src/layouts/payment-page/payment-page.vue index 4d307e34..bb10db65 100644 --- a/src/layouts/payment-page/payment-page.vue +++ b/src/layouts/payment-page/payment-page.vue @@ -470,7 +470,7 @@ export default { const paymentType = useMainStore().order.payment.paymentMethod; const alertToDisplay = to.query[queryStrings.DISPLAY_PAY_IN_ADVANCE_ALERT]; if (paymentType === paymentMethods.PAYPAL || alertToDisplay) { - useMainStore().savePaymentMethodChoice(paymentMethods.CREDIT_CARD); + useMainStore().savePaymentMethodChoice(paymentMethods.PayNow); } // Call APIs @@ -788,6 +788,7 @@ export default { } if (event.data.includes('afterpayClosed')) { + this.allowIFrameResize(iframe); if (this.isAfterPay) { this.backButtonAction(); } else { @@ -810,7 +811,6 @@ export default { useMainStore().savePaymentMethodChoice(paymentMethods.AFTERPAY); } if (event.data.includes('applepayOpened')) { - this.showIssLoadingModal(true); useMainStore().savePaymentMethodChoice(paymentMethods.APPLEPAY); } if (event.data.includes('showLoaderForPaypal')) { @@ -863,9 +863,16 @@ export default { const iframe = this.$refs.paymentFrame; if (iframe && iframe.contentWindow) { this.afterpayModalOpen = true; + this.lockIFrameSize(iframe); iframe.contentWindow.postMessage('afterpay-switch', '*'); } }, + lockIFrameSize(iframe) { + iframe.iFrameResizer.removeListeners(); + }, + allowIFrameResize(iframe) { + iframeResize({ checkOrigin: false }, iframe); + }, showIssLoadingModal } }; From 301f8f0c5b37319dc742d6b06e0435d1045ef096 Mon Sep 17 00:00:00 2001 From: Alex Humphries Date: Fri, 6 Mar 2026 10:20:02 -0500 Subject: [PATCH 7/7] Address some comments around guarding againt iframe loading issues --- src/layouts/payment-page/payment-page.vue | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/layouts/payment-page/payment-page.vue b/src/layouts/payment-page/payment-page.vue index bb10db65..d116b502 100644 --- a/src/layouts/payment-page/payment-page.vue +++ b/src/layouts/payment-page/payment-page.vue @@ -535,7 +535,9 @@ export default { displayAmount: this.getDisplayAmountDue(), payInAdvanceLineItems: '', shouldBlockInteraction: false, - afterpayModalOpen: false + afterpayModalOpen: false, + iframeLoaded: false, + iframeResizerLoaded: false }; }, computed: { @@ -776,6 +778,7 @@ export default { } else { iframe.contentWindow.postMessage('CreditCardChosen', '*'); } + this.iframeLoaded = true; } }; } @@ -855,23 +858,27 @@ export default { }, submitPaymentAction() { const iframe = this.$refs.paymentFrame; - if (iframe && iframe.contentWindow) { + if (iframe && iframe.contentWindow && this.iframeLoaded) { iframe.contentWindow.postMessage('submitPayment', '*'); } }, switchToAfterPay() { const iframe = this.$refs.paymentFrame; - if (iframe && iframe.contentWindow) { + if (iframe && iframe.contentWindow && this.iframeLoaded) { this.afterpayModalOpen = true; this.lockIFrameSize(iframe); iframe.contentWindow.postMessage('afterpay-switch', '*'); } }, lockIFrameSize(iframe) { - iframe.iFrameResizer.removeListeners(); + if (iframe && iframe.iFrameResizer) { + iframe.iFrameResizer.removeListeners(); + } }, allowIFrameResize(iframe) { - iframeResize({ checkOrigin: false }, iframe); + if (iframe) { + iframeResize({ checkOrigin: false }, iframe); + } }, showIssLoadingModal }