'/^AIza[0-9A-Za-z_\-]{35}\z/', 'geoapify' => '/^[0-9a-f]{32}\z/i', 'here' => '/^[0-9A-Za-z_\-]{30,64}\z/', ); /** Google Cloud console, credentials page. */ const CONSOLE_URL = 'https://console.cloud.google.com/google/maps-apis/credentials'; /** The plugin's own page on the author's site (setup guide, screenshots, contact). */ const PLUGIN_PAGE = 'https://wedoingood.com/plugins/streetfill/'; /** The plugin's support forum on WordPress.org. */ const SUPPORT_URL = 'https://wordpress.org/support/plugin/streetfill/'; /** A website that is never the shop's: the server half of the key check claims to come from it (Geoapify, HERE). */ const FOREIGN_SITE = 'https://key-check.streetfill.invalid'; /** Where each service's keys are managed. */ const CONSOLE_URLS = array( 'google' => 'https://console.cloud.google.com/google/maps-apis/credentials', 'geoapify' => 'https://myprojects.geoapify.com/', 'here' => 'https://platform.here.com/', ); /** * Service names, not translated. * * @return array */ public static function service_names() { return Settings::service_names(); } /** * Link text for where a service's keys are made. * * @param string $service google, geoapify or here. * @return string */ public static function console_label( $service ) { if ( 'google' === $service ) { return __( 'Open the Google Cloud console', 'streetfill' ); } return sprintf( /* translators: %s: address service, e.g. Geoapify or HERE */ __( 'Get a %s key', 'streetfill' ), self::service_names()[ $service ] ); } /** * Hooks. */ public static function init() { add_action( 'admin_menu', array( __CLASS__, 'menu' ), 60 ); add_action( 'admin_post_' . self::ACTION, array( __CLASS__, 'save' ) ); add_action( 'wp_ajax_' . self::CHECK, array( __CLASS__, 'key_check' ) ); add_action( 'admin_enqueue_scripts', array( __CLASS__, 'assets' ) ); add_action( 'admin_notices', array( __CLASS__, 'plugins_notice' ) ); add_filter( 'plugin_action_links_' . plugin_basename( STREETFILL_FILE ), array( __CLASS__, 'action_links' ) ); add_filter( 'plugin_row_meta', array( __CLASS__, 'row_meta' ), 10, 2 ); add_action( 'admin_init', array( __CLASS__, 'privacy_policy' ) ); } /** * Suggested text for the site's privacy policy (Settings → Privacy). */ public static function privacy_policy() { if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { return; } $privacy = array( 'google' => array( 'Google', 'https://policies.google.com/privacy' ), 'geoapify' => array( 'Geoapify', 'https://www.geoapify.com/privacy-policy/' ), 'here' => array( 'HERE', 'https://www.here.com/en-gb/privacy' ), ); $service = Settings::service(); $text = '

' . esc_html( sprintf( /* translators: 1: address service, e.g. Google Maps, Geoapify or HERE; 2: company, e.g. Google, Geoapify or HERE; 3: link to its privacy policy. This text is for the shop's customers. */ __( 'When you type your address at checkout or in your account, address suggestions come from %1$s. Your browser then sends the typed text, the selected country, the site language and your IP address to %2$s. %2$s’s privacy policy applies: %3$s', 'streetfill' ), self::service_names()[ $service ], $privacy[ $service ][0], $privacy[ $service ][1] ) ) . '

