PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / trunk
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More vtrunk
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 1.0.5 1.0.6 All 66 releases
better-payment / includes / Blocks / BlockActions.php

BlockActions.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More trunk, at includes/Blocks/BlockActions.php

938 lines 41.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Block Actions for Better Payment Gutenberg blocks.
4 *
5 * Handles payment processing for Gutenberg block forms, separate from Elementor widget actions.
6 * This class DOES NOT modify any existing Elementor methods - it provides block-specific handlers.
7 *
8 * @package Better_Payment
9 * @since 1.0.0
10 */
11
12 namespace Better_Payment\Lite\Blocks;
13
14 use Better_Payment\Lite\Classes\Handler;
15 use Better_Payment\Lite\Classes\PaymentRequestGuard;
16 use Better_Payment\Lite\Traits\Helper as TraitsHelper;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit;
20 }
21
22 /**
23 * Block Actions class.
24 *
25 * @since 1.0.0
26 */
27 class BlockActions {
28 use TraitsHelper;
29
30 /**
31 * Constructor.
32 *
33 * Register block-specific payment handlers with higher priority.
34 * These run BEFORE the Elementor widget handlers.
35 *
36 * @since 1.0.0
37 */
38 public function __construct() {
39 add_action( 'admin_post_paypal_form_handle', array( $this, 'block_paypal_form_handle' ) );
40 add_action( 'admin_post_nopriv_paypal_form_handle', array( $this, 'block_paypal_form_handle' ) );
41
42 add_action( 'wp_ajax_better_payment_stripe_get_token', array( $this, 'block_stripe_get_token' ) );
43 add_action( 'wp_ajax_nopriv_better_payment_stripe_get_token', array( $this, 'block_stripe_get_token' ) );
44
45 add_action( 'wp_ajax_better_payment_paystack_get_token', array( $this, 'block_paystack_get_token' ) );
46 add_action( 'wp_ajax_nopriv_better_payment_paystack_get_token', array( $this, 'block_paystack_get_token' ) );
47 }
48
49 /**
50 * Get block settings.
51 *
52 * First tries to get from transient (set during page render).
53 * If not found, parses the block directly from post content (works even with caching).
54 *
55 * @param int $page_id The page ID.
56 * @param string $widget_id The widget/block ID.
57 * @return array|false The block settings or false if not found.
58 */
59 private function get_block_settings( $page_id, $widget_id ) {
60 // page_id is the visitor's to choose. A form on a page they cannot see — draft,
61 // private, scheduled, password-protected — must be neither payable nor probeable,
62 // and that holds for the cached transient as much as for the post content.
63 if ( ! $this->is_payable_form_page( $page_id ) ) {
64 return false;
65 }
66
67 // First, try to get from transient (fastest).
68 $transient_key = 'bp_block_settings_' . $page_id . '_' . $widget_id;
69 $settings = get_transient( $transient_key );
70
71 if ( ! empty( $settings ) && is_array( $settings ) ) {
72 return $settings;
73 }
74
75 // Transient not found - parse block from post content.
76 // This handles cases where the page is cached.
77 $settings = $this->parse_block_settings_from_post( $page_id, $widget_id );
78
79 if ( ! empty( $settings ) && is_array( $settings ) ) {
80 // Cache in transient for future requests.
81 set_transient( $transient_key, $settings, HOUR_IN_SECONDS );
82 return $settings;
83 }
84
85 return false;
86 }
87
88 /**
89 * Parse block settings directly from post content.
90 *
91 * This method finds the Better Payment block in the post content and extracts its attributes.
92 * Similar to how Elementor's get_elementor_widget_settings works.
93 *
94 * @param int $page_id The page ID.
95 * @param string $widget_id The widget/block ID (blockId attribute).
96 * @return array|false The block settings or false if not found.
97 */
98 private function parse_block_settings_from_post( $page_id, $widget_id ) {
99 $post = get_post( $page_id );
100
101 if ( ! $post || empty( $post->post_content ) ) {
102 return false;
103 }
104
105 // Parse blocks from post content.
106 $blocks = parse_blocks( $post->post_content );
107
108 // Find the Better Payment block with matching blockId.
109 $block_attributes = $this->find_payment_block_recursive( $blocks, $widget_id );
110
111 if ( empty( $block_attributes ) ) {
112 return false;
113 }
114
115 // Get global settings.
116 $global_settings = \Better_Payment\Lite\Admin\DB::get_settings();
117
118 // Build settings using BlockManager's method.
119 $form_layout = ! empty( $block_attributes['formLayout'] ) ? $block_attributes['formLayout'] : 'layout-1';
120 $settings = BlockManager::get_instance()->build_block_settings( $block_attributes, $global_settings, $form_layout );
121
122 return $settings;
123 }
124
125 /**
126 * Recursively find a Better Payment block with matching blockId.
127 *
128 * @param array $blocks Array of parsed blocks.
129 * @param string $widget_id The blockId to find.
130 * @return array|false Block attributes or false if not found.
131 */
132 private function find_payment_block_recursive( $blocks, $widget_id ) {
133 foreach ( $blocks as $block ) {
134 // Check if this is a Better Payment block with matching blockId.
135 if ( 'better-payment/payment-form' === $block['blockName'] ) {
136 $block_id = isset( $block['attrs']['blockId'] ) ? $block['attrs']['blockId'] : '';
137
138 if ( $block_id === $widget_id ) {
139 return $block['attrs'];
140 }
141 }
142
143 // Check inner blocks.
144 if ( ! empty( $block['innerBlocks'] ) ) {
145 $found = $this->find_payment_block_recursive( $block['innerBlocks'], $widget_id );
146 if ( $found ) {
147 return $found;
148 }
149 }
150 }
151
152 return false;
153 }
154
155 /**
156 * Check if this is a block form submission.
157 *
158 * @param int $page_id The page ID.
159 * @param string $widget_id The widget/block ID.
160 * @return bool True if this is a block form, false otherwise.
161 */
162 private function is_block_form( $page_id, $widget_id ) {
163 $settings = $this->get_block_settings( $page_id, $widget_id );
164 return ! empty( $settings ) && is_array( $settings );
165 }
166
167 /**
168 * Handle PayPal form submission for Gutenberg blocks.
169 *
170 * This intercepts the form submission before the Elementor handler.
171 * If settings are found in transient (block form), process here.
172 * Otherwise, return and let the Elementor handler process it.
173 *
174 * @since 1.0.0
175 */
176 public function block_paypal_form_handle() {
177 // phpcs:disable WordPress.Security.NonceVerification.Missing
178 // Bail immediately if not a Gutenberg block submission — zero DB overhead for Elementor forms.
179 if ( empty( $_POST['better_payment_source'] ) || 'gutenberg' !== $_POST['better_payment_source'] ) {
180 return;
181 }
182 // phpcs:enable WordPress.Security.NonceVerification.Missing
183
184 // Verify nonce.
185 if ( ! check_admin_referer( 'better-payment-paypal', 'security' ) ) {
186 return;
187 }
188
189 // phpcs:disable WordPress.Security.NonceVerification.Missing
190 $page_id = ! empty( $_POST['better_payment_page_id'] ) ? intval( $_POST['better_payment_page_id'] ) : 0;
191 $widget_id = ! empty( $_POST['better_payment_widget_id'] ) ? sanitize_text_field( $_POST['better_payment_widget_id'] ) : '';
192 // phpcs:enable WordPress.Security.NonceVerification.Missing
193
194 if ( empty( $page_id ) || empty( $widget_id ) ) {
195 return; // Let the original handler deal with missing data.
196 }
197
198 // Check if this is a block form by looking for transient settings.
199 if ( ! $this->is_block_form( $page_id, $widget_id ) ) {
200 return; // Not a block form, let Elementor handler process it.
201 }
202
203 // This is a block form - handle the payment here.
204 $this->process_paypal_payment( $page_id, $widget_id );
205 }
206
207 /**
208 * Process PayPal payment for block forms.
209 *
210 * @param int $page_id The page ID.
211 * @param string $widget_id The widget/block ID.
212 */
213 private function process_paypal_payment( $page_id, $widget_id ) {
214 $el_settings = $this->get_block_settings( $page_id, $widget_id );
215
216 if ( empty( $el_settings ) ) {
217 $this->redirect_previous_page();
218 return;
219 }
220
221 if ( empty( $el_settings['better_payment_paypal_business_email'] ) ) {
222 $this->redirect_previous_page();
223 return;
224 }
225
226 if ( 'yes' === $el_settings['better_payment_paypal_live_mode'] ) {
227 $path = 'paypal';
228 } else {
229 $path = 'sandbox.paypal';
230 }
231
232 $el_settings_currency = $el_settings['better_payment_form_currency'];
233 $woo_product_id = ! empty( $el_settings['better_payment_form_woocommerce_product_id'] ) ? intval( $el_settings['better_payment_form_woocommerce_product_id'] ) : 0;
234 $woo_product_ids = ! empty( $el_settings['better_payment_form_woocommerce_product_ids'] ) ? $el_settings['better_payment_form_woocommerce_product_ids'] : array( 0 );
235 $fluentcart_product_id = ! empty( $el_settings['better_payment_form_fluentcart_product_id'] ) ? intval( $el_settings['better_payment_form_fluentcart_product_id'] ) : 0;
236 $fluentcart_product_ids = ! empty( $el_settings['better_payment_form_fluentcart_product_ids'] ) ? $el_settings['better_payment_form_fluentcart_product_ids'] : array( 0 );
237 $is_layout_6 = ! empty( $el_settings['better_payment_form_layout'] ) && 'layout-6-pro' === $el_settings['better_payment_form_layout'];
238 $is_fluentcart_layout = $is_layout_6 && ! empty( $el_settings['better_payment_form_layout_6_ecommerce_platform'] ) && 'fluentcart' === $el_settings['better_payment_form_layout_6_ecommerce_platform'];
239 $is_woo_layout = $is_layout_6 && ( empty( $el_settings['better_payment_form_layout_6_ecommerce_platform'] ) || 'woocommerce' === $el_settings['better_payment_form_layout_6_ecommerce_platform'] );
240
241 // phpcs:disable WordPress.Security.NonceVerification.Missing
242 // Amount, quantity, currency and campaign come from the stored settings, never
243 // from the request — see PaymentRequestGuard.
244 $guarded = ( new PaymentRequestGuard() )->resolve_form_payment(
245 $el_settings,
246 wp_unslash( $_POST ),
247 array(
248 'page_id' => $page_id,
249 'gateway' => 'paypal',
250 )
251 );
252
253 if ( is_wp_error( $guarded ) ) {
254 PaymentRequestGuard::reject_form_post( $guarded );
255 return;
256 }
257
258 $el_settings_currency = $guarded['currency'];
259 $el_settings_currency_symbol = $this->get_currency_symbol( esc_html( $el_settings_currency ) );
260
261 $primary_payment_amount = $guarded['amount'];
262 $primary_payment_amount_quantity = $guarded['quantity'];
263
264 $order_id = Handler::new_order_id( 'paypal' );
265 $paypal_button_type = ! empty( $el_settings['better_payment_paypal_button_type'] ) ? $el_settings['better_payment_paypal_button_type'] : '_xclick';
266 $site_url = get_permalink( $page_id );
267 $return_url = ! empty( $_POST['return'] ) ? wp_validate_redirect( esc_url_raw( $_POST['return'] ), $site_url ) : '';
268 $cancel_return_url = ! empty( $_POST['cancel_return'] ) ? wp_validate_redirect( esc_url_raw( $_POST['cancel_return'] ), $site_url ) : '';
269 $cancel_return_url = add_query_arg( 'better_payment_paypal_id', $order_id, $cancel_return_url );
270
271 $request_data = array(
272 'business' => $el_settings['better_payment_paypal_business_email'],
273 'currency_code' => $el_settings_currency,
274 'rm' => '2',
275 'return' => $return_url,
276 'cancel_return' => $cancel_return_url,
277 'item_number' => $order_id,
278 'item_name' => ! empty( $el_settings['better_payment_form_title'] ) ? esc_html__( $el_settings['better_payment_form_title'], 'better-payment' ) : esc_html__( 'Better Payment', 'better-payment' ),
279 'amount' => $primary_payment_amount,
280 'cmd' => $paypal_button_type,
281 'notify_url' => admin_url( 'admin-post.php?action=better_payment_paypal_ipn' ),
282 );
283
284 $product_ids = array(
285 'woo_product_ids' => $woo_product_ids,
286 'fluentcart_product_ids' => $fluentcart_product_ids,
287 );
288
289 $detailed_product_info = $this->get_detailed_product_info( $product_ids );
290
291 // Form fields data to send via email.
292 $better_form_fields = array(
293 'amount' => sanitize_text_field( $el_settings_currency_symbol ) . $primary_payment_amount,
294 'referer_page_id' => $page_id,
295 'referer_widget_id' => $widget_id,
296 'woo_product_id' => $woo_product_id,
297 'woo_product_ids' => maybe_serialize( $woo_product_ids ),
298 'fluentcart_product_id' => $fluentcart_product_id,
299 'fluentcart_product_ids' => maybe_serialize( $fluentcart_product_ids ),
300 'source' => 'paypal',
301 'amount_quantity' => ! empty( $primary_payment_amount_quantity ) ? intval( $primary_payment_amount_quantity ) : '',
302 'is_woo_layout' => $is_woo_layout,
303 'is_fluentcart_layout' => $is_fluentcart_layout,
304 'detailed_product_info' => maybe_serialize( $detailed_product_info ),
305 'paypal_business_email' => sanitize_email( $el_settings['better_payment_paypal_business_email'] ),
306 // Persisted so the async PayPal IPN can rebuild the e-mail body + on/off toggle.
307 'email_settings' => Handler::extract_email_settings( $el_settings ),
308 );
309
310 $better_form_fields = array_merge( $better_form_fields, $this->fetch_better_form_fields( $el_settings, $_POST ) );
311
312 if ( ! empty( $better_form_fields['primary_first_name'] ) ) {
313 $request_data['primary_first_name'] = sanitize_text_field( $better_form_fields['primary_first_name'] );
314 }
315
316 if ( ! empty( $better_form_fields['primary_last_name'] ) ) {
317 $request_data['primary_last_name'] = sanitize_text_field( $better_form_fields['primary_last_name'] );
318 }
319
320 if ( ! empty( $better_form_fields['primary_email'] ) ) {
321 $request_data['primary_email'] = sanitize_email( $better_form_fields['primary_email'] );
322 }
323
324 if ( ! empty( $better_form_fields['primary_reference_number'] ) ) {
325 $request_data['invoice'] = sanitize_text_field( $better_form_fields['primary_reference_number'] );
326 }
327
328 $campaign_id = $guarded['campaign_id'];
329 // phpcs:enable WordPress.Security.NonceVerification.Missing
330
331 Handler::payment_create(
332 array(
333 'amount' => floatval( $primary_payment_amount ),
334 'order_id' => $order_id,
335 'payment_date' => gmdate( 'Y-m-d H:i:s' ),
336 'source' => 'paypal',
337 'form_fields_info' => maybe_serialize( $better_form_fields ),
338 'currency' => sanitize_text_field( $el_settings_currency ),
339 'referer' => 'gutenberg-block',
340 'campaign_id' => $campaign_id,
341 )
342 );
343
344 $paypal_url = "https://www.$path.com/cgi-bin/webscr?";
345 $paypal_url .= http_build_query( $request_data );
346
347 wp_redirect( esc_url_raw( $paypal_url ) );
348 exit;
349 }
350
351 /**
352 * Handle Stripe token request for Gutenberg blocks.
353 *
354 * @since 1.0.0
355 */
356 public function block_stripe_get_token() {
357 // phpcs:disable WordPress.Security.NonceVerification.Missing
358 // Bail immediately if not a Gutenberg block submission — zero DB overhead for Elementor forms.
359 if ( empty( $_POST['setting_data']['source'] ) || 'gutenberg' !== $_POST['setting_data']['source'] ) {
360 return;
361 }
362 // phpcs:enable WordPress.Security.NonceVerification.Missing
363
364 // Verify nonce.
365 if ( ! check_admin_referer( 'better-payment', 'security' ) ) {
366 wp_send_json_error( esc_html__( 'Nonce verification failed.', 'better-payment' ) );
367 return;
368 }
369
370 // phpcs:disable WordPress.Security.NonceVerification.Missing
371 $page_id = isset( $_POST['setting_data']['page_id'] ) ? intval( $_POST['setting_data']['page_id'] ) : 0;
372 $widget_id = isset( $_POST['setting_data']['widget_id'] ) ? sanitize_text_field( $_POST['setting_data']['widget_id'] ) : '';
373 // phpcs:enable WordPress.Security.NonceVerification.Missing
374
375 if ( empty( $page_id ) || empty( $widget_id ) ) {
376 return; // Let the original handler deal with missing data.
377 }
378
379 // Check if this is a block form by looking for settings.
380 if ( ! $this->is_block_form( $page_id, $widget_id ) ) {
381 return; // Not a block form, let Elementor handler process it.
382 }
383
384 // This is a block form - handle the Stripe payment here.
385 $this->process_stripe_payment( $page_id, $widget_id );
386 }
387
388 /**
389 * Handle Paystack token request for Gutenberg blocks.
390 *
391 * @since 1.0.0
392 */
393 public function block_paystack_get_token() {
394 // phpcs:disable WordPress.Security.NonceVerification.Missing
395 // Bail immediately if not a Gutenberg block submission — zero DB overhead for Elementor forms.
396 if ( empty( $_POST['setting_data']['source'] ) || 'gutenberg' !== $_POST['setting_data']['source'] ) {
397 return;
398 }
399 // phpcs:enable WordPress.Security.NonceVerification.Missing
400
401 // Verify nonce.
402 if ( ! check_admin_referer( 'better-payment', 'security' ) ) {
403 return;
404 }
405
406 // phpcs:disable WordPress.Security.NonceVerification.Missing
407 $page_id = isset( $_POST['setting_data']['page_id'] ) ? intval( $_POST['setting_data']['page_id'] ) : 0;
408 $widget_id = isset( $_POST['setting_data']['widget_id'] ) ? sanitize_text_field( $_POST['setting_data']['widget_id'] ) : '';
409 // phpcs:enable WordPress.Security.NonceVerification.Missing
410
411 if ( empty( $page_id ) || empty( $widget_id ) ) {
412 return; // Let the original handler deal with missing data.
413 }
414
415 // Check if this is a block form by looking for transient settings.
416 if ( ! $this->is_block_form( $page_id, $widget_id ) ) {
417 return; // Not a block form, let Elementor handler process it.
418 }
419
420 // This is a block form - handle the Paystack payment here.
421 $this->process_paystack_payment( $page_id, $widget_id );
422 }
423
424 /**
425 * Process Stripe payment for block forms.
426 *
427 * @param int $page_id The page ID.
428 * @param string $widget_id The widget/block ID.
429 */
430 private function process_stripe_payment( $page_id, $widget_id ) {
431 $el_settings = $this->get_block_settings( $page_id, $widget_id );
432
433 if ( empty( $el_settings ) ) {
434 wp_send_json_error( esc_html__( 'Setting Data is missing', 'better-payment' ) );
435 }
436
437 $better_payment_keys = array(
438 'public_key' => 'yes' === sanitize_text_field( $el_settings['better_payment_stripe_live_mode'] ) ? sanitize_text_field( $el_settings['better_payment_stripe_public_key_live'] ) : sanitize_text_field( $el_settings['better_payment_stripe_public_key'] ),
439 'secret_key' => 'yes' === sanitize_text_field( $el_settings['better_payment_stripe_live_mode'] ) ? sanitize_text_field( $el_settings['better_payment_stripe_secret_key_live'] ) : sanitize_text_field( $el_settings['better_payment_stripe_secret_key'] ),
440 );
441
442 if ( empty( $better_payment_keys['public_key'] ) || empty( $better_payment_keys['secret_key'] ) ) {
443 wp_send_json_error( esc_html__( 'Stripe Key missing', 'better-payment' ) );
444 }
445
446 // phpcs:disable WordPress.Security.NonceVerification.Missing
447 // Amount, quantity, currency and campaign come from the stored settings, never
448 // from the request — see PaymentRequestGuard.
449 $guarded = ( new PaymentRequestGuard() )->resolve_form_payment(
450 $el_settings,
451 isset( $_POST['fields'] ) && is_array( $_POST['fields'] ) ? wp_unslash( $_POST['fields'] ) : array(),
452 array(
453 'page_id' => $page_id,
454 'gateway' => 'stripe',
455 )
456 );
457
458 if ( is_wp_error( $guarded ) ) {
459 wp_send_json_error( esc_html( $guarded->get_error_message() ) );
460 return;
461 }
462
463 $amount = $guarded['amount'];
464 $amount_quantity = $guarded['quantity'];
465
466 $header_info = array(
467 'Authorization' => 'Basic ' . base64_encode( sanitize_text_field( $better_payment_keys['secret_key'] ) . ':' ),
468 'Stripe-Version' => '2019-05-16',
469 );
470
471 $order_id = Handler::new_order_id( 'stripe' );
472 $el_settings_currency = $guarded['currency'];
473
474 $el_settings_currency_symbol = $this->get_currency_symbol( esc_html( $el_settings_currency ) );
475
476 $redirection_url_success = get_permalink( $page_id );
477 $redirection_url_error = get_permalink( $page_id );
478
479 $redirection_url_success = add_query_arg(
480 array(
481 'better_payment_stripe_status' => 'success',
482 'better_payment_widget_id' => $widget_id,
483 ),
484 $redirection_url_success
485 );
486
487 $redirection_url_error = add_query_arg(
488 array(
489 'better_payment_error_status' => 'error',
490 'better_payment_widget_id' => $widget_id,
491 ),
492 $redirection_url_error
493 );
494
495 // Build form fields.
496 $woo_product_id = ! empty( $el_settings['better_payment_form_woocommerce_product_id'] ) ? intval( $el_settings['better_payment_form_woocommerce_product_id'] ) : 0;
497 $woo_product_ids = ! empty( $el_settings['better_payment_form_woocommerce_product_ids'] ) ? $el_settings['better_payment_form_woocommerce_product_ids'] : array( 0 );
498 $fluentcart_product_id = ! empty( $el_settings['better_payment_form_fluentcart_product_id'] ) ? intval( $el_settings['better_payment_form_fluentcart_product_id'] ) : 0;
499 $fluentcart_product_ids = ! empty( $el_settings['better_payment_form_fluentcart_product_ids'] ) ? $el_settings['better_payment_form_fluentcart_product_ids'] : array( 0 );
500
501 $product_ids = array(
502 'woo_product_ids' => $woo_product_ids,
503 'fluentcart_product_ids' => $fluentcart_product_ids,
504 );
505
506 $detailed_product_info = $this->get_detailed_product_info( $product_ids );
507
508 $better_form_fields = array(
509 'amount' => sanitize_text_field( $el_settings_currency_symbol ) . $amount,
510 'referer_page_id' => $page_id,
511 'referer_widget_id' => $widget_id,
512 'woo_product_id' => $woo_product_id,
513 'woo_product_ids' => maybe_serialize( $woo_product_ids ),
514 'fluentcart_product_id' => $fluentcart_product_id,
515 'fluentcart_product_ids' => maybe_serialize( $fluentcart_product_ids ),
516 'source' => 'stripe',
517 'amount_quantity' => ! empty( $amount_quantity ) ? intval( $amount_quantity ) : '',
518 'detailed_product_info' => maybe_serialize( $detailed_product_info ),
519 );
520
521 $better_form_fields = array_merge( $better_form_fields, $this->fetch_better_form_fields( $el_settings, $_POST['fields'] ) );
522
523 $item_name = ! empty( $el_settings['better_payment_form_title'] ) ? esc_html__( $el_settings['better_payment_form_title'], 'better-payment' ) : esc_html__( 'Better Payment', 'better-payment' );
524
525 // The Stripe Price ID (when Payment Source = Stripe) is display-only —
526 // it pre-fills the amount input on the frontend (see layout-1/2/3.php line input value).
527 // The payment session always uses price_data with the submitted amount,
528 // matching the Elementor widget behaviour for layouts 1/2/3.
529 $line_item = array(
530 'price_data' => array(
531 'currency' => sanitize_text_field( $el_settings_currency ),
532 'unit_amount' => intval( $amount * 100 ),
533 'product_data' => array(
534 'name' => $item_name,
535 ),
536 ),
537 'quantity' => 1,
538 );
539
540 $request_body = array(
541 'line_items' => array( $line_item ),
542 'mode' => 'payment',
543 'locale' => 'auto',
544 'payment_method_types' => array( 'card' ),
545 'billing_address_collection' => 'required',
546 'client_reference_id' => time(),
547 'metadata' => array(
548 'order_id' => $order_id,
549 ),
550 'success_url' => esc_url_raw( $redirection_url_success ) . '&better_payment_stripe_id=' . $order_id,
551 'cancel_url' => add_query_arg(
552 array(
553 'better_payment_stripe_id' => $order_id,
554 ),
555 esc_url_raw( $redirection_url_error )
556 ),
557 );
558
559 $request_body['payment_intent_data'] = array(
560 'capture_method' => 'automatic',
561 'description' => $item_name,
562 'metadata' => array(
563 'order_id' => $order_id,
564 ),
565 );
566
567 $primary_email = ! empty( $better_form_fields['primary_email'] ) ? sanitize_email( $better_form_fields['primary_email'] ) : '';
568
569 if ( ! empty( $primary_email ) ) {
570 $request_body['customer_email'] = $primary_email;
571 $request_body['metadata']['customer_email'] = $primary_email;
572 $request_body['payment_intent_data']['metadata']['customer_email'] = $primary_email;
573 }
574
575 // Build customer name from first/last name fields (mirrors widget behaviour).
576 $customer_name = '';
577 if ( ! empty( $better_form_fields['primary_first_name'] ) ) {
578 $customer_name = sanitize_text_field( $better_form_fields['primary_first_name'] );
579 }
580 if ( ! empty( $better_form_fields['primary_last_name'] ) ) {
581 $customer_name = trim( $customer_name . ' ' . sanitize_text_field( $better_form_fields['primary_last_name'] ) );
582 }
583
584 if ( ! empty( $customer_name ) ) {
585 $request_body['metadata']['customer_name'] = $customer_name;
586 $request_body['payment_intent_data']['metadata']['customer_name'] = $customer_name;
587 }
588
589 $request = wp_remote_post(
590 'https://api.stripe.com/v1/checkout/sessions',
591 array(
592 'headers' => $header_info,
593 'body' => $request_body,
594 )
595 );
596
597 if ( is_wp_error( $request ) ) {
598 wp_send_json_error( sanitize_text_field( $request->get_error_message() ) );
599 return;
600 }
601
602 $response_ar = json_decode( wp_remote_retrieve_body( $request ) );
603
604 if ( null === $response_ar ) {
605 wp_send_json_error( esc_html__( 'Invalid response from Stripe.', 'better-payment' ) );
606 return;
607 }
608
609 if ( ! empty( $response_ar->payment_intent ) || ( ! empty( $response_ar->mode ) && 'subscription' === $response_ar->mode ) ) {
610 $campaign_id = $guarded['campaign_id'];
611
612 Handler::payment_create(
613 array(
614 'amount' => floatval( $amount ),
615 'order_id' => $order_id,
616 'payment_date' => gmdate( 'Y-m-d H:i:s' ),
617 'source' => 'stripe',
618 'transaction_id' => sanitize_text_field( $response_ar->payment_intent ),
619 'customer_info' => maybe_serialize( $response_ar ),
620 'form_fields_info' => maybe_serialize( $better_form_fields ),
621 'obj_id' => sanitize_text_field( $response_ar->id ),
622 'status' => sanitize_text_field( $response_ar->payment_status ),
623 'currency' => sanitize_text_field( $el_settings_currency ),
624 'referer' => 'gutenberg-block',
625 'campaign_id' => $campaign_id,
626 )
627 );
628
629 wp_send_json_success(
630 array(
631 'stripe_data' => sanitize_text_field( $response_ar->id ),
632 'stripe_public_key' => sanitize_text_field( $better_payment_keys['public_key'] ),
633 )
634 );
635 } else {
636 $error_message = 'Something went wrong!';
637
638 if ( isset( $response_ar->error ) ) {
639 $error_message = sanitize_text_field( $response_ar->error->message );
640 }
641
642 wp_send_json_error( $error_message );
643 }
644 // phpcs:enable WordPress.Security.NonceVerification.Missing
645 }
646
647 /**
648 * Process Paystack payment for block forms.
649 *
650 * @param int $page_id The page ID.
651 * @param string $widget_id The widget/block ID.
652 */
653 private function process_paystack_payment( $page_id, $widget_id ) {
654 $el_settings = $this->get_block_settings( $page_id, $widget_id );
655
656 if ( empty( $el_settings ) ) {
657 wp_send_json_error( esc_html__( 'Setting Data is missing', 'better-payment' ) );
658 }
659
660 if ( empty( $el_settings['better_payment_paystack_public_key'] ) || empty( $el_settings['better_payment_paystack_secret_key'] ) ) {
661 wp_send_json_error( esc_html__( 'Paystack Key missing', 'better-payment' ) );
662 }
663
664 // phpcs:disable WordPress.Security.NonceVerification.Missing
665 // Amount, quantity, currency and campaign come from the stored settings, never
666 // from the request — see PaymentRequestGuard.
667 $guarded = ( new PaymentRequestGuard() )->resolve_form_payment(
668 $el_settings,
669 isset( $_POST['fields'] ) && is_array( $_POST['fields'] ) ? wp_unslash( $_POST['fields'] ) : array(),
670 array(
671 'page_id' => $page_id,
672 'gateway' => 'paystack',
673 )
674 );
675
676 if ( is_wp_error( $guarded ) ) {
677 wp_send_json_error( esc_html( $guarded->get_error_message() ) );
678 return;
679 }
680
681 $amount = $guarded['amount'];
682 $amount_quantity = $guarded['quantity'];
683
684 $header_info = array(
685 'Authorization' => 'Bearer ' . sanitize_text_field( $el_settings['better_payment_paystack_secret_key'] ),
686 'Cache-Control: no-cache',
687 );
688
689 $order_id = Handler::new_order_id( 'paystack' );
690 $el_settings_currency = $guarded['currency'];
691
692 $el_settings_currency_symbol = $this->get_currency_symbol( esc_html( $el_settings_currency ) );
693
694 $redirection_url_success = get_permalink( $page_id );
695 $redirection_url_error = get_permalink( $page_id );
696
697 $redirection_url_success = add_query_arg(
698 array(
699 'better_payment_paystack_status' => 'success',
700 'better_payment_widget_id' => $widget_id,
701 ),
702 $redirection_url_success
703 );
704
705 $redirection_url_error = add_query_arg(
706 array(
707 'better_payment_error_status' => 'error',
708 'better_payment_widget_id' => $widget_id,
709 ),
710 $redirection_url_error
711 );
712
713 // Build form fields.
714 $woo_product_id = ! empty( $el_settings['better_payment_form_woocommerce_product_id'] ) ? intval( $el_settings['better_payment_form_woocommerce_product_id'] ) : 0;
715 $woo_product_ids = ! empty( $el_settings['better_payment_form_woocommerce_product_ids'] ) ? $el_settings['better_payment_form_woocommerce_product_ids'] : array( 0 );
716 $fluentcart_product_id = ! empty( $el_settings['better_payment_form_fluentcart_product_id'] ) ? intval( $el_settings['better_payment_form_fluentcart_product_id'] ) : 0;
717 $fluentcart_product_ids = ! empty( $el_settings['better_payment_form_fluentcart_product_ids'] ) ? $el_settings['better_payment_form_fluentcart_product_ids'] : array( 0 );
718
719 $product_ids = array(
720 'woo_product_ids' => $woo_product_ids,
721 'fluentcart_product_ids' => $fluentcart_product_ids,
722 );
723
724 $detailed_product_info = $this->get_detailed_product_info( $product_ids );
725
726 $better_form_fields = array(
727 'amount' => sanitize_text_field( $el_settings_currency_symbol ) . $amount,
728 'referer_page_id' => $page_id,
729 'referer_widget_id' => $widget_id,
730 'woo_product_id' => $woo_product_id,
731 'woo_product_ids' => maybe_serialize( $woo_product_ids ),
732 'fluentcart_product_id' => $fluentcart_product_id,
733 'fluentcart_product_ids' => maybe_serialize( $fluentcart_product_ids ),
734 'source' => 'paystack',
735 'amount_quantity' => ! empty( $amount_quantity ) ? intval( $amount_quantity ) : '',
736 'detailed_product_info' => maybe_serialize( $detailed_product_info ),
737 );
738
739 $better_form_fields = array_merge( $better_form_fields, $this->fetch_better_form_fields( $el_settings, $_POST['fields'] ) );
740
741 $primary_email = ! empty( $better_form_fields['primary_email'] ) ? sanitize_email( $better_form_fields['primary_email'] ) : '';
742
743 $request_body = array(
744 'amount' => intval( $amount * 100 ),
745 'currency' => sanitize_text_field( $el_settings_currency ),
746 'email' => $primary_email,
747 'callback_url' => esc_url_raw( $redirection_url_success ) . '&better_payment_paystack_id=' . $order_id,
748 'metadata' => array(
749 'cancel_action' => add_query_arg(
750 array(
751 'better_payment_paystack_id' => $order_id,
752 ),
753 esc_url_raw( $redirection_url_error )
754 ),
755 ),
756 );
757
758 $request = wp_remote_post(
759 'https://api.paystack.co/transaction/initialize',
760 array(
761 'headers' => $header_info,
762 'body' => $request_body,
763 )
764 );
765
766 if ( is_wp_error( $request ) ) {
767 wp_send_json_error( sanitize_text_field( $request->get_error_message() ) );
768 return;
769 }
770
771 $response_ar = json_decode( wp_remote_retrieve_body( $request ) );
772
773 if ( null === $response_ar ) {
774 wp_send_json_error( esc_html__( 'Invalid response from Paystack.', 'better-payment' ) );
775 return;
776 }
777
778 if ( empty( $response_ar->status ) || empty( $response_ar->data ) ) {
779 $error_message = ! empty( $response_ar->message ) ? sanitize_text_field( $response_ar->message ) : 'Something went wrong!';
780
781 if ( isset( $response_ar->error ) ) {
782 $error_message = sanitize_text_field( $response_ar->error->message );
783 }
784
785 wp_send_json_error( $error_message );
786 }
787
788 $campaign_id = $guarded['campaign_id'];
789
790 Handler::payment_create(
791 array(
792 'amount' => floatval( $amount ),
793 'order_id' => $order_id,
794 'payment_date' => gmdate( 'Y-m-d H:i:s' ),
795 'source' => 'paystack',
796 // Paystack's reference, so verification can bind it to this order.
797 'transaction_id' => ! empty( $response_ar->data->reference ) ? sanitize_text_field( $response_ar->data->reference ) : '',
798 'customer_info' => maybe_serialize( $response_ar ),
799 'form_fields_info' => maybe_serialize( $better_form_fields ),
800 'status' => 'unpaid',
801 'currency' => sanitize_text_field( $el_settings_currency ),
802 'referer' => 'gutenberg-block',
803 'campaign_id' => $campaign_id,
804 )
805 );
806
807 $authorization_url = ! empty( $response_ar->data->authorization_url ) ? esc_url_raw( $response_ar->data->authorization_url ) : '';
808
809 wp_send_json_success(
810 array(
811 'authorization_url' => $authorization_url,
812 )
813 );
814 // phpcs:enable WordPress.Security.NonceVerification.Missing
815 }
816
817 /**
818 * Redirect to referer page.
819 *
820 * @since 1.0.0
821 */
822 public function redirect_previous_page() {
823 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
824 $location = isset( $_SERVER['HTTP_REFERER'] ) ? wp_unslash( $_SERVER['HTTP_REFERER'] ) : home_url();
825 wp_safe_redirect( esc_url_raw( $location ) );
826 exit();
827 }
828
829 /**
830 * Get detailed product information.
831 *
832 * @param array $product_ids Array of product IDs.
833 * @param array $product_quantities Array of product quantities.
834 * @return array Detailed product information.
835 */
836 public function get_detailed_product_info( $product_ids, $product_quantities = array() ) {
837 $detailed_product_info = array();
838
839 if ( function_exists( 'wc_get_product' ) && ! empty( $product_ids['woo_product_ids'] ) ) {
840 foreach ( $product_ids['woo_product_ids'] as $key => $product_id ) {
841 if ( empty( $product_id ) ) {
842 continue;
843 }
844
845 $product = wc_get_product( $product_id );
846 if ( $product ) {
847 $quantity = isset( $product_quantities[ $key ] ) ? intval( $product_quantities[ $key ] ) : 1;
848 $price = floatval( $product->get_price() );
849 $total_price = $price * $quantity;
850
851 $detailed_product_info['woo_products'][ $product_id ] = array(
852 'name' => sanitize_text_field( $product->get_name() ),
853 'product_id' => intval( $product_id ),
854 'permalink' => esc_url( $product->get_permalink() ),
855 'image_src' => esc_url( wp_get_attachment_url( (int) $product->get_image_id() ) ),
856 'price' => $price,
857 'quantity' => $quantity,
858 'total_price' => $total_price,
859 );
860 }
861 }
862 }
863
864 return $detailed_product_info;
865 }
866
867 /**
868 * Fetch form fields from POST data.
869 *
870 * @param array $el_settings Widget/block settings.
871 * @param array $post_data_form_fields POST data.
872 * @return array Form fields data.
873 */
874 public function fetch_better_form_fields( $el_settings, $post_data_form_fields ) {
875 $better_form_fields = array();
876
877 $post_data_primary_first_name = '';
878 $post_data_primary_last_name = '';
879 $post_data_primary_email = '';
880
881 $post_fields = $post_data_form_fields;
882
883 $layout = ! empty( $el_settings['better_payment_form_layout'] ) ? sanitize_text_field( $el_settings['better_payment_form_layout'] ) : 'layout-1';
884
885 // Handle different layouts.
886 switch ( $layout ) {
887 case 'layout-4-pro':
888 $el_settings['better_payment_form_fields'] = isset( $el_settings['better_payment_form_fields_layout_4_5_6'] ) ? $el_settings['better_payment_form_fields_layout_4_5_6'] : array();
889 break;
890
891 case 'layout-5-pro':
892 $el_settings['better_payment_form_fields'] = isset( $el_settings['better_payment_form_fields_layout_4_5_6_desc'] ) ? $el_settings['better_payment_form_fields_layout_4_5_6_desc'] : array();
893 break;
894
895 case 'layout-6-pro':
896 $el_settings['better_payment_form_fields'] = isset( $el_settings['better_payment_form_fields_layout_4_5_6_woo'] ) ? $el_settings['better_payment_form_fields_layout_4_5_6_woo'] : array();
897 break;
898
899 default:
900 break;
901 }
902
903 $form_fields = isset( $el_settings['better_payment_form_fields'] ) ? $el_settings['better_payment_form_fields'] : array();
904
905 if ( ! empty( $form_fields ) && is_array( $form_fields ) ) {
906 foreach ( $form_fields as $form_field ) {
907 $field_type = ! empty( $form_field['better_payment_primary_field_type'] ) ? sanitize_text_field( $form_field['better_payment_primary_field_type'] ) : '';
908 $field_name = ! empty( $form_field['better_payment_field_name_heading'] ) ? sanitize_text_field( $form_field['better_payment_field_name_heading'] ) : '';
909
910 switch ( $field_type ) {
911 case 'primary_first_name':
912 $post_data_primary_first_name = isset( $post_fields['primary_first_name'] ) ? sanitize_text_field( $post_fields['primary_first_name'] ) : '';
913 $better_form_fields['primary_first_name'] = $post_data_primary_first_name;
914 break;
915
916 case 'primary_last_name':
917 $post_data_primary_last_name = isset( $post_fields['primary_last_name'] ) ? sanitize_text_field( $post_fields['primary_last_name'] ) : '';
918 $better_form_fields['primary_last_name'] = $post_data_primary_last_name;
919 break;
920
921 case 'primary_email':
922 $post_data_primary_email = isset( $post_fields['primary_email'] ) ? sanitize_email( $post_fields['primary_email'] ) : '';
923 $better_form_fields['primary_email'] = $post_data_primary_email;
924 break;
925
926 default:
927 if ( isset( $post_fields[ $field_type ] ) ) {
928 $better_form_fields[ $field_type ] = sanitize_text_field( $post_fields[ $field_type ] );
929 }
930 break;
931 }
932 }
933 }
934
935 return $better_form_fields;
936 }
937 }
938