Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* Dev - Implements WooCommerce constants for the tax statuses
* Fix - Ensure all Javascript strings use the correct text domain for translation
* Tweak - Use more specific selector in express checkout e2e tests
* Fix - Relax customer validation that was preventing payments from the pay for order page

= 9.8.0 - 2025-08-11 =
* Add - Adds the current setting value for the Optimized Checkout to the Stripe System Status Report data
Expand Down
80 changes: 63 additions & 17 deletions includes/class-wc-stripe-customer.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@
*/
class WC_Stripe_Customer {

/**
* Constant for the customer context when adding a payment method.
*/
public const CUSTOMER_CONTEXT_ADD_PAYMENT_METHOD = 'add_payment_method';

/**
* Constant for the customer context when paying for an order via the "Pay for Order" page.
*/
public const CUSTOMER_CONTEXT_PAY_FOR_ORDER = 'pay_for_order';

/**
* Constants for the customer contexts where minimal billing details are permitted.
*/
public const MINIMAL_BILLING_DETAILS_CONTEXTS = [
self::CUSTOMER_CONTEXT_ADD_PAYMENT_METHOD,
self::CUSTOMER_CONTEXT_PAY_FOR_ORDER,
];

/**
* String prefix for Stripe payment methods request transient.
*/
Expand Down Expand Up @@ -209,19 +227,19 @@ protected function generate_customer_request( $args = [] ) {
/**
* Validate that we have valid data before we try to create a customer.
*
* @param array $create_customer_request
* @param bool $is_add_payment_method_page
* @param array $create_customer_request The base data to build the customer request.
* @param null|string $current_context Flag to indicate whether we are in a context where limited details are permitted.
*
* @throws WC_Stripe_Exception
*/
private function validate_create_customer_request( $create_customer_request, $is_add_payment_method_page = false ) {
private function validate_create_customer_request( $create_customer_request, ?string $current_context = null ) {
/**
* Filters the required customer fields when creating a customer in Stripe.
*
* @since 9.7.0
* @param array $required_fields The required customer fields as derived from the required billing fields in checkout.
* @param array $required_fields The required customer fields as derived from the required billing fields in checkout. In some contexts, like adding a payment method, we allow minimal details to be provided.
*/
$required_fields = apply_filters( 'wc_stripe_create_customer_required_fields', $this->get_create_customer_required_fields( $is_add_payment_method_page ) );
$required_fields = apply_filters( 'wc_stripe_create_customer_required_fields', $this->get_create_customer_required_fields( $current_context ) );

foreach ( $required_fields as $field => $field_requirements ) {
if ( true === $field_requirements ) {
Expand Down Expand Up @@ -258,13 +276,12 @@ private function validate_create_customer_request( $create_customer_request, $is
/**
* Get the list of required fields for the create customer request.
*
* @param bool $is_add_payment_method_page
* @param string|null $current_context The context we are creating the customer in.
*
* @return array
*/
private function get_create_customer_required_fields( $is_add_payment_method_page = false ) {
// If we are on the add payment method page, we need to check just for the email field.
if ( $is_add_payment_method_page ) {
private function get_create_customer_required_fields( ?string $current_context = null ) {
if ( in_array( $current_context, self::MINIMAL_BILLING_DETAILS_CONTEXTS, true ) ) {
return [
'email' => true,
];
Expand Down Expand Up @@ -422,19 +439,22 @@ public function get_existing_customer( $email, $name ) {
* Create a customer via API.
*
* @param array $args
* @param bool $is_add_payment_method_page Whether the request is for the add payment method page.
* @param string|null $current_context The context we are creating the customer in.
* @return WP_Error|int
*
* @throws WC_Stripe_Exception
*/
public function create_customer( $args = [], $is_add_payment_method_page = false ) {
public function create_customer( $args = [], $current_context = null ) {
$args = $this->generate_customer_request( $args );

// For guest users, check if a customer already exists with the same email and name in Stripe account before creating a new one.
if ( ! $this->get_id() && 0 === $this->get_user_id() && ! empty( $args['email'] ) && ! empty( $args['name'] ) ) {
$response = $this->get_existing_customer( $args['email'], $args['name'] );
}

// $current_context was initially introduced as a boolean flag, so check for old callers.
$current_context = $this->normalize_current_context( $current_context );

if ( empty( $response ) ) {
/**
* Filters the arguments used to create a customer.
Expand All @@ -445,7 +465,7 @@ public function create_customer( $args = [], $is_add_payment_method_page = false
*/
$create_customer_args = apply_filters( 'wc_stripe_create_customer_args', $args );

$this->validate_create_customer_request( $create_customer_args, $is_add_payment_method_page );
$this->validate_create_customer_request( $create_customer_args, $current_context );

$response = WC_Stripe_API::request( $create_customer_args, 'customers' );
} else {
Expand Down Expand Up @@ -523,19 +543,45 @@ public function update_customer( $args = [], $is_retry = false ) {
* Updates existing Stripe customer or creates new customer for User through API.
*
* @param array $args Additional arguments for the request (optional).
* @param string|null $current_context The context we are creating the customer in.
*
* @return string Customer ID
*
* @throws WC_Stripe_Exception
*/
public function update_or_create_customer( $args = [], $is_add_payment_method_page = false ) {
public function update_or_create_customer( $args = [], $current_context = null ) {
if ( empty( $this->get_id() ) ) {
return $this->recreate_customer( $args, $is_add_payment_method_page );
// $current_context was initially introduced as a boolean flag, so check for old callers.
$current_context = $this->normalize_current_context( $current_context );

return $this->recreate_customer( $args, $current_context );
} else {
return $this->update_customer( $args );
}
}

/**
* Normalize the current context to a string, as the argument was initially introduced as a boolean flag.
*
* @param string|bool|null $current_context The current context.
* @return string|null The normalized context.
*/
private function normalize_current_context( $current_context ): ?string {
if ( null === $current_context ) {
return null;
}

if ( is_bool( $current_context ) ) {
return $current_context ? self::CUSTOMER_CONTEXT_ADD_PAYMENT_METHOD : null;
}

if ( is_string( $current_context ) ) {
return $current_context;
}

return null;
}

/**
* Checks to see if error is of invalid request
* error and it is no such customer.
Expand Down Expand Up @@ -978,13 +1024,13 @@ public function delete_id_from_meta() {
* Recreates the customer for this user.
*
* @param array $args Additional arguments for the request (optional).
* @param bool $is_add_payment_method_page Whether the request is for the add payment method page.
* @param string|null $current_context The context we are creating the customer in.
*
* @return string ID of the new Customer object.
*/
private function recreate_customer( $args = [], $is_add_payment_method_page = false ) {
private function recreate_customer( $args = [], ?string $current_context = null ) {
$this->delete_id_from_meta();
return $this->create_customer( $args, $is_add_payment_method_page );
return $this->create_customer( $args, $current_context );
}

/**
Expand Down
2 changes: 1 addition & 1 deletion includes/class-wc-stripe-intent-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ public function create_and_confirm_setup_intent_ajax() {
// Manually create the payment information array to create & confirm the setup intent.
$payment_information = [
'payment_method' => $payment_method,
'customer' => $customer->update_or_create_customer( [], true ),
'customer' => $customer->update_or_create_customer( [], WC_Stripe_Customer::CUSTOMER_CONTEXT_ADD_PAYMENT_METHOD ),
'selected_payment_type' => $payment_type,
'return_url' => wc_get_account_endpoint_url( 'payment-methods' ),
'use_stripe_sdk' => 'true', // We want the user to complete the next steps via the JS elements. ref https://docs.stripe.com/api/setup_intents/create#create_setup_intent-use_stripe_sdk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2858,10 +2858,12 @@ private function get_customer_id_for_order( WC_Order $order ): string {
$user = $this->get_user_from_order( $order );
$customer = new WC_Stripe_Customer( $user->ID );

$current_context = $this->is_valid_pay_for_order_endpoint() ? WC_Stripe_Customer::CUSTOMER_CONTEXT_PAY_FOR_ORDER : null;

// Pass the order object so we can retrieve billing details
// in payment flows where it is not present in the request.
$args = [ 'order' => $order ];
return $customer->update_or_create_customer( $args );
return $customer->update_or_create_customer( $args, $current_context );
}

/**
Expand Down
1 change: 1 addition & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
* Dev - Implements WooCommerce constants for the tax statuses
* Fix - Ensure all Javascript strings use the correct text domain for translation
* Tweak - Use more specific selector in express checkout e2e tests
* Fix - Relax customer validation that was preventing payments from the pay for order page

[See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).
54 changes: 47 additions & 7 deletions tests/phpunit/WC_Stripe_Customer_Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,61 @@ public function provide_test_validate_create_customer_request_cases(): array {
'expected_exception_message' => 'missing_required_customer_field: address->country',
'expected_exception_string' => 'Missing required customer field: address->country',
],
'add payment method page, all fields present and required, no overrides' => [
'add payment method page with boolean, all fields present and required, no overrides' => [
'billing_fields' => [], // only email is required
'woo_billing_fields' => null,
'stripe_billing_fields' => null,
'expected_exception_message' => null,
'expected_exception_string' => null,
'is_add_payment_method_page' => true,
'current_context' => true,
],
'add payment method page, email is empty string' => [
'add payment method page with context, all fields present and required, no overrides' => [
'billing_fields' => [], // only email is required
'woo_billing_fields' => null,
'stripe_billing_fields' => null,
'expected_exception_message' => null,
'expected_exception_string' => null,
'current_context' => \WC_Stripe_Customer::CUSTOMER_CONTEXT_ADD_PAYMENT_METHOD,
],
'add payment method page with boolean, email is empty string' => [
'billing_fields' => [ 'email' => '' ],
'woo_billing_fields' => null,
'stripe_billing_fields' => null,
'expected_exception_message' => 'missing_required_customer_field: email',
'expected_exception_string' => 'Missing required customer field: email',
'current_context' => true,
],
'add payment method page with context, email is empty string' => [
'billing_fields' => [ 'email' => '' ],
'woo_billing_fields' => null,
'stripe_billing_fields' => null,
'expected_exception_message' => 'missing_required_customer_field: email',
'expected_exception_string' => 'Missing required customer field: email',
'current_context' => \WC_Stripe_Customer::CUSTOMER_CONTEXT_ADD_PAYMENT_METHOD,
],
'pay for order page, only email present and required, no overrides' => [
'billing_fields' => [], // only email is required
'woo_billing_fields' => null,
'stripe_billing_fields' => null,
'expected_exception_message' => null,
'expected_exception_string' => null,
'current_context' => \WC_Stripe_Customer::CUSTOMER_CONTEXT_PAY_FOR_ORDER,
],
'pay for order page, only email is empty string' => [
'billing_fields' => [ 'email' => '' ],
'woo_billing_fields' => null,
'stripe_billing_fields' => null,
'expected_exception_message' => 'missing_required_customer_field: email',
'expected_exception_string' => 'Missing required customer field: email',
'is_add_payment_method_page' => true,
'current_context' => \WC_Stripe_Customer::CUSTOMER_CONTEXT_PAY_FOR_ORDER,
],
'all fields present and required, no overrides, context is false' => [
'billing_fields' => [],
'woo_billing_fields' => null,
'stripe_billing_fields' => null,
'expected_exception_message' => null,
'expected_exception_string' => null,
'current_context' => false,
],
];
}
Expand All @@ -126,9 +166,9 @@ public function test_validate_create_customer_request(
?array $stripe_billing_fields = null,
?string $expected_exception_message = null,
?string $expected_exception_string = null,
?bool $is_add_payment_method_page = false
$current_context = null
) {
if ( $is_add_payment_method_page ) {
if ( true === $current_context || in_array( $current_context, \WC_Stripe_Customer::MINIMAL_BILLING_DETAILS_CONTEXTS, true ) ) {
$default_billing_data = [
'email' => '[email protected]',
'first_name' => '',
Expand Down Expand Up @@ -278,7 +318,7 @@ public function test_validate_create_customer_request(
}

try {
$customer->create_customer( $args, $is_add_payment_method_page );
$customer->create_customer( $args, $current_context );
} catch ( \WC_Stripe_Exception $stripe_exception ) {
$was_exception_thrown = true;

Expand Down
Loading