'; wp_add_privacy_policy_content( __( 'Streetfill', 'streetfill' ), wp_kses_post( $text ) ); } /** * The screen under WooCommerce and its Help tab. */ public static function menu() { $hook = add_submenu_page( 'woocommerce', __( 'Streetfill', 'streetfill' ), __( 'Streetfill', 'streetfill' ), self::CAP, self::PAGE, array( __CLASS__, 'render' ) ); if ( $hook ) { add_action( 'load-' . $hook, array( __CLASS__, 'help' ) ); } } /** * Screen URL. * * @param string $tab Tab ('settings' is the screen without a tab). * @param array $args Extra query arguments. * @return string */ public static function url( $tab = 'settings', array $args = array() ) { $query = array( 'page' => self::PAGE ); if ( 'settings' !== $tab && '' !== $tab ) { $query['tab'] = sanitize_key( $tab ); } return add_query_arg( array_merge( $query, $args ), admin_url( 'admin.php' ) ); } /** * Tabs of the screen: Settings first, then the tabs of add-ons. * * @return array Slug => label. */ public static function tabs() { $tabs = array( 'settings' => __( 'Settings', 'streetfill' ) ); /** * Tabs an add-on shows after Settings. * * @internal For add-ons. Not a public API: may change without notice. * * @param array $extra Slug => label. */ $extra = apply_filters( 'streetfill_admin_tabs', array() ); foreach ( is_array( $extra ) ? $extra : array() as $slug => $label ) { $slug = is_string( $slug ) ? sanitize_key( $slug ) : ''; if ( '' !== $slug && ! isset( $tabs[ $slug ] ) && is_string( $label ) && '' !== trim( $label ) ) { $tabs[ $slug ] = $label; } } return $tabs; } /** * Tab shown on this request. * * @return string */ public static function current_tab() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only tab switch. $tab = isset( $_GET['tab'] ) && is_string( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'settings'; return isset( self::tabs()[ $tab ] ) ? $tab : 'settings'; } /** * "Settings" first in the plugin row. * * @param mixed $links Links. * @return mixed */ public static function action_links( $links ) { if ( is_array( $links ) && current_user_can( self::CAP ) ) { array_unshift( $links, '' . esc_html__( 'Settings', 'streetfill' ) . '' ); } return $links; } /** * Support forum in the plugin's own row on the Plugins screen, next to the * plugin page that WordPress adds from the Plugin URI header. * * @param mixed $links Row links. * @param string $file Plugin file the row belongs to. * @return mixed */ public static function row_meta( $links, $file ) { if ( is_array( $links ) && plugin_basename( STREETFILL_FILE ) === $file ) { $links[] = '' . esc_html__( 'Support forum', 'streetfill' ) . ''; } return $links; } /** * On the Plugins screen only: WooCommerce is older than Streetfill needs. */ public static function plugins_notice() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || 'plugins' !== $screen->id || Plugin::wc_supported() || ! current_user_can( 'activate_plugins' ) ) { return; } echo '

' . esc_html( sprintf( /* translators: %s: WooCommerce version number, e.g. 10.3 */ __( 'Streetfill needs WooCommerce %s or newer. Address suggestions stay off until WooCommerce is updated.', 'streetfill' ), STREETFILL_MIN_WC ) ) . '

