]*)>/i', array( $this, 'remove_wp_toolbar_from_html_tag' ), $buffer, 1 ); } /** * Strip wp-toolbar and add the onboarding class while preserving attributes. * * @param array $matches Regex matches. * @return string */ private function remove_wp_toolbar_from_html_tag( $matches ) { $quote = $matches[1]; $classes = preg_split( '/\s+/', trim( $matches[2] ) ); $classes = array_filter( $classes, function( $class ) { return 'wp-toolbar' !== $class; } ); $classes[] = 'propertyhive-onboarding-html'; $classes = array_unique( $classes ); return ''; } /** * Add onboarding screen to Property Hive admin screens. * * @param array $screen_ids Screen ids. * @return array */ public function add_screen_id( $screen_ids ) { $screen_ids[] = 'dashboard_page_ph-onboarding'; return $screen_ids; } /** * Add body class on the full-screen onboarding page. * * @param string $classes Body classes. * @return string */ public function admin_body_class( $classes ) { $screen = get_current_screen(); if ( $screen && 'dashboard_page_ph-onboarding' === $screen->id ) { $classes .= ' propertyhive-onboarding-screen'; } return $classes; } /** * Get onboarding state. * * @return array */ public static function get_state() { $state = get_option( self::OPTION_NAME, array() ); if ( ! is_array( $state ) ) { $state = array(); } return wp_parse_args( $state, array( 'status' => 'not_started', 'last_step' => '', 'departments' => array(), 'country' => '', 'office' => array(), 'office_id' => 0, 'usage' => array(), 'usage_tracking' => true, 'has_license_key' => 'no', 'license_key_type' => 'pro', 'license_activated' => false, 'demo_data_imported' => false, 'import_feature_enabled' => false, 'completed_via' => '', 'events' => array(), ) ); } /** * Save onboarding state. * * @param array $updates State updates. * @return array */ private static function update_state( $updates ) { $state = self::get_state(); $state = array_merge( $state, $updates ); update_option( self::OPTION_NAME, $state, false ); return $state; } /** * Add a local event, and optionally send it remotely. * * @param string $event Event name. * @param array $data Event data. */ public static function track_event( $event, $data = array() ) { $event = sanitize_key( $event ); if ( '' === $event ) { return; } $state = self::get_state(); $events = isset( $state['events'] ) && is_array( $state['events'] ) ? $state['events'] : array(); $events[] = array( 'event' => $event, 'data' => self::sanitize_event_data( $data ), 'created_at' => time(), ); if ( count( $events ) > 50 ) { $events = array_slice( $events, -50 ); } self::update_state( array( 'events' => $events ) ); $endpoint = apply_filters( 'propertyhive_onboarding_tracking_endpoint', '' ); if ( 'yes' !== get_option( 'propertyhive_data_sharing', 'no' ) || empty( $endpoint ) ) { return; } wp_remote_post( esc_url_raw( $endpoint ), array( 'timeout' => 5, 'body' => array( 'event' => $event, 'data' => wp_json_encode( self::sanitize_event_data( $data ) ), 'site_url' => home_url(), 'ph_version' => defined( 'PH_VERSION' ) ? PH_VERSION : '', ), ) ); } /** * Sanitize arbitrary event data. * * @param array $data Event data. * @return array */ private static function sanitize_event_data( $data ) { if ( ! is_array( $data ) ) { return array(); } $sanitized = array(); foreach ( $data as $key => $value ) { $key = sanitize_key( $key ); if ( is_array( $value ) ) { $sanitized[ $key ] = array_map( 'sanitize_text_field', wp_unslash( $value ) ); } else { $sanitized[ $key ] = sanitize_text_field( wp_unslash( $value ) ); } } return $sanitized; } /** * Save a wizard step via AJAX. */ public function ajax_save_step() { $this->check_ajax_permissions(); $step = isset( $_POST['step'] ) ? sanitize_key( wp_unslash( $_POST['step'] ) ) : ''; if ( ! in_array( $step, $this->steps, true ) ) { wp_send_json_error( array( 'message' => __( 'Invalid setup step.', 'propertyhive' ) ), 400 ); } switch ( $step ) { case 'intro': $state = $this->save_intro_step(); break; case 'departments': $state = $this->save_departments_step(); break; case 'country': $state = $this->save_country_step(); break; case 'office': $state = $this->save_office_step(); break; case 'usage': $state = $this->save_usage_step(); break; case 'license': $state = $this->save_license_step(); break; case 'demo-data': $state = $this->save_demo_data_step(); break; case 'complete': $state = $this->save_complete_step(); break; } self::track_event( 'step_saved', array( 'step' => $step ) ); wp_send_json_success( $state ); } /** * Skip the wizard. */ public function ajax_skip() { $this->check_ajax_permissions(); $step = isset( $_POST['step'] ) ? sanitize_key( wp_unslash( $_POST['step'] ) ) : ''; $updates = array( 'status' => 'skipped', 'last_step' => $step, 'skipped_at' => time(), ); if ( isset( $_POST['usage_tracking'] ) ) { $usage_tracking = 'yes' === sanitize_text_field( wp_unslash( $_POST['usage_tracking'] ) ); update_option( 'propertyhive_data_sharing', $usage_tracking ? 'yes' : 'no' ); $updates['usage_tracking'] = $usage_tracking; } $state = self::update_state( $updates ); self::track_event( 'skipped', array( 'step' => $step ) ); wp_send_json_success( array( 'state' => $state, 'redirect_url' => admin_url( 'admin.php?page=ph-settings' ), ) ); } /** * Track a view-only event. */ public function ajax_track() { $this->check_ajax_permissions(); $event = isset( $_POST['event'] ) ? sanitize_key( wp_unslash( $_POST['event'] ) ) : ''; $step = isset( $_POST['step'] ) ? sanitize_key( wp_unslash( $_POST['step'] ) ) : ''; if ( '' !== $event ) { self::track_event( $event, array( 'step' => $step ) ); } wp_send_json_success(); } /** * Activate a license key from the onboarding wizard. */ public function ajax_activate_license() { $this->check_ajax_permissions(); $license_key_type = isset( $_POST['license_key_type'] ) ? sanitize_key( wp_unslash( $_POST['license_key_type'] ) ) : ''; if ( ! in_array( $license_key_type, array( 'pro', 'old' ), true ) ) { wp_send_json_error( array( 'message' => __( 'Please choose a valid license type.', 'propertyhive' ) ), 400 ); } $license_key = isset( $_POST['license_key'] ) ? sanitize_text_field( ph_clean( wp_unslash( $_POST['license_key'] ) ) ) : ''; if ( '' === $license_key ) { wp_send_json_error( array( 'message' => __( 'Please enter your license key.', 'propertyhive' ) ), 400 ); } $snapshot = $this->snapshot_license_state(); update_option( 'missing_invalid_expired_license_key_notice_dismissed', '' ); if ( 'pro' === $license_key_type ) { $result = $this->activate_pro_license_for_onboarding( $license_key ); } else { $result = $this->activate_old_license_for_onboarding( $license_key ); } if ( empty( $result['activated'] ) ) { $this->restore_license_state( $snapshot ); } self::update_state( array( 'has_license_key' => 'yes', 'license_key_type' => $license_key_type, 'license_activated' => ! empty( $result['activated'] ), 'license_activated_at' => ! empty( $result['activated'] ) ? time() : 0, ) ); self::track_event( ! empty( $result['activated'] ) ? 'license_activated' : 'license_activation_failed', array( 'license_type' => $license_key_type ) ); wp_send_json_success( array( 'activated' => ! empty( $result['activated'] ), 'license_type' => $license_key_type, 'summary' => isset( $result['summary'] ) ? $result['summary'] : '', 'message' => isset( $result['message'] ) ? $result['message'] : '', 'renew_url' => isset( $result['renew_url'] ) ? $result['renew_url'] : '', ) ); } /** * Activate a Pro license key and confirm it with a fresh status check. * * @param string $license_key License key. * @return array */ private function activate_pro_license_for_onboarding( $license_key ) { $this->clear_pro_license_cache_surfaces(); update_option( 'propertyhive_license_type', 'pro' ); update_option( 'propertyhive_pro_license_key', $license_key ); $activation = PH()->license->activate_pro_license_key(); if ( ! isset( $activation['success'] ) || true !== $activation['success'] ) { return array( 'activated' => false, 'message' => ! empty( $activation['error'] ) ? $activation['error'] : __( "That key wasn't recognised. Check for typos, or recover your key. You can keep going and add it later - nothing is lost.", 'propertyhive' ), ); } if ( ! PH()->license->is_valid_pro_license_key( true ) ) { $license = PH()->license->get_current_pro_license(); return array( 'activated' => false, 'message' => ! empty( $license['error'] ) ? $license['error'] : __( "That key wasn't recognised. Check for typos, or recover your key. You can keep going and add it later - nothing is lost.", 'propertyhive' ), ); } return array( 'activated' => true, 'summary' => '', ); } /** * Activate an old-style license key. * * @param string $license_key License key. * @return array */ private function activate_old_license_for_onboarding( $license_key ) { update_option( 'propertyhive_license_type', 'old' ); update_option( 'propertyhive_license_key', $license_key ); PH()->license->ph_check_licenses( true ); $license = PH()->license->get_current_license(); $error = get_option( 'propertyhive_license_key_error', '' ); if ( '' !== $error ) { return array( 'activated' => false, 'message' => sprintf( /* translators: %s: license service error detail. */ __( "That key wasn't recognised. Check for typos, or recover your key. You can keep going and add it later - nothing is lost. %s", 'propertyhive' ), $error ), ); } if ( empty( $license ) || ! is_array( $license ) || ! isset( $license['active'] ) || '1' !== (string) $license['active'] ) { return array( 'activated' => false, 'message' => isset( $license['active'] ) && '1' !== (string) $license['active'] ? __( 'Your Property Hive license key is inactive. You can keep going and add it later - nothing is lost.', 'propertyhive' ) : __( "That key wasn't recognised. Check for typos, or recover your key. You can keep going and add it later - nothing is lost.", 'propertyhive' ), ); } if ( empty( $license['expires_at'] ) || ( strtotime( $license['expires_at'] ) + DAY_IN_SECONDS ) <= time() ) { $expires_at = ! empty( $license['expires_at'] ) ? date_i18n( 'jS F Y', strtotime( $license['expires_at'] ) ) : ''; return array( 'activated' => false, 'message' => '' !== $expires_at ? sprintf( /* translators: %s: license expiry date. */ __( 'License expired on %s.', 'propertyhive' ), $expires_at ) : __( "That key wasn't recognised. Check for typos, or recover your key. You can keep going and add it later - nothing is lost.", 'propertyhive' ), 'renew_url' => 'https://wp-property-hive.com/license-key/', ); } return array( 'activated' => true, 'summary' => date_i18n( 'jS F Y', strtotime( $license['expires_at'] ) ), ); } /** * Check whether the onboarding restart tool should be available. * * @return bool */ public static function can_restart_onboarding() { return in_array( self::get_environment_type(), array( 'local', 'development', 'staging' ), true ); } /** * Get the current WordPress environment type. * * @return string */ private static function get_environment_type() { if ( function_exists( 'wp_get_environment_type' ) ) { return wp_get_environment_type(); } return 'production'; } /** * Restart the onboarding wizard from a gated admin URL. */ public function maybe_restart_onboarding() { if ( empty( $_GET['propertyhive_restart_onboarding'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended return; } if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to restart setup.', 'propertyhive' ) ); } check_admin_referer( self::RESTART_NONCE_ACTION ); if ( ! self::can_restart_onboarding() ) { wp_die( esc_html__( 'The setup wizard can only be restarted on local, development or staging sites.', 'propertyhive' ) ); } delete_option( self::OPTION_NAME ); self::track_event( 'restarted', array( 'environment_type' => self::get_environment_type() ) ); wp_safe_redirect( admin_url( 'index.php?page=ph-onboarding' ) ); exit; } /** * Output a development-only setup restart prompt on the settings page. */ public function output_restart_onboarding_notice() { if ( ! current_user_can( 'manage_options' ) || ! self::can_restart_onboarding() ) { return; } ?>

