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

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

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