Auto-complete addresses at checkout

What you need

Add store variables

  1. Open your store in StoreConnect.
  2. Select New in the Store Variables section and add the following variables.
    1. Name - Address Auto-complete Google API Key - google_autocomplete_api_key Value - your Google Maps API key Available in Liquid = true
    2. Name - Address Auto-complete Countries Key - autocomplete_countries Value - 2 character, comma separated ISO code for each country you want to support i.e. US, CA (Google supports up to five at a time) Available in Liquid = true
    3. Name - Default Auto-complete Country Key - default_autocomplete_country Value - 2 character ISO code for one country you want to include Available in Liquid = true
  3. Save.

Update your theme layout template (in Liquid)

  1. Open your theme record in StoreConnect.
  2. Open the applicable Theme Template.
  3. If your theme doesn't already have a theme template, grab it here and create one for your theme: theme.liquid
  4. Add the following code to the very bottom of the theme template Content section. This ensures address fields have loaded before the layout runs.
  5. Select Save.

{%- assign google_autocomplete_api_key = store_variables['google_autocomplete_api_key'] | default: blank -%}
{%- assign autocomplete_countries = store_variables['autocomplete_countries'] | default: 'AU,US' | strip -%}
{%- assign default_autocomplete_country = store_variables['default_autocomplete_country'] | default: 'AU' -%}

// Google Maps loader
(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})
({key: "{{ google_autocomplete_api_key }}", v: "weekly"});

const allowedCountries = '{{ autocomplete_countries }}'.split(',').map(c => c.trim()).filter(c => c);

let autocomplete;
let address1Field;
let address2Field;
let cityField;
let postalField;
let countryField;
let stateField;

async function initAutocomplete() {
    try {
        // Import the places library
        await google.maps.importLibrary('places');

        // Get form fields
        address1Field = document.querySelector('#shipping_address_line_1');
        address2Field = document.querySelector('#shipping_address_line_2');
        cityField = document.querySelector('#shipping_city__customer_information');
        postalField = document.querySelector('#shipping_postal_code__customer_information');
        countryField = document.querySelector('#shipping_country__customer_information');
        stateField = document.querySelector('#shipping_state__customer_information');

        if (!address1Field) {
            console.error('Address field not found');
            return;
        }

        // Set default country
        if (countryField) {
            countryField.value = '{{ default_autocomplete_country }}';
            countryField.dispatchEvent(new Event('change', { bubbles: true }));
        }

        // Update placeholder
        address1Field.placeholder = 'Start typing your address...';

        // Create autocomplete on the existing input using the Autocomplete class
        autocomplete = new google.maps.places.Autocomplete(address1Field, {
            componentRestrictions: { country: ['{{ default_autocomplete_country }}'] },
            fields: ['address_components', 'geometry'],
            types: ['address']
        });

        // Listen for place selection
        autocomplete.addListener('place_changed', fillInAddress);

        // Listen for country changes
        if (countryField) {
            countryField.addEventListener('change', function() {
                const selectedCountry = this.value;

                if (selectedCountry && allowedCountries.includes(selectedCountry)) {
                    // Update autocomplete country restriction
                    autocomplete.setComponentRestrictions({
                        country: [selectedCountry]
                    });

                    // Clear form fields when country changes
                    if (address1Field) address1Field.value = '';
                    if (address2Field) address2Field.value = '';
                    if (cityField) cityField.value = '';
                    if (postalField) postalField.value = '';
                    if (stateField) stateField.value = '';
                }
            });
        }

    } catch (error) {
        console.error('Failed to initialize autocomplete:', error);
    }
}

function fillInAddress() {
    const place = autocomplete.getPlace();

    if (!place.address_components) {
        console.log('No address components found');
        return;
    }

    let address1 = '';
    let postcode = '';
    let city = '';
    let state = '';

    // Parse address components
    for (const component of place.address_components) {
        const types = component.types;

        if (types.includes('street_number')) {
            address1 = component.long_name + ' ';
        }
        if (types.includes('route')) {
            address1 += component.long_name;
        }
        if (types.includes('locality')) {
            city = component.long_name;
        }
        if (types.includes('administrative_area_level_1')) {
            state = component.short_name;
        }
        if (types.includes('postal_code')) {
            postcode = component.long_name;
        }
    }

    // Fill in the form fields
    address1Field.value = address1.trim();
    if (cityField) cityField.value = city;
    if (postalField) postalField.value = postcode;

    // Set state - give state dropdown time to populate
    if (stateField && state) {
        setTimeout(() => {
            stateField.value = state;
            stateField.dispatchEvent(new Event('change', { bubbles: true }));
        }, 500);
    }

    // Focus on address line 2 for apartment/unit number
    if (address2Field) {
        address2Field.focus();
    }

    console.log('Address filled:', { address1, city, state, postcode });
}

// Initialize when DOM is ready
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initAutocomplete);
} else {
    initAutocomplete();
}

/* Style the autocomplete dropdown */
.pac-container {
    z-index: 10000 !important;
    border-radius: 8px;
    margin-top: 4px;
    box-shadow: 0 2px 6px rgba(0,0,0,0.3);
    font-family: inherit;
}

.pac-item {
    padding: 10px 12px;
    cursor: pointer;
    font-size: 14px;
    line-height: 1.4;
}

.pac-item:hover {
    background-color: #f5f5f5;
}

.pac-item-query {
    font-size: 15px;
    color: #333;
}

.pac-matched {
    font-weight: 600;
}