Last active
May 3, 2026 00:23
-
-
Save Barnabas2/5b15e1540a509a4b6b1c56a4c5d4a13a to your computer and use it in GitHub Desktop.
An address autocomplete snippet based on: https://developer.woocommerce.com/docs/features/address-autocomplete/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| /** | |
| * Custom Google Places address autocomplete provider for WooCommerce 10.3+. | |
| * The snippet enables the Address autocomplete feature under Woocommerce -> Settings -> General | |
| * | |
| * Built to work alongside Stripe (by Payment Plugins) on Block-based checkout, | |
| * since Woo's built-in admin toggle is gated to WooPayments only. | |
| * | |
| * Requires: | |
| * - A Google Cloud project with Places API (New) enabled and billing active. | |
| * - The API key restricted to your server's outbound IPv4 + IPv6 addresses. | |
| * - A budget alert configured in Google Cloud Billing as a safety net. | |
| */ | |
| defined( 'ABSPATH' ) || exit; | |
| /* ------------------------------------------------------------------------- | |
| * Configuration | |
| * ------------------------------------------------------------------------- */ | |
| if ( ! defined( 'MYSTORE_ADDR_PROVIDER_ID' ) ) { | |
| define( 'MYSTORE_ADDR_PROVIDER_ID', 'mystore-google-places' ); | |
| define( 'MYSTORE_ADDR_REST_NS', 'mystore-address/v1' ); | |
| define( 'MYSTORE_ADDR_GOOGLE_KEY', 'Google-Places-API-KEY' ); //👈 Add your API key here | |
| } | |
| /* ------------------------------------------------------------------------- | |
| * Force-enable the address autocomplete checkbox and select our provider | |
| * as preferred, since Woo's admin UI gates these settings to WooPayments only. | |
| * ------------------------------------------------------------------------- */ | |
| add_action( 'init', function () { | |
| if ( get_option( 'woocommerce_address_autocomplete_enabled' ) !== 'yes' ) { | |
| update_option( 'woocommerce_address_autocomplete_enabled', 'yes' ); | |
| } | |
| if ( get_option( 'woocommerce_address_autocomplete_preferred_provider' ) !== MYSTORE_ADDR_PROVIDER_ID ) { | |
| update_option( 'woocommerce_address_autocomplete_preferred_provider', MYSTORE_ADDR_PROVIDER_ID ); | |
| } | |
| } ); | |
| /* ------------------------------------------------------------------------- | |
| * 1. Provider class — registered via woocommerce_address_providers filter. | |
| * ------------------------------------------------------------------------- */ | |
| add_action( 'plugins_loaded', function () { | |
| if ( ! class_exists( 'WC_Address_Provider' ) || class_exists( 'MyStore_Google_Places_Provider' ) ) { | |
| return; | |
| } | |
| class MyStore_Google_Places_Provider extends WC_Address_Provider implements JsonSerializable { | |
| public $id = 'mystore-google-places'; | |
| public $name = 'Google Places'; | |
| public $branding_html = '<div style="font-size:11px;color:#666;padding:4px 8px;">Powered by Google</div>'; | |
| public function __construct() { | |
| $this->id = 'mystore-google-places'; | |
| $this->name = 'Google Places'; | |
| $this->branding_html = '<div style="font-size:11px;color:#666;padding:4px 8px;">Powered by Google</div>'; | |
| } | |
| public function jsonSerialize() : array { | |
| return [ | |
| 'id' => $this->id, | |
| 'name' => $this->name, | |
| 'branding_html' => $this->branding_html, | |
| ]; | |
| } | |
| } | |
| }, 20 ); | |
| add_filter( 'woocommerce_address_providers', function ( $providers ) { | |
| if ( class_exists( 'MyStore_Google_Places_Provider' ) ) { | |
| $providers[] = new MyStore_Google_Places_Provider(); | |
| } | |
| return $providers; | |
| }, 10, 1 ); | |
| /* ------------------------------------------------------------------------- | |
| * 2. REST endpoints (search + details). | |
| * ------------------------------------------------------------------------- */ | |
| add_action( 'rest_api_init', function () { | |
| register_rest_route( MYSTORE_ADDR_REST_NS, '/address-search', [ | |
| 'methods' => 'POST', | |
| 'callback' => 'mystore_addr_handle_search', | |
| 'permission_callback' => 'mystore_addr_check_nonce', | |
| 'args' => [ | |
| 'query' => [ 'required' => true, 'type' => 'string' ], | |
| 'country' => [ 'required' => true, 'type' => 'string' ], | |
| 'type' => [ 'required' => false, 'type' => 'string' ], | |
| ], | |
| ] ); | |
| register_rest_route( MYSTORE_ADDR_REST_NS, '/address-details', [ | |
| 'methods' => 'POST', | |
| 'callback' => 'mystore_addr_handle_details', | |
| 'permission_callback' => 'mystore_addr_check_nonce', | |
| 'args' => [ | |
| 'place_id' => [ 'required' => true, 'type' => 'string' ], | |
| ], | |
| ] ); | |
| } ); | |
| function mystore_addr_check_nonce( WP_REST_Request $request ) { | |
| $nonce = $request->get_header( 'x-wp-nonce' ); | |
| return (bool) wp_verify_nonce( $nonce, 'wp_rest' ); | |
| } | |
| /** | |
| * POST /address-search → returns a list of suggestions for the given query. | |
| */ | |
| function mystore_addr_handle_search( WP_REST_Request $request ) { | |
| $query = sanitize_text_field( $request->get_param( 'query' ) ); | |
| $country = strtoupper( sanitize_text_field( $request->get_param( 'country' ) ) ); | |
| if ( strlen( $query ) < 3 ) { | |
| return rest_ensure_response( [] ); | |
| } | |
| $body = [ | |
| 'input' => $query, | |
| 'includedRegionCodes' => [ $country ], | |
| 'languageCode' => substr( get_locale(), 0, 2 ), | |
| ]; | |
| $response = wp_remote_post( 'https://places.googleapis.com/v1/places:autocomplete', [ | |
| 'timeout' => 8, | |
| 'headers' => [ | |
| 'Content-Type' => 'application/json', | |
| 'X-Goog-Api-Key' => MYSTORE_ADDR_GOOGLE_KEY, | |
| ], | |
| 'body' => wp_json_encode( $body ), | |
| ] ); | |
| if ( is_wp_error( $response ) ) { | |
| return rest_ensure_response( [] ); | |
| } | |
| $data = json_decode( wp_remote_retrieve_body( $response ), true ); | |
| $suggestions = []; | |
| if ( ! empty( $data['suggestions'] ) ) { | |
| foreach ( $data['suggestions'] as $s ) { | |
| if ( empty( $s['placePrediction'] ) ) { | |
| continue; | |
| } | |
| $pred = $s['placePrediction']; | |
| $matches = []; | |
| $matched = $pred['structuredFormat']['mainText']['matches'] ?? []; | |
| foreach ( $matched as $m ) { | |
| $start = (int) ( $m['startOffset'] ?? 0 ); | |
| $end = (int) ( $m['endOffset'] ?? 0 ); | |
| $matches[] = [ | |
| 'offset' => $start, | |
| 'length' => $end - $start, | |
| ]; | |
| } | |
| $suggestions[] = [ | |
| 'id' => $pred['placeId'], | |
| 'label' => $pred['text']['text'] ?? '', | |
| 'matchedSubstrings' => $matches, | |
| ]; | |
| } | |
| } | |
| return rest_ensure_response( $suggestions ); | |
| } | |
| /** | |
| * POST /address-details → returns full address fields for the chosen place_id. | |
| */ | |
| function mystore_addr_handle_details( WP_REST_Request $request ) { | |
| $place_id = sanitize_text_field( $request->get_param( 'place_id' ) ); | |
| if ( empty( $place_id ) ) { | |
| return new WP_Error( 'no_place_id', 'Missing place_id', [ 'status' => 400 ] ); | |
| } | |
| $response = wp_remote_get( 'https://places.googleapis.com/v1/places/' . rawurlencode( $place_id ), [ | |
| 'timeout' => 8, | |
| 'headers' => [ | |
| 'X-Goog-Api-Key' => MYSTORE_ADDR_GOOGLE_KEY, | |
| 'X-Goog-FieldMask' => 'addressComponents,formattedAddress', | |
| ], | |
| ] ); | |
| if ( is_wp_error( $response ) ) { | |
| return new WP_Error( 'http_error', 'Lookup failed', [ 'status' => 502 ] ); | |
| } | |
| $data = json_decode( wp_remote_retrieve_body( $response ), true ); | |
| if ( empty( $data['addressComponents'] ) ) { | |
| return new WP_Error( 'no_components', 'No address data', [ 'status' => 404 ] ); | |
| } | |
| return rest_ensure_response( mystore_addr_parse_components( $data['addressComponents'] ) ); | |
| } | |
| /** | |
| * Map Google's address components to WooCommerce address fields. | |
| */ | |
| function mystore_addr_parse_components( array $components ) { | |
| $get = function ( $type ) use ( $components ) { | |
| foreach ( $components as $c ) { | |
| if ( in_array( $type, $c['types'] ?? [], true ) ) { | |
| return $c; | |
| } | |
| } | |
| return null; | |
| }; | |
| $street_number = ( $c = $get( 'street_number' ) ) ? ( $c['shortText'] ?? '' ) : ''; | |
| $route = ( $c = $get( 'route' ) ) ? ( $c['longText'] ?? '' ) : ''; | |
| $subpremise = ( $c = $get( 'subpremise' ) ) ? ( $c['longText'] ?? '' ) : ''; | |
| $city = ''; | |
| foreach ( [ 'postal_town', 'locality', 'sublocality_level_1', 'administrative_area_level_2' ] as $type ) { | |
| if ( $c = $get( $type ) ) { | |
| $city = $c['longText'] ?? ''; | |
| if ( $city ) { | |
| break; | |
| } | |
| } | |
| } | |
| return [ | |
| 'address_1' => trim( $street_number . ' ' . $route ), | |
| 'address_2' => $subpremise, | |
| 'city' => $city, | |
| 'state' => ( $c = $get( 'administrative_area_level_1' ) ) ? ( $c['shortText'] ?? '' ) : '', | |
| 'postcode' => ( $c = $get( 'postal_code' ) ) ? ( $c['longText'] ?? '' ) : '', | |
| 'country' => ( $c = $get( 'country' ) ) ? ( $c['shortText'] ?? '' ) : '', | |
| ]; | |
| } | |
| /* ------------------------------------------------------------------------- | |
| * 3. Enqueue the client-side provider JS on checkout. | |
| * Config is interpolated into the JS so there's no race with separate | |
| * inline scripts. | |
| * ------------------------------------------------------------------------- */ | |
| add_action( 'wp_enqueue_scripts', function () { | |
| if ( ! function_exists( 'is_checkout' ) || ! is_checkout() ) { | |
| return; | |
| } | |
| if ( get_option( 'woocommerce_address_autocomplete_enabled' ) !== 'yes' ) { | |
| return; | |
| } | |
| $cfg_json = wp_json_encode( [ | |
| 'restUrl' => esc_url_raw( rest_url( MYSTORE_ADDR_REST_NS . '/' ) ), | |
| 'nonce' => wp_create_nonce( 'wp_rest' ), | |
| ] ); | |
| $provider_js = <<<JS | |
| ( function () { | |
| 'use strict'; | |
| const cfg = {$cfg_json}; | |
| const post = async ( endpoint, payload ) => { | |
| const res = await fetch( cfg.restUrl + endpoint, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'X-WP-Nonce': cfg.nonce, | |
| }, | |
| body: JSON.stringify( payload ), | |
| } ); | |
| if ( ! res.ok ) { | |
| throw new Error( 'Request failed: ' + res.status ); | |
| } | |
| return res.json(); | |
| }; | |
| const provider = { | |
| id: 'mystore-google-places', | |
| canSearch: function ( country ) { | |
| return Boolean( country ); | |
| }, | |
| search: async function ( query, country, type ) { | |
| try { | |
| const results = await post( 'address-search', { query, country, type } ); | |
| return Array.isArray( results ) ? results : []; | |
| } catch ( e ) { | |
| return []; | |
| } | |
| }, | |
| select: async function ( addressId ) { | |
| try { | |
| return await post( 'address-details', { place_id: addressId } ); | |
| } catch ( e ) { | |
| return null; | |
| } | |
| }, | |
| }; | |
| const register = () => { | |
| if ( | |
| window.wc && | |
| window.wc.addressAutocomplete && | |
| window.wc.addressAutocomplete.registerAddressAutocompleteProvider | |
| ) { | |
| try { | |
| window.wc.addressAutocomplete.registerAddressAutocompleteProvider( provider ); | |
| return true; | |
| } catch ( e ) { | |
| return false; | |
| } | |
| } | |
| return false; | |
| }; | |
| if ( register() ) return; | |
| document.addEventListener( 'DOMContentLoaded', register ); | |
| window.addEventListener( 'load', register ); | |
| let tries = 0; | |
| const t = setInterval( function () { | |
| if ( register() || ++tries > 300 ) { | |
| clearInterval( t ); | |
| } | |
| }, 200 ); | |
| } )(); | |
| JS; | |
| wp_register_script( 'mystore-address-provider', '', [ 'wc-address-autocomplete' ], '1.0', true ); | |
| wp_enqueue_script( 'mystore-address-provider' ); | |
| wp_add_inline_script( 'mystore-address-provider', $provider_js, 'after' ); | |
| } ); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment