PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.8
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.8
2.12.8 2.12.7 2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 All 98 releases
sureforms / inc / payments / stripe / stripe-helper.php

stripe-helper.php in SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz 2.12.8, at inc/payments/stripe/stripe-helper.php

840 lines 27.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Stripe Helper functions for SureForms Payments.
4 *
5 * @package sureforms
6 * @since 2.0.0
7 */
8
9 namespace SRFM\Inc\Payments\Stripe;
10
11 use SRFM\Inc\Database\Tables\Payments;
12 use SRFM\Inc\Payments\Payment_Helper;
13 use SRFM_Pro\Admin\Licensing;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 /**
20 * Stripe Helper functions for SureForms Payments.
21 *
22 * @since 2.0.0
23 */
24 class Stripe_Helper {
25 /**
26 * Static cache for webhook verification results during the same request.
27 *
28 * @since 2.0.0
29 * @var array<string, bool>
30 */
31 private static $webhook_verification_cache = [];
32
33 /**
34 * Check if Stripe is connected.
35 *
36 * @since 2.0.0
37 * @return bool True if Stripe is connected, false otherwise.
38 */
39 public static function is_stripe_connected() {
40 $payment_settings = self::get_all_stripe_settings();
41 return is_array( $payment_settings ) && isset( $payment_settings['stripe_connected'] ) && is_bool( $payment_settings['stripe_connected'] ) ? $payment_settings['stripe_connected'] : false;
42 }
43
44 /**
45 * Get the current Stripe mode (test or live).
46 *
47 * @since 2.0.0
48 * @return string The current payment mode ('test' or 'live').
49 */
50 public static function get_stripe_mode() {
51 return Payment_Helper::get_payment_mode();
52 }
53
54 /**
55 * Check if webhook is configured.
56 *
57 * Checks if webhooks are properly configured based on the current payment mode.
58 * Can optionally verify the webhook connection with Stripe API.
59 *
60 * @param string|null $mode The payment mode ('test' or 'live'). If null, uses current mode.
61 * @param bool $verify Whether to verify with Stripe API. Default false (checks local settings only).
62 * @since 2.0.0
63 * @return bool True if webhook is configured, false otherwise.
64 */
65 public static function is_webhook_configured( $mode = null, $verify = false ) {
66 // Get current payment mode.
67 $payment_mode = is_string( $mode ) && in_array( $mode, [ 'test', 'live' ], true ) ? $mode : self::get_stripe_mode();
68
69 // Get webhook settings.
70 $payment_settings = self::get_all_stripe_settings();
71
72 if ( ! is_array( $payment_settings ) ) {
73 return false;
74 }
75
76 // Check webhook secret exists based on mode.
77 $webhook_secret_key = 'webhook_' . $payment_mode . '_secret';
78 $has_secret = ! empty( $payment_settings[ $webhook_secret_key ] );
79
80 // If no secret found, webhook is not configured.
81 if ( ! $has_secret ) {
82 return false;
83 }
84
85 // If verification is not requested, return true (secret exists).
86 if ( ! $verify ) {
87 return true;
88 }
89
90 // Verify with Stripe API (returns boolean).
91 return self::verify_webhook_connection( $payment_mode );
92 }
93
94 /**
95 * Get Stripe secret key for the specified mode.
96 *
97 * @param string|null $mode The payment mode ('test' or 'live'). If null, uses current mode.
98 * @since 2.0.0
99 * @return string The secret key for the specified mode, or empty string if not found.
100 */
101 public static function get_stripe_secret_key( $mode = null ) {
102 $payment_settings = self::get_all_stripe_settings();
103
104 if ( null === $mode ) {
105 $mode = self::get_stripe_mode();
106 }
107
108 return is_array( $payment_settings ) && isset( $payment_settings[ 'stripe_' . $mode . '_secret_key' ] ) && is_string( $payment_settings[ 'stripe_' . $mode . '_secret_key' ] ) ? $payment_settings[ 'stripe_' . $mode . '_secret_key' ] : '';
109 }
110
111 /**
112 * Get Stripe publishable key for the specified mode.
113 *
114 * @param string|null $mode The payment mode ('test' or 'live'). If null, uses current mode.
115 * @since 2.0.0
116 * @return string The publishable key for the specified mode, or empty string if not found.
117 */
118 public static function get_stripe_publishable_key( $mode = null ) {
119 if ( null === $mode ) {
120 $mode = self::get_stripe_mode();
121 }
122
123 $payment_settings = self::get_all_stripe_settings();
124
125 return is_array( $payment_settings ) && isset( $payment_settings[ 'stripe_' . $mode . '_publishable_key' ] ) && is_string( $payment_settings[ 'stripe_' . $mode . '_publishable_key' ] ) ? $payment_settings[ 'stripe_' . $mode . '_publishable_key' ] : '';
126 }
127
128 /**
129 * Get the default currency from payment settings.
130 *
131 * @since 2.0.0
132 * @return string The currency code (e.g., 'USD').
133 */
134 public static function get_currency() {
135 return Payment_Helper::get_currency();
136 }
137
138 /**
139 * Get the Stripe settings page URL.
140 *
141 * This returns the URL to the SureForms Stripe settings page in the admin.
142 * As of now, the URL is:
143 * http://localhost:10008/wp-admin/admin.php?page=sureforms_form_settings&tab=payments-settings&subpage=payment-methods&gateway=stripe
144 * The site URL is dynamic and will adapt to the current WordPress installation.
145 *
146 * @since 2.0.0
147 * @return string The URL to the Stripe settings page.
148 */
149 public static function get_stripe_settings_url() {
150 return admin_url( 'admin.php?page=sureforms_form_settings&tab=payments-settings&subpage=payment-methods&gateway=stripe' );
151 }
152
153 /**
154 * Make a request to the Stripe API.
155 *
156 * @param string $endpoint The API endpoint to call.
157 * @param string $method The HTTP method (GET, POST, PUT, PATCH, DELETE). Default 'POST'.
158 * @param array<mixed> $data The data to send with the request. Default empty array.
159 * @param string $resource_id The resource ID to append to the endpoint. Default empty string.
160 * @param array<mixed> $extra_args Additional arguments to pass to the request. Default empty array.
161 * @since 2.0.0
162 * @return array<mixed> Response array with 'success' boolean and either 'data' or 'error' key.
163 */
164 public static function stripe_api_request( $endpoint, $method = 'POST', $data = [], $resource_id = '', $extra_args = [] ) {
165 if ( ! self::is_stripe_connected() ) {
166 return [
167 'success' => false,
168 'error' => [
169 'code' => 'stripe_not_connected',
170 'message' => __( 'Stripe is not connected.', 'sureforms' ),
171 'type' => 'auth',
172 'raw_response' => null,
173 ],
174 ];
175 }
176
177 $payment_mode = (string) self::get_stripe_mode();
178
179 if ( ! empty( $extra_args ) && is_array( $extra_args ) ) {
180 $payment_mode = isset( $extra_args['mode'] ) && is_string( $extra_args['mode'] ) && in_array( $extra_args['mode'], [ 'test', 'live' ], true ) ? $extra_args['mode'] : $payment_mode;
181 }
182
183 $secret_key = (string) self::get_stripe_secret_key( $payment_mode );
184
185 if ( empty( $secret_key ) ) {
186 return [
187 'success' => false,
188 'error' => [
189 'code' => 'missing_secret_key',
190 'message' => sprintf(
191 /* translators: %s: payment mode (test/live) */
192 __( 'Stripe %s secret key is missing.', 'sureforms' ),
193 $payment_mode
194 ),
195 'type' => 'auth',
196 'raw_response' => null,
197 ],
198 ];
199 }
200
201 $url = 'https://api.stripe.com/v1/' . $endpoint;
202 if ( ! empty( $resource_id ) ) {
203 $url .= '/' . $resource_id;
204 }
205
206 $headers = [
207 'Authorization' => 'Bearer ' . $secret_key,
208 'Content-Type' => 'application/x-www-form-urlencoded',
209 ];
210
211 $args = [
212 'method' => $method,
213 'headers' => $headers,
214 'timeout' => 30,
215 ];
216
217 if ( ! empty( $data ) && in_array( $method, [ 'POST', 'PUT', 'PATCH' ], true ) ) {
218 $args['body'] = http_build_query( self::flatten_stripe_data( $data ) );
219 } elseif ( ! empty( $data ) && 'GET' === $method ) {
220 $url .= '?' . http_build_query( self::flatten_stripe_data( $data ) );
221 }
222
223 $response = wp_remote_request( $url, $args );
224
225 if ( is_wp_error( $response ) ) {
226 $error_message = $response->get_error_message();
227 return [
228 'success' => false,
229 'error' => [
230 'code' => $response->get_error_code(),
231 'message' => sprintf(
232 /* translators: %s: network error message */
233 __( 'Network error: %s', 'sureforms' ),
234 $error_message
235 ),
236 'type' => 'network',
237 'raw_response' => $response,
238 ],
239 ];
240 }
241
242 $body = wp_remote_retrieve_body( $response );
243 $code = wp_remote_retrieve_response_code( $response );
244
245 // Try to decode the response body.
246 $decoded_body = json_decode( $body, true );
247 if ( json_last_error() !== JSON_ERROR_NONE ) {
248 return [
249 'success' => false,
250 'error' => [
251 'code' => 'invalid_response',
252 'message' => __( 'Invalid response format from Stripe API.', 'sureforms' ),
253 'type' => 'invalid_response',
254 'raw_response' => $body,
255 ],
256 ];
257 }
258
259 if ( $code >= 400 ) {
260 $stripe_error = is_array( $decoded_body ) && isset( $decoded_body['error'] ) && is_array( $decoded_body['error'] ) ? $decoded_body['error'] : [];
261 $error_code = isset( $stripe_error['code'] ) ? (string) $stripe_error['code'] : 'unknown_error';
262 $error_message = isset( $stripe_error['message'] ) ? (string) $stripe_error['message'] : 'Unknown Stripe API error';
263 $error_type = isset( $stripe_error['type'] ) ? (string) $stripe_error['type'] : 'api_error';
264
265 return [
266 'success' => false,
267 'error' => [
268 'code' => $error_code,
269 'message' => $error_message,
270 'type' => 'stripe_api',
271 'stripe_error_type' => $error_type,
272 'http_status' => $code,
273 'raw_response' => $decoded_body,
274 ],
275 ];
276 }
277
278 // Success case - return the decoded response with success indicator.
279 return [
280 'success' => true,
281 'data' => $decoded_body,
282 ];
283 }
284
285 /**
286 * Retrieve the middleware base URL for Stripe API communication.
287 *
288 * By default, returns the production middleware URL that securely proxies requests
289 * between the plugin and Stripe's API.
290 *
291 * Developers working in local or staging environments can override the SRFM_MIDDLEWARE_BASE_URL
292 * constant (for example, set it to "http://sureforms-payments-middleware.test") to point
293 * to a locally running payments middleware app (e.g., http://sureforms-payments-middleware.test/payments/stripe/).
294 *
295 * You can also modify the return value or use a filter hook to customize the URL as needed
296 * for testing, debugging, or customizing payment flows during development.
297 *
298 * @since 2.0.0
299 * @return string The middleware base URL.
300 */
301 public static function middle_ware_base_url() {
302 return SRFM_MIDDLEWARE_BASE_URL . 'payments/stripe/';
303 }
304
305 /**
306 * Get currency symbol.
307 *
308 * @param string $currency Currency code.
309 * @return string
310 * @since 2.0.0
311 */
312 public static function get_currency_symbol( $currency ) {
313 return Payment_Helper::get_currency_symbol( $currency );
314 }
315
316 /**
317 * Check if currency is zero-decimal.
318 *
319 * @param string $currency Currency code.
320 * @since 2.0.0
321 * @return bool True if zero-decimal currency.
322 */
323 public static function is_zero_decimal_currency( $currency ) {
324 return Payment_Helper::is_zero_decimal_currency( $currency );
325 }
326
327 /**
328 * Convert amount to Stripe's smallest currency unit.
329 *
330 * For two-decimal currencies (USD, EUR, etc.): multiplies by 100
331 * For zero-decimal currencies (JPY, KRW, etc.): returns as-is
332 *
333 * @param float|string|int $amount Amount in major currency unit (can contain commas).
334 * @param string $currency Currency code.
335 * @since 2.0.0
336 * @return int Amount in smallest currency unit (cents for 2-decimal, whole for 0-decimal).
337 */
338 public static function amount_to_stripe_format( $amount, $currency ) {
339 $amount = self::clean_amount( $amount );
340 return self::is_zero_decimal_currency( $currency )
341 ? (int) round( $amount )
342 : (int) round( $amount * 100 );
343 }
344
345 /**
346 * Convert amount from Stripe's smallest currency unit to major unit.
347 *
348 * For two-decimal currencies (USD, EUR, etc.): divides by 100
349 * For zero-decimal currencies (JPY, KRW, etc.): returns as-is
350 *
351 * @param int|string|float $amount Amount in smallest currency unit (can contain commas).
352 * @param string $currency Currency code.
353 * @since 2.0.0
354 * @return float Amount in major currency unit.
355 */
356 public static function amount_from_stripe_format( $amount, $currency ) {
357 $amount = self::clean_amount( $amount );
358 return self::is_zero_decimal_currency( $currency )
359 ? $amount
360 : $amount / 100;
361 }
362
363 /**
364 * Generate unique payment ID using base36 encoding and random string, always 14 characters.
365 *
366 * Format: {base36_encoded_id}{random_chars}
367 * Example: 3F7B9A1E4C7D2A (exactly 14 chars)
368 *
369 * @param int $auto_increment_id The database auto-increment ID.
370 * @since 2.0.0
371 * @return string Generated unique payment ID (always 14 characters).
372 */
373 public static function generate_unique_payment_id( $auto_increment_id ) {
374 // Convert the auto-increment ID to base36.
375 $encoded_id = base_convert( (string) $auto_increment_id, 10, 36 );
376 // Calculate the length of random part needed to make the ID exactly 14 chars.
377 $random_length = 14 - strlen( $encoded_id );
378 if ( $random_length < 1 ) {
379 $random_length = 1; // Always leave at least 1 random char for collision prevention.
380 }
381 // Generate random part using only valid base36 (alphanumeric) chars.
382 // bin2hex gives 2 chars per byte, so we need ceil($random_length / 2) bytes.
383 $bytes_needed = max( 1, (int) ceil( $random_length / 2 ) ); // Ensure at least 1 byte.
384 $random_bytes = bin2hex( random_bytes( $bytes_needed ) );
385 $random_part = substr( $random_bytes, 0, $random_length );
386 $unique_id = strtoupper( $encoded_id . $random_part );
387 // Ensure exactly 14 chars.
388 return substr( $unique_id, 0, 14 );
389 }
390
391 /**
392 * Get the SureForms Pro License Key.
393 *
394 * @param bool $allow_remote_check Whether a license-cache miss may block on the
395 * SureCart API. Pass false from front-end request
396 * paths - see is_pro_license_active().
397 * @since 2.0.0
398 * @since 2.12.8 Added the $allow_remote_check parameter.
399 * @return string The SureForms Pro License Key.
400 */
401 public static function get_license_key( $allow_remote_check = true ) {
402 $licensing = self::get_licensing_instance();
403 if ( ! $licensing ||
404 ! method_exists( $licensing, 'licensing_setup' ) || ! method_exists( $licensing->licensing_setup(), 'settings' ) ) {
405 return '';
406 }
407 // Check if the SureForms Pro license is active.
408 $is_license_active = self::is_pro_license_active( $allow_remote_check );
409 // If the license is active, get the license key.
410 $license_setup = $licensing->licensing_setup();
411 return ! empty( $is_license_active ) && is_object( $license_setup ) && method_exists( $license_setup, 'settings' ) ? $license_setup->settings()->license_key : '';
412 }
413
414 /**
415 * Check if the SureForms Pro license is active.
416 *
417 * On a cache miss the Pro licensing check makes blocking wp_remote_request calls
418 * with a 30 second timeout each. That is fine on an admin screen, but this helper
419 * is also reached from wp_ajax_nopriv_ checkout handlers, where an unauthenticated
420 * visitor would wait on SureCart before their payment intent is created - and a
421 * single slow response would pin the cached verdict for the whole site.
422 *
423 * Pass false from those paths to read the last known status instead.
424 *
425 * @param bool $allow_remote_check Whether a cache miss may block on the SureCart API.
426 * @since 2.0.0
427 * @since 2.12.8 Added the $allow_remote_check parameter.
428 * @return bool|string True if the SureForms Pro license is active, false otherwise.
429 */
430 public static function is_pro_license_active( $allow_remote_check = true ) {
431 $licensing = self::get_licensing_instance();
432 if ( ! $licensing || ! method_exists( $licensing, 'is_license_active' )
433 ) {
434 return '';
435 }
436 // Check if the SureForms Pro license is active.
437 return $licensing->is_license_active( $allow_remote_check );
438 }
439
440 /**
441 * Get the webhook URL for Stripe.
442 *
443 * Returns the dynamic webhook URL based on the site's REST API endpoint.
444 * Example: http://localhost:10008/wp-json/sureforms/webhook
445 *
446 * @param string $mode The payment mode ('test' or 'live'). Default is 'test'.
447 *
448 * @since 2.0.0
449 * @return string The webhook URL.
450 */
451 public static function get_webhook_url( $mode = 'test' ) {
452 return 'test' === $mode ? rest_url( 'sureforms/webhook_test' ) : rest_url( 'sureforms/webhook_live' );
453 }
454
455 /**
456 * Verify webhook connection with Stripe.
457 *
458 * Checks if the webhook endpoint exists and is enabled in Stripe
459 * based on the current payment mode. Uses static cache for same request.
460 *
461 * @param string|null $mode The payment mode ('test' or 'live'). If null, uses current mode.
462 * @since 2.0.0
463 * @return bool True if webhook is enabled, false otherwise.
464 */
465 public static function verify_webhook_connection( $mode = null ) {
466 // Get current payment mode.
467 $payment_mode = is_string( $mode ) && in_array( $mode, [ 'test', 'live' ], true ) ? $mode : self::get_stripe_mode();
468
469 // Check static cache first to avoid repeated API calls in same request.
470 $cache_key = 'webhook_' . $payment_mode;
471 if ( isset( self::$webhook_verification_cache[ $cache_key ] ) ) {
472 return self::$webhook_verification_cache[ $cache_key ];
473 }
474
475 // Get webhook settings.
476 $payment_settings = self::get_all_stripe_settings();
477
478 if ( ! is_array( $payment_settings ) ) {
479 self::$webhook_verification_cache[ $cache_key ] = false;
480 return false;
481 }
482
483 // Get webhook ID based on mode.
484 $webhook_id_key = 'webhook_' . $payment_mode . '_id';
485 $webhook_id = isset( $payment_settings[ $webhook_id_key ] ) && is_string( $payment_settings[ $webhook_id_key ] ) ? $payment_settings[ $webhook_id_key ] : '';
486
487 if ( empty( $webhook_id ) ) {
488 self::$webhook_verification_cache[ $cache_key ] = false;
489 return false;
490 }
491
492 // Make API request to verify webhook.
493 $response = self::stripe_api_request( 'webhook_endpoints', 'GET', [], $webhook_id, [ 'mode' => $payment_mode ] );
494
495 // If API call failed (webhook not found, deleted, or error), clear webhook data.
496 if ( ! $response['success'] ) {
497 self::clear_webhook_data( $payment_mode, $payment_settings );
498 self::$webhook_verification_cache[ $cache_key ] = false;
499 return false;
500 }
501
502 // Check webhook status and mode match.
503 $webhook_data = $response['data'];
504 $is_enabled = isset( $webhook_data['status'] ) && 'enabled' === $webhook_data['status'];
505
506 // Verify the livemode matches the current mode.
507 $webhook_livemode = isset( $webhook_data['livemode'] ) && is_bool( $webhook_data['livemode'] ) ? $webhook_data['livemode'] : false;
508 $expected_livemode = 'live' === $payment_mode;
509 $mode_matches = $webhook_livemode === $expected_livemode;
510
511 // Webhook is connected only if enabled and mode matches.
512 $is_connected = $is_enabled && $mode_matches;
513
514 // If webhook is not connected, clear the webhook data from settings.
515 if ( ! $is_connected ) {
516 self::clear_webhook_data( $payment_mode, $payment_settings );
517 }
518
519 // Cache result for this request.
520 self::$webhook_verification_cache[ $cache_key ] = $is_connected;
521
522 return $is_connected;
523 }
524
525 /**
526 * Check if any transaction is present in the payments table.
527 *
528 * @since 2.0.0
529 * @return bool True if at least one transaction exists, false otherwise.
530 */
531 public static function is_transaction_present() {
532 global $wpdb;
533
534 // Get payments table name.
535 $payments_table = Payments::get_instance()->get_tablename();
536
537 if ( empty( $payments_table ) || ! is_string( $payments_table ) ) {
538 return false;
539 }
540
541 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
542 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Custom table query to check transaction existence; table name from get_tablename() and validated (not user input); cannot be parameterized with prepare().
543 $count = $wpdb->get_var(
544 "SELECT COUNT(*) FROM {$payments_table} LIMIT 1"
545 );
546 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
547
548 return ! empty( $count ) && absint( $count ) > 0;
549 }
550
551 /**
552 * Get all Stripe settings from srfm_options.
553 *
554 * Retrieves the complete Stripe settings array from the nested structure:
555 * srfm_options -> payment_settings -> stripe
556 *
557 * @since 2.0.0
558 * @return array<string, mixed> The Stripe settings array, or default settings if not found.
559 */
560 public static function get_all_stripe_settings() {
561 $stripe_settings = Payment_Helper::get_gateway_settings( 'stripe' );
562
563 // Return default settings if empty.
564 return ! empty( $stripe_settings ) ? $stripe_settings : self::get_default_stripe_settings();
565 }
566
567 /**
568 * Update all Stripe settings in srfm_options.
569 *
570 * Stores the complete Stripe settings array in the nested structure:
571 * srfm_options -> payment_settings -> stripe
572 *
573 * @param array<string, mixed> $settings The Stripe settings array to save.
574 * @since 2.0.0
575 * @return bool True on success, false on failure.
576 */
577 public static function update_all_stripe_settings( $settings ) {
578 if ( ! is_array( $settings ) ) {
579 return false;
580 }
581
582 return Payment_Helper::update_gateway_settings( 'stripe', $settings );
583 }
584
585 /**
586 * Get a specific Stripe setting value by key.
587 *
588 * @param string $key The setting key to retrieve.
589 * @param mixed $default The default value to return if key doesn't exist.
590 * @since 2.0.0
591 * @return mixed The setting value or default if not found.
592 */
593 public static function get_stripe_setting( $key, $default = '' ) {
594 if ( ! is_string( $key ) || empty( $key ) ) {
595 return $default;
596 }
597
598 $settings = self::get_all_stripe_settings();
599
600 return $settings[ $key ] ?? $default;
601 }
602
603 /**
604 * Update a specific Stripe setting value by key.
605 *
606 * @param string $key The setting key to update.
607 * @param mixed $value The value to set.
608 * @since 2.0.0
609 * @return bool True on success, false on failure.
610 */
611 public static function update_stripe_setting( $key, $value ) {
612 if ( ! is_string( $key ) || empty( $key ) ) {
613 return false;
614 }
615
616 $settings = self::get_all_stripe_settings();
617 $settings[ $key ] = $value;
618
619 return self::update_all_stripe_settings( $settings );
620 }
621
622 /**
623 * Get default Stripe settings structure.
624 *
625 * Note: currency and payment_mode are now stored in global settings.
626 *
627 * @since 2.0.0
628 * @return array<string, mixed> Default Stripe settings array.
629 */
630 public static function get_default_stripe_settings() {
631 return [
632 'stripe_connected' => false,
633 'stripe_account_id' => '',
634 'stripe_account_email' => '',
635 'stripe_live_publishable_key' => '',
636 'stripe_live_secret_key' => '',
637 'stripe_test_publishable_key' => '',
638 'stripe_test_secret_key' => '',
639 'payment_mode' => 'test',
640 'webhook_test_secret' => '',
641 'webhook_test_url' => '',
642 'webhook_test_id' => '',
643 'webhook_live_secret' => '',
644 'webhook_live_url' => '',
645 'webhook_live_id' => '',
646 'account_name' => '',
647 ];
648 }
649
650 /**
651 * Get Stripe Connect URL
652 *
653 * @since 2.0.0
654 * @return \WP_REST_Response
655 */
656 public static function get_stripe_connect_url() {
657 // Stripe client ID from checkout-plugins-stripe-woo.
658 $client_id = 'ca_KOXfLe7jv1m4L0iC4KNEMc5fT8AXWWuL';
659
660 // Use the same redirect URI pattern as checkout-plugins-stripe-woo.
661 $redirect_url = admin_url( 'admin.php?page=sureforms_form_settings&tab=payments-settings&subpage=payment-methods&gateway=stripe' );
662 $nonce = wp_create_nonce( 'stripe-connect' );
663 $redirect_with_nonce = add_query_arg( 'srfm_stripe_connect_nonce', $nonce, $redirect_url );
664
665 // Store our own callback data.
666 set_transient( 'srfm_stripe_connect_nonce_' . get_current_user_id(), $nonce, HOUR_IN_SECONDS );
667
668 // Create state parameter exactly like checkout-plugins-stripe-woo.
669 $state_param = wp_json_encode(
670 [
671 'redirect' => $redirect_with_nonce,
672 ]
673 );
674 $state = '';
675 if ( is_string( $state_param ) ) {
676 $state = base64_encode( $state_param ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
677 }
678
679 $connect_url = add_query_arg(
680 [
681 'response_type' => 'code',
682 'client_id' => $client_id,
683 'stripe_landing' => 'login',
684 'always_prompt' => 'true',
685 'scope' => 'read_write',
686 'state' => $state,
687 ],
688 'https://connect.stripe.com/oauth/authorize'
689 );
690
691 return rest_ensure_response( [ 'url' => $connect_url ] );
692 }
693
694 /**
695 * Get the Stripe account ID.
696 *
697 * @since 2.5.1
698 * @return string The Stripe account ID.
699 */
700 public static function get_stripe_account_id() {
701 $account = self::get_stripe_setting( 'stripe_account_id' );
702 if ( empty( $account ) || ! is_string( $account ) ) {
703 return '';
704 }
705 return $account;
706 }
707
708 /**
709 * Send payment data to middleware intersect endpoint.
710 *
711 * @param string $charge_id Stripe charge ID (ch_xxx format).
712 * @param string $secret_key Stripe secret key.
713 * @param string $stripe_account_id Stripe account ID (optional).
714 * @param string $plugin_name Plugin name (default: 'SureForms').
715 * @since 2.5.1
716 * @return void
717 */
718 public static function intersect_payment( $charge_id, $secret_key = '', $stripe_account_id = '', $plugin_name = 'SureForms' ) {
719 // Validate charge ID format (must be ch_xxx).
720 if ( empty( $charge_id ) || ! preg_match( '/^ch_[a-zA-Z0-9]+$/', $charge_id ) ) {
721 return;
722 }
723
724 if ( empty( $secret_key ) ) {
725 return;
726 }
727
728 // Prepare request data.
729 $request_data = [
730 'plugin_name' => $plugin_name,
731 'secret_key' => $secret_key,
732 'transaction_id' => $charge_id,
733 'account_id' => $stripe_account_id,
734 ];
735
736 // Encode and send to middleware.
737 $request_body = wp_json_encode( $request_data );
738 $request_body = is_string( $request_body ) ? $request_body : '';
739 $request_body = base64_encode( $request_body ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
740
741 if ( empty( $request_body ) ) {
742 return;
743 }
744
745 // Send to middleware intersect endpoint.
746 wp_remote_post(
747 self::middle_ware_base_url() . 'payment/intersect',
748 [
749 'timeout' => 30,
750 'body' => $request_body,
751 'headers' => [
752 'Content-Type' => 'application/json',
753 ],
754 ]
755 );
756 }
757
758 /**
759 * Clear webhook data from settings for a specific mode.
760 *
761 * Removes webhook_secret, webhook_id, and webhook_url for the specified mode.
762 *
763 * @param string $mode The payment mode ('test' or 'live').
764 * @param array<string, mixed> $payment_settings The payment settings array.
765 * @since 2.0.0
766 * @return void
767 */
768 private static function clear_webhook_data( $mode, $payment_settings ) {
769 $updated_settings = $payment_settings;
770
771 if ( 'live' === $mode ) {
772 $updated_settings['webhook_live_secret'] = '';
773 $updated_settings['webhook_live_id'] = '';
774 $updated_settings['webhook_live_url'] = '';
775 } else {
776 $updated_settings['webhook_test_secret'] = '';
777 $updated_settings['webhook_test_id'] = '';
778 $updated_settings['webhook_test_url'] = '';
779 }
780
781 self::update_all_stripe_settings( $updated_settings );
782 }
783
784 /**
785 * Clean up amount to float.
786 *
787 * Removes commas, spaces, and ensures a numeric float value.
788 *
789 * @param float|string|int $amount Amount to clean up.
790 * @since 2.0.0
791 * @return float Clean float value.
792 */
793 private static function clean_amount( $amount ) {
794 if ( is_string( $amount ) ) {
795 $amount = str_replace( [ ',', ' ' ], '', $amount );
796 }
797 return is_numeric( $amount ) ? (float) $amount : 0.0;
798 }
799
800 /**
801 * Flattens a multidimensional array into a single-level array using Stripe's bracket notation.
802 *
803 * This is useful for preparing data to be sent to the Stripe API, which expects
804 * nested parameters to be formatted as key[subkey]=value.
805 *
806 * @param array<mixed> $data The multidimensional array to flatten.
807 * @param string $prefix (Optional) The prefix for nested keys. Default is an empty string.
808 * @since 2.0.0
809 * @return array<mixed> The flattened array with bracket notation keys.
810 */
811 private static function flatten_stripe_data( $data, $prefix = '' ) {
812 $result = [];
813
814 foreach ( $data as $key => $value ) {
815 $new_key = $prefix ? $prefix . '[' . $key . ']' : $key;
816
817 if ( is_array( $value ) ) {
818 $result = array_merge( $result, self::flatten_stripe_data( $value, $new_key ) );
819 } else {
820 $result[ $new_key ] = $value;
821 }
822 }
823
824 return $result;
825 }
826
827 /**
828 * Get the Licensing Instance.
829 *
830 * @since 2.0.0
831 * @return object|null The Licensing Instance.
832 */
833 private static function get_licensing_instance() {
834 if ( ! class_exists( 'SRFM_Pro\Admin\Licensing' ) ) {
835 return null;
836 }
837 return Licensing::get_instance();
838 }
839 }
840