trailingslashit( $rest_path ) . self::ONBOARDING_STEP_BUSINESS_VERIFICATION . '/kyc_session/finish' ), ), 'kyc_fallback' => array( 'type' => self::ACTION_TYPE_REDIRECT, 'href' => $this->get_onboarding_kyc_fallback_url(), ), 'finish' => array( 'type' => self::ACTION_TYPE_REST, 'href' => rest_url( trailingslashit( $rest_path ) . self::ONBOARDING_STEP_BUSINESS_VERIFICATION . '/finish' ), ), ); } $steps[] = $business_verification_step; // Do a complete list standardization, for safety. return $this->standardize_onboarding_steps_details( $steps, $location, $rest_path ); } /** * Standardize (and sanity check) the onboarding step details. * * @param array $step_details The onboarding step details to standardize. * @param string $location The location for which we are onboarding. * This is a ISO 3166-1 alpha-2 country code. * @param string $rest_path The REST API path to use for constructing REST API URLs. * * @return array The standardized onboarding step details. * @throws Exception If the onboarding step details are missing required entries or if the step ID is invalid. */ private function standardize_onboarding_step_details( array $step_details, string $location, string $rest_path ): array { // If the required keys are not present, throw. if ( ! isset( $step_details['id'] ) ) { /* translators: %s: The required key that is missing. */ throw new Exception( sprintf( esc_html__( 'The onboarding step is missing required entries: %s', 'woocommerce' ), 'id' ) ); } // Validate the step ID. if ( ! $this->is_valid_onboarding_step_id( $step_details['id'] ) ) { /* translators: %s: The invalid step ID. */ throw new Exception( sprintf( esc_html__( 'The onboarding step ID is invalid: %s', 'woocommerce' ), esc_attr( $step_details['id'] ) ) ); } if ( empty( $step_details['status'] ) ) { $step_details['status'] = $this->get_onboarding_step_status( $step_details['id'], $location ); } if ( empty( $step_details['errors'] ) ) { $step_details['errors'] = array(); // For blocked or failed steps, we include any stored error. if ( in_array( $step_details['status'], array( self::ONBOARDING_STEP_STATUS_BLOCKED, self::ONBOARDING_STEP_STATUS_FAILED ), true ) ) { $stored_error = $this->get_onboarding_step_error( $step_details['id'], $location ); if ( ! empty( $stored_error ) ) { $step_details['errors'] = array( $stored_error ); } } } // Ensure that any step has the general actions. if ( empty( $step_details['actions'] ) ) { $step_details['actions'] = array(); } // Any step can be checked for its status. if ( empty( $step_details['actions']['check'] ) ) { $step_details['actions']['check'] = array( 'type' => self::ACTION_TYPE_REST, 'href' => rest_url( trailingslashit( $rest_path ) . $step_details['id'] . '/check' ), ); } // Any step can be cleaned of its progress. if ( empty( $step_details['actions']['clean'] ) ) { $step_details['actions']['clean'] = array( 'type' => self::ACTION_TYPE_REST, 'href' => rest_url( trailingslashit( $rest_path ) . $step_details['id'] . '/clean' ), ); } return array( 'id' => $step_details['id'], 'path' => $step_details['path'] ?? trailingslashit( self::ONBOARDING_PATH_BASE ) . $step_details['id'], 'required_steps' => $step_details['required_steps'] ?? $this->get_onboarding_step_required_steps( $step_details['id'] ), 'status' => $step_details['status'], 'errors' => $step_details['errors'], 'actions' => $step_details['actions'], 'context' => $step_details['context'] ?? array(), ); } /** * Standardize (and sanity check) the onboarding steps list. * * @param array $steps The onboarding steps list to standardize. * @param string $location The location for which we are onboarding. * This is a ISO 3166-1 alpha-2 country code. * @param string $rest_path The REST API path to use for constructing REST API URLs. * * @return array The standardized onboarding steps list. * @throws Exception If some onboarding steps are missing required entries or if invalid step IDs are present. */ private function standardize_onboarding_steps_details( array $steps, string $location, string $rest_path ): array { $standardized_steps = array(); foreach ( $steps as $step ) { $standardized_steps[] = $this->standardize_onboarding_step_details( $step, $location, $rest_path ); } return $standardized_steps; } /** * Get the entire stored NOX profile data * * @return array The stored NOX profile. */ private function get_nox_profile(): array { $nox_profile = $this->proxy->call_function( 'get_option', self::NOX_PROFILE_OPTION_KEY, array() ); if ( empty( $nox_profile ) ) { $nox_profile = array(); } else { $nox_profile = maybe_unserialize( $nox_profile ); } return $nox_profile; } /** * Get the onboarding step data from the NOX profile. * * @param string $step_id The ID of the onboarding step. * @param string $location The location for which we are onboarding. * This is a ISO 3166-1 alpha-2 country code. * * @return array The onboarding step stored data from the NOX profile. * If the step data is not found, an empty array is returned. */ private function get_nox_profile_onboarding_step( string $step_id, string $location ): array { $nox_profile = $this->get_nox_profile(); if ( empty( $nox_profile['onboarding'] ) ) { $nox_profile['onboarding'] = array(); } if ( empty( $nox_profile['onboarding'][ $location ] ) ) { $nox_profile['onboarding'][ $location ] = array(); } if ( empty( $nox_profile['onboarding'][ $location ]['steps'] ) ) { $nox_profile['onboarding'][ $location ]['steps'] = array(); } if ( empty( $nox_profile['onboarding'][ $location ]['steps'][ $step_id ] ) ) { $nox_profile['onboarding'][ $location ]['steps'][ $step_id ] = array(); } return $nox_profile['onboarding'][ $location ]['steps'][ $step_id ]; } /** * Save the onboarding step data in the NOX profile. * * @param string $step_id The ID of the onboarding step. * @param string $location The location for which we are onboarding. * This is a ISO 3166-1 alpha-2 country code. * @param array $data The onboarding step data to save in the profile. * * @return bool Whether the onboarding step data was saved. */ private function save_nox_profile_onboarding_step( string $step_id, string $location, array $data ): bool { $nox_profile = $this->get_nox_profile(); if ( empty( $nox_profile['onboarding'] ) ) { $nox_profile['onboarding'] = array(); } if ( empty( $nox_profile['onboarding'][ $location ] ) ) { $nox_profile['onboarding'][ $location ] = array(); } if ( empty( $nox_profile['onboarding'][ $location ]['steps'] ) ) { $nox_profile['onboarding'][ $location ]['steps'] = array(); } // Update the stored step data. $nox_profile['onboarding'][ $location ]['steps'][ $step_id ] = $data; return $this->proxy->call_function( 'update_option', self::NOX_PROFILE_OPTION_KEY, $nox_profile, false ); } /** * Get an entry from the NOX profile onboarding step details. * * @param string $step_id The ID of the onboarding step. * @param string $location The location for which we are onboarding. * This is a ISO 3166-1 alpha-2 country code. * @param string $entry The entry to get from the step data. * @param mixed $default_value The default value to return if the entry is not found. * * @return mixed The entry from the NOX profile step details. If the entry is not found, the default value is returned. */ private function get_nox_profile_onboarding_step_entry( string $step_id, string $location, string $entry, $default_value = array() ): array { $step_details = $this->get_nox_profile_onboarding_step( $step_id, $location ); if ( ! isset( $step_details[ $entry ] ) ) { return $default_value; } return $step_details[ $entry ]; } /** * Save an entry in the NOX profile onboarding step details. * * @param string $step_id The ID of the onboarding step. * @param string $location The location for which we are onboarding. * This is a ISO 3166-1 alpha-2 country code. * @param string $entry The entry key under which to save in the step data. * @param array $data The data to save in the step data. * * @return bool Whether the onboarding step data was saved. */ private function save_nox_profile_onboarding_step_entry( string $step_id, string $location, string $entry, array $data ): bool { $step_details = $this->get_nox_profile_onboarding_step( $step_id, $location ); // Update the stored step data. $step_details[ $entry ] = $data; return $this->save_nox_profile_onboarding_step( $step_id, $location, $step_details ); } /** * Get a data entry from the NOX profile onboarding step details. * * @param string $step_id The ID of the onboarding step. * @param string $location The location for which we are onboarding. * This is a ISO 3166-1 alpha-2 country code. * @param string $entry The entry to get from the step `data`. * @param mixed $default_value The default value to return if the entry is not found. * * @return mixed The entry value from the NOX profile stored step data. * If the entry is not found, the default value is returned. */ private function get_nox_profile_onboarding_step_data_entry( string $step_id, string $location, string $entry, $default_value = false ) { $step_details_data = $this->get_nox_profile_onboarding_step_entry( $step_id, $location, 'data' ); if ( ! isset( $step_details_data[ $entry ] ) ) { return $default_value; } return $step_details_data[ $entry ]; } /** * Save a data entry in the NOX profile onboarding step details. * * @param string $step_id The ID of the onboarding step. * @param string $location The location for which we are onboarding. * This is a ISO 3166-1 alpha-2 country code. * @param string $entry The entry key under which to save in the step `data`. * @param mixed $data The value to save. * * @return bool Whether the onboarding step data was saved. */ private function save_nox_profile_onboarding_step_data_entry( string $step_id, string $location, string $entry, $data ): bool { $step_details_data = $this->get_nox_profile_onboarding_step_entry( $step_id, $location, 'data' ); // Update the stored step data. $step_details_data[ $entry ] = $data; return $this->save_nox_profile_onboarding_step_entry( $step_id, $location, 'data', $step_details_data ); } /** * Get the IDs of the onboarding steps that are required for the given step. * * @param string $step_id The ID of the onboarding step. * * @return array|string[] The IDs of the onboarding steps that are required for the given step. */ private function get_onboarding_step_required_steps( string $step_id ): array { switch ( $step_id ) { // Both the test account and business verification (live account) steps require a working WPCOM connection. case self::ONBOARDING_STEP_TEST_ACCOUNT: case self::ONBOARDING_STEP_BUSINESS_VERIFICATION: return array( self::ONBOARDING_STEP_WPCOM_CONNECTION, ); default: return array(); } } /** * Check if the requirements for an onboarding step are met. * * @param string $step_id The ID of the onboarding step. * @param string $location The location for which we are onboarding. * This is a ISO 3166-1 alpha-2 country code. * * @return bool Whether the onboarding step requirements are met. * @throws ApiArgumentException If the given onboarding step ID is invalid. */ private function check_onboarding_step_requirements( string $step_id, string $location ): bool { $requirements = $this->get_onboarding_step_required_steps( $step_id ); foreach ( $requirements as $required_step_id ) { if ( $this->get_onboarding_step_status( $required_step_id, $location ) !== self::ONBOARDING_STEP_STATUS_COMPLETED ) { return false; } } return true; } /** * Get the payment methods state for onboarding. * * @param string $location The location for which we are onboarding. * This is a ISO 3166-1 alpha-2 country code. * * @return array The onboarding payment methods state. */ private function get_onboarding_payment_methods_state( string $location ): array { // First, get the recommended payment methods details from the provider. // We will use their enablement state as the default. // Note: The list is validated and standardized by the provider, so we don't need to do it here. $recommended_pms = $this->get_onboarding_recommended_payment_methods( $location ); // Grab the stored payment methods state // (a key-value array of payment method IDs and if they should be automatically enabled or not). $step_pms_data = (array) $this->get_nox_profile_onboarding_step_data_entry( self::ONBOARDING_STEP_PAYMENT_METHODS, $location, 'payment_methods' ); $payment_methods_state = array(); $apple_pay_enabled = false; $google_pay_enabled = false; foreach ( $recommended_pms as $recommended_pm ) { $pm_id = $recommended_pm['id']; /** * We need to handle Apple Pay and Google Pay separately. * They are not stored in the same way as the other payment methods. */ if ( 'apple_pay' === $pm_id ) { $apple_pay_enabled = $recommended_pm['enabled']; continue; } if ( 'google_pay' === $pm_id ) { $google_pay_enabled = $recommended_pm['enabled']; continue; } // Start with the recommended enabled state. $payment_methods_state[ $pm_id ] = $recommended_pm['enabled']; // Force enable if required. if ( $recommended_pm['required'] ) { $payment_methods_state[ $pm_id ] = true; continue; } // Check the stored state, if any. if ( isset( $step_pms_data[ $pm_id ] ) ) { $payment_methods_state[ $pm_id ] = filter_var( $step_pms_data[ $pm_id ], FILTER_VALIDATE_BOOLEAN ); } } // Combine Apple Pay and Google Pay into a single `apple_google` entry. $apple_google_enabled = $apple_pay_enabled || $google_pay_enabled; // Optionally also respect stored state or forced requirements if needed here. $payment_methods_state['apple_google'] = $apple_google_enabled; return $payment_methods_state; } /** * Get the WPCOM (Jetpack) connection authorization details. * * @param string $return_url The URL to redirect to after the connection is set up. * * @return array The WPCOM connection authorization details. */ private function get_wpcom_connection_authorization( string $return_url ): array { return $this->proxy->call_static( Utils::class, 'get_wpcom_connection_authorization', $return_url ); } /** * Get the store's WPCOM (Jetpack) connection state. * * @return array The WPCOM connection state. */ private function get_wpcom_connection_state(): array { $is_connected = $this->wpcom_connection_manager->is_connected(); $has_connected_owner = $this->wpcom_connection_manager->has_connected_owner(); return array( 'has_working_connection' => $this->has_working_wpcom_connection(), 'is_store_connected' => $is_connected, 'has_connected_owner' => $has_connected_owner, 'is_connection_owner' => $has_connected_owner && $this->wpcom_connection_manager->is_connection_owner(), ); } /** * Check if the store has a working WPCOM connection. * * The store is considered to have a working WPCOM connection if: * - The store is connected to WPCOM (blog ID and tokens are set). * - The store connection has a connected owner (connection owner is set). * * @return bool Whether the store has a working WPCOM connection. */ private function has_working_wpcom_connection(): bool { return $this->wpcom_connection_manager->is_connected() && $this->wpcom_connection_manager->has_connected_owner(); } /** * Check if the WooPayments plugin is active. * * @return boolean */ private function is_extension_active(): bool { return $this->proxy->call_function( 'class_exists', '\WC_Payments' ); } /** * Get the main payment gateway instance. * * @return \WC_Payment_Gateway The main payment gateway instance. */ private function get_payment_gateway(): \WC_Payment_Gateway { return $this->proxy->call_static( '\WC_Payments', 'get_gateway' ); } /** * Determine if WooPayments has an account set up. * * @return bool Whether WooPayments has an account set up. */ private function has_account(): bool { return $this->provider->is_account_connected( $this->get_payment_gateway() ); } /** * Determine if WooPayments has a valid, fully onboarded account set up. * * @return bool Whether WooPayments has a valid, fully onboarded account set up. */ private function has_valid_account(): bool { if ( ! $this->has_account() ) { return false; } $account_service = $this->proxy->call_static( '\WC_Payments', 'get_account_service' ); return $account_service->is_stripe_account_valid(); } /** * Determine if WooPayments has a working account set up. * * This is a more specific check than has_valid_account() and checks if payments are enabled for the account. * * @return bool Whether WooPayments has a working account set up. */ private function has_working_account(): bool { if ( ! $this->has_account() ) { return false; } $account_service = $this->proxy->call_static( '\WC_Payments', 'get_account_service' ); $account_status = $account_service->get_account_status_data(); return ! empty( $account_status['paymentsEnabled'] ); } /** * Determine if WooPayments has a test account set up. * * @return bool Whether WooPayments has a test account set up. */ private function has_test_account(): bool { if ( ! $this->has_account() ) { return false; } $account_service = $this->proxy->call_static( '\WC_Payments', 'get_account_service' ); $account_status = $account_service->get_account_status_data(); return ! empty( $account_status['testDrive'] ); } /** * Determine if WooPayments has a live account set up. * * @return bool Whether WooPayments has a test account set up. */ private function has_live_account(): bool { if ( ! $this->has_account() ) { return false; } $account_service = $this->proxy->call_static( '\WC_Payments', 'get_account_service' ); $account_status = $account_service->get_account_status_data(); return ! empty( $account_status['isLive'] ); } /** * Get the onboarding fields data for the KYC business verification. * * @param string $location The location for which we are onboarding. * This is a ISO 3166-1 alpha-2 country code. * * @return array The onboarding fields data. * @throws Exception If the onboarding fields data could not be retrieved or there was an error. */ private function get_onboarding_kyc_fields( string $location ): array { // Call the WooPayments API to get the onboarding fields. $response = $this->proxy->call_static( Utils::class, 'rest_endpoint_get_request', '/wc/v3/payments/onboarding/fields' ); if ( is_wp_error( $response ) ) { throw new Exception( esc_html( $response->get_error_message() ) ); } if ( ! is_array( $response ) || ! isset( $response['data'] ) ) { throw new Exception( esc_html__( 'Failed to get onboarding fields data.', 'woocommerce' ) ); } $fields = $response['data']; // If there is no available_countries entry, add it. if ( ! isset( $fields['available_countries'] ) && $this->proxy->call_function( 'is_callable', '\WC_Payments_Utils::supported_countries' ) ) { $fields['available_countries'] = $this->proxy->call_static( '\WC_Payments_Utils', 'supported_countries' ); } $fields['location'] = $location; return $fields; } /** * Get the fallback URL for the embedded KYC flow. * * @return string The fallback URL for the embedded KYC flow. */ private function get_onboarding_kyc_fallback_url(): string { if ( $this->proxy->call_function( 'is_callable', '\WC_Payments_Account::get_connect_url' ) ) { return $this->proxy->call_static( '\WC_Payments_Account', 'get_connect_url', self::FROM_NOX_IN_CONTEXT ); } // Fall back to the provider onboarding URL. return $this->provider->get_onboarding_url( $this->get_payment_gateway(), Utils::wc_payments_settings_url( self::ONBOARDING_PATH_BASE, array( 'from' => self::FROM_KYC ) ) ); } /** * Get the WooPayments Overview page URL. * * @return string The WooPayments Overview page URL. */ private function get_overview_page_url(): string { if ( $this->proxy->call_function( 'is_callable', '\WC_Payments_Account::get_overview_page_url' ) ) { return add_query_arg( array( 'from' => self::FROM_NOX_IN_CONTEXT, ), $this->proxy->call_static( '\WC_Payments_Account', 'get_overview_page_url' ) ); } // Fall back to the known WooPayments Overview page URL. return add_query_arg( array( 'page' => 'wc-admin', 'path' => '/payments/overview', 'from' => self::FROM_NOX_IN_CONTEXT, ), admin_url( 'admin.php' ) ); } /** * Get the business location country code for the Payments settings. * * @return string The ISO 3166-1 alpha-2 country code to use for the overall business location. * If the user didn't set a location, the WC base location country code is used. */ private function get_country(): string { $user_nox_meta = get_user_meta( get_current_user_id(), self::PAYMENTS_NOX_PROFILE_KEY, true ); if ( ! empty( $user_nox_meta['business_country_code'] ) ) { return $user_nox_meta['business_country_code']; } return WC()->countries->get_base_country(); } /** * Send a Tracks event. * * By default, Woo adds `url`, `blog_lang`, `blog_id`, `store_id`, `products_count`, and `wc_version` * properties to every event. * * @param string $name The event name. * If it is not prefixed with self::EVENT_PREFIX, it will be prefixed with it. * @param string $business_country The business registration country code as set in the WooCommerce Payments settings. * This is a ISO 3166-1 alpha-2 country code. * @param array $properties Optional. The event custom properties. * These properties will be merged with the default properties. * Default properties values take precedence over the provided ones. * * @return void */ private function record_event( string $name, string $business_country, array $properties = array() ) { if ( ! function_exists( 'wc_admin_record_tracks_event' ) ) { return; } // If the event name is empty, we don't record it. if ( empty( $name ) ) { return; } // If the event name is not prefixed with `settings_payments_`, we prefix it. if ( ! str_starts_with( $name, self::EVENT_PREFIX ) ) { $name = self::EVENT_PREFIX . $name; } // Add default properties to every event and overwrite custom properties with the same keys. $properties = array_merge( $properties, array( 'business_country' => $business_country, ), ); wc_admin_record_tracks_event( $name, $properties ); } }