'; } /** * The address pattern a key should be restricted to, e.g. https://shop.example/*. * * @return string */ public static function site_pattern() { return trailingslashit( home_url() ) . '*'; } /** * Script and style of our screen. * * @param string $hook Admin page hook. */ public static function assets( $hook ) { if ( self::SCREEN !== $hook ) { return; } wp_enqueue_style( 'streetfill-admin', STREETFILL_URL . 'assets/css/admin.css', array(), STREETFILL_VERSION ); wp_enqueue_script( 'streetfill-admin', STREETFILL_URL . 'assets/js/admin.js', array( 'wp-i18n' ), STREETFILL_VERSION, true ); wp_set_script_translations( 'streetfill-admin', 'streetfill', STREETFILL_PATH . 'languages' ); wp_add_inline_script( 'streetfill-admin', 'var streetfillAdmin = ' . wp_json_encode( array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( self::CHECK ), 'action' => self::CHECK, 'service' => Settings::service(), 'names' => self::service_names(), 'mapsUrl' => 'google' === Settings::service() && Settings::has_key() ? Assets::maps_url() : '', 'savedKey' => Settings::key(), 'site' => self::site_pattern(), 'consoleUrl' => self::CONSOLE_URLS[ Settings::service() ], ) ) . ';', 'before' ); } /** * Help tab: how to get a key, what the check results mean, FAQ. */ public static function help() { $screen = get_current_screen(); if ( ! $screen ) { return; } $steps = array( 'Google Maps' => array( __( 'Open the Google Cloud console and create a project with billing turned on. Google includes free monthly usage; see “Google Maps Platform pricing” on the right of this Help panel.', 'streetfill' ), __( 'Enable two APIs: “Maps JavaScript API” and “Places API (New)”.', 'streetfill' ), sprintf( // translators: %s: the shop's address pattern, e.g. https://example.com/* (keeps the trailing /*). __( 'Create an API key. Under “Application restrictions” choose “Websites” and add %s (it covers your shop and its admin). Under “API restrictions” allow only the two APIs above.', 'streetfill' ), self::site_pattern() ), __( 'Paste the key into Streetfill, save, and press “Check key”.', 'streetfill' ), __( 'Optional: set a daily request limit (quota) for “Places API (New)” in the Google Cloud console, so a mistake can never cost more than you planned.', 'streetfill' ), ), 'Geoapify' => array( __( 'Sign up at myprojects.geoapify.com (no payment card needed) and create a project.', 'streetfill' ), sprintf( // translators: %s: the shop's address pattern, e.g. https://example.com/* (keeps the trailing /*). __( 'Copy the project’s API key. In the key’s settings, allow only your website: %s.', 'streetfill' ), self::site_pattern() ), __( 'Choose Geoapify under “Address service”, paste the key, save, and press “Check key”.', 'streetfill' ), ), 'HERE' => array( __( 'Sign up at platform.here.com (Base plan) and create an app.', 'streetfill' ), sprintf( // translators: %s: the shop's address, e.g. https://example.com (without a trailing slash). __( 'Create an API key for the app and copy it. Under “Trusted domains”, turn on “Enable trusted domains” and add %s.', 'streetfill' ), untrailingslashit( home_url() ) ), __( 'Choose HERE under “Address service”, paste the key, save, and press “Check key”.', 'streetfill' ), ), ); // The chosen service first. $names = self::service_names(); $order = array_unique( array_merge( array( Settings::service() ), array_keys( $names ) ) ); $html = ''; foreach ( $order as $slug ) { $html .= '

' . esc_html( $names[ $slug ] ) . '

    '; foreach ( $steps[ $names[ $slug ] ] as $step ) { $html .= '
  1. ' . esc_html( $step ) . '
  2. '; } $html .= '

' . esc_html( self::console_label( $slug ) ) . ' 

'; } $html .= '

' . esc_html__( 'Then turn on WooCommerce → Settings → General → Address autocomplete. If several are listed, choose Streetfill.', 'streetfill' ) . '

