* @copyright 2019 Route App Inc. Copyright (c) https://www.routeapp.io/ * @license https://www.routeapp.io/merchant-terms-of-use Proprietary License * @link https://magento.routeapp.io/magento2/index.html */ class Route_Setup { const WOOCOMMERCE = "woocommerce"; const REGISTRATION_FIXED = '0'; const REGISTRATION_STEP_USER_LOGIN_SUCCESS = '1-200'; const FAILED_REGISTRATION_STEP_USER = "1"; const FAILED_REGISTRATION_STEP_USER_DUPLICATED = '1-409'; const FAILED_REGISTRATION_STEP_USER_LOGIN_FAILED = '1-401'; const FAILED_REGISTRATION_STEP_USER_ACTIVATION = '2'; const FAILED_REGISTRATION_STEP_MERCHANT = '3'; const FAILED_REGISTRATION_STEP_MERCHANT_DUPLICATED = '3-409'; const FAILED_REGISTRATION = 'routeapp_failed_registration'; const USER_TOKEN_OPTION = 'routeapp_user_token'; const USER_ID_OPTION = 'routeapp_user_id'; const SECRET_TOKEN_OPTION = 'routeapp_secret_token'; const PUBLIC_TOKEN_OPTION = 'routeapp_public_token'; const USER_ID = 'routeapp_user_id'; const REGISTRATION_STEP = 'routeapp_route_registration_step'; const DASHBOARD = 'https://dashboard.route.com/login?redirect=onboarding'; const DASHBOARD_STAGE = 'https://dashboard-stage.route.com/login?redirect=onboarding'; const REDIRECTED = '2'; const MODULE_INSTALLED = '1'; /** * After duplicate-user / login-conflict notice auto-redirect runs once, show manual link only (avoids redirect loops). */ const USER_CONFLICT_AUTO_REDIRECT_DONE_OPTION = 'routeapp_user_conflict_auto_redirect_done'; const ACTIVATION_LINK = 'activation_link'; const SETUP_CHECK_WIDGET_INSTALLATION_DATE = 'route_setup_check_widget_installation_date'; const SETUP_CHECK_WIDGET_API_CALL = 'route_setup_check_widget_api_call'; const SETUP_CHECK_WIDGET_PHP = 'route_setup_check_widget_php'; const SETUP_CHECK_WIDGET_JS = 'route_setup_check_widget_js'; const ACTIVE = 'Active'; /** * Stable per-site identifier for ownership / OTP callbacks (used in the ownership-resolution URL path). * * @var string */ const STORE_HASH_OPTION = 'routeapp_store_hash'; /** * Short alphanumeric id length (10–12) for /app/ownership-resolution/{hash}. */ const STORE_HASH_LENGTH = 12; /** * Register rewrite, query vars, and front controller for /app/ownership-resolution/{storeHash}. */ public static function init_ownership_routing() { add_action( 'init', array( __CLASS__, 'register_ownership_rewrite_rule' ), 5 ); add_filter( 'query_vars', array( __CLASS__, 'ownership_query_vars' ) ); add_action( 'template_redirect', array( __CLASS__, 'template_redirect_ownership_resolution' ), 1 ); } /** * @param array $vars Query vars. * @return array */ public static function ownership_query_vars( $vars ) { $vars[] = 'routeapp_ownership'; $vars[] = 'routeapp_store_hash'; return $vars; } /** * Pretty URL: {site}/app/ownership-resolution/{storeHash} on the store origin. */ public static function register_ownership_rewrite_rule() { add_rewrite_rule( '^app/ownership-resolution/([a-zA-Z0-9]{10,12})/?$', 'index.php?routeapp_ownership=1&routeapp_store_hash=$matches[1]', 'top' ); } /** * Handle GET /app/ownership-resolution/{storeHash}?token= / ?oneTimeToken= */ public static function template_redirect_ownership_resolution() { if ( ! (int) get_query_var( 'routeapp_ownership' ) ) { return; } $path_hash = get_query_var( 'routeapp_store_hash' ); $token = ''; if ( isset( $_GET['oneTimeToken'] ) ) { $token = sanitize_text_field( wp_unslash( $_GET['oneTimeToken'] ) ); } if ( $token === '' && isset( $_GET['token'] ) ) { $token = sanitize_text_field( wp_unslash( $_GET['token'] ) ); } self::ownership_resolution_handle( $path_hash, $token ); } public static function init() { self::ensure_store_hash(); if ( ! self::get_route_public_instance() ) { return; } if ( ! self::is_fresh_new_installation() ) { self::update_merchant_status(); return; } self::create_user(); } /** * True when activation should run create_user() / merchant onboarding against the Route API. * * Gated only on missing valid merchant credentials. Requiring an empty routeapp_route_registration_step * used to block re-onboarding after tokens were cleared while the step option stayed set. * * @return bool */ public static function is_fresh_new_installation() { return ! self::has_valid_merchant_tokens(); } /** * Public/secret missing but we have prior onboarding state — user should use Dashboard login + OTP or POST user_login. * * @return bool */ public static function should_prompt_merchant_token_recovery() { if ( self::has_merchant_tokens() ) { return false; } return ! empty( get_option( self::USER_TOKEN_OPTION ) ) || ! empty( self::registration_step() ) || ! empty( get_option( 'routeapp_merchant_id' ) ); } /** * Create and persist a unique store hash for this WordPress installation (used in ownership-resolution URL). * * @return string */ /** * Whether the stored hash matches the short format (10–12 alnum). * * @param string $hash Hash. * @return bool */ private static function is_valid_store_hash_format( $hash ) { return is_string( $hash ) && (bool) preg_match( '/^[a-zA-Z0-9]{10,12}$/', $hash ); } public static function ensure_store_hash() { $hash = get_option( self::STORE_HASH_OPTION ); if ( self::is_valid_store_hash_format( $hash ) ) { return $hash; } if ( function_exists( 'wp_generate_password' ) ) { $hash = wp_generate_password( self::STORE_HASH_LENGTH, false, false ); } else { $hash = substr( bin2hex( random_bytes( 8 ) ), 0, self::STORE_HASH_LENGTH ); } update_option( self::STORE_HASH_OPTION, $hash, false ); return $hash; } /** * Stored store hash (must exist before building platformUrl for Route Dashboard). * * @return string */ public static function get_store_hash() { return (string) get_option( self::STORE_HASH_OPTION, '' ); } /** * Callback URL for Route Dashboard platformUrl: {store-web-url}/app/ownership-resolution/{storeHash} * (avoids wp-json / rest_route encoding on the merchant origin). * * @return string */ public static function get_ownership_resolution_platform_url() { $store_hash = self::ensure_store_hash(); return home_url( '/app/ownership-resolution/' . $store_hash ); } /** * Prepare user data, call user create request * and handle response * * @return bool */ public static function create_user() { $routeapp_public = self::get_route_public_instance(); $response = $routeapp_public->routeapp_create_user(self::get_user_data()); try{ if ($routeapp_public->routeapp_last_request_has_failed()) { self::set_registration_failed_as( $routeapp_public->routeapp_last_request_has_conflicted() ? self::FAILED_REGISTRATION_STEP_USER_DUPLICATED: self::FAILED_REGISTRATION_STEP_USER ); throw new Exception($routeapp_public->routeapp_last_request_has_conflicted() ? 'FAILED_REGISTRATION_STEP_USER_DUPLICATED': 'FAILED_REGISTRATION_STEP_USER'); } }catch (Exception $exception) { $extraData = array( 'params' => self::get_user_data(), 'method' => 'POST', 'endpoint' => 'users' ); $routeapp_public->routeapp_log($exception, $extraData); return false; } if (self::register_user($response)) { self::set_registration_failed_as(self::REGISTRATION_FIXED); self::clear_user_conflict_auto_redirect_flag(); self::set_as_installed(); return true; } } /** * Prepare user data, call user create request * and handle response * * @return bool */ public static function register_user_login($username, $password) { $routeapp_public = self::get_route_public_instance(); $response = $routeapp_public->routeapp_user_login($username, $password); try{ if ($routeapp_public->routeapp_last_request_has_failed()) { self::set_registration_failed_as(self::FAILED_REGISTRATION_STEP_USER_LOGIN_FAILED); throw new Exception('FAILED_REGISTRATION_STEP_USER_LOGIN_FAILED'); } }catch (Exception $exception) { $extraData = array( 'params' => array('username' => $username, 'password' => 'XXXXX'), 'method' => 'POST', 'endpoint' => 'login' ); $routeapp_public->routeapp_log($exception, $extraData); return false; } if (self::register_user($response, true)) { self::set_registration_failed_as(self::REGISTRATION_STEP_USER_LOGIN_SUCCESS); self::clear_user_conflict_auto_redirect_flag(); self::set_as_installed(); return true; } } /** * @param $response * @param $is_active * * @return bool */ public static function register_user($response, $is_active = false) { self::set_user_key($response->token); self::set_user_id($response->id); if (!$is_active) { self::active_account(); } return self::create_merchant(); } /** * Activate User Account * * @return bool */ public static function active_account() { $routeapp_public = self::get_route_public_instance(); try{ $response = $routeapp_public->routeapp_activate_account(self::get_current_email()); try{ if(!$response){ self::set_registration_failed_as(self::FAILED_REGISTRATION_STEP_USER_ACTIVATION); throw new Exception('FAILED_REGISTRATION_STEP_USER_ACTIVATION'); } } catch (Exception $exception) { $extraData = array( 'params' => self::get_current_email(), 'method' => 'POST', 'endpoint' => 'activate_account' ); $routeapp_public->routeapp_log($exception, $extraData); return false; } self::set_activation_link($response->set_password_url); }catch (Exception $e){ self::set_registration_failed_as(self::FAILED_REGISTRATION_STEP_USER_ACTIVATION); $extraData = array( 'params' => self::get_current_email(), 'method' => 'POST', 'endpoint' => 'activate_account' ); $routeapp_public->routeapp_log($e, $extraData); return false; } self::set_registration_failed_as(self::REGISTRATION_FIXED); return true; } public static function has_valid_merchant_tokens(){ $routeapp_public = self::get_route_public_instance(); if ( ! $routeapp_public ) { return false; } return self::has_merchant_tokens() && ! empty( $routeapp_public->routeapp_api_client->get_merchant() ); } public static function has_merchant_tokens(){ $routeapp_public = self::get_route_public_instance(); if ( ! $routeapp_public ) { return false; } return ! empty( $routeapp_public->routeapp_get_public_token() ) && ! empty( $routeapp_public->routeapp_get_secret_token() ); } public static function get_route_public_instance(){ global $routeapp_public; return $routeapp_public; } /** * @param $store_domain * @param $merchant_store_domain * @return bool */ public static function is_same_domain($store_domain, $merchant_store_domain) { if(strpos($store_domain,'www.') === 0 || strpos($merchant_store_domain,'www.') === 0){ return strpos($store_domain, $merchant_store_domain) !== false || strpos($merchant_store_domain, $store_domain) !== false; } return $merchant_store_domain === $store_domain; } /** * Prepare merchant data, call merchant create request * and handle response * * @return bool */ public static function create_merchant() { $created_domains = []; $responses = []; foreach (self::get_all_sites() as $site){ //Avoid duplicated merchant to the same user account if (in_array($site->domain, $created_domains)) { continue; } $associated = false; $responses = []; $created_domains[$site->blog_id] = $site->domain; $merchant_data = self::get_merchant_data($site); $routeapp_public = self::get_route_public_instance(); $merchant_creation_response = $routeapp_public->routeapp_create_merchant($merchant_data); if($merchant_creation_response){ $responses[$site->blog_id] = $merchant_creation_response; } try{ if ($routeapp_public->routeapp_last_request_has_failed()) { $merchants_by_user = $routeapp_public->routeapp_get_merchants(); if(!empty($merchants_by_user) && is_array($merchants_by_user)){ foreach ($merchants_by_user as $merchant) { if(self::is_same_domain($merchant->store_domain, $site->domain)){ $responses[$site->blog_id] = $merchant; $associated = true; } } } if (empty($responses)) { self::set_registration_failed_as( $routeapp_public->routeapp_last_request_has_conflicted() ? self::FAILED_REGISTRATION_STEP_MERCHANT_DUPLICATED: self::FAILED_REGISTRATION_STEP_MERCHANT ); throw new Exception($routeapp_public->routeapp_last_request_has_conflicted() ? 'FAILED_REGISTRATION_STEP_MERCHANT_DUPLICATED': 'FAILED_REGISTRATION_STEP_MERCHANT'); } } } catch (Exception $exception) { $routeapp_public->routeapp_log($exception); return false; } //TODO Currently when we try to create merchant that already exists it's returning 200 http code success //TODO So we need try to update it with current platform if (!$associated && $routeapp_public->routeapp_last_request_has_success()) { try{ if (!self::merchant_can_be_updated($responses[$site->blog_id])) { self::set_registration_failed_as(self::FAILED_REGISTRATION_STEP_MERCHANT); throw new Exception('FAILED_REGISTRATION_STEP_MERCHANT'); } } catch (Exception $exception) { $routeapp_public->routeapp_log($exception); return false; } //TODO This is happening when we try to update merchant that user doesn't own //TODO So we need try to recreate it with additional slash at the store's domain end $merchant_data['store_domain'] = $merchant_data['store_domain'] . '/'; $responses[$site->blog_id] = $routeapp_public->routeapp_create_merchant($merchant_data); try{ if ($routeapp_public->routeapp_last_request_has_failed()) { self::set_registration_failed_as( $routeapp_public->routeapp_last_request_has_conflicted() ? self::FAILED_REGISTRATION_STEP_MERCHANT_DUPLICATED : self::FAILED_REGISTRATION_STEP_MERCHANT); throw new Exception($routeapp_public->routeapp_last_request_has_conflicted() ? 'FAILED_REGISTRATION_STEP_MERCHANT_DUPLICATED' : 'FAILED_REGISTRATION_STEP_MERCHANT'); } } catch (Exception $exception) { $routeapp_public->routeapp_log($exception); return false; } } } foreach ($responses as $blog_id => $response){ self::set_public_key($response->public_api_key, $blog_id); self::set_secret_key($response->prod_api_secret, $blog_id); } return true; } /** * @param $response * @return bool */ public static function merchant_can_be_updated($response) { return (isset($response->platform_id) && strtolower($response->platform_id) == 'email') || (isset($response->status) && strtolower($response->status) != 'active'); } /** * @param $data * @return int Total of orders in last month */ public static function get_calculate_dealsize() { $created_at_min = date( 'Y-m-d', strtotime( '-1 month' ) ); $ordersCollection = wc_get_orders( array( 'limit' => -1, 'date_after' => $created_at_min, 'return' => 'ids', ) ); return strval( count( $ordersCollection ) ); } /** * @param $data * @return array */ private static function get_user_data() { $userData = []; $user = self::get_current_user(); $userData['name'] = $user->user_firstname; $userData['password'] = self::generate_temp_pass(); $userData['platform_id'] = self::WOOCOMMERCE; $userData['phone'] = ''; $userData['primary_email'] = self::get_current_email(); return $userData; } /** * @return string */ private static function get_current_email() { return get_bloginfo('admin_email'); } /** * @param $data * @return array */ private static function get_merchant_data($site) { $merchantData = []; $merchantData['platform_id'] = self::WOOCOMMERCE; $merchantData['store_domain'] = $site->domain; $merchantData['store_name'] = self::get_blog_name($site); $merchantData['deal_size_order_count'] = self::get_calculate_dealsize(); $merchantData['country'] = self::get_store_country($site); $merchantData['currency'] = self::get_currency($site); $merchantData['source'] = self::WOOCOMMERCE; $merchantData['status'] = self::ACTIVE; return $merchantData; } /** * Prepare Merchant Data * @param $store * @return mixed */ public function prepare_compatibility_data() { $widgetPhpFlag = $this->get_setup_check_widget(self::SETUP_CHECK_WIDGET_PHP); $widgetJsFlag = $this->get_setup_check_widget(self::SETUP_CHECK_WIDGET_JS); $creationDate = $this->get_setup_check_widget(self::SETUP_CHECK_WIDGET_INSTALLATION_DATE); $successInstallation = $widgetPhpFlag && $widgetJsFlag; $data = []; $data['php_flag'] = $widgetPhpFlag ? $widgetPhpFlag : 0; $data['js_flag'] = $widgetJsFlag ? $widgetJsFlag : 0; $data['success_installation'] = $successInstallation; $data['install_date'] = $creationDate; $data['store_domain'] = parse_url(get_site_url(get_current_blog_id()), PHP_URL_HOST); $data['platform_id'] = self::WOOCOMMERCE; // If get_plugins() isn't available, require it if ( ! function_exists( 'get_plugins' ) ) require_once( ABSPATH . 'wp-admin/includes/plugin.php' ); $data['modules'] = get_plugins(); $data['subject'] = $successInstallation ? 'Success Installation' : 'Installation Issues'; return $data; } public function can_send_compatibility_report(){ $apiCalled = $this->get_setup_check_widget(self::SETUP_CHECK_WIDGET_API_CALL); $creationDate = $this->get_setup_check_widget(self::SETUP_CHECK_WIDGET_INSTALLATION_DATE); return (time() > $creationDate && !$apiCalled); } /** * @return array| */ public static function get_all_sites() { if(is_multisite()) { return get_sites(); } $site = new stdClass(); $site->domain = parse_url(get_site_url(), PHP_URL_HOST); $site->blog_id = 1; return [$site]; } /** * Retrieve the blog name for a given site. * * Falls back to the site domain if the blog name is empty * to prevent merchant creation failures on the Route API. * * @param $site * @return string The blog name or domain as fallback. */ private static function get_blog_name($site) { $name = is_multisite() ? get_blog_option($site->blog_id, 'blogname') : get_option('blogname'); return empty($name) ? $site->domain : $name; } /** * @param $site * @return mixed */ private static function get_currency($site) { if(is_multisite()) return get_blog_option($site->blog_id, 'woocommerce_currency'); return get_woocommerce_currency(); } /** * @param $site * @return mixed|string */ private static function get_store_country($site){ if(is_multisite()) { $country = get_blog_option($site->blog_id, 'woocommerce_default_country'); if (strpos($country, ':') > 0) { $countryState = explode(':', $country); return isset($countryState[0]) ? $countryState[0] : $country; } return $country; } return get_woocommerce_currency(); } private static function generate_temp_pass() { return "pass" . substr(hash('sha256', rand()), 0, 10); } private static function set_registration_failed_as($step){ update_option(self::FAILED_REGISTRATION , $step); } /** * Reset so the next duplicate-user / login-conflict flow can auto-redirect once again. */ public static function clear_user_conflict_auto_redirect_flag() { delete_option( self::USER_CONFLICT_AUTO_REDIRECT_DONE_OPTION ); } private static function get_registration_failed_as(){ return get_option(self::FAILED_REGISTRATION); } private static function set_secret_key($token, $blog_id = null){ if (isset($blog_id) && is_multisite()) { return update_blog_option($blog_id, self::SECRET_TOKEN_OPTION, $token); } return update_option(self::SECRET_TOKEN_OPTION , $token); } private static function set_user_key($token, $blog_id = null){ if (isset($blog_id) && is_multisite()) { return update_blog_option($blog_id, self::USER_TOKEN_OPTION, $token); } return update_option(self::USER_TOKEN_OPTION , $token); } private static function set_user_id($userId, $blog_id = null){ if (isset($blog_id) && is_multisite()) { return update_blog_option($blog_id, self::USER_ID_OPTION, $userId); } return update_option(self::USER_ID_OPTION , $userId); } private static function set_public_key($token, $blog_id = null){ if (isset($blog_id) && is_multisite()) { return update_blog_option($blog_id, self::PUBLIC_TOKEN_OPTION, $token); } return update_option(self::PUBLIC_TOKEN_OPTION , $token); } private static function get_activation_link(){ return get_option(self::ACTIVATION_LINK); } private static function set_as_installed(){ update_option(self::REGISTRATION_STEP, self::MODULE_INSTALLED); self::set_setup_check_widget(self::SETUP_CHECK_WIDGET_INSTALLATION_DATE, time()); } private static function set_activation_link($activation_link){ update_option(self::ACTIVATION_LINK, $activation_link); } private static function registration_step(){ return get_option(self::REGISTRATION_STEP); } public static function is_installed(){ return self::registration_step() == self::MODULE_INSTALLED; } public static function has_user_login_succeed(){ return self::get_registration_failed_as() == self::REGISTRATION_STEP_USER_LOGIN_SUCCESS; } /** * Set setup check widget * * @param $config * @param bool $value */ public static function set_setup_check_widget($config, $value = false) { if (!get_option($config)) { $value = $value ? $value : 1; update_option($config, $value); } } /** * Get setup check widget config * * @param $config * @return mixed */ public function get_setup_check_widget($config) { return get_option($config); } /** * @return WP_User */ private static function get_current_user() { return wp_get_current_user(); } public static function retry_user(){ self::create_user(); wp_redirect( self_admin_url( "plugins.php" ) ); die(); } public static function user_login() { self::register_user_login($_POST['username'],$_POST['password']); wp_redirect( self_admin_url( "plugins.php" ) ); die(); } public static function retry_merchant(){ self::create_merchant(); wp_redirect( self_admin_url( "plugins.php" ) ); die(); } public static function get_route_redirect(){ if(self::has_user_login_succeed()){ $custom_env = getenv('ROUTEAPP_ENVIRONMENT_ENDPOINT'); if (is_null($custom_env) || !$custom_env) { $custom_env = isset($_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT']) ? $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] : ''; } if ($custom_env == 'stage') { return self::DASHBOARD_STAGE; } return self::DASHBOARD ; } return self::get_activation_link(); } private static function update_merchant_status(){ $routeapp_public = self::get_route_public_instance(); $merchant = $routeapp_public->routeapp_api_client->get_merchant(); if (empty($merchant)) { return; } if (property_exists($merchant, 'status')) { $routeapp_public->routeapp_api_client->update_merchant_status('Active'); } } /** * Route Dashboard base URL (no path), respecting stage env like other helpers. * * @return string */ public static function get_dashboard_root_url() { $custom_env = getenv( 'ROUTEAPP_ENVIRONMENT_ENDPOINT' ); if ( is_null( $custom_env ) || ! $custom_env ) { $custom_env = isset( $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] ) ? $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] : ''; } return ( 'stage' === $custom_env ) ? 'https://dashboard-stage.route.com' : 'https://dashboard.route.com'; } /** * Route Dashboard login URL with platformUrl pointing at this site's OTP callback. * * @return string */ public static function get_dashboard_ownership_login_url() { $platform_url = self::get_ownership_resolution_platform_url(); return self::get_dashboard_root_url() . '/login?platformUrl=' . rawurlencode( $platform_url ); } /** * Dashboard login URL for Auth0 / post-activation flows (not legacy create-account). * * @return string */ public static function get_dashboard_login_url() { return self::get_dashboard_root_url() . '/login'; } /** * Whether the OTP merchant record matches this WordPress site's host. * * @param string $shop_domain Host from get_site_url(). * @param array $merchant Result from verify_otp. * @return bool */ public static function merchant_store_domain_matches_otp_merchant( $shop_domain, array $merchant ) { if ( empty( $merchant['store_domain'] ) || ! is_string( $merchant['store_domain'] ) ) { return false; } $m_domain = $merchant['store_domain']; if ( 0 === strpos( $shop_domain, 'www.' ) ) { return false !== strpos( $shop_domain, $m_domain ) || false !== strpos( $m_domain, $shop_domain ); } return $m_domain === $shop_domain; } /** * Persist merchant credentials after successful OTP and mark onboarding complete. * * @param array $merchant Result from Routeapp_API_Client::verify_otp. */ public static function apply_ownership_verified_merchant( array $merchant ) { $blog_id = is_multisite() ? get_current_blog_id() : null; self::set_public_key( $merchant['public_api_key'], $blog_id ); self::set_secret_key( $merchant['prod_api_secret'], $blog_id ); $client = Routeapp_API_Client::getInstance(); $client->set_public_token( $merchant['public_api_key'] ); $client->set_secret_token( $merchant['prod_api_secret'] ); $client->set_merchant_id( $merchant['id'], $blog_id ); self::set_registration_failed_as( self::REGISTRATION_FIXED ); self::clear_user_conflict_auto_redirect_flag(); self::set_as_installed(); global $routeapp_public; if ( $routeapp_public && isset( $routeapp_public->routeapp_api_client ) ) { $routeapp_public->routeapp_api_client = new Routeapp_API_Client( $merchant['public_api_key'], $merchant['prod_api_secret'] ); $routeapp_public->routeapp_api_client->set_merchant_id( $merchant['id'], $blog_id ); $m = $routeapp_public->routeapp_api_client->get_merchant(); if ( $m && is_object( $m ) && property_exists( $m, 'status' ) ) { $routeapp_public->routeapp_api_client->update_merchant_status( 'Active' ); } } } /** * Shared handler: path {storeHash} must match DB; verifies OTP and links merchant. * * @param string $path_hash Segment from URL. * @param string $token OTP from ?token= or ?oneTimeToken=. */ public static function ownership_resolution_handle( $path_hash, $token ) { $expected = self::get_store_hash(); if ( $expected === '' ) { self::ensure_store_hash(); $expected = self::get_store_hash(); } if ( empty( $path_hash ) || $path_hash !== $expected ) { wp_safe_redirect( add_query_arg( 'route_ownership_error', rawurlencode( 'invalid_store' ), self::get_dashboard_login_url() ) ); exit; } if ( empty( $token ) ) { wp_safe_redirect( add_query_arg( 'route_ownership_error', rawurlencode( 'missing_token' ), self::get_dashboard_login_url() ) ); exit; } $merchant = Routeapp_API_Client::getInstance()->verify_otp( $token ); if ( is_wp_error( $merchant ) ) { wp_safe_redirect( add_query_arg( 'route_ownership_error', rawurlencode( 'otp_verify_failed' ), self::get_dashboard_login_url() ) ); exit; } $shop_host = parse_url( get_site_url(), PHP_URL_HOST ); if ( ! self::merchant_store_domain_matches_otp_merchant( $shop_host, $merchant ) ) { wp_safe_redirect( add_query_arg( 'route_ownership_error', rawurlencode( 'domain_mismatch' ), self::get_dashboard_login_url() ) ); exit; } self::apply_ownership_verified_merchant( $merchant ); if ( ! class_exists( 'Routeapp_Webhooks', false ) ) { require_once dirname( dirname( __FILE__ ) ) . '/admin/class-routeapp-webhooks.php'; } if ( class_exists( 'Routeapp_Webhooks' ) ) { $webhooks = new Routeapp_Webhooks(); $webhooks->upsert_webhooks(); } wp_safe_redirect( self::get_dashboard_root_url() ); exit; } }