PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.4.1
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.4.1
2.4.1 2.4.0 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.2 2.2.1 2.2.0 2.1.2 2.1.1 trunk 0.0.1 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 All 68 releases
better-payment / includes / Classes / StripeService.php

StripeService.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.4.1, at includes/Classes/StripeService.php

326 lines 12.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Better_Payment\Lite\Classes;
4
5 use Better_Payment\Lite\Admin\DB;
6
7 /**
8 * Exit if accessed directly
9 */
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit;
12 }
13
14 /**
15 * Thin Stripe API service over the plugin's global Stripe configuration.
16 *
17 * Extracted as a reusable, WooCommerce-agnostic entry point so integration
18 * modules (the WooCommerce gateway today, future consumers tomorrow) can
19 * create Checkout Sessions through the exact same wire protocol the existing
20 * surfaces use (see Classes\Actions::better_payment_stripe_get_token()):
21 * HTTP Basic auth with the secret key, `Stripe-Version: 2019-05-16`, the
22 * legacy `line_items` shape, and a 70s timeout. The existing surfaces are
23 * deliberately NOT migrated onto this service — their behavior must not
24 * change (they resolve keys per-widget, not globally).
25 *
26 * @since 2.4.0
27 */
28 class StripeService {
29
30 /**
31 * Pinned Stripe API version — must match every existing Better Payment
32 * Stripe call so all plugin traffic speaks one protocol version.
33 */
34 const API_VERSION = '2019-05-16';
35
36 /**
37 * Stripe API base URL.
38 */
39 const API_BASE = 'https://api.stripe.com/v1';
40
41 /**
42 * Resolve the global Stripe keys (Better Payment → Settings → Stripe).
43 *
44 * Live/test resolution mirrors API\AdminAPI::get_stripe_product_details()'s
45 * canonical pattern over the `better_payment_settings` option.
46 *
47 * @return array{public_key: string, secret_key: string, live_mode: bool}
48 */
49 public static function get_global_keys() {
50 $is_live_mode = 'yes' === DB::get_settings( 'better_payment_settings_payment_stripe_live_mode' );
51
52 $public_key = $is_live_mode
53 ? DB::get_settings( 'better_payment_settings_payment_stripe_live_public' )
54 : DB::get_settings( 'better_payment_settings_payment_stripe_test_public' );
55 $secret_key = $is_live_mode
56 ? DB::get_settings( 'better_payment_settings_payment_stripe_live_secret' )
57 : DB::get_settings( 'better_payment_settings_payment_stripe_test_secret' );
58
59 return array(
60 'public_key' => is_string( $public_key ) ? trim( $public_key ) : '',
61 'secret_key' => is_string( $secret_key ) ? trim( $secret_key ) : '',
62 'live_mode' => $is_live_mode,
63 );
64 }
65
66 /**
67 * Whether the global Stripe configuration is usable (both keys present
68 * for the currently selected mode).
69 *
70 * @return bool
71 */
72 public static function is_configured() {
73 $keys = self::get_global_keys();
74
75 return ! empty( $keys['public_key'] ) && ! empty( $keys['secret_key'] );
76 }
77
78 /**
79 * Create a Stripe Checkout Session.
80 *
81 * Same request mechanics as the existing surfaces (Basic auth header,
82 * pinned API version, form-encoded body, 70s timeout). The caller owns
83 * the request array shape; this method owns only the transport.
84 *
85 * @param mixed $session_args Checkout Session request parameters (array).
86 * @param string $secret_key Stripe secret key.
87 *
88 * @return object|\WP_Error Decoded session object on success.
89 */
90 public static function create_checkout_session( $session_args, $secret_key ) {
91 if ( empty( $secret_key ) || ! is_array( $session_args ) ) {
92 return new \WP_Error( 'stripe_error', __( 'Stripe is not configured.', 'better-payment' ) );
93 }
94
95 $response = wp_safe_remote_post(
96 self::API_BASE . '/checkout/sessions',
97 array(
98 'method' => 'POST',
99 'headers' => array(
100 'Authorization' => 'Basic ' . base64_encode( sanitize_text_field( $secret_key ) . ':' ),
101 'Stripe-Version' => self::API_VERSION,
102 ),
103 'body' => $session_args,
104 'timeout' => 70,
105 )
106 );
107
108 if ( is_wp_error( $response ) ) {
109 return $response;
110 }
111
112 $body = wp_remote_retrieve_body( $response );
113
114 if ( empty( $body ) ) {
115 return new \WP_Error( 'stripe_error', __( 'There was a problem connecting to the Stripe API endpoint.', 'better-payment' ) );
116 }
117
118 $session = json_decode( $body );
119
120 if ( ! is_object( $session ) ) {
121 return new \WP_Error( 'stripe_error', __( 'Unexpected response from the Stripe API endpoint.', 'better-payment' ) );
122 }
123
124 if ( isset( $session->error ) ) {
125 $message = ! empty( $session->error->message )
126 ? sanitize_text_field( $session->error->message )
127 : __( 'There was a problem connecting to the Stripe API endpoint.', 'better-payment' );
128
129 return new \WP_Error( 'stripe_error', $message, self::error_details( $session->error ) );
130 }
131
132 if ( empty( $session->id ) ) {
133 return new \WP_Error( 'stripe_error', __( 'Unexpected response from the Stripe API endpoint.', 'better-payment' ) );
134 }
135
136 return $session;
137 }
138
139 /**
140 * Retrieve a Checkout Session, optionally expanding related objects.
141 *
142 * Added for the WooCommerce subscriptions feature: after a verified
143 * subscription checkout the module retrieves the session with
144 * `expand[]=payment_intent` to learn the reusable customer + payment
145 * method Stripe attached. Same transport mechanics as
146 * create_checkout_session() (Basic auth, pinned API version, 70s
147 * timeout); that method keeps its own inline handling untouched.
148 *
149 * @since 2.4.0
150 *
151 * @param mixed $session_id Checkout Session id (cs_...).
152 * @param string $secret_key Stripe secret key.
153 * @param mixed $expand Object paths to expand (e.g. array( 'payment_intent' )).
154 *
155 * @return object|\WP_Error Decoded session object on success.
156 */
157 public static function retrieve_checkout_session( $session_id, $secret_key, $expand = array() ) {
158 if ( empty( $secret_key ) || empty( $session_id ) || ! is_string( $session_id ) ) {
159 return new \WP_Error( 'stripe_error', __( 'Stripe is not configured.', 'better-payment' ) );
160 }
161
162 $url = self::API_BASE . '/checkout/sessions/' . rawurlencode( sanitize_text_field( $session_id ) );
163
164 if ( ! empty( $expand ) && is_array( $expand ) ) {
165 $url = add_query_arg( array( 'expand' => array_map( 'sanitize_text_field', array_values( $expand ) ) ), $url );
166 }
167
168 $response = wp_safe_remote_get(
169 $url,
170 array(
171 'headers' => array(
172 'Authorization' => 'Basic ' . base64_encode( sanitize_text_field( $secret_key ) . ':' ),
173 'Stripe-Version' => self::API_VERSION,
174 ),
175 'timeout' => 70,
176 )
177 );
178
179 return self::decode_object_response( $response );
180 }
181
182 /**
183 * Create a PaymentIntent.
184 *
185 * Added for the WooCommerce subscriptions feature: renewal orders are
186 * charged server-side with an off-session PaymentIntent against the
187 * customer + payment method saved at the first checkout. The caller owns
188 * the request array shape; this method owns only the transport.
189 *
190 * @since 2.4.0
191 *
192 * @param mixed $intent_args PaymentIntent request parameters (array).
193 * @param string $secret_key Stripe secret key.
194 *
195 * @return object|\WP_Error Decoded PaymentIntent object on success.
196 */
197 public static function create_payment_intent( $intent_args, $secret_key ) {
198 if ( empty( $secret_key ) || ! is_array( $intent_args ) ) {
199 return new \WP_Error( 'stripe_error', __( 'Stripe is not configured.', 'better-payment' ) );
200 }
201
202 $response = wp_safe_remote_post(
203 self::API_BASE . '/payment_intents',
204 array(
205 'method' => 'POST',
206 'headers' => array(
207 'Authorization' => 'Basic ' . base64_encode( sanitize_text_field( $secret_key ) . ':' ),
208 'Stripe-Version' => self::API_VERSION,
209 ),
210 'body' => $intent_args,
211 'timeout' => 70,
212 )
213 );
214
215 return self::decode_object_response( $response );
216 }
217
218 /**
219 * Create a Stripe Customer.
220 *
221 * Added for the WooCommerce subscriptions feature's free-trial checkout:
222 * a $0 order collects the card through a setup-mode Checkout Session,
223 * and a setup session only attaches the saved payment method to a
224 * customer the caller supplies — so one is created up front. The caller
225 * owns the request array shape; this method owns only the transport.
226 *
227 * @since 2.4.0
228 *
229 * @param mixed $customer_args Customer request parameters (array).
230 * @param string $secret_key Stripe secret key.
231 *
232 * @return object|\WP_Error Decoded Customer object on success.
233 */
234 public static function create_customer( $customer_args, $secret_key ) {
235 if ( empty( $secret_key ) || ! is_array( $customer_args ) ) {
236 return new \WP_Error( 'stripe_error', __( 'Stripe is not configured.', 'better-payment' ) );
237 }
238
239 $response = wp_safe_remote_post(
240 self::API_BASE . '/customers',
241 array(
242 'method' => 'POST',
243 'headers' => array(
244 'Authorization' => 'Basic ' . base64_encode( sanitize_text_field( $secret_key ) . ':' ),
245 'Stripe-Version' => self::API_VERSION,
246 ),
247 'body' => $customer_args,
248 'timeout' => 70,
249 )
250 );
251
252 return self::decode_object_response( $response );
253 }
254
255 /**
256 * Decode a Stripe API response into an object, normalizing transport
257 * failures and Stripe error payloads into WP_Error. Shared by the
258 * transports added for subscriptions.
259 *
260 * @since 2.4.0
261 *
262 * @param array|\WP_Error $response wp_safe_remote_* result.
263 * @return object|\WP_Error
264 */
265 private static function decode_object_response( $response ) {
266 if ( is_wp_error( $response ) ) {
267 return $response;
268 }
269
270 $body = wp_remote_retrieve_body( $response );
271
272 if ( empty( $body ) ) {
273 return new \WP_Error( 'stripe_error', __( 'There was a problem connecting to the Stripe API endpoint.', 'better-payment' ) );
274 }
275
276 $object = json_decode( $body );
277
278 if ( ! is_object( $object ) ) {
279 return new \WP_Error( 'stripe_error', __( 'Unexpected response from the Stripe API endpoint.', 'better-payment' ) );
280 }
281
282 if ( isset( $object->error ) ) {
283 $message = ! empty( $object->error->message )
284 ? sanitize_text_field( $object->error->message )
285 : __( 'There was a problem connecting to the Stripe API endpoint.', 'better-payment' );
286
287 // The error code/type travels as WP_Error DATA, never as its code:
288 // every existing consumer matches on 'stripe_error' and on the
289 // message. Callers that must tell a declined card from a
290 // mis-configured Stripe account read it from here (see
291 // WooCommerce\Subscriptions::renewal_failure_is_configuration()).
292 return new \WP_Error( 'stripe_error', $message, self::error_details( $object->error ) );
293 }
294
295 if ( empty( $object->id ) ) {
296 return new \WP_Error( 'stripe_error', __( 'Unexpected response from the Stripe API endpoint.', 'better-payment' ) );
297 }
298
299 return $object;
300 }
301
302 /**
303 * Pure: the `type`/`code`/`decline_code` of a Stripe error payload, as
304 * the WP_Error data array. Keys are always present (empty strings when
305 * Stripe sent none), so a consumer never has to isset() its way through.
306 *
307 * @since 2.4.0
308 *
309 * @param mixed $error Stripe error object.
310 * @return array `type`, `code`, `decline_code`.
311 */
312 public static function error_details( $error ) {
313 $read = static function ( $key ) use ( $error ) {
314 return is_object( $error ) && isset( $error->{$key} ) && is_scalar( $error->{$key} )
315 ? sanitize_text_field( (string) $error->{$key} )
316 : '';
317 };
318
319 return array(
320 'type' => $read( 'type' ),
321 'code' => $read( 'code' ),
322 'decline_code' => $read( 'decline_code' ),
323 );
324 }
325 }
326