'; $screen->add_help_tab( array( 'id' => 'streetfill-key', 'title' => __( 'How to get a key', 'streetfill' ), 'content' => $html, ) ); $results = array( __( 'The key works and is restricted to your website.', 'streetfill' ) => __( 'All set.', 'streetfill' ), 'google' => '', __( 'The key works, but anyone who copies it can use it at your cost.', 'streetfill' ) => __( 'The key is not restricted to your website. Browser keys are visible in the page, so add your website under “Application restrictions → Websites” in the Google Cloud console.', 'streetfill' ), __( 'The key works. Could not check the key’s restrictions from the server.', 'streetfill' ) => __( 'Searches work, but the server check gave no clear answer (for example, your server could not reach Google). Check “Application restrictions” in the Google Cloud console yourself.', 'streetfill' ), __( 'Turn on “Places API (New)” and “Maps JavaScript API” for this key.', 'streetfill' ) => __( 'Google refused the search because one of the two APIs is not enabled in the key’s project, or not allowed under the key’s “API restrictions”. Enable and allow both.', 'streetfill' ), __( 'Google says this key does not exist.', 'streetfill' ) => __( 'The key was mistyped, cut short or deleted. Copy it again from the Google Cloud console.', 'streetfill' ), __( 'Google rejected the key on this site.', 'streetfill' ) => __( 'The key is restricted to other websites. Check the list under “Websites” in the key’s restrictions.', 'streetfill' ), __( 'Google Maps did not answer within 15 seconds.', 'streetfill' ) => __( 'The browser could not load Google Maps: a slow connection, an ad blocker, or a security plugin that blocks maps.googleapis.com.', 'streetfill' ), 'other' => '', __( 'Geoapify or HERE says this key is not valid.', 'streetfill' ) => __( 'The key was mistyped, cut short or deleted. Copy it again from your Geoapify project or HERE app.', 'streetfill' ), __( 'Geoapify or HERE rejected the key on this site.', 'streetfill' ) => __( 'The key is limited to other websites. Check the allowed websites in the key’s settings.', 'streetfill' ), ); $html = ''; $heads = array( 'google' => $names['google'], 'other' => $names['geoapify'] . ' / ' . $names['here'], ); foreach ( $results as $title => $text ) { if ( isset( $heads[ $title ] ) ) { $html .= '

' . esc_html( $heads[ $title ] ) . '

'; continue; } $html .= '

' . esc_html( (string) $title ) . '

' . esc_html( $text ) . '

'; } $screen->add_help_tab( array( 'id' => 'streetfill-check', 'title' => __( 'Check key results', 'streetfill' ), 'content' => $html, ) ); $faq = array( __( 'Where do I find Streetfill after installing it?', 'streetfill' ) => __( 'In the admin menu, go to WooCommerce → Streetfill. You can also click “Settings” under the plugin’s name on the Plugins screen.', 'streetfill' ), __( 'Where do customers see suggestions?', 'streetfill' ) => __( 'In the billing and shipping address of the block checkout and the classic checkout, and when they edit an address in My account. WooCommerce shows the list; Streetfill gets the addresses from the service you chose.', 'streetfill' ), __( 'Which service should I choose?', 'streetfill' ) => __( 'Google Maps has the most complete addresses, but needs a Google Cloud account with billing. Geoapify needs no payment card and suits shops in countries with good OpenStreetMap coverage. HERE has very good data in Europe and North America. Try the suggestions for a few of your customers’ streets; you can switch at any time.', 'streetfill' ), __( 'What does it cost?', 'streetfill' ) => __( 'Streetfill is free. The service bills your own account for the searches beyond its free usage. Suggestions start from the third letter, and Streetfill waits until the customer stops typing for a moment before it asks.', 'streetfill' ), __( 'Why is there a name under the list?', 'streetfill' ) => __( 'Each service requires its attribution when its addresses are shown without a map (“Google Maps”, “Powered by Geoapify · © OpenStreetMap contributors” or “© HERE”). It stays visible even when the Streetfill look is off.', 'streetfill' ), ); /** * Questions and answers of the FAQ help tab (question => answer, plain text). * * @internal For add-ons. Not a public API: may change without notice. * * @param array $faq Questions and answers. */ $faq = (array) apply_filters( 'streetfill_help_faq', $faq ); $html = ''; foreach ( $faq as $question => $answer ) { $html .= '

' . esc_html( (string) $question ) . '

' . esc_html( (string) $answer ) . '

'; } $screen->add_help_tab( array( 'id' => 'streetfill-faq', 'title' => __( 'FAQ', 'streetfill' ), 'content' => $html, ) ); $screen->set_help_sidebar( '

' . esc_html__( 'More help', 'streetfill' ) . '

' . '

' . esc_html__( 'Plugin page', 'streetfill' ) . '

' . '

' . esc_html__( 'Support forum', 'streetfill' ) . '

' . '

' . esc_html__( 'Google Cloud console', 'streetfill' ) . '

' . '

' . esc_html__( 'Google Maps Platform pricing', 'streetfill' ) . '

