PluginProbe ʕ •ᴥ•ʔ
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.12.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.12.1
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 0.0.2 0.0.3 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8 0.0.9 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.1.0 1.1.1 1.1.2 1.10.0 1.10.1 1.11.0 1.12.0 1.12.1 1.12.2 1.12.3 1.13.0 1.13.1 1.13.2 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.5.0 1.5.1 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.8.0 1.9.0 1.9.1 2.0.0 2.0.1 2.0.2 2.1.0 2.1.1 2.2.0 2.2.1 2.2.2 2.3.0 2.4.0 2.5.0 2.5.2 2.6.0
sureforms / inc / payments / stripe / stripe-helper.php
sureforms / inc / payments / stripe Last commit date
admin-stripe-handler.php 2 months ago payments-settings.php 5 months ago stripe-helper.php 2 months ago stripe-webhook.php 2 months ago
stripe-helper.php
826 lines
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 * @since 2.0.0
395 * @return string The SureForms Pro License Key.
396 */
397 public static function get_license_key() {
398 $licensing = self::get_licensing_instance();
399 if ( ! $licensing ||
400 ! method_exists( $licensing, 'licensing_setup' ) || ! method_exists( $licensing->licensing_setup(), 'settings' ) ) {
401 return '';
402 }
403 // Check if the SureForms Pro license is active.
404 $is_license_active = self::is_pro_license_active();
405 // If the license is active, get the license key.
406 $license_setup = $licensing->licensing_setup();
407 return ! empty( $is_license_active ) && is_object( $license_setup ) && method_exists( $license_setup, 'settings' ) ? $license_setup->settings()->license_key : '';
408 }
409
410 /**
411 * Check if the SureForms Pro license is active.
412 *
413 * @since 2.0.0
414 * @return bool|string True if the SureForms Pro license is active, false otherwise.
415 */
416 public static function is_pro_license_active() {
417 $licensing = self::get_licensing_instance();
418 if ( ! $licensing || ! method_exists( $licensing, 'is_license_active' )
419 ) {
420 return '';
421 }
422 // Check if the SureForms Pro license is active.
423 return $licensing->is_license_active();
424 }
425
426 /**
427 * Get the webhook URL for Stripe.
428 *
429 * Returns the dynamic webhook URL based on the site's REST API endpoint.
430 * Example: http://localhost:10008/wp-json/sureforms/webhook
431 *
432 * @param string $mode The payment mode ('test' or 'live'). Default is 'test'.
433 *
434 * @since 2.0.0
435 * @return string The webhook URL.
436 */
437 public static function get_webhook_url( $mode = 'test' ) {
438 return 'test' === $mode ? rest_url( 'sureforms/webhook_test' ) : rest_url( 'sureforms/webhook_live' );
439 }
440
441 /**
442 * Verify webhook connection with Stripe.
443 *
444 * Checks if the webhook endpoint exists and is enabled in Stripe
445 * based on the current payment mode. Uses static cache for same request.
446 *
447 * @param string|null $mode The payment mode ('test' or 'live'). If null, uses current mode.
448 * @since 2.0.0
449 * @return bool True if webhook is enabled, false otherwise.
450 */
451 public static function verify_webhook_connection( $mode = null ) {
452 // Get current payment mode.
453 $payment_mode = is_string( $mode ) && in_array( $mode, [ 'test', 'live' ], true ) ? $mode : self::get_stripe_mode();
454
455 // Check static cache first to avoid repeated API calls in same request.
456 $cache_key = 'webhook_' . $payment_mode;
457 if ( isset( self::$webhook_verification_cache[ $cache_key ] ) ) {
458 return self::$webhook_verification_cache[ $cache_key ];
459 }
460
461 // Get webhook settings.
462 $payment_settings = self::get_all_stripe_settings();
463
464 if ( ! is_array( $payment_settings ) ) {
465 self::$webhook_verification_cache[ $cache_key ] = false;
466 return false;
467 }
468
469 // Get webhook ID based on mode.
470 $webhook_id_key = 'webhook_' . $payment_mode . '_id';
471 $webhook_id = isset( $payment_settings[ $webhook_id_key ] ) && is_string( $payment_settings[ $webhook_id_key ] ) ? $payment_settings[ $webhook_id_key ] : '';
472
473 if ( empty( $webhook_id ) ) {
474 self::$webhook_verification_cache[ $cache_key ] = false;
475 return false;
476 }
477
478 // Make API request to verify webhook.
479 $response = self::stripe_api_request( 'webhook_endpoints', 'GET', [], $webhook_id, [ 'mode' => $payment_mode ] );
480
481 // If API call failed (webhook not found, deleted, or error), clear webhook data.
482 if ( ! $response['success'] ) {
483 self::clear_webhook_data( $payment_mode, $payment_settings );
484 self::$webhook_verification_cache[ $cache_key ] = false;
485 return false;
486 }
487
488 // Check webhook status and mode match.
489 $webhook_data = $response['data'];
490 $is_enabled = isset( $webhook_data['status'] ) && 'enabled' === $webhook_data['status'];
491
492 // Verify the livemode matches the current mode.
493 $webhook_livemode = isset( $webhook_data['livemode'] ) && is_bool( $webhook_data['livemode'] ) ? $webhook_data['livemode'] : false;
494 $expected_livemode = 'live' === $payment_mode;
495 $mode_matches = $webhook_livemode === $expected_livemode;
496
497 // Webhook is connected only if enabled and mode matches.
498 $is_connected = $is_enabled && $mode_matches;
499
500 // If webhook is not connected, clear the webhook data from settings.
501 if ( ! $is_connected ) {
502 self::clear_webhook_data( $payment_mode, $payment_settings );
503 }
504
505 // Cache result for this request.
506 self::$webhook_verification_cache[ $cache_key ] = $is_connected;
507
508 return $is_connected;
509 }
510
511 /**
512 * Check if any transaction is present in the payments table.
513 *
514 * @since 2.0.0
515 * @return bool True if at least one transaction exists, false otherwise.
516 */
517 public static function is_transaction_present() {
518 global $wpdb;
519
520 // Get payments table name.
521 $payments_table = Payments::get_instance()->get_tablename();
522
523 if ( empty( $payments_table ) || ! is_string( $payments_table ) ) {
524 return false;
525 }
526
527 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
528 // 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().
529 $count = $wpdb->get_var(
530 "SELECT COUNT(*) FROM {$payments_table} LIMIT 1"
531 );
532 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
533
534 return ! empty( $count ) && absint( $count ) > 0;
535 }
536
537 /**
538 * Get all Stripe settings from srfm_options.
539 *
540 * Retrieves the complete Stripe settings array from the nested structure:
541 * srfm_options -> payment_settings -> stripe
542 *
543 * @since 2.0.0
544 * @return array<string, mixed> The Stripe settings array, or default settings if not found.
545 */
546 public static function get_all_stripe_settings() {
547 $stripe_settings = Payment_Helper::get_gateway_settings( 'stripe' );
548
549 // Return default settings if empty.
550 return ! empty( $stripe_settings ) ? $stripe_settings : self::get_default_stripe_settings();
551 }
552
553 /**
554 * Update all Stripe settings in srfm_options.
555 *
556 * Stores the complete Stripe settings array in the nested structure:
557 * srfm_options -> payment_settings -> stripe
558 *
559 * @param array<string, mixed> $settings The Stripe settings array to save.
560 * @since 2.0.0
561 * @return bool True on success, false on failure.
562 */
563 public static function update_all_stripe_settings( $settings ) {
564 if ( ! is_array( $settings ) ) {
565 return false;
566 }
567
568 return Payment_Helper::update_gateway_settings( 'stripe', $settings );
569 }
570
571 /**
572 * Get a specific Stripe setting value by key.
573 *
574 * @param string $key The setting key to retrieve.
575 * @param mixed $default The default value to return if key doesn't exist.
576 * @since 2.0.0
577 * @return mixed The setting value or default if not found.
578 */
579 public static function get_stripe_setting( $key, $default = '' ) {
580 if ( ! is_string( $key ) || empty( $key ) ) {
581 return $default;
582 }
583
584 $settings = self::get_all_stripe_settings();
585
586 return $settings[ $key ] ?? $default;
587 }
588
589 /**
590 * Update a specific Stripe setting value by key.
591 *
592 * @param string $key The setting key to update.
593 * @param mixed $value The value to set.
594 * @since 2.0.0
595 * @return bool True on success, false on failure.
596 */
597 public static function update_stripe_setting( $key, $value ) {
598 if ( ! is_string( $key ) || empty( $key ) ) {
599 return false;
600 }
601
602 $settings = self::get_all_stripe_settings();
603 $settings[ $key ] = $value;
604
605 return self::update_all_stripe_settings( $settings );
606 }
607
608 /**
609 * Get default Stripe settings structure.
610 *
611 * Note: currency and payment_mode are now stored in global settings.
612 *
613 * @since 2.0.0
614 * @return array<string, mixed> Default Stripe settings array.
615 */
616 public static function get_default_stripe_settings() {
617 return [
618 'stripe_connected' => false,
619 'stripe_account_id' => '',
620 'stripe_account_email' => '',
621 'stripe_live_publishable_key' => '',
622 'stripe_live_secret_key' => '',
623 'stripe_test_publishable_key' => '',
624 'stripe_test_secret_key' => '',
625 'payment_mode' => 'test',
626 'webhook_test_secret' => '',
627 'webhook_test_url' => '',
628 'webhook_test_id' => '',
629 'webhook_live_secret' => '',
630 'webhook_live_url' => '',
631 'webhook_live_id' => '',
632 'account_name' => '',
633 ];
634 }
635
636 /**
637 * Get Stripe Connect URL
638 *
639 * @since 2.0.0
640 * @return \WP_REST_Response
641 */
642 public static function get_stripe_connect_url() {
643 // Stripe client ID from checkout-plugins-stripe-woo.
644 $client_id = 'ca_KOXfLe7jv1m4L0iC4KNEMc5fT8AXWWuL';
645
646 // Use the same redirect URI pattern as checkout-plugins-stripe-woo.
647 $redirect_url = admin_url( 'admin.php?page=sureforms_form_settings&tab=payments-settings&subpage=payment-methods&gateway=stripe' );
648 $nonce = wp_create_nonce( 'stripe-connect' );
649 $redirect_with_nonce = add_query_arg( 'srfm_stripe_connect_nonce', $nonce, $redirect_url );
650
651 // Store our own callback data.
652 set_transient( 'srfm_stripe_connect_nonce_' . get_current_user_id(), $nonce, HOUR_IN_SECONDS );
653
654 // Create state parameter exactly like checkout-plugins-stripe-woo.
655 $state_param = wp_json_encode(
656 [
657 'redirect' => $redirect_with_nonce,
658 ]
659 );
660 $state = '';
661 if ( is_string( $state_param ) ) {
662 $state = base64_encode( $state_param ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
663 }
664
665 $connect_url = add_query_arg(
666 [
667 'response_type' => 'code',
668 'client_id' => $client_id,
669 'stripe_landing' => 'login',
670 'always_prompt' => 'true',
671 'scope' => 'read_write',
672 'state' => $state,
673 ],
674 'https://connect.stripe.com/oauth/authorize'
675 );
676
677 return rest_ensure_response( [ 'url' => $connect_url ] );
678 }
679
680 /**
681 * Get the Stripe account ID.
682 *
683 * @since 2.5.1
684 * @return string The Stripe account ID.
685 */
686 public static function get_stripe_account_id() {
687 $account = self::get_stripe_setting( 'stripe_account_id' );
688 if ( empty( $account ) || ! is_string( $account ) ) {
689 return '';
690 }
691 return $account;
692 }
693
694 /**
695 * Send payment data to middleware intersect endpoint.
696 *
697 * @param string $charge_id Stripe charge ID (ch_xxx format).
698 * @param string $secret_key Stripe secret key.
699 * @param string $stripe_account_id Stripe account ID (optional).
700 * @param string $plugin_name Plugin name (default: 'SureForms').
701 * @since 2.5.1
702 * @return void
703 */
704 public static function intersect_payment( $charge_id, $secret_key = '', $stripe_account_id = '', $plugin_name = 'SureForms' ) {
705 // Validate charge ID format (must be ch_xxx).
706 if ( empty( $charge_id ) || ! preg_match( '/^ch_[a-zA-Z0-9]+$/', $charge_id ) ) {
707 return;
708 }
709
710 if ( empty( $secret_key ) ) {
711 return;
712 }
713
714 // Prepare request data.
715 $request_data = [
716 'plugin_name' => $plugin_name,
717 'secret_key' => $secret_key,
718 'transaction_id' => $charge_id,
719 'account_id' => $stripe_account_id,
720 ];
721
722 // Encode and send to middleware.
723 $request_body = wp_json_encode( $request_data );
724 $request_body = is_string( $request_body ) ? $request_body : '';
725 $request_body = base64_encode( $request_body ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
726
727 if ( empty( $request_body ) ) {
728 return;
729 }
730
731 // Send to middleware intersect endpoint.
732 wp_remote_post(
733 self::middle_ware_base_url() . 'payment/intersect',
734 [
735 'timeout' => 30,
736 'body' => $request_body,
737 'headers' => [
738 'Content-Type' => 'application/json',
739 ],
740 ]
741 );
742 }
743
744 /**
745 * Clear webhook data from settings for a specific mode.
746 *
747 * Removes webhook_secret, webhook_id, and webhook_url for the specified mode.
748 *
749 * @param string $mode The payment mode ('test' or 'live').
750 * @param array<string, mixed> $payment_settings The payment settings array.
751 * @since 2.0.0
752 * @return void
753 */
754 private static function clear_webhook_data( $mode, $payment_settings ) {
755 $updated_settings = $payment_settings;
756
757 if ( 'live' === $mode ) {
758 $updated_settings['webhook_live_secret'] = '';
759 $updated_settings['webhook_live_id'] = '';
760 $updated_settings['webhook_live_url'] = '';
761 } else {
762 $updated_settings['webhook_test_secret'] = '';
763 $updated_settings['webhook_test_id'] = '';
764 $updated_settings['webhook_test_url'] = '';
765 }
766
767 self::update_all_stripe_settings( $updated_settings );
768 }
769
770 /**
771 * Clean up amount to float.
772 *
773 * Removes commas, spaces, and ensures a numeric float value.
774 *
775 * @param float|string|int $amount Amount to clean up.
776 * @since 2.0.0
777 * @return float Clean float value.
778 */
779 private static function clean_amount( $amount ) {
780 if ( is_string( $amount ) ) {
781 $amount = str_replace( [ ',', ' ' ], '', $amount );
782 }
783 return is_numeric( $amount ) ? (float) $amount : 0.0;
784 }
785
786 /**
787 * Flattens a multidimensional array into a single-level array using Stripe's bracket notation.
788 *
789 * This is useful for preparing data to be sent to the Stripe API, which expects
790 * nested parameters to be formatted as key[subkey]=value.
791 *
792 * @param array<mixed> $data The multidimensional array to flatten.
793 * @param string $prefix (Optional) The prefix for nested keys. Default is an empty string.
794 * @since 2.0.0
795 * @return array<mixed> The flattened array with bracket notation keys.
796 */
797 private static function flatten_stripe_data( $data, $prefix = '' ) {
798 $result = [];
799
800 foreach ( $data as $key => $value ) {
801 $new_key = $prefix ? $prefix . '[' . $key . ']' : $key;
802
803 if ( is_array( $value ) ) {
804 $result = array_merge( $result, self::flatten_stripe_data( $value, $new_key ) );
805 } else {
806 $result[ $new_key ] = $value;
807 }
808 }
809
810 return $result;
811 }
812
813 /**
814 * Get the Licensing Instance.
815 *
816 * @since 2.0.0
817 * @return object|null The Licensing Instance.
818 */
819 private static function get_licensing_instance() {
820 if ( ! class_exists( 'SRFM_Pro\Admin\Licensing' ) ) {
821 return null;
822 }
823 return Licensing::get_instance();
824 }
825 }
826