Files
wp-multisite-waas/inc/checkout/class-checkout.php
2025-02-07 19:02:33 -07:00

2400 lines
61 KiB
PHP

<?php
/**
* Handles the processing of new membership purchases.
*
* @package WP_Ultimo
* @subpackage Checkout
* @since 2.0.0
*/
namespace WP_Ultimo\Checkout;
// Exit if accessed directly
defined('ABSPATH') || exit;
use Psr\Log\LogLevel;
use WP_Ultimo\Database\Sites\Site_Type;
use WP_Ultimo\Database\Payments\Payment_Status;
use WP_Ultimo\Database\Memberships\Membership_Status;
use WP_Ultimo\Checkout\Checkout_Pages;
use WP_Ultimo\Objects\Billing_Address;
use WP_Ultimo\Models\Site;
/**
* Handles the processing of new membership purchases.
*
* @since 2.0.0
*/
class Checkout {
use \WP_Ultimo\Traits\Singleton;
/**
* Holds checkout errors.
*
* @since 2.0.0
* @var \WP_Error|null
*/
public $errors;
/**
* Keeps a reference to our order.
*
* @since 2.0.0
* @var Cart
*/
protected $order;
/*
* Checkout progress info
*/
/**
* Current step of the signup flow.
*
* @since 2.0.0
* @var string
*/
public $step;
/**
* Keeps the name of the step.
*
* @since 2.0.0
* @var string
*/
public $step_name;
/**
* The current checkout form being used.
*
* @since 2.0.0
* @var \WP_Ultimo\Models\Checkout_Form
*/
public $checkout_form;
/**
* List of steps for the signup flow.
*
* @since 2.0.0
* @var array
*/
public $steps;
/**
* Session object.
*
* @since 2.0.0
* @var \WP_Ultimo\Contracts\Session
*/
protected $session;
/**
* Checkout type.
*
* @since 2.0.0
* @var string
*/
protected $type = 'new';
/**
* Check if setup method already run.
*
* @since 2.0.18
* @var bool
*/
protected $already_setup = false;
/**
* Checks if a list of fields has an auto-submittable field.
*
* @since 2.1.2
* @var false|string
*/
protected $auto_submittable_field;
/**
* The gateway id.
*
* @since 2.1.2
* @var string|bool
*/
protected $gateway_id;
/**
* The customer object.
*
* @since 2.1.2
* @var \WP_Ultimo\Models\Customer
*/
protected $customer;
/**
* The membership object.
*
* @since 2.0.23
* @var \WP_Ultimo\Models\Membership
*/
protected $membership;
/**
* The pending site object.
*
* @since 2.1.2
* @var Site
*/
protected $pending_site;
/**
* The payment object.
*
* @since 2.1.2
* @var \WP_Ultimo\Models\Payment
*/
protected $payment;
/**
* Initializes the Checkout singleton and adds hooks.
*
* @since 2.0.0
* @return void
*/
public function init() {
/*
* Setup and handle checkout
*/
add_action('wu_setup_checkout', array($this, 'setup_checkout'));
add_action('wu_setup_checkout', array($this, 'maybe_process_checkout'), 20);
/*
* Add the rewrite rules.
*/
add_action('init', array($this, 'add_rewrite_rules'), 20);
add_filter('wu_request', array($this, 'get_checkout_from_query_vars'), 10, 2);
/*
* Creates the order object to display to the customer
*/
add_action('wu_ajax_wu_create_order', array($this, 'create_order'));
add_action('wu_ajax_nopriv_wu_create_order', array($this, 'create_order'));
/*
* Validates form and process preflight.
*/
add_action('wu_ajax_wu_validate_form', array($this, 'maybe_handle_order_submission'));
add_action('wu_ajax_nopriv_wu_validate_form', array($this, 'maybe_handle_order_submission'));
/*
* Adds the necessary scripts
*/
add_action('wu_checkout_scripts', array($this, 'register_scripts'));
/*
* Errors
*/
add_action('wu_checkout_errors', array($this, 'maybe_display_checkout_errors'));
}
/**
* Add checkout rewrite rules.
*
* Adds the following URL structures.
* For this example, let's use /register as the registration page.
*
* It registers:
* 1. site.com/register/plan_id: Pre-selects the plan_id;
* 2. site.com/register/plan_id/3: Pre-selects the plan_id and 3 months;
* 3. site.com/register/plan_id/12/year: Pre-selects the plan and the duration unit.
*
* @since 2.0.0
* @return void
*/
public function add_rewrite_rules() {
$register = Checkout_Pages::get_instance()->get_signup_page('register');
if ( ! is_a($register, '\WP_Post')) {
return;
}
$register_slug = $register->post_name;
/*
* The first rewrite rule.
*
* This will match the registration URL and a plan
* slug.
*
* Example: site.com/register/premium
* Will pre-select the premium product.
*/
add_rewrite_rule(
"{$register_slug}\/([0-9a-zA-Z-_]+)[\/]?$",
'index.php?pagename=' . $register_slug . '&products[]=$matches[1]&wu_preselected=products',
'top'
);
/*
* This one is here for backwards compatibility.
* It always assign to months.
*/
add_rewrite_rule(
"{$register_slug}\/([0-9a-zA-Z-_]+)\/([0-9]+)[\/]?$",
'index.php?pagename=' . $register_slug . '&products[]=$matches[1]&duration=$matches[2]&duration_unit=month&wu_preselected=products',
'top'
);
/*
* This is the one we really want.
* It allows us to create custom registration URLs
* such as /register/premium/1/year
*/
add_rewrite_rule(
"{$register_slug}\/([0-9a-zA-Z-_]+)\/([0-9]+)[\/]?([a-z]+)[\/]?$",
'index.php?pagename=' . $register_slug . '&products[]=$matches[1]&duration=$matches[2]&duration_unit=$matches[3]&wu_preselected=products',
'top'
);
/*
* By the default, the template selection
* URL structure uses the word template.
* This can be changed using the filter below.
*/
$template_slug = apply_filters('wu_template_selection_rewrite_rule_slug', 'template', $register_slug);
/*
* Template site pre-selection.
* Allows for registration urls such as
* /register/template/starter
*/
add_rewrite_rule(
"{$register_slug}\/{$template_slug}\/([0-9a-zA-Z-_]+)[\/]?$",
'index.php?pagename=' . $register_slug . '&template_name=$matches[1]&wu_preselected=template_id',
'top'
);
}
/**
* Filters the wu_request with the query vars.
*
* @since 2.0.0
*
* @param mixed $value The value from wu_request.
* @param string $key The key value.
* @return mixed
*/
public function get_checkout_from_query_vars($value, $key) {
if ( ! did_action('wp')) {
return $value;
}
$from_query = get_query_var($key);
$cart_arguments = apply_filters(
'wu_get_checkout_from_query_vars',
array(
'products',
'duration',
'duration_unit',
'template_id',
'wu_preselected',
)
);
/**
* Deal with site templates in a specific manner.
*
* @since 2.0.8
*/
if ($key === 'template_id') {
$template_name = get_query_var('template_name', null);
if ($template_name !== null) {
$d = wu_get_site_domain_and_path($template_name);
$wp_site = get_site_by_path($d->domain, $d->path);
$site = $wp_site ? wu_get_site($wp_site->blog_id) : false;
if ($site && $site->get_type() === Site_Type::SITE_TEMPLATE) {
return $site->get_id();
}
}
}
/*
* Otherwise, simply check for its existence
* on the query object.
*/
if (in_array($key, $cart_arguments, true) && $from_query) {
return $from_query;
}
return $value;
}
/**
* Setups the necessary boilerplate code to have checkouts work.
*
* @since 2.0.0
* @param \WP_Ultimo\UI\Checkout_Element $element The checkout element.
* @return void
*/
public function setup_checkout($element = null) {
if ($this->already_setup) {
return;
}
$checkout_form_slug = wu_request('checkout_form');
if (wu_request('pre-flight')) {
$checkout_form_slug = false;
$_REQUEST['pre_selected'] = $_REQUEST;
}
if ( ! $checkout_form_slug && is_a($element, \WP_Ultimo\UI\Checkout_Element::class)) {
$pre_loaded_checkout_form_slug = $element->get_pre_loaded_attribute('slug', $checkout_form_slug);
$checkout_form_slug = $pre_loaded_checkout_form_slug ? $pre_loaded_checkout_form_slug : $checkout_form_slug;
}
$this->checkout_form = wu_get_checkout_form_by_slug($checkout_form_slug);
if ($this->session === null) {
$this->session = wu_get_session('signup');
}
if ($this->checkout_form) {
$this->steps = $this->checkout_form->get_steps_to_show();
$first_step = current($this->steps);
$step_name = wu_request('checkout_step', wu_get_isset($first_step, 'id', 'checkout'));
$this->step_name = $step_name;
$this->step = $this->checkout_form->get_step($this->step_name, true);
$this->step['fields'] ??= array();
$this->auto_submittable_field = $this->contains_auto_submittable_field($this->step['fields']);
$this->step['fields'] = wu_create_checkout_fields($this->step['fields']);
}
if (is_user_logged_in()) {
$_REQUEST['user_id'] = get_current_user_id();
}
$this->already_setup = true;
wu_no_cache(); // Prevent the registration page from being cached.
}
/**
* Checks if a list of fields has an auto-submittable field.
*
* @since 2.0.4
*
* @param array $fields The list of fields of a step we need to check.
* @return false|string False if no auto-submittable field is present, the field to watch otherwise.
*/
public function contains_auto_submittable_field($fields) {
$relevant_fields = array();
$field_types_to_ignore = array(
'hidden',
'products',
'submit_button',
'period_selection',
'steps',
);
// Extra check to prevent error messages from being displayed.
if ( ! is_array($fields)) {
$fields = array();
}
foreach ($fields as $field) {
if (in_array($field['type'], $field_types_to_ignore, true)) {
continue;
}
$relevant_fields[] = $field;
if (count($relevant_fields) > 1) {
return false;
}
}
if ( ! $relevant_fields) {
return false;
}
$auto_submittable_field = $relevant_fields[0]['type'];
return wu_get_isset($this->get_auto_submittable_fields(), $auto_submittable_field, false);
}
/**
* Returns a list of auto-submittable fields.
*
* @since 2.0.4
* @return array
*/
public function get_auto_submittable_fields() {
/**
* They key should be the signup field ID to search for,
* while the value should be the parameter we should watch for changes
* so we can submit the form when we detect one.
*/
$auto_submittable_fields = array(
'template_selection' => 'template_id',
'pricing_table' => 'products',
);
return apply_filters('wu_checkout_get_auto_submittable_fields', $auto_submittable_fields, $this);
}
/**
* Decides if we want to handle a step submission or a full checkout submission.
*
* @since 2.0.0
* @return void
*/
public function maybe_handle_order_submission() {
$this->setup_checkout();
if ($this->is_last_step()) {
$this->handle_order_submission();
} else {
$validation = $this->validate();
if (is_wp_error($validation)) {
wp_send_json_error($validation);
}
wp_send_json_success(array());
}
}
/**
* Validates the order submission, and then delegates the processing to the gateway.
*
* We use database transactions in here to prevent failed sign-ups from being
* committed to the database. This means that if a \Throwable or a WP_Error
* happens anywhere in the process, we halt it and rollback on writes up to that point.
*
* @since 2.0.0
* @return void
*/
public function handle_order_submission() {
global $wpdb;
$wpdb->query('START TRANSACTION');
try {
/*
* Allow developers to intercept an order submission.
*/
do_action('wu_before_handle_order_submission', $this);
/*
* Here's where we actually process the order.
*
* Throwables are caught and they rollback
* any database writes sent up until this point.
*
* @see process_order below.
* @since 2.0.0
*/
$results = $this->process_order();
/*
* Allow developers to change the results an order submission.
*/
do_action('wu_after_handle_order_submission', $results, $this);
if (is_wp_error($results)) {
$this->errors = $results;
}
} catch (\Throwable $e) {
wu_maybe_log_error($e);
$wpdb->query('ROLLBACK');
$this->errors = new \WP_Error('exception-order-submission', $e->getMessage(), $e->getTrace());
}
if (is_wp_error($this->errors)) {
$wpdb->query('ROLLBACK');
wp_send_json_error($this->errors);
}
$wpdb->query('COMMIT');
$this->session->set('signup', array());
$this->session->commit();
wp_send_json_success($results);
}
/**
* Process an order.
*
* This method is responsible for
* creating all the data elements we
* need in order to actually process a
* checkout.
*
* Those include:
* - A customer;
* - A pending payment;
* - A membership.
*
* With those elements, we can then
* delegate to the gateway to run their
* own preparations (@see run_preflight).
*
* We then return everything to be added
* to the front-end form. That data then
* gets submitted with the rest of the form,
* and eventually handled by process_checkout.
*
* @see process_checkout
*
* @since 2.0.0
* @return mixed[]|\WP_Error
*/
public function process_order() {
global $current_site, $wpdb;
/*
* First, we start to work on the cart object.
* We need to take into consideration the date we receive from
* the form submission.
*/
$cart = new Cart(
apply_filters(
'wu_cart_parameters',
array(
'products' => $this->request_or_session('products', array()),
'discount_code' => $this->request_or_session('discount_code'),
'country' => $this->request_or_session('billing_country'),
'state' => $this->request_or_session('billing_state'),
'city' => $this->request_or_session('billing_city'),
'membership_id' => $this->request_or_session('membership_id'),
'payment_id' => $this->request_or_session('payment_id'),
'auto_renew' => $this->request_or_session('auto_renew', false),
'duration' => $this->request_or_session('duration'),
'duration_unit' => $this->request_or_session('duration_unit'),
'cart_type' => $this->request_or_session('cart_type', 'new'),
),
$this
)
);
/*
* Check if our order is valid.
*
* The is valid method checks for
* cart setup issues, as well as
*/
if ($cart->is_valid() === false) {
return $cart->get_errors();
}
/*
* Update the checkout type
* based on the cart type we have on hand.
*/
$this->type = $cart->get_cart_type();
/*
* Gets the gateway object we want to use.
*
* This will have been set on a previous step (session)
* or is going to be passed via the form (request)
*/
$gateway_id = $this->request_or_session('gateway');
$gateway = wu_get_gateway($gateway_id);
/*
* We need to handle free payments separately.
*
* In the same manner, if the order
* IS NOT free, we need to make sure
* the customer is not trying to game the system
* passing the free gateway to get an free account.
*
* That's what's we checking on the else case.
*/
if ($cart->should_collect_payment() === false) {
$gateway = wu_get_gateway('free');
} elseif ( ! $gateway || $gateway->get_id() === 'free') {
$this->errors = new \WP_Error('no-gateway', __('Payment gateway not registered.', 'wp-ultimo'));
return false;
}
/*
* If we do not have a gateway object,
* we need to bail.
*/
if ( ! $gateway) {
return new \WP_Error('no-gateway', __('Payment gateway not registered.', 'wp-ultimo'));
}
$this->gateway_id = $gateway->get_id();
/*
* Now we need to validate the form.
*
* Here we use the validation rules set.
* @see validation_rules
*/
$validation = $this->validate();
/*
* Bail on error.
*/
if (is_wp_error($validation)) {
return $validation;
}
/*
* From now on, logic can be delegated to
* special methods, so we need to set
* the order as globally accessible.
*/
$this->order = $cart;
/*
* Handles display names, if needed.
*/
add_filter('pre_user_display_name', array($this, 'handle_display_name'));
/*
* If we get to this point, most of the validations are done.
* Now, we will actually begin to create new data elements
* if necessary.
*
* First, we need to check for a customer.
*/
$this->customer = $this->maybe_create_customer();
/*
* We encountered errors while trying to create
* a new customer or retrieve an existing one.
*/
if (is_wp_error($this->customer)) {
return $this->customer;
}
/*
* Next, we need to create a membership.
*
* The cart object has a couple of handy methods
* that allow us to easily convert from it
* to an array of data that we can use
* to create a membership.
*/
$this->membership = $this->maybe_create_membership();
/*
* We encountered errors while trying to create
* a new membership or retrieve an existing one.
*/
if (is_wp_error($this->membership)) {
return $this->membership;
}
/*
* Next step: maybe create a site.
*
* Depending on the status of the cart,
* we might need to create a pending site to
* attach to the membership.
*/
$this->pending_site = $this->maybe_create_site();
/*
* It's not really possible to get a wp error
* in here for now but completeness dictates I add this.
*/
if (is_wp_error($this->pending_site)) {
return $this->pending_site;
}
/*
* Next, we need to create a payment.
*
* The cart object has a couple of handy methods
* that allow us to easily convert from it
* to an array of data that we can use
* to create a payment.
*/
$this->payment = $this->maybe_create_payment();
/*
* We encountered errors while trying to create
* a new payment or retrieve an existing one.
*/
if (is_wp_error($this->payment)) {
return $this->payment;
}
/**
* Keep the cart used in checkout for later reference.
*/
$this->payment->update_meta('wu_original_cart', $this->order);
/*
* Hey champs!
*
* If we are here, we have almost everything we
* need. Now is time to prepare things to hand
* over to the gateway.
*/
$this->order->set_customer($this->customer);
$this->order->set_membership($this->membership);
$this->order->set_payment($this->payment);
$gateway->set_order($this->order);
/*
* Before we move on,
* let's check if the user is logged in,
* and if not, let's do that.
*/
if ( ! is_user_logged_in()) {
wp_clear_auth_cookie();
wp_set_current_user($this->customer->get_user_id());
wp_set_auth_cookie($this->customer->get_user_id());
}
/*
* Action time.
*
* Here's where we actually call the gateway
* and build the success data we want to return to the
* front-end form.
*/
try {
/*
* Checks for free memberships.
*/
if ($this->order->is_free() && $this->order->get_recurring_total() === 0.0 && (! wu_get_setting('enable_email_verification', true) || $this->customer->get_email_verification() !== 'pending')) {
if ($this->order->get_plan_id() === $this->membership->get_plan_id()) {
$this->membership->set_status(Membership_Status::ACTIVE);
$this->membership->save();
}
/**
* Trigger payment received manually.
*
* @since 2.0.10
*/
$gateway->trigger_payment_processed($this->payment, $this->membership);
} elseif ($this->order->has_trial()) {
$this->membership->set_date_trial_end(gmdate('Y-m-d 23:59:59', $this->order->get_billing_start_date()));
$this->membership->set_date_expiration(gmdate('Y-m-d 23:59:59', $this->order->get_billing_start_date()));
if (wu_get_setting('allow_trial_without_payment_method') && (! wu_get_setting('enable_email_verification', true) || $this->customer->get_email_verification() !== 'pending')) {
/*
* In this particular case, we need to set the status to trialing here as we will not update the membership after and then, publish the site.
*/
$this->membership->set_status(Membership_Status::TRIALING);
$this->membership->publish_pending_site_async();
}
$this->membership->save();
/**
* Trigger payment received manually.
*
* @since 2.0.10
*/
$gateway->trigger_payment_processed($this->payment, $this->membership);
}
$success_data = array(
'nonce' => wp_create_nonce('wp-ultimo-register-nonce'),
'customer' => $this->customer->to_search_results(),
'total' => $this->order->get_total(),
'recurring_total' => $this->order->get_recurring_total(),
'membership_id' => $this->membership->get_id(),
'payment_id' => $this->payment->get_id(),
'cart_type' => $this->order->get_cart_type(),
'auto_renew' => $this->order->should_auto_renew(),
'gateway' => array(
'slug' => $gateway->get_id(),
'data' => array(),
),
);
/*
* Let's the gateway do its thing.
*
* Here gateways will run pre-flight code
* such as setting up payment intents and other
* important things that we need to be able to finish
* the process.
*/
$result = $gateway->run_preflight();
/*
* Attach the gateway results to the return array.
*/
$success_data['gateway']['data'] = $result && is_array($result) ? $result : array();
/*
* On error, bail.
*/
if (is_wp_error($result)) {
return $result;
}
} catch (\Throwable $e) {
wu_maybe_log_error($e);
return new \WP_Error('exception', $e->getMessage(), $e->getTrace());
}
/**
* Allow developers to triggers additional hooks.
*
* @since 2.0.9
*
* @param Checkout $checkout The checkout object instance.
* @param Cart $cart The checkout cart instance.
* @return void
*/
do_action('wu_checkout_after_process_order', $this, $this->order);
/*
* All set!
*/
return $success_data;
}
/**
* Checks if a customer exists, otherwise, creates a new one.
*
* @since 2.0.0
* @return \WP_Ultimo\Models\Customer|\WP_Error
*/
protected function maybe_create_customer() {
/*
* Check if we have
* a customer for the current user.
*/
$customer = wu_get_current_customer();
/*
* Get the form slug to save with the customer.
*/
$form_slug = $this->checkout_form ? $this->checkout_form->get_slug() : 'none';
/*
* We don't have one,
* so we'll need to create it.
*
* We can't return early because we need
* to set and validate the billing address,
* and that happens at the end of this method.
*/
if (empty($customer)) {
$username = $this->request_or_session('username');
/*
* Handles auto-generation based on the email address.
*/
if ($this->request_or_session('auto_generate_username') === 'email') {
$username = wu_username_from_email($this->request_or_session('email_address'));
/*
* Case where the site title is also auto-generated, based on the username.
*/
if ($this->request_or_session('auto_generate_site_title') && $this->request_or_session('site_title', '') === '') {
$_REQUEST['site_title'] = $username;
}
}
/*
* If we get to this point,
* we don't have an existing customer.
*
* Next step then would be to create one.
*/
$customer_data = array(
'username' => $username,
'email' => $this->request_or_session('email_address'),
'password' => $this->request_or_session('password'),
'email_verification' => $this->get_customer_email_verification_status(),
'signup_form' => $form_slug,
'meta' => array(),
);
/*
* If the user is logged in,
* we use the existing email address to create the customer.
*/
if ($this->is_existing_user()) {
$customer_data = array(
'email' => wp_get_current_user()->user_email,
'email_verification' => 'verified',
);
} elseif (isset($customer_data['email']) && get_user_by('email', $customer_data['email'])) {
return new \WP_Error('email_exists', __('The email address you entered is already in use.', 'wp-ultimo'));
}
/*
* Tries to create it.
*/
$customer = wu_create_customer($customer_data);
/*
* Something failed, bail.
*/
if (is_wp_error($customer)) {
return $customer;
}
}
/*
* Updates IP, and country
*/
$customer->update_last_login(true, true);
/*
* Next, we need to validate the billing address,
* and save it.
*/
$billing_address = $customer->get_billing_address();
/*
* I know this appears super unsafe,
* but we clean the data up on the billing address
* class, so there's no problem in passing
* the entire post array in here.
*/
$session = $this->session->get('signup') ?? array();
$billing_address->attributes(array_merge($session, $_POST));
/*
* Validates the address.
*/
$valid_address = $billing_address->validate();
/*
* There's something invalid on the address,
* bail with the errors.
*/
if (is_wp_error($valid_address)) {
return $valid_address;
}
$customer->set_billing_address($billing_address);
$address_saved = $customer->save();
/*
* This should rarely happen, but if something goes
* wrong with the customer update, we return a general error.
*/
if ( ! $address_saved) {
return new \WP_Error('address_failure', __('Something wrong happened while attempting to save the customer billing address', 'wp-ultimo'));
}
/*
* Handle meta fields.
*
* Gets all the meta fields for customers and
* save them to the customer as meta.
*/
$this->handle_customer_meta_fields($customer, $form_slug);
/**
* Allow plugin developers to do additional stuff when the customer
* is added.
*
* Here's where we add the hooks for adding the customer->user to
* the main site as well, for example.
*
* @since 2.0.0
* @param Customer $customer The customer that was maybe created.
* @param Checkout $this The current checkout class.
*/
do_action('wu_maybe_create_customer', $customer, $this);
/*
* Otherwise, get the customer back.
*/
return $customer;
}
/**
* Save meta data related to customers.
*
* @since 2.0.0
*
* @param Customer $customer The created customer.
* @param string $form_slug The form slug.
* @return void
*/
protected function handle_customer_meta_fields($customer, $form_slug) {
if (empty($form_slug) || $form_slug === 'none') {
return;
}
$checkout_form = wu_get_checkout_form_by_slug($form_slug);
if ($checkout_form) {
$customer_meta_fields = $checkout_form->get_all_meta_fields('customer_meta');
$meta_repository = array();
foreach ($customer_meta_fields as $customer_meta_field) {
/*
* Adds to the repository so we can save it again.
* in filters, if we need be.
*/
$meta_repository[ $customer_meta_field['id'] ] = $this->request_or_session($customer_meta_field['id']);
wu_update_customer_meta(
$customer->get_id(),
$customer_meta_field['id'],
$this->request_or_session($customer_meta_field['id']),
$customer_meta_field['type'],
$customer_meta_field['name']
);
}
/**
* Allow plugin developers to save meta
* data in different ways if they need to.
*
* @since 2.0.0
* @param array $meta_repository The list of meta fields, key => value structured.
* @param Customer $customer The WP Multisite WaaS customer object.
* @param Checkout $this The checkout class.
*/
do_action('wu_handle_customer_meta_fields', $meta_repository, $customer, $this);
/**
* Do basically the same thing, now for user meta.
*
* @since 2.0.4
*/
$user_meta_fields = $checkout_form->get_all_meta_fields('user_meta');
$user = $customer->get_user();
$user_meta_repository = array();
foreach ($user_meta_fields as $user_meta_field) {
/*
* Adds to the repository so we can save it again.
* in filters, if we need be.
*/
$user_meta_repository[ $user_meta_field['id'] ] = $this->request_or_session($user_meta_field['id']);
update_user_meta($customer->get_user_id(), $user_meta_field['id'], $this->request_or_session($user_meta_field['id']));
}
/**
* Allow plugin developers to save user meta
* data in different ways if they need to.
*
* @since 2.0.4
* @param array $meta_repository The list of meta fields, key => value structured.
* @param \WP_User $user The WordPress user object.
* @param Customer $customer The WP Multisite WaaS customer object.
* @param Checkout $this The checkout class.
*/
do_action('wu_handle_user_meta_fields', $user_meta_repository, $user, $customer, $this);
}
}
/**
* Checks if a membership exists, otherwise, creates a new one.
*
* @since 2.0.0
* @return \WP_Ultimo\Models\Membership|\WP_Error
*/
protected function maybe_create_membership() {
/*
* The first thing we'll do is check the cart
* to see if a membership was passed.
*/
if ($this->order->get_membership()) {
return $this->order->get_membership();
}
/*
* If that's not the case,
* we'll need to create a new one.
*
* The cart object has a couple of handy methods
* that allow us to easily convert from it
* to an array of data that we can use
* to create a membership.
*/
$membership_data = $this->order->to_membership_data();
/*
* Append additional data to the membership.
*/
$membership_data['customer_id'] = $this->customer->get_id();
$membership_data['user_id'] = $this->customer->get_user_id();
$membership_data['gateway'] = $this->gateway_id;
$membership_data['signup_method'] = wu_request('signup_method');
/*
* Important dates.
*/
$membership_data['date_expiration'] = $this->order->get_billing_start_date();
$membership = wu_create_membership($membership_data);
$discount_code = $this->order->get_discount_code();
if ($discount_code) {
$membership->set_discount_code($discount_code);
$membership->save();
}
return $membership;
}
/**
* Checks if a pending site exists, otherwise, creates a new one.
*
* @since 2.0.0
* @return bool|\WP_Ultimo\Models\Site|\WP_Error
*/
protected function maybe_create_site() {
/*
* Let's get a list of membership sites.
* This list includes pending sites as well.
*/
$sites = $this->membership->get_sites();
/*
* Decide if we should create a new site or not.
*
* When should we create a new pending site?
* There are a couple of rules:
* - The membership must not have a pending site;
* - The membership must not have an existing site;
*
* The get_sites method already includes pending sites,
* so we can safely rely on it.
*/
if ( ! empty($sites)) {
/*
* Returns the first site on that list.
* This is not ideal, but since we'll usually only have
* one site here, it's ok. for now.
*/
return current($sites);
}
$site_url = $this->request_or_session('site_url');
$site_title = $this->request_or_session('site_title');
if ( ! $site_url && ! $site_title) {
return false;
}
$auto_generate_url = $this->request_or_session('auto_generate_site_url');
$site_title = ! $site_title && ! $auto_generate_url ? $site_url : $site_title;
/*
* Let's handle auto-generation of site URLs.
*
* To decide if we need to auto-generate the site URL,
* we'll check the request for the auto_generate_site_url = username request value.
*
* If that's present and no site_url is present, then we need to auto-generate this.
* The strategy here is simple, we basically set the site_url to the username and
* check if it is already taken.
*/
if (empty($site_url) || $auto_generate_url === 'username') {
if ($auto_generate_url === 'username') {
$site_url = $this->customer->get_username();
$site_title = $site_title ? $site_title : $site_url;
} else {
$site_url = strtolower(str_replace(' ', '', preg_replace('/&([a-z])[a-z]+;/i', '$1', htmlentities(trim((string) $site_title)))));
}
$d = wu_get_site_domain_and_path($site_url, $this->request_or_session('site_domain'));
$n = 0;
while (domain_exists($d->domain, $d->path)) {
++$n;
$site_url = $this->customer->get_username() . $n;
$d = wu_get_site_domain_and_path($site_url, $this->request_or_session('site_domain'));
}
}
$d = wu_get_site_domain_and_path($site_url, $this->request_or_session('site_domain'));
/*
* Validates the site url.
*/
$results = wpmu_validate_blog_signup($site_url, $site_title, $this->customer->get_user());
if ($results['errors']->has_errors()) {
return $results['errors'];
}
/*
* Get the form slug to save with the customer.
*/
$form_slug = $this->checkout_form ? $this->checkout_form->get_slug() : 'none';
/*
* Get the transient data to save with the site
* that way we can use it when actually registering
* the site on WordPress.
*/
$transient = array();
if ($this->checkout_form) {
$site_meta_fields = $this->checkout_form->get_all_fields();
foreach ($site_meta_fields as $site_meta_field) {
/*
* Removes password fields from transient data,
* to make sure plain passwords do not get stored
* on the database.
*/
if (strpos((string) $site_meta_field['id'], 'password') !== false ) {
continue;
}
$transient[ $site_meta_field['id'] ] = $this->request_or_session($site_meta_field['id']);
}
}
/*
* Gets the template id from the request.
* Here, there's some logic we need to do to
* try to get the template id if we get a
* template name instead of a number.
*
* This logic is handled inside the
* get_checkout_from_query_vars() method.
*
* @see get_checkout_from_query_vars()
*/
$template_id = apply_filters('wu_checkout_template_id', (int) $this->request_or_session('template_id'), $this->membership, $this);
$site_data = array(
'domain' => $d->domain,
'path' => $d->path,
'title' => $site_title,
'template_id' => $template_id,
'customer_id' => $this->customer->get_id(),
'membership_id' => $this->membership->get_id(),
'transient' => $transient,
'signup_options' => $this->get_site_meta_fields($form_slug, 'site_option'),
'signup_meta' => $this->get_site_meta_fields($form_slug, 'site_meta'),
'type' => Site_Type::CUSTOMER_OWNED,
);
$pending_site = $this->membership->create_pending_site($site_data);
return $pending_site;
}
/**
* Gets list of site meta data.
*
* @since 2.0.0
*
* @param string $form_slug The form slug.
* @param string $meta_type The meta type. Can be site_meta or site_option.
* @return array
*/
protected function get_site_meta_fields($form_slug, $meta_type = 'site_meta') {
if (empty($form_slug) || $form_slug === 'none') {
return array();
}
$checkout_form = wu_get_checkout_form_by_slug($form_slug);
$list = array();
if ($checkout_form) {
$site_meta_fields = $checkout_form->get_all_meta_fields($meta_type);
foreach ($site_meta_fields as $site_meta_field) {
$list[ $site_meta_field['id'] ] = $this->request_or_session($site_meta_field['id']);
}
}
return $list;
}
/**
* Checks if a pending payment exists, otherwise, creates a new one.
*
* @since 2.0.0
* @return \WP_Ultimo\Models\Payment|\WP_Error
*/
protected function maybe_create_payment() {
/*
* The first thing we'll do is check the cart
* to see if a payment was passed.
*/
$payment = $this->order->get_payment();
if ($payment) {
/**
* Set the gateway in existing payment
*/
if ($payment->get_gateway() !== $this->gateway_id) {
$payment->set_gateway($this->gateway_id);
$payment->save();
}
return $this->order->get_payment();
}
/*
* The membership might have a previous payment.
* We'll go ahead and cancel that one out in cases
* of a upgrade/downgrade or add-on.
*/
$previous_payment = $this->membership->get_last_pending_payment();
$cancel_types = array(
'upgrade',
'downgrade',
'addon',
);
if ($previous_payment && in_array($this->type, $cancel_types, true)) {
$previous_payment->set_status(Payment_Status::CANCELLED);
/*
* This can actually return a wp_error,
* but to be honest, we don't really care if we
* were able to cancel the previous payment or not.
*/
$previous_payment->save();
}
/*
* If that's not the case,
* we'll need to create a new one.
*
* The cart object has a couple of handy methods
* that allow us to easily convert from it
* to an array of data that we can use
* to create a payment.
*/
$payment_data = $this->order->to_payment_data();
/*
* Append additional data to the payment.
*/
$payment_data['customer_id'] = $this->customer->get_id();
$payment_data['membership_id'] = $this->membership->get_id();
$payment_data['gateway'] = $this->gateway_id;
/*
* If this is a free order and a downgrade we need
* to handle the status here as the payment is not
* passed to process_checkout method in this case.
*/
if ( ! $this->order->should_collect_payment() && $this->type === 'downgrade') {
$payment_data['status'] = Payment_Status::COMPLETED;
}
/*
* Create new payment.
*/
$payment = wu_create_payment($payment_data);
/*
* Then, if this is a trial,
* we need to set the payment value to zero.
*/
if ($this->order->has_trial()) {
$payment->attributes(
array(
'tax_total' => 0,
'subtotal' => 0,
'refund_total' => 0,
'total' => 0,
)
);
$payment->save();
}
return $payment;
}
/**
* Validates the checkout form to see if it's valid por not.
*
* @since 2.0.0
* @return void
*/
public function validate_form() {
$validation = $this->validate();
if (is_wp_error($validation)) {
wp_send_json_error($validation);
}
wp_send_json_success();
}
/**
* Creates an order object to display the order summary tables.
*
* @since 2.0.0
* @return void
*/
public function create_order() {
$this->setup_checkout();
// Set billing address to be used on the order
$country = ! empty($this->request_or_session('country')) ? $this->request_or_session('country') : $this->request_or_session('billing_country', '');
$state = ! empty($this->request_or_session('state')) ? $this->request_or_session('state') : $this->request_or_session('billing_state', '');
$city = ! empty($this->request_or_session('city')) ? $this->request_or_session('city') : $this->request_or_session('billing_city', '');
$cart = new Cart(
apply_filters(
'wu_cart_parameters',
array(
'products' => $this->request_or_session('products', array()),
'discount_code' => $this->request_or_session('discount_code'),
'country' => $country,
'state' => $state,
'city' => $city,
'membership_id' => $this->request_or_session('membership_id'),
'payment_id' => $this->request_or_session('payment_id'),
'auto_renew' => $this->request_or_session('auto_renew', false),
'duration' => $this->request_or_session('duration'),
'duration_unit' => $this->request_or_session('duration_unit'),
'cart_type' => $this->request_or_session('cart_type', 'new'),
),
$this
)
);
/**
* Calculate state and city options, if necessary.
*
* @since 2.0.11
*/
$country_data = wu_get_country($cart->get_country());
wp_send_json_success(
array(
'order' => $cart->done(),
'states' => wu_key_map_to_array($country_data->get_states_as_options(), 'code', 'name'),
'cities' => wu_key_map_to_array($country_data->get_cities_as_options($state), 'code', 'name'),
'labels' => array(
'state_field' => $country_data->get_administrative_division_name(null, true),
'city_field' => $country_data->get_municipality_name(null, true),
),
)
);
}
/**
* Returns the checkout variables.
*
* @since 2.0.0
* @return array
*/
public function get_checkout_variables() {
global $current_site;
/*
* Localized strings.
*/
$i18n = array(
'loading' => __('Loading...', 'wp-ultimo'),
'added_to_order' => __('The item was added!', 'wp-ultimo'),
'weak_password' => __('The Password entered is too weak.', 'wp-ultimo'),
);
/*
* Get the default gateway.
*/
$default_gateway = current(array_keys(wu_get_active_gateway_as_options()));
$d = wu_get_site_domain_and_path('replace');
$site_domain = str_replace('replace.', '', (string) $d->domain);
$duration = $this->request_or_session('duration');
$duration_unit = $this->request_or_session('duration_unit');
// If duration is not set we check for a previous period_selection field in form to use;
if (empty($duration) && $this->steps) {
foreach ($this->steps as $step) {
foreach ($step['fields'] as $field) {
if ($field['type'] === 'period_selection') {
$duration = $field['period_options'][0]['duration'];
$duration_unit = $field['period_options'][0]['duration_unit'];
break;
}
}
if ($duration) {
break;
}
}
}
$products = array_merge($this->request_or_session('products', array()), wu_request('products', array()));
$geolocation = \WP_Ultimo\Geolocation::geolocate_ip('', true);
/*
* Set the default variables.
*/
$variables = array(
'i18n' => $i18n,
'ajaxurl' => wu_ajax_url(),
'late_ajaxurl' => wu_ajax_url('init'),
'baseurl' => remove_query_arg('pre-flight', wu_get_current_url()),
'country' => $this->request_or_session('billing_country', $geolocation['country']),
'state' => $this->request_or_session('billing_state', $geolocation['state']),
'city' => $this->request_or_session('billing_city'),
'duration' => $duration,
'duration_unit' => $duration_unit,
'site_url' => $this->request_or_session('site_url'),
'site_domain' => $this->request_or_session('site_domain', preg_replace('#^https?://#', '', $site_domain)),
'is_subdomain' => is_subdomain_install(),
'gateway' => wu_request('gateway', $default_gateway),
'needs_billing_info' => true,
'auto_renew' => true,
'products' => array_unique($products),
);
/*
* There's a couple of things we need to determine.
*
* First, we need to check for a payment parameter.
*/
$payment_hash = wu_request('payment');
/*
* If a hash exists, we need to retrieve the ID.
*/
$payment = wu_get_payment_by_hash($payment_hash);
$payment_id = $payment ? $payment->get_id() : 0;
/*
* With the payment id in hand, we can
* we do not pass the products, as this is
* a retry.
*/
if ($payment_id) {
$variables['payment_id'] = $payment_id;
}
/*
* The next case we need to take care of
* are addons, upgrades and downgrades.
*
* Those occur when we have a membership hash present
* and additional products, including or not a plan.
*/
$membership_hash = wu_request('membership');
/*
* If a hash exists, we need to retrieve the ID.
*/
$membership = wu_get_membership_by_hash($membership_hash);
$membership_id = $membership ? $membership->get_id() : 0;
/*
* With the membership id in hand, we can
* we do not pass the products, as this is
* a retry.
*/
if ($membership_id) {
$variables['membership_id'] = $membership_id;
}
list($plan, $other_products) = wu_segregate_products($variables['products']);
$variables['plan'] = $plan ? $plan->get_id() : 0;
/*
* Try to fetch the template_id
*/
$variables['template_id'] = $this->request_or_session('template_id', 0);
/*
* Let's also create a cart object,
* so we can pre-configure the form on the front-end
* accordingly.
*/
$variables['order'] = (new Cart($variables))->done();
/**
* Allow plugin developers to filter the pre-sets of a checkout page.
*
* Be careful, missing keys can completely break the checkout
* on the front-end.
*
* @since 2.0.0
* @param array $variables Localized variables.
* @param Checkout $this The checkout class.
* @return array The new variables array.
*/
return apply_filters('wu_get_checkout_variables', $variables, $this);
}
/**
* Returns the validation rules for the fields.
*
* @todo The fields needs to declare this themselves.
*
* @since 2.0.0
* @return array
*/
public function validation_rules() {
/*
* Validations rules change
* depending on the type of order.
*
* For example, the only type that
* requires the site fields
* are the 'new'.
*
* First, let's set upm the general rules:
*/
$rules = array(
'email_address' => 'required_without:user_id|email|unique:\WP_User,email',
'username' => 'required_without:user_id|alpha_dash|min:4|lowercase|unique:\WP_User,login',
'password' => 'required_without:user_id|min:6',
'password_conf' => 'same:password',
'template_id' => 'integer|site_template',
'products' => 'products',
'gateway' => '',
'valid_password' => 'accepted',
'billing_country' => 'country|required_with:billing_country',
'billing_zip_code' => 'required_with:billing_zip_code',
'billing_state' => 'state',
'billing_city' => 'city',
);
/*
* Add rules for site when creating a new account.
*/
if ($this->type === 'new') {
// char limit according https://datatracker.ietf.org/doc/html/rfc1034#section-3.1
$rules['site_title'] = 'min:4';
$rules['site_url'] = 'min:3|max:63|lowercase|unique_site';
}
return apply_filters('wu_checkout_validation_rules', $rules, $this);
}
/**
* Returns the list of validation rules.
*
* If we are dealing with a step submission, we will return
* only the validation rules that refer to the keys sent via POST.
*
* If this is the submission of the last step, though, we return all
* validation rules so we can validate the entire signup.
*
* @since 2.0.0
* @return array
*/
public function get_validation_rules() {
$validation_rules = $this->validation_rules();
if (wu_request('pre-flight') || wu_request('checkout_form') === 'wu-finish-checkout') {
$validation_rules = array();
return $validation_rules;
}
if ($this->step_name && $this->is_last_step() === false) {
$fields_available = array_column($this->step['fields'], 'id');
/*
* Re-adds the template id check
*/
if (wu_request('template_id', null) !== null) {
$fields_available[] = 'template_id';
}
$validation_rules = array_filter($validation_rules, fn($rule) => in_array($rule, $fields_available, true), ARRAY_FILTER_USE_KEY);
}
// We'll use this to validate product fields
$product_fields = array(
'pricing_table',
'products',
);
/**
* Add the additional required fields.
*/
foreach ($this->step['fields'] as $field) {
/*
* General required fields
*/
if (wu_get_isset($field, 'required') && wu_get_isset($field, 'id')) {
if (isset($validation_rules[ $field['id'] ])) {
$validation_rules[ $field['id'] ] .= '|required';
} else {
$validation_rules[ $field['id'] ] = 'required';
}
}
/*
* Product fields
*/
if (wu_get_isset($field, 'id') && in_array($field['id'], $product_fields, true)) {
$validation_rules['products'] = 'products|required';
}
}
/**
* Allow plugin developers to filter the validation rules.
*
* @since 2.0.20
* @param array $validation_rules The validation rules to be used.
* @param Checkout $this The checkout class.
*/
return apply_filters('wu_checkout_validation_rules', $validation_rules, $this);
}
/**
* Validates the rules and make sure we only save models when necessary.
*
* @since 2.0.0
* @param array $rules Custom rules to use instead of the default ones.
* @return true|\WP_Error
*/
public function validate($rules = null) {
$validator = new \WP_Ultimo\Helpers\Validator();
$session = $this->session->get('signup');
$stack = $_REQUEST;
if (is_array($session)) {
$stack = array_merge($session, $_REQUEST);
}
if ($rules === null) {
$rules = $this->get_validation_rules();
}
$base_aliases = array();
$checkout_form_fields = $this->checkout_form ? $this->checkout_form->get_all_fields() : array();
// Add current form fields
foreach ($checkout_form_fields as $field) {
$base_aliases[ $field['id'] ] = wu_get_isset($field, 'name', '');
}
// Add Billing Address fields
foreach (Billing_Address::fields() as $field_key => $field) {
$base_aliases[ $field_key ] = wu_get_isset($field, 'title', '');
}
// Add some hidden or compound fields ids
$validation_aliases = array_merge(
array(
'password_conf' => __('Password confirmation', 'wp-ultimo'),
'template_id' => __('Template ID', 'wp-ultimo'),
'valid_password' => __('Valid password', 'wp-ultimo'),
'products' => __('Products', 'wp-ultimo'),
'gateway' => __('Payment Gateway', 'wp-ultimo'),
),
$base_aliases
);
/**
* Allow plugin developers to add custom aliases in form validator.
*
* @since 2.1
* @param array $validation_aliases The array with id => alias.
* @param Checkout $this The checkout class.
*/
$validation_aliases = apply_filters('wu_checkout_validation_aliases', $validation_aliases, $this);
$validator->validate($stack, $rules, $validation_aliases);
if ($validator->fails()) {
$errors = $validator->get_errors();
$errors->remove('valid_password');
return $errors;
}
return true;
}
/**
* Decides if we are to process a checkout.
*
* Needs to decide if we are simply putting the customer through the next step
* or if we need to actually process the checkout.
* It checks of the current checkout is multi-step;
* If it is, process info, save into session and send to the next step.
* Otherwise, we process the checkout.
*
* @since 2.0.0
* @return void
*/
public function maybe_process_checkout() {
/*
* Sets up the checkout
* environment.
*/
$this->setup_checkout();
/*
* Checks if we should be here.
* We can only process a checkout
* if certain conditions are met.
*/
if ( ! $this->should_process_checkout()) {
return;
}
/*
* Checks if we are in the last step.
*
* WP Multisite WaaS supports multi-step checkout
* flows. That means that we do different
* things on the intermediary steps (mostly
* add things to the session) and on the final,
* where we process the checkout.
*
* Let's deal with the last step case first.
*/
if ($this->is_last_step()) {
/*
* We are in the last step and
* we can process the checkout normally.
*/
$results = $this->process_checkout();
/*
* Error!
*
* We redirect the customer back to the
* checkout page, passing the payment query
* arg so the customer can try again.
*/
if (is_wp_error($results)) {
$redirect_url = wu_get_current_url();
$this->session->set('errors', $results);
/*
* We attach the payment data
* to the error, so we can retrieve it here.
*/
$payment = wu_get_isset($results->get_error_data(), 'payment');
/*
* If the payment exists,
* use the hash to redirect the customer
* to a try again page.
*/
if ($payment) {
$redirect_url = add_query_arg(
array(
'payment' => $payment->get_hash(),
'status' => 'error',
),
$redirect_url
);
}
/*
* Redirect go burrr!
*/
wp_redirect($redirect_url);
exit;
}
/*
* This is not the final step,
* so we just clean the data and save it
* for later.
*/
} else {
/*
* Cleans data and add it to the session.
*
* Here we remove the items that either
* have checkout_ on their name, or start
* with a underscore.
*/
$to_save = array_filter($_POST, fn($item) => strncmp((string) $item, 'checkout_', strlen('checkout_')) !== 0 && strncmp((string) $item, '_', strlen('_')) !== 0, ARRAY_FILTER_USE_KEY);
if (isset($to_save['pre-flight'])) {
unset($to_save['pre-flight']);
$this->session->add_values('signup', array('pre_selected' => $to_save));
}
/*
* Append the cleaned date to the
* active session.
*/
$this->session->add_values('signup', $to_save);
$this->session->commit();
/**
* Whether we should advance to the next step.
* This prevents breaking the checkout flow when triggered from a shortcode page.
*/
if ( ! wu_request('pre-flight')) {
/*
* Go to the next step.
*/
$next_step = $this->get_next_step_name();
wp_redirect(add_query_arg('step', $next_step));
exit;
}
}
}
/**
* Runs pre-checks to see if we should process the checkout.
*
* @since 2.0.0
* @return boolean
*/
public function should_process_checkout() {
return wu_request('checkout_action') === 'wu_checkout' && ! wp_doing_ajax();
}
/**
* Handles the checkout submission.
*
* @since 2.0.0
* @return mixed
*/
public function process_checkout() {
/**
* Before we process the checkout.
*
* @since 2.0.11
* @param Checkout $checkout The current checkout instance;
*/
do_action('wu_checkout_before_process_checkout', $this);
$this->setup_checkout();
$gateway = wu_get_gateway(wu_request('gateway'));
$payment = wu_get_payment($this->request_or_session('payment_id'));
$customer = $payment->get_customer();
$membership = $payment->get_membership();
/**
* Get the original cart from saved payment meta so we can finish the process.
* It ensure that the cart is the same used in beginning of the process.
*/
$this->order = $payment->get_meta('wu_original_cart');
$this->order->set_membership($membership);
$this->order->set_customer($customer);
$this->order->set_payment($payment);
try {
/*
* We need to handle free payments and trials w/o cc separately.
*
* In the same manner, if the order
* IS NOT free, we need to make sure
* the customer is not trying to game the system
* passing the free gateway to get an free account.
*
* That's what's we checking on the else case.
*/
if ($payment->get_status() === Payment_Status::COMPLETED) {
$gateway = wu_get_gateway($payment->get_gateway());
} elseif ($this->order->should_collect_payment() === false) {
$gateway = wu_get_gateway('free');
} elseif ($gateway->get_id() === 'free') {
$this->errors = new \WP_Error('no-gateway', __('Payment gateway not registered.', 'wp-ultimo'));
return false;
}
if ( ! $gateway) {
$this->errors = new \WP_Error('no-gateway', __('Payment gateway not registered.', 'wp-ultimo'));
return false;
}
/*
* Set the gateway data.
*/
$gateway->set_order($this->order);
/*
* Let's grab the cart type.
* We'll use it to perform the necessary actions
* with memberships, payments, and such.
*/
$type = $this->order->get_cart_type();
/*
* Here's where the action actually happens.
*
* The gateway takes in the info about the transaction
* and perform the necessary steps to make sure the
* data on the gateway correctly reflects the data on WP Multisite WaaS.
*/
$status = $gateway->process_checkout($payment, $membership, $customer, $this->order, $type);
/*
* If the gateway returns a explicit false value
* we understand that as a signal that the gateway wants to
* deal with the modifications by itself.
*
* In that case, we simply return.
*/
if ($status === false) {
return;
}
/*
* Run after every checkout processing.
*
* @since 2.0.4
*/
do_action('wu_checkout_done', $payment, $membership, $customer, $this->order, $type, $this);
/*
* Deprecated hook for registration.
*/
if (has_action('wp_ultimo_registration')) {
$_payment = wu_get_payment($payment->get_id());
$args = array(
0, // Site ID is not yet available at this point
$customer->get_user_id(),
$this->session->get('signup'),
$_payment && $_payment->get_membership() ? new \WU_Plan($_payment->get_membership()->get_plan()) : false,
);
ob_start();
do_action_deprecated('wp_ultimo_registration', $args, '2.0.0');
ob_flush();
}
/*
* Otherwise, we redirect
* to the thank you page
* of the front-end mode.
*/
$redirect_url = $gateway->get_return_url();
if ( ! is_admin()) {
/**
* Set the redirect URL.
*
* This is a legacy filter. Some of the parameters
* passed are not available, such as the $site_id.
*
* @since 1.1.3 Let developers filter the redirect URL.
*/
$redirect_url = apply_filters('wp_ultimo_redirect_url_after_signup', $redirect_url, 0, get_current_user_id(), $_POST);
$redirect_url = add_query_arg(
array(
'payment' => $payment ? $payment->get_hash() : 'none',
'status' => 'done',
),
$redirect_url
);
}
wp_redirect($redirect_url);
exit;
} catch (\Throwable $e) {
$membership_id = $this->order->get_membership() ? $this->order->get_membership()->get_id() : 'unknown';
$log_message = sprintf(__('Checkout failed for customer %s: ', 'wp-ultimo'), $membership_id);
$log_message .= $e->getMessage();
wu_log_add('checkout', $log_message, LogLevel::ERROR);
return new \WP_Error(
'error',
$e->getMessage(),
array(
'trace' => $e->getTrace(),
'payment' => $payment,
)
);
}
}
/**
* Handle user display names, if first and last names are available.
*
* @since 2.0.4
*
* @param string $display_name The current display name.
* @return string
*/
public function handle_display_name($display_name) {
$first_name = $this->request_or_session('first_name', '');
$last_name = $this->request_or_session('last_name', '');
if ($first_name || $last_name) {
$display_name = trim("$first_name $last_name");
}
return $display_name;
}
/*
* Helper methods
*
* These mostly deal with
* multi-step checkout control
* and can be mostly ignored!
*/
/**
* Get thank you page URL.
*
* @since 2.0.0
* @return string
*/
public function get_thank_you_page() {
return wu_get_current_url();
}
/**
* Checks if the user already exists.
*
* @since 2.0.0
* @return boolean
*/
public function is_existing_user() {
return is_user_logged_in();
}
/**
* Returns the customer email verification status we want to use depending on the type of checkout.
*
* @since 2.0.0
* @return string
*/
public function get_customer_email_verification_status() {
$should_confirm_email = wu_get_setting('enable_email_verification', true);
return $this->order->should_collect_payment() === false && $should_confirm_email ? 'pending' : 'none';
}
/**
* Adds the checkout scripts.
*
* @see $this->get_checkout_variables()
*
* @since 2.0.0
* @return void
*/
public function register_scripts() {
$custom_css = apply_filters('wu_checkout_custom_css', '');
wp_add_inline_style('wu-checkout', $custom_css);
wp_enqueue_style('wu-checkout');
wp_enqueue_style('wu-admin');
wp_register_script('wu-checkout', wu_get_asset('checkout.js', 'js'), array('jquery-core', 'wu-vue', 'moment', 'wu-block-ui', 'wu-functions', 'password-strength-meter', 'underscore', 'wp-polyfill', 'wp-hooks', 'wu-cookie-helpers'), wu_get_version(), true);
wp_localize_script('wu-checkout', 'wu_checkout', $this->get_checkout_variables());
wp_enqueue_script('wu-checkout');
}
/**
* Gets the info either from the request or session.
*
* We try to get the key from the session object, but
* if that doesn't work or it doesn't exist, we try
* to get it from the request instead.
*
* @since 2.0.0
*
* @param string $key Key to retrieve the value for.
* @param mixed $default The default value to return, when nothing is found.
* @return mixed
*/
public function request_or_session($key, $default = false) {
$value = $default;
if ($this->session !== null) {
$session = $this->session->get('signup');
if (isset($session[ $key ])) {
$value = $session[ $key ];
}
}
$value = wu_request($key, $value);
return $value;
}
/**
* Returns the name of the next step on the flow.
*
* @since 2.0.0
* @return string
*/
public function get_next_step_name() {
$steps = $this->steps;
$keys = array_column($steps, 'id');
$current_step_index = array_search($this->step_name, array_values($keys), true);
/*
* If we enter the if statement below,
* it means that we don't have a step name set
* so we need to set it to the first.
*/
if ($current_step_index === false) {
$current_step_index = 0;
}
$index = $current_step_index + 1;
return isset($keys[ $index ]) ? $keys[ $index ] : $keys[ $current_step_index ];
}
/**
* Checks if we are in the first step of the signup.
*
* @since 2.0.0
* @return boolean
*/
public function is_first_step() {
$step_names = array_column($this->steps, 'id');
if (empty($step_names)) {
return true;
}
return $this->step_name === array_shift($step_names);
}
/**
* Checks if we are in the last step of the signup.
*
* @since 2.0.0
* @return boolean
*/
public function is_last_step() {
/**
* What is this pre-flight parameter, you may ask...
*
* Well, some shortcodes can jump-start the signup process
* for example, the pricing table shortcode allows the
* customers to select a plan.
*
* This submits the form inside the shortcode to the registration
* page, but if that page is a one-step signup, this class-checkout
* will deal with it as if it was a last-step submission.
*
* The presence of the pre-flight URL parameter prevents that
* from happening.
*
* The summary is: if you need to post info to the registration page
* you need to add the ?pre-flight to the action URL.
*/
if (wu_request('pre-flight')) {
return false;
}
$step_names = array_column($this->steps, 'id');
if (empty($step_names)) {
return true;
}
return $this->step_name === array_pop($step_names);
}
/**
* Decides if we should display errors on the checkout screen.
*
* @since 2.0.0
* @return void
*/
public function maybe_display_checkout_errors() {
if (wu_request('status') !== 'error') {
return;
}
}
}