' . '

' . esc_html__( 'Geoapify pricing', 'streetfill' ) . '

' . '

' . esc_html__( 'HERE pricing', 'streetfill' ) . '

' ); } /** * Form values and errors of a refused save, read once on the next screen. * * @var array{errors: array, values: array}|null */ private static $form_state = null; /** * Name of the short-lived note that brings a refused form back. * * @return string */ private static function form_state_name() { return 'streetfill_form_state_' . get_current_user_id(); } /** * Errors and entered values after a refused save (empty otherwise). * Add-ons read it to mark their own fields. * * @return array{errors: array, values: array} */ public static function form_state() { if ( null === self::$form_state ) { $state = get_transient( self::form_state_name() ); self::$form_state = array( 'errors' => is_array( $state ) && isset( $state['errors'] ) && is_array( $state['errors'] ) ? $state['errors'] : array(), 'values' => is_array( $state ) && isset( $state['values'] ) && is_array( $state['values'] ) ? $state['values'] : array(), ); delete_transient( self::form_state_name() ); } return self::$form_state; } /** * Saves the settings form. */ public static function save() { if ( ! isset( $_POST['_wpnonce'] ) || ! is_string( $_POST['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), self::NONCE ) ) { wp_die( esc_html__( 'The link has expired. Reload the page and try again.', 'streetfill' ), '', array( 'response' => 403 ) ); } if ( ! current_user_can( self::CAP ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to change these settings.', 'streetfill' ), '', array( 'response' => 403 ) ); } $keys = array(); foreach ( Settings::SERVICES as $service => $setting ) { $field = 'google' === $service ? 'streetfill_api_key' : 'streetfill_' . $setting; $keys[ $service ] = isset( $_POST[ $field ] ) && is_string( $_POST[ $field ] ) ? trim( sanitize_text_field( wp_unslash( $_POST[ $field ] ) ) ) : ''; } $provider = isset( $_POST['streetfill_provider'] ) && is_string( $_POST['streetfill_provider'] ) ? sanitize_key( wp_unslash( $_POST['streetfill_provider'] ) ) : 'google'; $provider = isset( Settings::SERVICES[ $provider ] ) ? $provider : 'google'; $style = isset( $_POST['streetfill_style'] ) && is_string( $_POST['streetfill_style'] ) && 'on' === sanitize_key( wp_unslash( $_POST['streetfill_style'] ) ) ? 'on' : 'off'; /** * Settings form fields of an add-on: name => type ('flag' or 'text'). * * @internal For add-ons. Not a public API: may change without notice. * * @param array $fields Fields. */ $fields = apply_filters( 'streetfill_form_fields', array() ); $addon = array(); foreach ( is_array( $fields ) ? $fields : array() as $name => $type ) { if ( ! is_string( $name ) || ! preg_match( '/^[a-z0-9_]{1,64}$/', $name ) ) { continue; } $raw = isset( $_POST[ $name ] ) && is_string( $_POST[ $name ] ) ? sanitize_textarea_field( wp_unslash( $_POST[ $name ] ) ) : null; if ( 'flag' === $type ) { $addon[ $name ] = '1' === $raw; } elseif ( 'text' === $type ) { $addon[ $name ] = null === $raw ? '' : substr( $raw, 0, 2000 ); } } $errors = array(); $not_text = isset( $_POST['streetfill_api_key'] ) && ! is_string( $_POST['streetfill_api_key'] ); $messages = array( 'google' => __( 'This doesn’t look like a Google Maps API key. A key starts with “AIza” and has 39 characters. Copy it again from the Google Cloud console → “Credentials”.', 'streetfill' ), 'geoapify' => __( 'This doesn’t look like a Geoapify key. A key has 32 letters and digits. Copy it again from your project at myprojects.geoapify.com.', 'streetfill' ), 'here' => __( 'This doesn’t look like a HERE key. Copy it again from your app at platform.here.com.', 'streetfill' ), ); $saved = Settings::all(); foreach ( Settings::SERVICES as $slug => $setting ) { $bad = ( 'google' === $slug && $not_text ) || ( '' !== $keys[ $slug ] && 1 !== preg_match( self::KEY_PATTERNS[ $slug ], $keys[ $slug ] ) ); if ( ! $bad ) { continue; } if ( $slug === $provider ) { $errors[ 'google' === $slug ? 'streetfill_api_key' : 'streetfill_' . $setting ] = $messages[ $slug ]; } else { // A hidden row: keep what was saved rather than block the save. $keys[ $slug ] = (string) $saved[ $setting ]; } } /** * Errors of an add-on's settings fields (field name => message). Nothing is saved while there is one. * * @internal For add-ons. Not a public API: may change without notice. * * @param array $errors Errors so far. * @param array $addon The add-on's field values, read after the nonce and capability checks. */ $errors = apply_filters( 'streetfill_validate_settings', $errors, $addon ); if ( is_array( $errors ) && ! empty( $errors ) ) { set_transient( self::form_state_name(), array( 'errors' => array_map( 'strval', $errors ), 'values' => array_merge( $addon, array( 'streetfill_provider' => $provider, 'streetfill_api_key' => substr( $keys['google'], 0, 200 ), 'streetfill_geoapify_key' => substr( $keys['geoapify'], 0, 200 ), 'streetfill_here_key' => substr( $keys['here'], 0, 200 ), ) ), ), 5 * MINUTE_IN_SECONDS ); wp_safe_redirect( self::url( 'settings', array( 'error' => '1' ) ) ); exit; } $had_key = Settings::has_key() && Settings::service() === $provider; Settings::save( array( 'provider' => $provider, 'api_key' => $keys['google'], 'geoapify_key' => $keys['geoapify'], 'here_key' => $keys['here'], 'style' => $style, ) ); /** * Saves an add-on's settings fields, after Streetfill's own and after validation. * * @internal For add-ons. Not a public API: may change without notice. * * @param array $addon Field values. */ do_action( 'streetfill_save_settings', $addon ); $result = '1'; if ( ! Settings::has_key() ) { $result = $had_key ? 'removed' : 'nokey'; } wp_safe_redirect( self::url( 'settings', array( 'saved' => $result ) ) ); exit; } /** * Server half of the key check: one search from the server, without a referrer (Google) * or as if from another website (Geoapify, HERE). An answer means the key is not restricted to the shop's website. */ public static function key_check() { check_ajax_referer( self::CHECK, 'nonce' ); if ( ! current_user_can( self::CAP ) ) { wp_send_json_error( array( 'message' => __( 'Sorry, you are not allowed to do that.', 'streetfill' ) ), 403 ); } if ( ! Settings::has_key() ) { wp_send_json_error( array( 'code' => 'no_key' ) ); } $args = array( 'timeout' => 10, 'redirection' => 0, 'limit_response_size' => 65536, ); $service = Settings::service(); if ( 'google' !== $service ) { // Geoapify and HERE answer requests without a referrer even for a restricted key, // so the search claims to come from another website: a restricted key is refused. $args['headers'] = array( 'Referer' => self::FOREIGN_SITE . '/', 'Origin' => self::FOREIGN_SITE, ); } if ( 'geoapify' === $service ) { $response = wp_safe_remote_get( add_query_arg( array( 'text' => 'Wien', 'limit' => 1, 'apiKey' => rawurlencode( Settings::key() ), ), 'https://api.geoapify.com/v1/geocode/autocomplete' ), $args ); } elseif ( 'here' === $service ) { $response = wp_safe_remote_get( add_query_arg( array( 'q' => 'Wien', 'limit' => 1, 'apiKey' => rawurlencode( Settings::key() ), ), 'https://autocomplete.search.hereapi.com/v1/autocomplete' ), $args ); } else { $args['headers'] = array( 'Content-Type' => 'application/json', 'X-Goog-Api-Key' => Settings::key(), ); $args['body'] = wp_json_encode( array( 'input' => 'Wien' ) ); $response = wp_safe_remote_post( 'https://places.googleapis.com/v1/places:autocomplete', $args ); } wp_send_json_success( array( 'restriction' => self::restriction_from_response( $response, $service ) ) ); } /** * What a service's answer to a request without a referrer says about the key. * * @param array|\WP_Error $response wp_remote_*() result. * @param string $service google, geoapify or here. * @return string 'none' (not restricted), 'website', 'invalid' or 'unknown'. */ public static function restriction_from_response( $response, $service = 'google' ) { if ( is_wp_error( $response ) ) { return 'unknown'; } $code = (int) wp_remote_retrieve_response_code( $response ); $body = (string) wp_remote_retrieve_body( $response ); if ( 200 === $code ) { return 'none'; } if ( 'google' !== $service ) { if ( 401 === $code ) { return 'invalid'; } return 403 === $code ? 'website' : 'unknown'; } if ( false !== stripos( $body, 'REFERRER' ) || false !== stripos( $body, 'referer' ) ) { return 'website'; } if ( false !== stripos( $body, 'API_KEY_INVALID' ) || false !== stripos( $body, 'API key not valid' ) ) { return 'invalid'; } return 'unknown'; } /** * A "Done" / "To do" badge. * * @param bool $done Done. */ private static function badge( $done ) { echo '' . esc_html( $done ? _x( 'Done', 'setup step status', 'streetfill' ) : _x( 'To do', 'setup step status', 'streetfill' ) ) . ''; } /** * The settings screen. */ public static function render() { if ( ! current_user_can( self::CAP ) ) { return; } $s = Settings::all(); $tab = self::current_tab(); echo '

' . esc_html__( 'Streetfill', 'streetfill' ) . '


'; $tabs = self::tabs(); if ( count( $tabs ) > 1 ) { echo ''; } if ( 'settings' !== $tab ) { /** * Prints an add-on tab. * * @internal For add-ons. Not a public API: may change without notice. * * @param string $tab Tab slug. */ do_action( 'streetfill_admin_tab', $tab ); echo '
'; return; } // View only: notices after our own redirect; nothing is processed here. // phpcs:disable WordPress.Security.NonceVerification.Recommended $saved = isset( $_GET['saved'] ) && is_string( $_GET['saved'] ) ? sanitize_key( wp_unslash( $_GET['saved'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended $state = self::form_state(); $service = isset( $state['values']['streetfill_provider'] ) && is_string( $state['values']['streetfill_provider'] ) && isset( Settings::SERVICES[ $state['values']['streetfill_provider'] ] ) ? $state['values']['streetfill_provider'] : $s['provider']; $names = self::service_names(); $any_bad = false; foreach ( array( 'streetfill_api_key', 'streetfill_geoapify_key', 'streetfill_here_key' ) as $f ) { $any_bad = $any_bad || isset( $state['errors'][ $f ] ); } if ( ! empty( $state['errors'] ) ) { echo '

' . esc_html__( 'Settings not saved. Fix the marked fields.', 'streetfill' ) . '

'; } elseif ( 'removed' === $saved ) { echo '

' . esc_html__( 'Key removed. Address suggestions are off until you add a key.', 'streetfill' ) . '

'; } elseif ( 'nokey' === $saved ) { echo '

' . esc_html( sprintf( /* translators: %s: address service, e.g. Google Maps, Geoapify or HERE */ __( 'Settings saved. Address suggestions are off until you add a %s key.', 'streetfill' ), $names[ $s['provider'] ] ) ) . '

'; } elseif ( '1' === $saved ) { echo '

' . esc_html__( 'Settings saved.', 'streetfill' ) . '

'; } echo '

' . esc_html__( 'Customers type a few letters of their street, pick their address from the suggestions, and WooCommerce fills in the street, city, postcode and state. The addresses come from Google Maps, Geoapify or HERE, with your own key.', 'streetfill' ) . '

'; // Setup card. $wc_ok = Plugin::wc_supported(); $key_ok = Settings::has_key(); $auto_ok = Assets::wc_autocomplete_enabled(); $all_ok = $wc_ok && $key_ok && $auto_ok; echo '

' . esc_html( $all_ok ? __( 'Address suggestions are on at checkout', 'streetfill' ) : __( 'Setup', 'streetfill' ) ) . '

'; echo '
'; echo ''; wp_nonce_field( self::NONCE ); // Service card. $about = array( 'google' => __( 'The most complete addresses worldwide. Needs a Google Cloud account with billing turned on; Google includes free monthly usage.', 'streetfill' ), 'geoapify' => __( 'Addresses from OpenStreetMap. A free key without a payment card, currently 3,000 free searches a day. House numbers are missing in some areas.', 'streetfill' ), 'here' => __( 'Data from HERE, strong in Europe and North America. Currently 30,000 free searches a month.', 'streetfill' ), ); echo '

' . esc_html__( 'Address service', 'streetfill' ) . '

'; echo '

' . esc_html__( 'Where the suggestions come from. Not sure? Try a few of your customers’ streets with each; you can switch at any time, and each service keeps its own key here. After switching, save the settings: the setup list and the cards below follow the saved service.', 'streetfill' ) . '

'; echo '
'; // Key card: one row per service, the chosen one shown. $restrict = array( /* translators: %s: the shop's address pattern, e.g. https://example.com/* (keeps the trailing /*). */ 'google' => __( 'Restrict the key to %s in the Google Cloud console.', 'streetfill' ), /* translators: %s: the shop's address pattern, e.g. https://example.com/* (keeps the trailing /*). */ 'geoapify' => __( 'Restrict the key to %s in your Geoapify project (API key settings).', 'streetfill' ), /* translators: %s: the shop's address, e.g. https://example.com */ 'here' => __( 'Restrict the key to %s under “Trusted domains” of your HERE app.', 'streetfill' ), ); echo '

' . esc_html__( 'Key', 'streetfill' ) . '

'; echo '

' . esc_html__( 'Streetfill uses your own key of the chosen service. The service bills your account for usage beyond its free tier.', 'streetfill' ) . '

'; echo '

' . esc_html__( 'No key yet? Open “Help” at the top right of this screen → “How to get a key”.', 'streetfill' ) . '

'; echo ''; foreach ( Settings::SERVICES as $slug => $setting ) { $fname = 'google' === $slug ? 'streetfill_api_key' : 'streetfill_' . $setting; $fid = 'google' === $slug ? 'streetfill-api-key' : 'streetfill-' . str_replace( '_', '-', $setting ); $error = isset( $state['errors'][ $fname ] ) ? (string) $state['errors'][ $fname ] : ''; $value = isset( $state['values'][ $fname ] ) && is_string( $state['values'][ $fname ] ) ? $state['values'][ $fname ] : (string) $s[ $setting ]; $desc = $fid . '-desc' . ( '' !== $error ? ' ' . $fid . '-error' : '' ); echo ''; } echo '
'; // Suggestion list card. echo '

' . esc_html__( 'Suggestion list', 'streetfill' ) . '

'; echo '

' . esc_html__( 'How the suggestion list looks at checkout and in My account.', 'streetfill' ) . '

'; echo '
'; /** * Prints an add-on's settings cards after Streetfill's cards and before Plugin data, inside the settings form. * * @internal For add-ons. Not a public API: may change without notice. */ do_action( 'streetfill_settings_cards' ); echo '

' . esc_html__( 'Plugin data', 'streetfill' ) . '

'; echo ''; /** * Prints an add-on's rows () in the "Plugin data" card. * * @internal For add-ons. Not a public API: may change without notice. */ do_action( 'streetfill_settings_data_rows' ); echo '
'; echo '

'; echo '
'; } }