is_string( $public_key ) ? trim( $public_key ) : '', 'secret_key' => is_string( $secret_key ) ? trim( $secret_key ) : '', 'live_mode' => $is_live_mode, ); } /** * Whether the global Stripe configuration is usable (both keys present * for the currently selected mode). * * @return bool */ public static function is_configured() { $keys = self::get_global_keys(); return ! empty( $keys['public_key'] ) && ! empty( $keys['secret_key'] ); } /** * Create a Stripe Checkout Session. * * Same request mechanics as the existing surfaces (Basic auth header, * pinned API version, form-encoded body, 70s timeout). The caller owns * the request array shape; this method owns only the transport. * * @param mixed $session_args Checkout Session request parameters (array). * @param string $secret_key Stripe secret key. * * @return object|\WP_Error Decoded session object on success. */ public static function create_checkout_session( $session_args, $secret_key ) { if ( empty( $secret_key ) || ! is_array( $session_args ) ) { return new \WP_Error( 'stripe_error', __( 'Stripe is not configured.', 'better-payment' ) ); } $response = wp_safe_remote_post( self::API_BASE . '/checkout/sessions', array( 'method' => 'POST', 'headers' => array( 'Authorization' => 'Basic ' . base64_encode( sanitize_text_field( $secret_key ) . ':' ), 'Stripe-Version' => self::API_VERSION, ), 'body' => $session_args, 'timeout' => 70, ) ); if ( is_wp_error( $response ) ) { return $response; } $body = wp_remote_retrieve_body( $response ); if ( empty( $body ) ) { return new \WP_Error( 'stripe_error', __( 'There was a problem connecting to the Stripe API endpoint.', 'better-payment' ) ); } $session = json_decode( $body ); if ( ! is_object( $session ) ) { return new \WP_Error( 'stripe_error', __( 'Unexpected response from the Stripe API endpoint.', 'better-payment' ) ); } if ( isset( $session->error ) ) { $message = ! empty( $session->error->message ) ? sanitize_text_field( $session->error->message ) : __( 'There was a problem connecting to the Stripe API endpoint.', 'better-payment' ); return new \WP_Error( 'stripe_error', $message, self::error_details( $session->error ) ); } if ( empty( $session->id ) ) { return new \WP_Error( 'stripe_error', __( 'Unexpected response from the Stripe API endpoint.', 'better-payment' ) ); } return $session; } /** * Retrieve a Checkout Session, optionally expanding related objects. * * Added for the WooCommerce subscriptions feature: after a verified * subscription checkout the module retrieves the session with * `expand[]=payment_intent` to learn the reusable customer + payment * method Stripe attached. Same transport mechanics as * create_checkout_session() (Basic auth, pinned API version, 70s * timeout); that method keeps its own inline handling untouched. * * @since 2.4.0 * * @param mixed $session_id Checkout Session id (cs_...). * @param string $secret_key Stripe secret key. * @param mixed $expand Object paths to expand (e.g. array( 'payment_intent' )). * * @return object|\WP_Error Decoded session object on success. */ public static function retrieve_checkout_session( $session_id, $secret_key, $expand = array() ) { if ( empty( $secret_key ) || empty( $session_id ) || ! is_string( $session_id ) ) { return new \WP_Error( 'stripe_error', __( 'Stripe is not configured.', 'better-payment' ) ); } $url = self::API_BASE . '/checkout/sessions/' . rawurlencode( sanitize_text_field( $session_id ) ); if ( ! empty( $expand ) && is_array( $expand ) ) { $url = add_query_arg( array( 'expand' => array_map( 'sanitize_text_field', array_values( $expand ) ) ), $url ); } $response = wp_safe_remote_get( $url, array( 'headers' => array( 'Authorization' => 'Basic ' . base64_encode( sanitize_text_field( $secret_key ) . ':' ), 'Stripe-Version' => self::API_VERSION, ), 'timeout' => 70, ) ); return self::decode_object_response( $response ); } /** * Create a PaymentIntent. * * Added for the WooCommerce subscriptions feature: renewal orders are * charged server-side with an off-session PaymentIntent against the * customer + payment method saved at the first checkout. The caller owns * the request array shape; this method owns only the transport. * * @since 2.4.0 * * @param mixed $intent_args PaymentIntent request parameters (array). * @param string $secret_key Stripe secret key. * * @return object|\WP_Error Decoded PaymentIntent object on success. */ public static function create_payment_intent( $intent_args, $secret_key ) { if ( empty( $secret_key ) || ! is_array( $intent_args ) ) { return new \WP_Error( 'stripe_error', __( 'Stripe is not configured.', 'better-payment' ) ); } $response = wp_safe_remote_post( self::API_BASE . '/payment_intents', array( 'method' => 'POST', 'headers' => array( 'Authorization' => 'Basic ' . base64_encode( sanitize_text_field( $secret_key ) . ':' ), 'Stripe-Version' => self::API_VERSION, ), 'body' => $intent_args, 'timeout' => 70, ) ); return self::decode_object_response( $response ); } /** * Create a Stripe Customer. * * Added for the WooCommerce subscriptions feature's free-trial checkout: * a $0 order collects the card through a setup-mode Checkout Session, * and a setup session only attaches the saved payment method to a * customer the caller supplies — so one is created up front. The caller * owns the request array shape; this method owns only the transport. * * @since 2.4.0 * * @param mixed $customer_args Customer request parameters (array). * @param string $secret_key Stripe secret key. * * @return object|\WP_Error Decoded Customer object on success. */ public static function create_customer( $customer_args, $secret_key ) { if ( empty( $secret_key ) || ! is_array( $customer_args ) ) { return new \WP_Error( 'stripe_error', __( 'Stripe is not configured.', 'better-payment' ) ); } $response = wp_safe_remote_post( self::API_BASE . '/customers', array( 'method' => 'POST', 'headers' => array( 'Authorization' => 'Basic ' . base64_encode( sanitize_text_field( $secret_key ) . ':' ), 'Stripe-Version' => self::API_VERSION, ), 'body' => $customer_args, 'timeout' => 70, ) ); return self::decode_object_response( $response ); } /** * Decode a Stripe API response into an object, normalizing transport * failures and Stripe error payloads into WP_Error. Shared by the * transports added for subscriptions. * * @since 2.4.0 * * @param array|\WP_Error $response wp_safe_remote_* result. * @return object|\WP_Error */ private static function decode_object_response( $response ) { if ( is_wp_error( $response ) ) { return $response; } $body = wp_remote_retrieve_body( $response ); if ( empty( $body ) ) { return new \WP_Error( 'stripe_error', __( 'There was a problem connecting to the Stripe API endpoint.', 'better-payment' ) ); } $object = json_decode( $body ); if ( ! is_object( $object ) ) { return new \WP_Error( 'stripe_error', __( 'Unexpected response from the Stripe API endpoint.', 'better-payment' ) ); } if ( isset( $object->error ) ) { $message = ! empty( $object->error->message ) ? sanitize_text_field( $object->error->message ) : __( 'There was a problem connecting to the Stripe API endpoint.', 'better-payment' ); // The error code/type travels as WP_Error DATA, never as its code: // every existing consumer matches on 'stripe_error' and on the // message. Callers that must tell a declined card from a // mis-configured Stripe account read it from here (see // WooCommerce\Subscriptions::renewal_failure_is_configuration()). return new \WP_Error( 'stripe_error', $message, self::error_details( $object->error ) ); } if ( empty( $object->id ) ) { return new \WP_Error( 'stripe_error', __( 'Unexpected response from the Stripe API endpoint.', 'better-payment' ) ); } return $object; } /** * Pure: the `type`/`code`/`decline_code` of a Stripe error payload, as * the WP_Error data array. Keys are always present (empty strings when * Stripe sent none), so a consumer never has to isset() its way through. * * @since 2.4.0 * * @param mixed $error Stripe error object. * @return array `type`, `code`, `decline_code`. */ public static function error_details( $error ) { $read = static function ( $key ) use ( $error ) { return is_object( $error ) && isset( $error->{$key} ) && is_scalar( $error->{$key} ) ? sanitize_text_field( (string) $error->{$key} ) : ''; }; return array( 'type' => $read( 'type' ), 'code' => $read( 'code' ), 'decline_code' => $read( 'decline_code' ), ); } }