' . esc_html( self::get_environment_type() ) . '' ); ?>

__( 'You do not have permission to run setup.', 'propertyhive' ) ), 403 ); } } /** * Return a validation error response. * * @param string $message Error summary. * @param array $errors Field errors. */ private function send_validation_error( $message, $errors = array() ) { wp_send_json_error( array( 'message' => $message, 'errors' => $errors, ), 400 ); } /** * Get the first validation error message. * * @param array $errors Field errors. * @param string $fallback Fallback message. * @return string */ private function get_first_validation_error_message( $errors, $fallback ) { foreach ( $errors as $message ) { if ( is_string( $message ) && '' !== $message ) { return $message; } } return $fallback; } /** * Snapshot license options and transients so failed attempts can roll back. * * @return array */ private function snapshot_license_state() { $option_names = array( 'propertyhive_license_type', 'propertyhive_pro_license_key', 'propertyhive_license_key', 'propertyhive_license_key_details', 'propertyhive_license_key_error', 'propertyhive_pro_license_key_status', 'propertyhive_pro_license_key_last_checked', 'ph_pro_last_known_license_product_id_and_package', 'propertyhive_pro_instance_id', ); $transient_names = array( 'ph_pro_license_status', 'ph_pro_license_product_id_and_package', ); $snapshot = array( 'options' => array(), 'transients' => array(), ); foreach ( $option_names as $option_name ) { $snapshot['options'][ $option_name ] = $this->snapshot_option( $option_name ); } foreach ( $transient_names as $transient_name ) { $snapshot['transients'][ $transient_name ] = array( 'value' => $this->snapshot_option( '_transient_' . $transient_name ), 'timeout' => $this->snapshot_option( '_transient_timeout_' . $transient_name ), ); } return $snapshot; } /** * Snapshot one option while retaining whether it existed. * * @param string $option_name Option name. * @return array */ private function snapshot_option( $option_name ) { $missing = '__propertyhive_onboarding_missing_option__'; $value = get_option( $option_name, $missing ); return array( 'exists' => $missing !== $value, 'value' => $value, ); } /** * Restore a previously snapshotted option. * * @param string $option_name Option name. * @param array $snapshot Snapshot. */ private function restore_option_snapshot( $option_name, $snapshot ) { if ( empty( $snapshot['exists'] ) ) { delete_option( $option_name ); return; } update_option( $option_name, $snapshot['value'] ); } /** * Restore license options and transients after a failed activation. * * @param array $snapshot Snapshot. */ private function restore_license_state( $snapshot ) { if ( ! empty( $snapshot['options'] ) && is_array( $snapshot['options'] ) ) { foreach ( $snapshot['options'] as $option_name => $option_snapshot ) { $this->restore_option_snapshot( $option_name, $option_snapshot ); } } if ( ! empty( $snapshot['transients'] ) && is_array( $snapshot['transients'] ) ) { foreach ( $snapshot['transients'] as $transient_name => $transient_snapshot ) { $this->restore_option_snapshot( '_transient_' . $transient_name, $transient_snapshot['value'] ); $this->restore_option_snapshot( '_transient_timeout_' . $transient_name, $transient_snapshot['timeout'] ); } } } /** * Clear all Pro license cache surfaces before validating a submitted key. */ private function clear_pro_license_cache_surfaces() { delete_transient( 'ph_pro_license_status' ); delete_transient( 'ph_pro_license_product_id_and_package' ); delete_option( 'propertyhive_pro_license_key_status' ); delete_option( 'propertyhive_pro_license_key_last_checked' ); delete_option( 'ph_pro_last_known_license_product_id_and_package' ); } /** * Save intro step. * * @return array */ private function save_intro_step() { $usage_tracking = isset( $_POST['usage_tracking'] ) && 'yes' === sanitize_text_field( wp_unslash( $_POST['usage_tracking'] ) ); update_option( 'propertyhive_data_sharing', $usage_tracking ? 'yes' : 'no' ); return self::update_state( array( 'status' => 'in_progress', 'last_step' => 'intro', 'started_at' => $this->get_started_at(), 'usage_tracking' => $usage_tracking, ) ); } /** * Save departments. * * @return array */ private function save_departments_step() { $departments = isset( $_POST['departments'] ) && is_array( $_POST['departments'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_POST['departments'] ) ) : array(); $allowed = array( 'residential-sales', 'residential-lettings', 'commercial' ); $departments = array_values( array_intersect( $allowed, $departments ) ); if ( empty( $departments ) ) { $this->send_validation_error( __( 'Please choose at least one property sector.', 'propertyhive' ), array( 'departments' => __( 'Please choose at least one property sector.', 'propertyhive' ), ) ); } update_option( 'propertyhive_active_departments_sales', in_array( 'residential-sales', $departments, true ) ? 'yes' : 'no' ); update_option( 'propertyhive_active_departments_lettings', in_array( 'residential-lettings', $departments, true ) ? 'yes' : 'no' ); update_option( 'propertyhive_active_departments_commercial', in_array( 'commercial', $departments, true ) ? 'yes' : 'no' ); update_option( 'propertyhive_primary_department', reset( $departments ) ); return self::update_state( array( 'status' => 'in_progress', 'last_step' => 'departments', 'departments' => $departments, 'started_at' => $this->get_started_at(), ) ); } /** * Save country. * * @return array */ private function save_country_step() { $country = isset( $_POST['country'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_POST['country'] ) ) ) : ''; $countries = $this->get_countries(); if ( ! isset( $countries[ $country ] ) ) { $this->send_validation_error( __( 'Please choose a valid country.', 'propertyhive' ), array( 'country' => __( 'Please choose a valid country.', 'propertyhive' ), ) ); } update_option( 'propertyhive_default_country', $country ); update_option( 'propertyhive_countries', array( $country ) ); if ( ! empty( $countries[ $country ]['currency_code'] ) ) { update_option( 'propertyhive_search_form_currency', sanitize_text_field( $countries[ $country ]['currency_code'] ) ); } return self::update_state( array( 'status' => 'in_progress', 'last_step' => 'country', 'country' => $country, ) ); } /** * Save primary office details. * * @return array */ private function save_office_step() { $office = $this->sanitize_office_payload(); $errors = $this->validate_office_payload( $office ); if ( ! empty( $errors ) ) { $this->send_validation_error( $this->get_first_validation_error_message( $errors, __( 'Please check your office details.', 'propertyhive' ) ), $errors ); } $office['email_address'] = sanitize_email( $office['email_address'] ); $office_id = $this->get_primary_office_id(); if ( ! $office_id ) { $office_id = wp_insert_post( array( 'post_title' => '' !== $office['name'] ? $office['name'] : __( 'My Office', 'propertyhive' ), 'post_content' => '', 'post_status' => 'publish', 'post_type' => 'office', ), true ); if ( is_wp_error( $office_id ) || ! $office_id ) { wp_send_json_error( array( 'message' => __( 'Office details could not be saved.', 'propertyhive' ) ), 500 ); } } update_post_meta( $office_id, 'primary', '1' ); if ( '' !== $office['name'] && get_the_title( $office_id ) !== $office['name'] ) { wp_update_post( array( 'ID' => $office_id, 'post_title' => $office['name'], ) ); } update_post_meta( $office_id, '_office_address_1', $office['address_1'] ); update_post_meta( $office_id, '_office_address_2', $office['address_2'] ); update_post_meta( $office_id, '_office_address_3', $office['address_3'] ); update_post_meta( $office_id, '_office_address_4', $office['address_4'] ); update_post_meta( $office_id, '_office_address_postcode', $office['postcode'] ); foreach ( $this->get_office_contact_departments() as $department ) { $department_key = str_replace( 'residential-', '', $department ); update_post_meta( $office_id, '_office_telephone_number_' . $department_key, $office['telephone_number'] ); update_post_meta( $office_id, '_office_email_address_' . $department_key, $office['email_address'] ); } return self::update_state( array( 'status' => 'in_progress', 'last_step' => 'office', 'office' => $office, 'office_id' => absint( $office_id ), ) ); } /** * Save intended usage. * * @return array */ private function save_usage_step() { $usage = isset( $_POST['usage'] ) && is_array( $_POST['usage'] ) ? array_map( 'sanitize_key', wp_unslash( $_POST['usage'] ) ) : array(); $usage = array_values( array_intersect( array( 'import_properties', 'crm', 'portal_uploads', 'not_sure' ), $usage ) ); if ( empty( $usage ) ) { $this->send_validation_error( __( 'Please choose how you will use Property Hive.', 'propertyhive' ), array( 'usage' => __( 'Please choose how you will use Property Hive.', 'propertyhive' ), ) ); } $crm_enabled = in_array( 'crm', $usage, true ); $disabled = $crm_enabled ? 'no' : 'yes'; update_option( 'propertyhive_module_disabled_contacts', $disabled ); update_option( 'propertyhive_module_disabled_appraisals', $disabled ); update_option( 'propertyhive_module_disabled_viewings', $disabled ); update_option( 'propertyhive_module_disabled_offers_sales', $disabled ); update_option( 'propertyhive_module_disabled_tenancies', $disabled ); return self::update_state( array( 'status' => 'in_progress', 'last_step' => 'usage', 'usage' => $usage, ) ); } /** * Save license step choices. * * @return array */ private function save_license_step() { $has_license_key = isset( $_POST['has_license_key'] ) ? sanitize_key( wp_unslash( $_POST['has_license_key'] ) ) : 'no'; $license_key_type = isset( $_POST['license_key_type'] ) ? sanitize_key( wp_unslash( $_POST['license_key_type'] ) ) : 'pro'; if ( ! in_array( $has_license_key, array( 'yes', 'no' ), true ) ) { $has_license_key = 'no'; } if ( ! in_array( $license_key_type, array( 'pro', 'old' ), true ) ) { $license_key_type = 'pro'; } return self::update_state( array( 'status' => 'in_progress', 'last_step' => 'license', 'has_license_key' => $has_license_key, 'license_key_type' => $license_key_type, ) ); } /** * Save demo-data result. * * @return array */ private function save_demo_data_step() { $demo_choice = isset( $_POST['demo_data_choice'] ) ? sanitize_key( wp_unslash( $_POST['demo_data_choice'] ) ) : ''; $imported = isset( $_POST['demo_data_imported'] ) && 'yes' === sanitize_text_field( wp_unslash( $_POST['demo_data_imported'] ) ); if ( ! in_array( $demo_choice, array( 'yes', 'no' ), true ) ) { $this->send_validation_error( __( 'Please choose whether to import demo data.', 'propertyhive' ), array( 'demo_data_choice' => __( 'Please choose whether to import demo data.', 'propertyhive' ), ) ); } if ( 'yes' === $demo_choice && ! $imported ) { $this->send_validation_error( __( 'Demo data could not be imported. Please try again or choose No.', 'propertyhive' ), array( 'demo_data_choice' => __( 'Demo data could not be imported. Please try again or choose No.', 'propertyhive' ), ) ); } self::track_event( $imported ? 'demo_data_completed' : 'demo_data_skipped' ); return self::update_state( array( 'status' => 'in_progress', 'last_step' => 'demo-data', 'demo_data_imported' => $imported, ) ); } /** * Complete the wizard. * * @return array */ private function save_complete_step() { $import_feature = $this->get_import_feature_for_onboarding(); $import_feature_active = $this->is_import_feature_active( ! empty( $import_feature['plugin_file'] ) ? $import_feature['plugin_file'] : '' ); $completed_via = isset( $_POST['completed_via'] ) ? sanitize_key( wp_unslash( $_POST['completed_via'] ) ) : ''; if ( ! in_array( $completed_via, array( 'import', 'properties', 'site', 'settings' ), true ) ) { $completed_via = ''; } $state = self::update_state( array( 'status' => 'completed', 'last_step' => 'complete', 'completed_at' => time(), 'import_feature_enabled' => $import_feature_active, 'completed_via' => $completed_via, ) ); self::track_event( 'completed', array( 'completed_via' => $completed_via ) ); return $state; } /** * Get existing start time or current time. * * @return int */ private function get_started_at() { $state = self::get_state(); if ( ! empty( $state['started_at'] ) ) { return absint( $state['started_at'] ); } self::track_event( 'started' ); return time(); } /** * Get license state for rendering without remote checks. * * @return array */ private function get_license_render_state() { $pro_status = get_transient( 'ph_pro_license_status' ); if ( false === $pro_status ) { $pro_status = get_option( 'propertyhive_pro_license_key_status', '' ); } if ( is_array( $pro_status ) && isset( $pro_status['success'] ) && true === $pro_status['success'] ) { return array( 'type' => 'pro', 'active' => true, 'message' => __( 'You already have an active Property Hive subscription on this site.', 'propertyhive' ), ); } // TO DO: TAKE INTO ACCOUNT LICENSE TYPE SELECTED // SCENARIO - HAVING PRO TICKED AND NO LICENSE KEY BUT AN ACTIVE OLD KEY $license = PH()->license->get_current_license(); if ( is_array( $license ) && isset( $license['active'] ) && '1' === (string) $license['active'] && ! empty( $license['expires_at'] ) && ( strtotime( $license['expires_at'] ) + DAY_IN_SECONDS ) > time() ) { return array( 'type' => 'old', 'active' => true, 'message' => __( 'Your license is active - add-on updates are enabled.', 'propertyhive' ), ); } return array( 'type' => '', 'active' => false, 'message' => '', ); } /** * Resolve the Property Import feature from the cached add-ons feed. * * @return array */ private function get_import_feature_for_onboarding() { $features = get_transient( 'propertyhive_features' ); if ( false === $features || ! is_array( $features ) ) { return array(); } foreach ( $features as $feature ) { if ( empty( $feature['wordpress_plugin_file'] ) || ! is_string( $feature['wordpress_plugin_file'] ) ) { continue; } $plugin_file = $feature['wordpress_plugin_file']; $plugin_parts = explode( '/', $plugin_file ); $slug = $plugin_parts[0]; $haystack = strtolower( $plugin_file . ' ' . $slug ); if ( false === strpos( $haystack, 'property-import' ) && false === strpos( $haystack, 'property_import' ) && false === strpos( $haystack, 'wp-property-hive-property-import' ) ) { continue; } return array( 'slug' => $slug, 'plugin_file' => $plugin_file, 'active' => $this->is_import_feature_active( $plugin_file ), ); } return array(); } /** * Determine whether the Property Import feature is active. * * @param string $plugin_file Optional plugin file. * @return bool */ private function is_import_feature_active( $plugin_file = '' ) { if ( ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } if ( '' !== $plugin_file && is_plugin_active( $plugin_file ) ) { return true; } return class_exists( 'PH_Property_Import' ); } /** * Output onboarding page. */ public function output() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to run setup.', 'propertyhive' ) ); } $state = self::get_state(); $countries = $this->get_countries(); $default_country = $this->get_default_country(); $office = $this->get_office_defaults( $state ); $demo_active = class_exists( 'PH_Demo_Data' ); $video_url = apply_filters( 'propertyhive_onboarding_video_url', '' ); $departments = ! empty( $state['departments'] ) && is_array( $state['departments'] ) ? $state['departments'] : array( 'residential-sales', 'residential-lettings' ); $usage = ! empty( $state['usage'] ) && is_array( $state['usage'] ) ? $state['usage'] : array(); $usage_tracking = 'yes' === get_option( 'propertyhive_data_sharing', 'yes' ); $has_license_key = ! empty( $state['has_license_key'] ) && 'yes' === $state['has_license_key'] ? 'yes' : 'no'; $license_key_type = ! empty( $state['license_key_type'] ) && in_array( $state['license_key_type'], array( 'pro', 'old' ), true ) ? $state['license_key_type'] : 'pro'; $license_render_state = $this->get_license_render_state(); $license_activated = ! empty( $state['license_activated'] ) || ! empty( $license_render_state['active'] ); $import_feature = $this->get_import_feature_for_onboarding(); $import_feature_active = ! empty( $import_feature['active'] ) || $this->is_import_feature_active(); $demo_choice = ! empty( $state['demo_data_imported'] ) ? 'yes' : 'no'; $address_lookup_enabled = function_exists( 'ph_is_development_environment' ) && ph_is_development_environment() && '' !== trim( (string) get_option( 'propertyhive_getaddress_api_key', '' ) ); $search_results_page_id = function_exists( 'ph_get_page_id' ) ? absint( ph_get_page_id( 'search_results' ) ) : 0; $site_preview_url = home_url( '/' ); if ( $search_results_page_id && get_post( $search_results_page_id ) ) { $search_results_permalink = get_permalink( $search_results_page_id ); if ( $search_results_permalink ) { $site_preview_url = $search_results_permalink; } } $current_step = $this->get_current_step( $state ); self::track_event( 'step_viewed', array( 'step' => $current_step ) ); $this->localize_script( $demo_active, $import_feature, $import_feature_active, $license_activated ); ?>
<?php esc_attr_e( 'Property Hive', 'propertyhive' ); ?>

output_choice_card( 'departments', 'residential-sales', __( 'Residential sales', 'propertyhive' ), __( 'Market and manage properties for sale.', 'propertyhive' ), in_array( 'residential-sales', $departments, true ) ); ?> output_choice_card( 'departments', 'residential-lettings', __( 'Residential lettings', 'propertyhive' ), __( 'Manage lettings, tenancies and landlords.', 'propertyhive' ), in_array( 'residential-lettings', $departments, true ) ); ?> output_choice_card( 'departments', 'commercial', __( 'Commercial', 'propertyhive' ), __( 'Manage commercial properties.', 'propertyhive' ), in_array( 'commercial', $departments, true ) ); ?>
output_field_error( 'departments' ); ?> output_settings_note( __( 'General', 'propertyhive' ) ); ?>

output_settings_note( __( 'General > International', 'propertyhive' ) ); ?>

output_settings_note( __( 'Offices', 'propertyhive' ) ); ?>

output_choice_card( 'usage', 'import_properties', __( 'Import properties', 'propertyhive' ), __( 'Keep your website in sync with properties from your CRM.', 'propertyhive' ), in_array( 'import_properties', $usage, true ), true ); ?> output_choice_card( 'usage', 'crm', __( 'Use it as a CRM', 'propertyhive' ), __( 'Manage contacts, appraisals, viewings, offers and tenancies.', 'propertyhive' ), in_array( 'crm', $usage, true ) ); ?> output_choice_card( 'usage', 'portal_uploads', __( 'Upload to portals', 'propertyhive' ), __( 'Automatically send your properties to third party portals.', 'propertyhive' ), in_array( 'portal_uploads', $usage, true ), true ); ?> output_choice_card( 'usage', 'not_sure', __( 'Just the website for now', 'propertyhive' ), __( 'No problem. You can always choose later.', 'propertyhive' ), in_array( 'not_sure', $usage, true ) ); ?>
output_field_error( 'usage' ); ?>
>

output_settings_note( __( 'General > Modules', 'propertyhive' ) );*/ ?>

*/ ?>
✓

output_radio_card( 'has_license_key', 'yes', __( 'Yes, I already have a subscription', 'propertyhive' ), __( 'Enter your licence key to connect it to this site.', 'propertyhive' ), 'yes' === $has_license_key, false ); ?> output_radio_card( 'has_license_key', 'no', __( 'No, not yet', 'propertyhive' ), __( 'That\'s fine. You can carry on with the free version.', 'propertyhive' ), 'no' === $has_license_key, false ); ?>
output_field_error( 'license_key' ); ?>

output_settings_note( __( 'License', 'propertyhive' ) ); ?>

output_radio_card( 'demo_data_choice', 'no', __( 'No', 'propertyhive' ), __( 'Skip the demo data and use my own.', 'propertyhive' ), 'no' === $demo_choice, false ); ?> output_radio_card( 'demo_data_choice', 'yes', __( 'Yes', 'propertyhive' ), __( 'Add example data so I can explore Property Hive first.', 'propertyhive' ), 'yes' === $demo_choice, false ); ?>
output_field_error( 'demo_data_choice' ); ?>
output_settings_note( __( 'Demo Data', 'propertyhive' ) ); ?>

> > > " data-onboarding-exit data-exit="site" > */ ?>

International. */ esc_html__( "You can change this later under 'Property Hive > Settings > %s'.", 'propertyhive' ), esc_html( $location ) ); ?>

admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( self::NONCE_ACTION ), 'steps' => $this->steps, 'settings_url' => admin_url( 'admin.php?page=ph-settings' ), 'features_url' => admin_url( 'admin.php?page=ph-settings&tab=features&profilter=free' ), 'import_features_url' => admin_url( 'admin.php?page=ph-settings&tab=features&profilter=import' ), 'import_setup_url' => admin_url( 'admin.php?page=propertyhive_import_properties' ), 'license_trial_url' => $this->get_url( '/pricing', 'license-step-trial' ), 'demo_data_active' => $demo_active ? 'yes' : 'no', 'demo_data_nonce' => wp_create_nonce( 'propertyhive_demo_data' ), 'demo_data_feature_slug' => 'propertyhive-demo-data', 'license_activated' => $license_activated ? 'yes' : 'no', 'updates_nonce' => wp_create_nonce( 'updates' ), 'can_install_plugins' => current_user_can( 'install_plugins' ) ? 'yes' : 'no', 'import_feature_slug' => ! empty( $import_feature['slug'] ) ? $import_feature['slug'] : '', 'import_feature_plugin' => ! empty( $import_feature['plugin_file'] ) ? $import_feature['plugin_file'] : '', 'import_feature_active' => $import_feature_active ? 'yes' : 'no', 'address_lookup_enabled' => ( function_exists( 'ph_is_development_environment' ) && ph_is_development_environment() && '' !== trim( (string) get_option( 'propertyhive_getaddress_api_key', '' ) ) ) ? 'yes' : 'no', 'address_lookup_nonce' => wp_create_nonce( 'propertyhive_getaddress_lookup' ), 'demo_base_sections' => array( 'applicant', 'property' ), 'demo_crm_sub_sections' => apply_filters( 'propertyhive_onboarding_demo_crm_sub_sections', array( 'appraisal', 'viewing', 'offer', 'sale', 'tenancy', 'enquiry' ) ), 'i18n' => array( 'continue' => __( 'Continue', 'propertyhive' ), 'saving' => __( 'Saving...', 'propertyhive' ), 'importing' => __( 'Importing demo data...', 'propertyhive' ), 'activatingDemoData' => __( 'Activating Demo Data...', 'propertyhive' ), 'importComplete' => __( 'Demo data imported.', 'propertyhive' ), 'importFailed' => __( 'Demo data could not be imported. You can continue setup and try again later.', 'propertyhive' ), 'demoDataInactive' => __( 'The Demo Data feature could not be activated on this site.', 'propertyhive' ), 'licenseKeyRequired' => __( 'Please enter your license key.', 'propertyhive' ), 'licenseChecking' => __( 'Checking your key... Contacting wp-property-hive.com - a couple of seconds.', 'propertyhive' ), 'licenseProSuccess' => __( 'Pro activated. The features in your plan are ready to use.', 'propertyhive' ), 'licenseOldSuccess' => __( 'License valid - updates are enabled for your purchased add-ons.', 'propertyhive' ), 'licenseError' => __( "That key wasn't recognised. Check for typos, or recover your key. You can keep going and add it later - nothing is lost.", 'propertyhive' ), 'licenseOldTrial' => __( 'Want the Pro features too? Start a free 7-day trial.', 'propertyhive' ), 'importFeatureEnabled' => __( 'Property Import feature enabled', 'propertyhive' ), 'renewLicense' => __( 'Renew License', 'propertyhive' ), 'chooseDepartment' => __( 'Please choose at least one property sector.', 'propertyhive' ), 'chooseCountry' => __( 'Please choose a valid country.', 'propertyhive' ), 'chooseUsage' => __( 'Please choose how you will use Property Hive.', 'propertyhive' ), 'chooseDemoData' => __( 'Please choose whether to import demo data.', 'propertyhive' ), 'officeNameTooLong' => __( 'Office name must be 120 characters or fewer.', 'propertyhive' ), 'officeAddressTooLong' => __( 'Address Line 1 must be 120 characters or fewer.', 'propertyhive' ), 'officeAddress2TooLong' => __( 'Address Line 2 must be 120 characters or fewer.', 'propertyhive' ), 'officeAddress3TooLong' => __( 'Address Line 3 must be 120 characters or fewer.', 'propertyhive' ), 'officeAddress4TooLong' => __( 'Address Line 4 must be 120 characters or fewer.', 'propertyhive' ), 'officePostcodeTooLong' => __( 'Postcode must be 20 characters or fewer.', 'propertyhive' ), 'officePhoneTooLong' => __( 'Phone number must be 30 characters or fewer.', 'propertyhive' ), 'officePhoneInvalid' => __( 'Please enter a valid phone number.', 'propertyhive' ), 'officeEmailTooLong' => __( 'Email address must be 100 characters or fewer.', 'propertyhive' ), 'officeEmailInvalid' => __( 'Please enter a valid email address.', 'propertyhive' ), 'addressLookupSearching' => __( 'Searching addresses...', 'propertyhive' ), 'addressLookupNoResults' => __( 'No matching addresses found.', 'propertyhive' ), 'addressLookupFailed' => __( 'Address lookup could not be completed. Please enter the address manually.', 'propertyhive' ), 'addressLookupSelected' => __( 'Address selected.', 'propertyhive' ), 'skipConfirm' => __( 'Skip setup? You can still configure Property Hive from settings later.', 'propertyhive' ), ), ) ); } /** * Get available countries. * * @return array */ private function get_countries() { $countries = new PH_Countries(); return is_array( $countries->countries ) ? $countries->countries : array(); } /** * Infer the default country for onboarding. * * @return string */ private function get_default_country() { $countries = $this->get_countries(); $existing = get_option( 'propertyhive_default_country', '' ); if ( isset( $countries[ $existing ] ) ) { return $existing; } $locale = get_locale(); if ( is_string( $locale ) && false !== strpos( $locale, '_' ) ) { $locale_country = strtoupper( substr( strrchr( $locale, '_' ), 1 ) ); if ( isset( $countries[ $locale_country ] ) ) { return $locale_country; } } $timezone = wp_timezone_string(); $mapping = array( 'Europe/London' => 'GB', 'America/New_York' => 'US', 'America/Chicago' => 'US', 'America/Denver' => 'US', 'America/Los_Angeles' => 'US', 'Europe/Dublin' => 'IE', 'Europe/Paris' => 'FR', 'Europe/Madrid' => 'ES', 'Europe/Berlin' => 'DE', 'Australia/Sydney' => 'AU', 'Pacific/Auckland' => 'NZ', ); if ( isset( $mapping[ $timezone ], $countries[ $mapping[ $timezone ] ] ) ) { return $mapping[ $timezone ]; } return isset( $countries['GB'] ) ? 'GB' : key( $countries ); } /** * Sanitize office details from the onboarding form. * * @return array */ private function sanitize_office_payload() { return array( 'name' => isset( $_POST['office_name'] ) ? sanitize_text_field( wp_unslash( $_POST['office_name'] ) ) : '', 'address_1' => isset( $_POST['office_address_1'] ) ? sanitize_text_field( wp_unslash( $_POST['office_address_1'] ) ) : '', 'address_2' => isset( $_POST['office_address_2'] ) ? sanitize_text_field( wp_unslash( $_POST['office_address_2'] ) ) : '', 'address_3' => isset( $_POST['office_address_3'] ) ? sanitize_text_field( wp_unslash( $_POST['office_address_3'] ) ) : '', 'address_4' => isset( $_POST['office_address_4'] ) ? sanitize_text_field( wp_unslash( $_POST['office_address_4'] ) ) : '', 'postcode' => isset( $_POST['office_postcode'] ) ? sanitize_text_field( wp_unslash( $_POST['office_postcode'] ) ) : '', 'telephone_number' => isset( $_POST['office_telephone_number'] ) ? sanitize_text_field( wp_unslash( $_POST['office_telephone_number'] ) ) : '', 'email_address' => isset( $_POST['office_email_address'] ) ? sanitize_text_field( wp_unslash( $_POST['office_email_address'] ) ) : '', ); } /** * Validate office details. * * @param array $office Office payload. * @return array */ private function validate_office_payload( $office ) { $errors = array(); $field_labels = array( 'name' => __( 'Office name', 'propertyhive' ), 'address_1' => __( 'Address Line 1', 'propertyhive' ), 'address_2' => __( 'Address Line 2', 'propertyhive' ), 'address_3' => __( 'Address Line 3', 'propertyhive' ), 'address_4' => __( 'Address Line 4', 'propertyhive' ), 'postcode' => __( 'Postcode', 'propertyhive' ), 'telephone_number' => __( 'Phone number', 'propertyhive' ), 'email_address' => __( 'Email address', 'propertyhive' ), ); $field_keys = array( 'name' => 'office_name', 'address_1' => 'office_address_1', 'address_2' => 'office_address_2', 'address_3' => 'office_address_3', 'address_4' => 'office_address_4', 'postcode' => 'office_postcode', 'telephone_number' => 'office_telephone_number', 'email_address' => 'office_email_address', ); $max_lengths = array( 'name' => 120, 'address_1' => 120, 'address_2' => 120, 'address_3' => 120, 'address_4' => 120, 'postcode' => 20, 'telephone_number' => 30, 'email_address' => 100, ); foreach ( $max_lengths as $field => $max_length ) { if ( isset( $office[ $field ] ) && strlen( $office[ $field ] ) > $max_length ) { $errors[ $field_keys[ $field ] ] = sprintf( /* translators: 1: field label, 2: maximum character count. */ __( '%1$s must be %2$d characters or fewer.', 'propertyhive' ), $field_labels[ $field ], $max_length ); } } /*if ( ! empty( $office['telephone_number'] ) ) { $digits = preg_replace( '/\D+/', '', $office['telephone_number'] ); if ( strlen( $digits ) < 7 || ! preg_match( '/^[0-9+\-\s().]+$/', $office['telephone_number'] ) ) { $errors['office_telephone_number'] = __( 'Please enter a valid phone number.', 'propertyhive' ); } }*/ if ( ! empty( $office['email_address'] ) && ! is_email( $office['email_address'] ) ) { $errors['office_email_address'] = __( 'Please enter a valid email address.', 'propertyhive' ); } return $errors; } /** * Get defaults for the office address step. * * @param array $state Onboarding state. * @return array */ private function get_office_defaults( $state ) { $office = array( 'name' => '', 'address_1' => '', 'address_2' => '', 'address_3' => '', 'address_4' => '', 'postcode' => '', 'telephone_number' => '', 'email_address' => '', ); $office_id = $this->get_primary_office_id(); if ( $office_id ) { $office = array( 'name' => get_the_title( $office_id ), 'address_1' => get_post_meta( $office_id, '_office_address_1', true ), 'address_2' => get_post_meta( $office_id, '_office_address_2', true ), 'address_3' => get_post_meta( $office_id, '_office_address_3', true ), 'address_4' => get_post_meta( $office_id, '_office_address_4', true ), 'postcode' => get_post_meta( $office_id, '_office_address_postcode', true ), 'telephone_number' => $this->get_primary_office_contact_value( $office_id, 'telephone_number' ), 'email_address' => $this->get_primary_office_contact_value( $office_id, 'email_address' ), ); } if ( ! empty( $state['office'] ) && is_array( $state['office'] ) ) { foreach ( array_keys( $office ) as $key ) { if ( isset( $state['office'][ $key ] ) && '' !== $state['office'][ $key ] ) { $office[ $key ] = $state['office'][ $key ]; } } } return $office; } /** * Get the active departments that should receive shared office contact details. * * @return array */ private function get_office_contact_departments() { $state = self::get_state(); $allowed = array( 'residential-sales', 'residential-lettings', 'commercial' ); $departments = ! empty( $state['departments'] ) && is_array( $state['departments'] ) ? array_values( array_intersect( $allowed, $state['departments'] ) ) : array(); if ( empty( $departments ) ) { foreach ( $allowed as $department ) { if ( 'yes' === get_option( 'propertyhive_active_departments_' . str_replace( 'residential-', '', $department ), 'no' ) ) { $departments[] = $department; } } } return ! empty( $departments ) ? $departments : $allowed; } /** * Get the first saved office contact value across active departments. * * @param int $office_id Office post id. * @param string $field Contact field name. * @return string */ private function get_primary_office_contact_value( $office_id, $field ) { $meta_prefix = 'email_address' === $field ? '_office_email_address_' : '_office_telephone_number_'; foreach ( $this->get_office_contact_departments() as $department ) { $value = get_post_meta( $office_id, $meta_prefix . str_replace( 'residential-', '', $department ), true ); if ( '' !== $value ) { return $value; } } return ''; } /** * Get the primary office id, falling back to the first office. * * @return int */ private function get_primary_office_id() { $office_ids = get_posts( array( 'post_type' => 'office', 'post_status' => 'any', 'posts_per_page' => 1, 'fields' => 'ids', 'meta_key' => 'primary', 'meta_value' => '1', ) ); if ( ! empty( $office_ids ) ) { return absint( $office_ids[0] ); } $office_ids = get_posts( array( 'post_type' => 'office', 'post_status' => 'any', 'posts_per_page' => 1, 'fields' => 'ids', ) ); return ! empty( $office_ids ) ? absint( $office_ids[0] ) : 0; } /** * Get current step. * * @param array $state Onboarding state. * @return string */ private function get_current_step( $state ) { if ( isset( $state['status'] ) && 'completed' === $state['status'] ) { return 'complete'; } if ( ! empty( $state['last_step'] ) && in_array( $state['last_step'], $this->steps, true ) && 'completed' !== $state['status'] ) { return $state['last_step']; } return 'intro'; } /** * Get pricing page URL. * * @param string $utm_content UTM content value identifying which link was clicked. * @return string */ private function get_url( $url, $utm_content = '' ) { $args = array( 'src' => 'plugin-onboarding', 'utm_source' => 'propertyhive-plugin', 'utm_medium' => 'onboarding', 'utm_campaign' => 'setup-wizard', ); if ( '' !== $utm_content ) { $args['utm_content'] = $utm_content; } $url = add_query_arg( $args, 'https://wp-property-hive.com' . $url ); return apply_filters( 'propertyhive_onboarding_external_url', $url, $utm_content ); } } endif; return new PH_Admin_Onboarding();