OrderAttributionBlocksController.php 0000644 00000011444 15073235525 0013772 0 ustar 00 extend_schema = $extend_schema;
$this->features_controller = $features_controller;
$this->order_attribution_controller = $order_attribution_controller;
}
/**
* Hook into WP.
*
* @return void
*/
public function register() {
// Bail if the feature is not enabled.
if ( ! $this->features_controller->feature_is_enabled( 'order_attribution' ) ) {
return;
}
$this->extend_api();
// Bail early on admin requests to avoid asset registration.
if ( is_admin() ) {
return;
}
add_action(
'init',
function() {
$this->register_assets();
}
);
add_action(
'wp_enqueue_scripts',
function() {
$this->enqueue_scripts();
}
);
}
/**
* Register scripts.
*/
private function register_assets() {
wp_register_script(
'wc-order-attribution-blocks',
plugins_url(
"assets/js/frontend/order-attribution-blocks{$this->get_script_suffix()}.js",
WC_PLUGIN_FILE
),
array( 'wc-order-attribution', 'wp-data', 'wc-blocks-checkout' ),
Constants::get_constant( 'WC_VERSION' ),
true
);
}
/**
* Enqueue the Order Attribution script.
*
* @return void
*/
private function enqueue_scripts() {
wp_enqueue_script( 'wc-order-attribution-blocks' );
}
/**
* Extend the Store API.
*
* @return void
*/
private function extend_api() {
$this->extend_schema->register_endpoint_data(
array(
'endpoint' => CheckoutSchema::IDENTIFIER,
'namespace' => 'woocommerce/order-attribution',
'schema_callback' => $this->get_schema_callback(),
)
);
// Update order based on extended data.
add_action(
'woocommerce_store_api_checkout_update_order_from_request',
function ( $order, $request ) {
$extensions = $request->get_param( 'extensions' );
$params = $extensions['woocommerce/order-attribution'] ?? array();
if ( empty( $params ) ) {
return;
}
/**
* Run an action to save order attribution data.
*
* @since 8.5.0
*
* @param WC_Order $order The order object.
* @param array $params Unprefixed order attribution data.
*/
do_action( 'woocommerce_order_save_attribution_data', $order, $params );
},
10,
2
);
}
/**
* Get the schema callback.
*
* @return callable
*/
private function get_schema_callback() {
return function() {
$schema = array();
$fields = $this->order_attribution_controller->get_fields();
$validate_callback = function( $value ) {
if ( ! is_string( $value ) && null !== $value ) {
return new WP_Error(
'api-error',
sprintf(
/* translators: %s is the property type */
esc_html__( 'Value of type %s was posted to the order attribution callback', 'woocommerce' ),
gettype( $value )
)
);
}
return true;
};
$sanitize_callback = function( $value ) {
return sanitize_text_field( $value );
};
foreach ( $fields as $field ) {
$schema[ $field ] = array(
'description' => sprintf(
/* translators: %s is the field name */
__( 'Order attribution field: %s', 'woocommerce' ),
esc_html( $field )
),
'type' => array( 'string', 'null' ),
'context' => array(),
'arg_options' => array(
'validate_callback' => $validate_callback,
'sanitize_callback' => $sanitize_callback,
),
);
}
return $schema;
};
}
}
IppFunctions.php 0000644 00000004243 15073235525 0007710 0 ustar 00 get_status(), array( 'pending', 'on-hold', 'processing' ), true );
$has_payment_method = in_array( $order->get_payment_method(), array( 'cod', 'woocommerce_payments', 'none' ), true );
$order_is_not_paid = null === $order->get_date_paid();
$order_is_not_refunded = empty( $order->get_refunds() );
$order_has_no_subscription_products = true;
foreach ( $order->get_items() as $item ) {
$product = $item->get_product();
if ( is_object( $product ) && $product->is_type( 'subscription' ) ) {
$order_has_no_subscription_products = false;
break;
}
}
return $has_status && $has_payment_method && $order_is_not_paid && $order_is_not_refunded && $order_has_no_subscription_products;
}
/**
* Returns if store is eligible to accept In-Person Payments.
*
* @return bool true if store is eligible, false otherwise
*/
public static function is_store_in_person_payment_eligible(): bool {
$is_store_usa_based = self::has_store_specified_country_currency( 'US', 'USD' );
$is_store_canada_based = self::has_store_specified_country_currency( 'CA', 'CAD' );
return $is_store_usa_based || $is_store_canada_based;
}
/**
* Checks if the store has specified country location and currency used.
*
* @param string $country country to compare store's country with.
* @param string $currency currency to compare store's currency with.
*
* @return bool true if specified country and currency match the store's ones. false otherwise
*/
public static function has_store_specified_country_currency( string $country, string $currency ): bool {
return ( WC()->countries->get_base_country() === $country && get_woocommerce_currency() === $currency );
}
}
OrderAttributionController.php 0000644 00000031126 15073235525 0012633 0 ustar 00 proxy = $proxy;
$this->feature_controller = $controller;
$this->logger = $logger ?? $proxy->call_function( 'wc_get_logger' );
$this->set_fields_and_prefix();
}
/**
* Register this class instance to the appropriate hooks.
*
* @return void
*/
public function register() {
// Don't run during install.
if ( Constants::get_constant( 'WC_INSTALLING' ) ) {
return;
}
// Bail if the feature is not enabled.
if ( ! $this->feature_controller->feature_is_enabled( 'order_attribution' ) ) {
return;
}
add_action(
'wp_enqueue_scripts',
function() {
$this->enqueue_scripts_and_styles();
}
);
add_action(
'admin_enqueue_scripts',
function() {
$this->enqueue_admin_scripts_and_styles();
}
);
// Include our hidden fields on order notes and registration form.
$source_form_fields = function() {
$this->source_form_fields();
};
add_action( 'woocommerce_after_order_notes', $source_form_fields );
add_action( 'woocommerce_register_form', $source_form_fields );
// Update order based on submitted fields.
add_action(
'woocommerce_checkout_order_created',
function( $order ) {
// Nonce check is handled by WooCommerce before woocommerce_checkout_order_created hook.
// phpcs:ignore WordPress.Security.NonceVerification
$params = $this->get_unprefixed_fields( $_POST );
/**
* Run an action to save order attribution data.
*
* @since 8.5.0
*
* @param WC_Order $order The order object.
* @param array $params Unprefixed order attribution data.
*/
do_action( 'woocommerce_order_save_attribution_data', $order, $params );
}
);
add_action(
'woocommerce_order_save_attribution_data',
function( $order, $data ) {
$source_data = $this->get_source_values( $data );
$this->send_order_tracks( $source_data, $order );
$this->set_order_source_data( $source_data, $order );
},
10,
2
);
add_action(
'user_register',
function( $customer_id ) {
try {
$customer = new WC_Customer( $customer_id );
$this->set_customer_source_data( $customer );
} catch ( Exception $e ) {
$this->log( $e->getMessage(), __METHOD__, WC_Log_Levels::ERROR );
}
}
);
// Add origin data to the order table.
add_action(
'admin_init',
function() {
$this->register_order_origin_column();
}
);
add_action(
'woocommerce_new_order',
function( $order_id, $order ) {
$this->maybe_set_admin_source( $order );
},
2,
10
);
}
/**
* If the order is created in the admin, set the source type and origin to admin/Web admin.
*
* @param WC_Order $order The recently created order object.
*
* @since 8.5.0
*/
private function maybe_set_admin_source( WC_Order $order ) {
if ( function_exists( 'is_admin' ) && is_admin() ) {
$order->add_meta_data( $this->get_meta_prefixed_field( 'type' ), 'admin' );
$order->save();
}
}
/**
* Get all of the fields.
*
* @return array
*/
public function get_fields(): array {
return $this->fields;
}
/**
* Get the prefix for the fields.
*
* @return string
*/
public function get_prefix(): string {
return $this->field_prefix;
}
/**
* Scripts & styles for custom source tracking and cart tracking.
*/
private function enqueue_scripts_and_styles() {
wp_enqueue_script(
'sourcebuster-js',
plugins_url( "assets/js/sourcebuster/sourcebuster{$this->get_script_suffix()}.js", WC_PLUGIN_FILE ),
array(),
Constants::get_constant( 'WC_VERSION' ),
true
);
wp_enqueue_script(
'wc-order-attribution',
plugins_url( "assets/js/frontend/order-attribution{$this->get_script_suffix()}.js", WC_PLUGIN_FILE ),
array( 'sourcebuster-js' ),
Constants::get_constant( 'WC_VERSION' ),
true
);
/**
* Filter the lifetime of the cookie used for source tracking.
*
* @since 8.5.0
*
* @param float $lifetime The lifetime of the Sourcebuster cookies in months.
*
* The default value forces Sourcebuster into making the cookies valid for the current session only.
*/
$lifetime = (float) apply_filters( 'wc_order_attribution_cookie_lifetime_months', 0.00001 );
/**
* Filter the session length for source tracking.
*
* @since 8.5.0
*
* @param int $session_length The session length in minutes.
*/
$session_length = (int) apply_filters( 'wc_order_attribution_session_length_minutes', 30 );
/**
* Filter to allow tracking.
*
* @since 8.5.0
*
* @param bool $allow_tracking True to allow tracking, false to disable.
*/
$allow_tracking = wc_bool_to_string( apply_filters( 'wc_order_attribution_allow_tracking', true ) );
// Create Order Attribution JS namespace with parameters.
$namespace = array(
'params' => array(
'lifetime' => $lifetime,
'session' => $session_length,
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'prefix' => $this->field_prefix,
'allowTracking' => $allow_tracking,
),
);
wp_localize_script( 'wc-order-attribution', 'wc_order_attribution', $namespace );
}
/**
* Enqueue the stylesheet for admin pages.
*
* @return void
*/
private function enqueue_admin_scripts_and_styles() {
$screen = get_current_screen();
if ( $screen->id !== $this->get_order_screen_id() ) {
return;
}
// phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotInFooter
wp_enqueue_script(
'woocommerce-order-attribution-admin-js',
plugins_url( "assets/js/admin/order-attribution-admin{$this->get_script_suffix()}.js", WC_PLUGIN_FILE ),
array( 'jquery' ),
Constants::get_constant( 'WC_VERSION' )
);
}
/**
* Display the origin column in the orders table.
*
* @param int $order_id The order ID.
*
* @return void
*/
private function display_origin_column( $order_id ): void {
try {
// Ensure we've got a valid order.
$order = $this->get_hpos_order_object( $order_id );
$this->output_origin_column( $order );
} catch ( Exception $e ) {
return;
}
}
/**
* Output the translated origin label for the Origin column in the orders table.
*
* Default to "Unknown" if no origin is set.
*
* @param WC_Order $order The order object.
*
* @return void
*/
private function output_origin_column( WC_Order $order ) {
$source_type = $order->get_meta( $this->get_meta_prefixed_field( 'type' ) );
$source = $order->get_meta( $this->get_meta_prefixed_field( 'utm_source' ) );
$origin = $this->get_origin_label( $source_type, $source );
if ( empty( $origin ) ) {
$origin = __( 'Unknown', 'woocommerce' );
}
echo esc_html( $origin );
}
/**
* Add attribution hidden input fields for checkout & customer register froms.
*/
private function source_form_fields() {
foreach ( $this->fields as $field ) {
printf( '', esc_attr( $this->get_prefixed_field( $field ) ) );
}
}
/**
* Save source data for a Customer object.
*
* @param WC_Customer $customer The customer object.
*
* @return void
*/
private function set_customer_source_data( WC_Customer $customer ) {
// Nonce check is handled before user_register hook.
// phpcs:ignore WordPress.Security.NonceVerification
foreach ( $this->get_source_values( $this->get_unprefixed_fields( $_POST ) ) as $key => $value ) {
$customer->add_meta_data( $this->get_meta_prefixed_field( $key ), $value );
}
$customer->save_meta_data();
}
/**
* Save source data for an Order object.
*
* @param array $source_data The source data.
* @param WC_Order $order The order object.
*
* @return void
*/
private function set_order_source_data( array $source_data, WC_Order $order ) {
foreach ( $source_data as $key => $value ) {
$order->add_meta_data( $this->get_meta_prefixed_field( $key ), $value );
}
$order->save_meta_data();
}
/**
* Log a message as a debug log entry.
*
* @param string $message The message to log.
* @param string $method The method that is logging the message.
* @param string $level The log level.
*/
private function log( string $message, string $method, string $level = WC_Log_Levels::DEBUG ) {
/**
* Filter to enable debug mode.
*
* @since 8.5.0
*
* @param string $enabled 'yes' to enable debug mode, 'no' to disable.
*/
if ( 'yes' !== apply_filters( 'wc_order_attribution_debug_mode_enabled', 'no' ) ) {
return;
}
$this->logger->log(
$level,
sprintf( '%s %s', $method, $message ),
array( 'source' => 'woocommerce-order-attribution' )
);
}
/**
* Send order source data to Tracks.
*
* @param array $source_data The source data.
* @param WC_Order $order The order object.
*
* @return void
*/
private function send_order_tracks( array $source_data, WC_Order $order ) {
$origin_label = $this->get_origin_label(
$source_data['type'] ?? '',
$source_data['utm_source'] ?? '',
false
);
$customer_identifier = $order->get_customer_id() ? $order->get_customer_id() : $order->get_billing_email();
$customer_info = $this->get_customer_history( $customer_identifier );
$tracks_data = array(
'order_id' => $order->get_id(),
'type' => $source_data['type'] ?? '',
'medium' => $source_data['utm_medium'] ?? '',
'source' => $source_data['utm_source'] ?? '',
'device_type' => strtolower( $source_data['device_type'] ?? '(unknown)' ),
'origin_label' => strtolower( $origin_label ),
'session_pages' => $source_data['session_pages'] ?? 0,
'session_count' => $source_data['session_count'] ?? 0,
'order_total' => $order->get_total(),
// Add 1 to include the current order (which is currently still Pending when the event is sent).
'customer_order_count' => $customer_info['order_count'] + 1,
'customer_registered' => $order->get_customer_id() ? 'yes' : 'no',
);
$this->proxy->call_static( WC_Tracks::class, 'record_event', 'order_attribution', $tracks_data );
}
/**
* Get the screen ID for the orders page.
*
* @return string
*/
private function get_order_screen_id(): string {
return OrderUtil::custom_orders_table_usage_is_enabled() ? wc_get_page_screen_id( 'shop-order' ) : 'shop_order';
}
/**
* Register the origin column in the orders table.
*
* This accounts for the differences in hooks based on whether HPOS is enabled or not.
*
* @return void
*/
private function register_order_origin_column() {
$screen_id = $this->get_order_screen_id();
$add_column = function( $columns ) {
$columns['origin'] = esc_html__( 'Origin', 'woocommerce' );
return $columns;
};
// HPOS and non-HPOS use different hooks.
add_filter( "manage_{$screen_id}_columns", $add_column );
add_filter( "manage_edit-{$screen_id}_columns", $add_column );
$display_column = function( $column_name, $order_id ) {
if ( 'origin' !== $column_name ) {
return;
}
$this->display_origin_column( $order_id );
};
// HPOS and non-HPOS use different hooks.
add_action( "manage_{$screen_id}_custom_column", $display_column, 10, 2 );
add_action( "manage_{$screen_id}_posts_custom_column", $display_column, 10, 2 );
}
}
CouponsController.php 0000644 00000007022 15073235525 0010757 0 ustar 00 add_coupon_discount( $_POST );
ob_start();
include __DIR__ . '/../../../includes/admin/meta-boxes/views/html-order-items.php';
$response['html'] = ob_get_clean();
ob_start();
$notes = wc_get_order_notes( array( 'order_id' => $order->get_id() ) );
include __DIR__ . '/../../../includes/admin/meta-boxes/views/html-order-notes.php';
$response['notes_html'] = ob_get_clean();
} catch ( Exception $e ) {
wp_send_json_error( array( 'error' => $e->getMessage() ) );
}
// wp_send_json_success must be outside the try block not to break phpunit tests.
wp_send_json_success( $response );
}
/**
* Add order discount programmatically.
*
* @param array $post_variables Contents of the $_POST array that would be passed in an Ajax call.
* @return object The retrieved order object.
* @throws \Exception Invalid order or coupon.
*/
public function add_coupon_discount( array $post_variables ): object {
$order_id = isset( $post_variables['order_id'] ) ? absint( $post_variables['order_id'] ) : 0;
$order = wc_get_order( $order_id );
$calculate_tax_args = array(
'country' => isset( $post_variables['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['country'] ) ) ) : '',
'state' => isset( $post_variables['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['state'] ) ) ) : '',
'postcode' => isset( $post_variables['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['postcode'] ) ) ) : '',
'city' => isset( $post_variables['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['city'] ) ) ) : '',
);
if ( ! $order ) {
throw new Exception( __( 'Invalid order', 'woocommerce' ) );
}
$coupon = ArrayUtil::get_value_or_default( $post_variables, 'coupon' );
if ( StringUtil::is_null_or_whitespace( $coupon ) ) {
throw new Exception( __( 'Invalid coupon', 'woocommerce' ) );
}
// Add user ID and/or email so validation for coupon limits works.
$user_id_arg = isset( $post_variables['user_id'] ) ? absint( $post_variables['user_id'] ) : 0;
$user_email_arg = isset( $post_variables['user_email'] ) ? sanitize_email( wp_unslash( $post_variables['user_email'] ) ) : '';
if ( $user_id_arg ) {
$order->set_customer_id( $user_id_arg );
}
if ( $user_email_arg ) {
$order->set_billing_email( $user_email_arg );
}
$order->calculate_taxes( $calculate_tax_args );
$order->calculate_totals( false );
$code = wc_format_coupon_code( wp_unslash( $coupon ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$result = $order->apply_coupon( $code );
if ( is_wp_error( $result ) ) {
throw new Exception( html_entity_decode( wp_strip_all_tags( $result->get_error_message() ) ) );
}
// translators: %s coupon code.
$order->add_order_note( esc_html( sprintf( __( 'Coupon applied: "%s".', 'woocommerce' ), $code ) ), 0, true );
return $order;
}
}
MobileMessagingHandler.php 0000644 00000012742 15073235525 0011635 0 ustar 00 diff( $now )->days <= self::OPEN_ORDER_INTERVAL_DAYS;
$has_jetpack = null !== $blog_id;
if ( IppFunctions::is_store_in_person_payment_eligible() && IppFunctions::is_order_in_person_payment_eligible( $order ) ) {
return self::accept_payment_message( $blog_id, $domain );
} else {
if ( $used_app_in_last_month && $has_jetpack ) {
return self::manage_order_message( $blog_id, $order->get_id(), $domain );
} else {
return self::no_app_message( $blog_id, $domain );
}
}
} catch ( Exception $e ) {
return null;
}
}
/**
* Returns the closest date of last usage of any mobile app platform.
*
* @return ?DateTime
*/
private static function get_closer_mobile_usage_date(): ?DateTime {
$mobile_usage = WC_Tracker::get_woocommerce_mobile_usage();
if ( ! $mobile_usage ) {
return null;
}
$last_ios_used = self::get_last_used_or_null( 'ios', $mobile_usage );
$last_android_used = self::get_last_used_or_null( 'android', $mobile_usage );
return max( $last_android_used, $last_ios_used );
}
/**
* Returns last used date of specified mobile app platform.
*
* @param string $platform mobile platform to check.
* @param array $mobile_usage mobile apps usage data.
*
* @return ?DateTime last used date of specified mobile app
*/
private static function get_last_used_or_null(
string $platform, array $mobile_usage
): ?DateTime {
try {
if ( array_key_exists( $platform, $mobile_usage ) ) {
return new DateTime( $mobile_usage[ $platform ]['last_used'] );
} else {
return null;
}
} catch ( Exception $e ) {
return null;
}
}
/**
* Prepares message with a deep link to mobile payment.
*
* @param ?int $blog_id blog id to deep link to.
* @param string $domain URL of the current site.
*
* @return string formatted message
*/
private static function accept_payment_message( ?int $blog_id, $domain ): string {
$deep_link_url = add_query_arg(
array_merge(
array(
'blog_id' => absint( $blog_id ),
),
self::prepare_utm_parameters( 'deeplinks_payments', $blog_id, $domain )
),
'https://woo.com/mobile/payments'
);
return sprintf(
/* translators: 1: opening link tag 2: closing link tag. */
esc_html__(
'%1$sCollect payments easily%2$s from your customers anywhere with our mobile app.',
'woocommerce'
),
'',
''
);
}
/**
* Prepares message with a deep link to manage order details.
*
* @param int $blog_id blog id to deep link to.
* @param int $order_id order id to deep link to.
* @param string $domain URL of the current site.
*
* @return string formatted message
*/
private static function manage_order_message( int $blog_id, int $order_id, string $domain ): string {
$deep_link_url = add_query_arg(
array_merge(
array(
'blog_id' => absint( $blog_id ),
'order_id' => absint( $order_id ),
),
self::prepare_utm_parameters( 'deeplinks_orders_details', $blog_id, $domain )
),
'https://woo.com/mobile/orders/details'
);
return sprintf(
/* translators: 1: opening link tag 2: closing link tag. */
esc_html__(
'%1$sManage the order%2$s with the app.',
'woocommerce'
),
'',
''
);
}
/**
* Prepares message with a deep link to learn more about mobile app.
*
* @param ?int $blog_id blog id used for tracking.
* @param string $domain URL of the current site.
*
* @return string formatted message
*/
private static function no_app_message( ?int $blog_id, string $domain ): string {
$deep_link_url = add_query_arg(
array_merge(
array(
'blog_id' => absint( $blog_id ),
),
self::prepare_utm_parameters( 'deeplinks_promote_app', $blog_id, $domain )
),
'https://woo.com/mobile'
);
return sprintf(
/* translators: 1: opening link tag 2: closing link tag. */
esc_html__(
'Process your orders on the go. %1$sGet the app%2$s.',
'woocommerce'
),
'',
''
);
}
/**
* Prepares array of parameters used by Woo.com for tracking.
*
* @param string $campaign name of the deep link campaign.
* @param int|null $blog_id blog id of the current site.
* @param string $domain URL of the current site.
*
* @return array
*/
private static function prepare_utm_parameters(
string $campaign,
?int $blog_id,
string $domain
): array {
return array(
'utm_campaign' => $campaign,
'utm_medium' => 'email',
'utm_source' => $domain,
'utm_term' => absint( $blog_id ),
);
}
}
TaxesController.php 0000644 00000003462 15073235525 0010421 0 ustar 00 calc_line_taxes( $_POST );
include __DIR__ . '/../../../includes/admin/meta-boxes/views/html-order-items.php';
wp_die();
}
/**
* Calculate line taxes programmatically.
*
* @param array $post_variables Contents of the $_POST array that would be passed in an Ajax call.
* @return object The retrieved order object.
*/
public function calc_line_taxes( array $post_variables ): object {
$order_id = absint( $post_variables['order_id'] );
$calculate_tax_args = array(
'country' => isset( $post_variables['country'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['country'] ) ) ) : '',
'state' => isset( $post_variables['state'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['state'] ) ) ) : '',
'postcode' => isset( $post_variables['postcode'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['postcode'] ) ) ) : '',
'city' => isset( $post_variables['city'] ) ? wc_strtoupper( wc_clean( wp_unslash( $post_variables['city'] ) ) ) : '',
);
// Parse the jQuery serialized items.
$items = array();
parse_str( wp_unslash( $post_variables['items'] ), $items );
// Save order items first.
wc_save_order_items( $order_id, $items );
// Grab the order and recalculate taxes.
$order = wc_get_order( $order_id );
$order->calculate_taxes( $calculate_tax_args );
$order->calculate_totals( false );
return $order;
}
}