PluginProbe
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More / 2.4.0
Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More v2.4.0
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.0, at includes/Blocks/BlockActions.php

918 lines 40.2 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 // Persisted so the async PayPal IPN can rebuild the e-mail body + on/off toggle.
303 'email_settings' => Handler::extract_email_settings( $el_settings ),
304 );
305
306 // Fields only Pro's forms store (layout 6's platform flags) — see PaymentEntitlement.
307 $better_form_fields = PaymentEntitlement::transaction_fields( $better_form_fields, $el_settings );
308
309 $better_form_fields = array_merge( $better_form_fields, $this->fetch_better_form_fields( $el_settings, $_POST ) );
310
311 if ( ! empty( $better_form_fields['primary_first_name'] ) ) {
312 $request_data['primary_first_name'] = sanitize_text_field( $better_form_fields['primary_first_name'] );
313 }
314
315 if ( ! empty( $better_form_fields['primary_last_name'] ) ) {
316 $request_data['primary_last_name'] = sanitize_text_field( $better_form_fields['primary_last_name'] );
317 }
318
319 if ( ! empty( $better_form_fields['primary_email'] ) ) {
320 $request_data['primary_email'] = sanitize_email( $better_form_fields['primary_email'] );
321 }
322
323 if ( ! empty( $better_form_fields['primary_reference_number'] ) ) {
324 $request_data['invoice'] = sanitize_text_field( $better_form_fields['primary_reference_number'] );
325 }
326
327 $campaign_id = $guarded['campaign_id'];
328 // phpcs:enable WordPress.Security.NonceVerification.Missing
329
330 Handler::payment_create(
331 array(
332 'amount' => floatval( $primary_payment_amount ),
333 'order_id' => $order_id,
334 'payment_date' => gmdate( 'Y-m-d H:i:s' ),
335 'source' => 'paypal',
336 'form_fields_info' => maybe_serialize( $better_form_fields ),
337 'currency' => sanitize_text_field( $el_settings_currency ),
338 'referer' => 'gutenberg-block',
339 'campaign_id' => $campaign_id,
340 )
341 );
342
343 $paypal_url = "https://www.$path.com/cgi-bin/webscr?";
344 $paypal_url .= http_build_query( $request_data );
345
346 wp_redirect( esc_url_raw( $paypal_url ) );
347 exit;
348 }
349
350 /**
351 * Handle Stripe token request for Gutenberg blocks.
352 *
353 * @since 1.0.0
354 */
355 public function block_stripe_get_token() {
356 // phpcs:disable WordPress.Security.NonceVerification.Missing
357 // Bail immediately if not a Gutenberg block submission — zero DB overhead for Elementor forms.
358 if ( empty( $_POST['setting_data']['source'] ) || 'gutenberg' !== $_POST['setting_data']['source'] ) {
359 return;
360 }
361 // phpcs:enable WordPress.Security.NonceVerification.Missing
362
363 // Verify nonce.
364 if ( ! check_admin_referer( 'better-payment', 'security' ) ) {
365 wp_send_json_error( esc_html__( 'Nonce verification failed.', 'better-payment' ) );
366 return;
367 }
368
369 // phpcs:disable WordPress.Security.NonceVerification.Missing
370 $page_id = isset( $_POST['setting_data']['page_id'] ) ? intval( $_POST['setting_data']['page_id'] ) : 0;
371 $widget_id = isset( $_POST['setting_data']['widget_id'] ) ? sanitize_text_field( $_POST['setting_data']['widget_id'] ) : '';
372 // phpcs:enable WordPress.Security.NonceVerification.Missing
373
374 if ( empty( $page_id ) || empty( $widget_id ) ) {
375 return; // Let the original handler deal with missing data.
376 }
377
378 // Check if this is a block form by looking for settings.
379 if ( ! $this->is_block_form( $page_id, $widget_id ) ) {
380 return; // Not a block form, let Elementor handler process it.
381 }
382
383 // This is a block form - handle the Stripe payment here.
384 $this->process_stripe_payment( $page_id, $widget_id );
385 }
386
387 /**
388 * Handle Paystack token request for Gutenberg blocks.
389 *
390 * @since 1.0.0
391 */
392 public function block_paystack_get_token() {
393 // phpcs:disable WordPress.Security.NonceVerification.Missing
394 // Bail immediately if not a Gutenberg block submission — zero DB overhead for Elementor forms.
395 if ( empty( $_POST['setting_data']['source'] ) || 'gutenberg' !== $_POST['setting_data']['source'] ) {
396 return;
397 }
398 // phpcs:enable WordPress.Security.NonceVerification.Missing
399
400 // Verify nonce.
401 if ( ! check_admin_referer( 'better-payment', 'security' ) ) {
402 return;
403 }
404
405 // phpcs:disable WordPress.Security.NonceVerification.Missing
406 $page_id = isset( $_POST['setting_data']['page_id'] ) ? intval( $_POST['setting_data']['page_id'] ) : 0;
407 $widget_id = isset( $_POST['setting_data']['widget_id'] ) ? sanitize_text_field( $_POST['setting_data']['widget_id'] ) : '';
408 // phpcs:enable WordPress.Security.NonceVerification.Missing
409
410 if ( empty( $page_id ) || empty( $widget_id ) ) {
411 return; // Let the original handler deal with missing data.
412 }
413
414 // Check if this is a block form by looking for transient settings.
415 if ( ! $this->is_block_form( $page_id, $widget_id ) ) {
416 return; // Not a block form, let Elementor handler process it.
417 }
418
419 // This is a block form - handle the Paystack payment here.
420 $this->process_paystack_payment( $page_id, $widget_id );
421 }
422
423 /**
424 * Process Stripe payment for block forms.
425 *
426 * @param int $page_id The page ID.
427 * @param string $widget_id The widget/block ID.
428 */
429 private function process_stripe_payment( $page_id, $widget_id ) {
430 $el_settings = $this->get_block_settings( $page_id, $widget_id );
431
432 if ( empty( $el_settings ) ) {
433 wp_send_json_error( esc_html__( 'Setting Data is missing', 'better-payment' ) );
434 }
435
436 $better_payment_keys = array(
437 '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'] ),
438 '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'] ),
439 );
440
441 if ( empty( $better_payment_keys['public_key'] ) || empty( $better_payment_keys['secret_key'] ) ) {
442 wp_send_json_error( esc_html__( 'Stripe Key missing', 'better-payment' ) );
443 }
444
445 // phpcs:disable WordPress.Security.NonceVerification.Missing
446 // Amount, quantity, currency and campaign come from the stored settings, never
447 // from the request — see PaymentRequestGuard.
448 $guarded = ( new PaymentRequestGuard() )->resolve_form_payment(
449 $el_settings,
450 isset( $_POST['fields'] ) && is_array( $_POST['fields'] ) ? wp_unslash( $_POST['fields'] ) : array(),
451 array(
452 'page_id' => $page_id,
453 'gateway' => 'stripe',
454 )
455 );
456
457 if ( is_wp_error( $guarded ) ) {
458 wp_send_json_error( esc_html( $guarded->get_error_message() ) );
459 return;
460 }
461
462 $amount = $guarded['amount'];
463 $amount_quantity = $guarded['quantity'];
464
465 $header_info = array(
466 'Authorization' => 'Basic ' . base64_encode( sanitize_text_field( $better_payment_keys['secret_key'] ) . ':' ),
467 'Stripe-Version' => '2019-05-16',
468 );
469
470 $order_id = Handler::new_order_id( 'stripe' );
471 $el_settings_currency = $guarded['currency'];
472
473 $el_settings_currency_symbol = $this->get_currency_symbol( esc_html( $el_settings_currency ) );
474
475 $redirection_url_success = get_permalink( $page_id );
476 $redirection_url_error = get_permalink( $page_id );
477
478 $redirection_url_success = add_query_arg(
479 array(
480 'better_payment_stripe_status' => 'success',
481 'better_payment_widget_id' => $widget_id,
482 ),
483 $redirection_url_success
484 );
485
486 $redirection_url_error = add_query_arg(
487 array(
488 'better_payment_error_status' => 'error',
489 'better_payment_widget_id' => $widget_id,
490 ),
491 $redirection_url_error
492 );
493
494 // Build form fields.
495 $woo_product_id = ! empty( $el_settings['better_payment_form_woocommerce_product_id'] ) ? intval( $el_settings['better_payment_form_woocommerce_product_id'] ) : 0;
496 $woo_product_ids = ! empty( $el_settings['better_payment_form_woocommerce_product_ids'] ) ? $el_settings['better_payment_form_woocommerce_product_ids'] : array( 0 );
497 $fluentcart_product_id = ! empty( $el_settings['better_payment_form_fluentcart_product_id'] ) ? intval( $el_settings['better_payment_form_fluentcart_product_id'] ) : 0;
498 $fluentcart_product_ids = ! empty( $el_settings['better_payment_form_fluentcart_product_ids'] ) ? $el_settings['better_payment_form_fluentcart_product_ids'] : array( 0 );
499
500 $product_ids = array(
501 'woo_product_ids' => $woo_product_ids,
502 'fluentcart_product_ids' => $fluentcart_product_ids,
503 );
504
505 $detailed_product_info = $this->get_detailed_product_info( $product_ids );
506
507 $better_form_fields = array(
508 'amount' => sanitize_text_field( $el_settings_currency_symbol ) . $amount,
509 'referer_page_id' => $page_id,
510 'referer_widget_id' => $widget_id,
511 'woo_product_id' => $woo_product_id,
512 'woo_product_ids' => maybe_serialize( $woo_product_ids ),
513 'fluentcart_product_id' => $fluentcart_product_id,
514 'fluentcart_product_ids' => maybe_serialize( $fluentcart_product_ids ),
515 'source' => 'stripe',
516 'amount_quantity' => ! empty( $amount_quantity ) ? intval( $amount_quantity ) : '',
517 'detailed_product_info' => maybe_serialize( $detailed_product_info ),
518 );
519
520 $better_form_fields = array_merge( $better_form_fields, $this->fetch_better_form_fields( $el_settings, $_POST['fields'] ) );
521
522 $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' );
523
524 // The Stripe Price ID (when Payment Source = Stripe) is display-only —
525 // it pre-fills the amount input on the frontend (see layout-1/2/3.php line input value).
526 // The payment session always uses price_data with the submitted amount,
527 // matching the Elementor widget behaviour for layouts 1/2/3.
528 $line_item = array(
529 'price_data' => array(
530 'currency' => sanitize_text_field( $el_settings_currency ),
531 'unit_amount' => intval( $amount * 100 ),
532 'product_data' => array(
533 'name' => $item_name,
534 ),
535 ),
536 'quantity' => 1,
537 );
538
539 $request_body = array(
540 'line_items' => array( $line_item ),
541 'mode' => 'payment',
542 'locale' => 'auto',
543 'payment_method_types' => array( 'card' ),
544 'billing_address_collection' => 'required',
545 'client_reference_id' => time(),
546 'metadata' => array(
547 'order_id' => $order_id,
548 ),
549 'success_url' => esc_url_raw( $redirection_url_success ) . '&better_payment_stripe_id=' . $order_id,
550 'cancel_url' => add_query_arg(
551 array(
552 'better_payment_stripe_id' => $order_id,
553 ),
554 esc_url_raw( $redirection_url_error )
555 ),
556 );
557
558 $request_body['payment_intent_data'] = array(
559 'capture_method' => 'automatic',
560 'description' => $item_name,
561 'metadata' => array(
562 'order_id' => $order_id,
563 ),
564 );
565
566 $primary_email = ! empty( $better_form_fields['primary_email'] ) ? sanitize_email( $better_form_fields['primary_email'] ) : '';
567
568 if ( ! empty( $primary_email ) ) {
569 $request_body['customer_email'] = $primary_email;
570 $request_body['metadata']['customer_email'] = $primary_email;
571 $request_body['payment_intent_data']['metadata']['customer_email'] = $primary_email;
572 }
573
574 // Build customer name from first/last name fields (mirrors widget behaviour).
575 $customer_name = '';
576 if ( ! empty( $better_form_fields['primary_first_name'] ) ) {
577 $customer_name = sanitize_text_field( $better_form_fields['primary_first_name'] );
578 }
579 if ( ! empty( $better_form_fields['primary_last_name'] ) ) {
580 $customer_name = trim( $customer_name . ' ' . sanitize_text_field( $better_form_fields['primary_last_name'] ) );
581 }
582
583 if ( ! empty( $customer_name ) ) {
584 $request_body['metadata']['customer_name'] = $customer_name;
585 $request_body['payment_intent_data']['metadata']['customer_name'] = $customer_name;
586 }
587
588 $request = wp_remote_post(
589 'https://api.stripe.com/v1/checkout/sessions',
590 array(
591 'headers' => $header_info,
592 'body' => $request_body,
593 )
594 );
595
596 if ( is_wp_error( $request ) ) {
597 wp_send_json_error( sanitize_text_field( $request->get_error_message() ) );
598 return;
599 }
600
601 $response_ar = json_decode( wp_remote_retrieve_body( $request ) );
602
603 if ( null === $response_ar ) {
604 wp_send_json_error( esc_html__( 'Invalid response from Stripe.', 'better-payment' ) );
605 return;
606 }
607
608 if ( ! empty( $response_ar->payment_intent ) || ( ! empty( $response_ar->mode ) && 'subscription' === $response_ar->mode ) ) {
609 $campaign_id = $guarded['campaign_id'];
610
611 Handler::payment_create(
612 array(
613 'amount' => floatval( $amount ),
614 'order_id' => $order_id,
615 'payment_date' => gmdate( 'Y-m-d H:i:s' ),
616 'source' => 'stripe',
617 'transaction_id' => sanitize_text_field( $response_ar->payment_intent ),
618 'customer_info' => maybe_serialize( $response_ar ),
619 'form_fields_info' => maybe_serialize( $better_form_fields ),
620 'obj_id' => sanitize_text_field( $response_ar->id ),
621 'status' => sanitize_text_field( $response_ar->payment_status ),
622 'currency' => sanitize_text_field( $el_settings_currency ),
623 'referer' => 'gutenberg-block',
624 'campaign_id' => $campaign_id,
625 )
626 );
627
628 wp_send_json_success(
629 array(
630 'stripe_data' => sanitize_text_field( $response_ar->id ),
631 'stripe_public_key' => sanitize_text_field( $better_payment_keys['public_key'] ),
632 )
633 );
634 } else {
635 $error_message = 'Something went wrong!';
636
637 if ( isset( $response_ar->error ) ) {
638 $error_message = sanitize_text_field( $response_ar->error->message );
639 }
640
641 wp_send_json_error( $error_message );
642 }
643 // phpcs:enable WordPress.Security.NonceVerification.Missing
644 }
645
646 /**
647 * Process Paystack payment for block forms.
648 *
649 * @param int $page_id The page ID.
650 * @param string $widget_id The widget/block ID.
651 */
652 private function process_paystack_payment( $page_id, $widget_id ) {
653 $el_settings = $this->get_block_settings( $page_id, $widget_id );
654
655 if ( empty( $el_settings ) ) {
656 wp_send_json_error( esc_html__( 'Setting Data is missing', 'better-payment' ) );
657 }
658
659 if ( empty( $el_settings['better_payment_paystack_public_key'] ) || empty( $el_settings['better_payment_paystack_secret_key'] ) ) {
660 wp_send_json_error( esc_html__( 'Paystack Key missing', 'better-payment' ) );
661 }
662
663 // phpcs:disable WordPress.Security.NonceVerification.Missing
664 // Amount, quantity, currency and campaign come from the stored settings, never
665 // from the request — see PaymentRequestGuard.
666 $guarded = ( new PaymentRequestGuard() )->resolve_form_payment(
667 $el_settings,
668 isset( $_POST['fields'] ) && is_array( $_POST['fields'] ) ? wp_unslash( $_POST['fields'] ) : array(),
669 array(
670 'page_id' => $page_id,
671 'gateway' => 'paystack',
672 )
673 );
674
675 if ( is_wp_error( $guarded ) ) {
676 wp_send_json_error( esc_html( $guarded->get_error_message() ) );
677 return;
678 }
679
680 $amount = $guarded['amount'];
681 $amount_quantity = $guarded['quantity'];
682
683 $header_info = array(
684 'Authorization' => 'Bearer ' . sanitize_text_field( $el_settings['better_payment_paystack_secret_key'] ),
685 'Cache-Control: no-cache',
686 );
687
688 $order_id = Handler::new_order_id( 'paystack' );
689 $el_settings_currency = $guarded['currency'];
690
691 $el_settings_currency_symbol = $this->get_currency_symbol( esc_html( $el_settings_currency ) );
692
693 $redirection_url_success = get_permalink( $page_id );
694 $redirection_url_error = get_permalink( $page_id );
695
696 $redirection_url_success = add_query_arg(
697 array(
698 'better_payment_paystack_status' => 'success',
699 'better_payment_widget_id' => $widget_id,
700 ),
701 $redirection_url_success
702 );
703
704 $redirection_url_error = add_query_arg(
705 array(
706 'better_payment_error_status' => 'error',
707 'better_payment_widget_id' => $widget_id,
708 ),
709 $redirection_url_error
710 );
711
712 // Build form fields.
713 $woo_product_id = ! empty( $el_settings['better_payment_form_woocommerce_product_id'] ) ? intval( $el_settings['better_payment_form_woocommerce_product_id'] ) : 0;
714 $woo_product_ids = ! empty( $el_settings['better_payment_form_woocommerce_product_ids'] ) ? $el_settings['better_payment_form_woocommerce_product_ids'] : array( 0 );
715 $fluentcart_product_id = ! empty( $el_settings['better_payment_form_fluentcart_product_id'] ) ? intval( $el_settings['better_payment_form_fluentcart_product_id'] ) : 0;
716 $fluentcart_product_ids = ! empty( $el_settings['better_payment_form_fluentcart_product_ids'] ) ? $el_settings['better_payment_form_fluentcart_product_ids'] : array( 0 );
717
718 $product_ids = array(
719 'woo_product_ids' => $woo_product_ids,
720 'fluentcart_product_ids' => $fluentcart_product_ids,
721 );
722
723 $detailed_product_info = $this->get_detailed_product_info( $product_ids );
724
725 $better_form_fields = array(
726 'amount' => sanitize_text_field( $el_settings_currency_symbol ) . $amount,
727 'referer_page_id' => $page_id,
728 'referer_widget_id' => $widget_id,
729 'woo_product_id' => $woo_product_id,
730 'woo_product_ids' => maybe_serialize( $woo_product_ids ),
731 'fluentcart_product_id' => $fluentcart_product_id,
732 'fluentcart_product_ids' => maybe_serialize( $fluentcart_product_ids ),
733 'source' => 'paystack',
734 'amount_quantity' => ! empty( $amount_quantity ) ? intval( $amount_quantity ) : '',
735 'detailed_product_info' => maybe_serialize( $detailed_product_info ),
736 );
737
738 $better_form_fields = array_merge( $better_form_fields, $this->fetch_better_form_fields( $el_settings, $_POST['fields'] ) );
739
740 $primary_email = ! empty( $better_form_fields['primary_email'] ) ? sanitize_email( $better_form_fields['primary_email'] ) : '';
741
742 $request_body = array(
743 'amount' => intval( $amount * 100 ),
744 'currency' => sanitize_text_field( $el_settings_currency ),
745 'email' => $primary_email,
746 'callback_url' => esc_url_raw( $redirection_url_success ) . '&better_payment_paystack_id=' . $order_id,
747 'metadata' => array(
748 'cancel_action' => add_query_arg(
749 array(
750 'better_payment_paystack_id' => $order_id,
751 ),
752 esc_url_raw( $redirection_url_error )
753 ),
754 ),
755 );
756
757 $request = wp_remote_post(
758 'https://api.paystack.co/transaction/initialize',
759 array(
760 'headers' => $header_info,
761 'body' => $request_body,
762 )
763 );
764
765 if ( is_wp_error( $request ) ) {
766 wp_send_json_error( sanitize_text_field( $request->get_error_message() ) );
767 return;
768 }
769
770 $response_ar = json_decode( wp_remote_retrieve_body( $request ) );
771
772 if ( null === $response_ar ) {
773 wp_send_json_error( esc_html__( 'Invalid response from Paystack.', 'better-payment' ) );
774 return;
775 }
776
777 if ( empty( $response_ar->status ) || empty( $response_ar->data ) ) {
778 $error_message = ! empty( $response_ar->message ) ? sanitize_text_field( $response_ar->message ) : 'Something went wrong!';
779
780 if ( isset( $response_ar->error ) ) {
781 $error_message = sanitize_text_field( $response_ar->error->message );
782 }
783
784 wp_send_json_error( $error_message );
785 }
786
787 $campaign_id = $guarded['campaign_id'];
788
789 Handler::payment_create(
790 array(
791 'amount' => floatval( $amount ),
792 'order_id' => $order_id,
793 'payment_date' => gmdate( 'Y-m-d H:i:s' ),
794 'source' => 'paystack',
795 // Paystack's reference, so verification can bind it to this order.
796 'transaction_id' => ! empty( $response_ar->data->reference ) ? sanitize_text_field( $response_ar->data->reference ) : '',
797 'customer_info' => maybe_serialize( $response_ar ),
798 'form_fields_info' => maybe_serialize( $better_form_fields ),
799 'status' => 'unpaid',
800 'currency' => sanitize_text_field( $el_settings_currency ),
801 'referer' => 'gutenberg-block',
802 'campaign_id' => $campaign_id,
803 )
804 );
805
806 $authorization_url = ! empty( $response_ar->data->authorization_url ) ? esc_url_raw( $response_ar->data->authorization_url ) : '';
807
808 wp_send_json_success(
809 array(
810 'authorization_url' => $authorization_url,
811 )
812 );
813 // phpcs:enable WordPress.Security.NonceVerification.Missing
814 }
815
816 /**
817 * Redirect to referer page.
818 *
819 * @since 1.0.0
820 */
821 public function redirect_previous_page() {
822 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
823 $location = isset( $_SERVER['HTTP_REFERER'] ) ? wp_unslash( $_SERVER['HTTP_REFERER'] ) : home_url();
824 wp_safe_redirect( esc_url_raw( $location ) );
825 exit();
826 }
827
828 /**
829 * Get detailed product information.
830 *
831 * @param array $product_ids Array of product IDs.
832 * @param array $product_quantities Array of product quantities.
833 * @return array Detailed product information.
834 */
835 public function get_detailed_product_info( $product_ids, $product_quantities = array() ) {
836 $detailed_product_info = array();
837
838 if ( function_exists( 'wc_get_product' ) && ! empty( $product_ids['woo_product_ids'] ) ) {
839 foreach ( $product_ids['woo_product_ids'] as $key => $product_id ) {
840 if ( empty( $product_id ) ) {
841 continue;
842 }
843
844 $product = wc_get_product( $product_id );
845 if ( $product ) {
846 $quantity = isset( $product_quantities[ $key ] ) ? intval( $product_quantities[ $key ] ) : 1;
847 $price = floatval( $product->get_price() );
848 $total_price = $price * $quantity;
849
850 $detailed_product_info['woo_products'][ $product_id ] = array(
851 'name' => sanitize_text_field( $product->get_name() ),
852 'product_id' => intval( $product_id ),
853 'permalink' => esc_url( $product->get_permalink() ),
854 'image_src' => esc_url( wp_get_attachment_url( (int) $product->get_image_id() ) ),
855 'price' => $price,
856 'quantity' => $quantity,
857 'total_price' => $total_price,
858 );
859 }
860 }
861 }
862
863 return $detailed_product_info;
864 }
865
866 /**
867 * Fetch form fields from POST data.
868 *
869 * @param array $el_settings Widget/block settings.
870 * @param array $post_data_form_fields POST data.
871 * @return array Form fields data.
872 */
873 public function fetch_better_form_fields( $el_settings, $post_data_form_fields ) {
874 $better_form_fields = array();
875
876 $post_data_primary_first_name = '';
877 $post_data_primary_last_name = '';
878 $post_data_primary_email = '';
879
880 $post_fields = $post_data_form_fields;
881
882 // The fields repeater a Pro form renders is Pro's to name — see PaymentEntitlement.
883 $form_fields = PaymentEntitlement::form_fields( $el_settings );
884
885 if ( ! empty( $form_fields ) ) {
886 foreach ( $form_fields as $form_field ) {
887 $field_type = ! empty( $form_field['better_payment_primary_field_type'] ) ? sanitize_text_field( $form_field['better_payment_primary_field_type'] ) : '';
888 $field_name = ! empty( $form_field['better_payment_field_name_heading'] ) ? sanitize_text_field( $form_field['better_payment_field_name_heading'] ) : '';
889
890 switch ( $field_type ) {
891 case 'primary_first_name':
892 $post_data_primary_first_name = isset( $post_fields['primary_first_name'] ) ? sanitize_text_field( $post_fields['primary_first_name'] ) : '';
893 $better_form_fields['primary_first_name'] = $post_data_primary_first_name;
894 break;
895
896 case 'primary_last_name':
897 $post_data_primary_last_name = isset( $post_fields['primary_last_name'] ) ? sanitize_text_field( $post_fields['primary_last_name'] ) : '';
898 $better_form_fields['primary_last_name'] = $post_data_primary_last_name;
899 break;
900
901 case 'primary_email':
902 $post_data_primary_email = isset( $post_fields['primary_email'] ) ? sanitize_email( $post_fields['primary_email'] ) : '';
903 $better_form_fields['primary_email'] = $post_data_primary_email;
904 break;
905
906 default:
907 if ( isset( $post_fields[ $field_type ] ) ) {
908 $better_form_fields[ $field_type ] = sanitize_text_field( $post_fields[ $field_type ] );
909 }
910 break;
911 }
912 }
913 }
914
915 return $better_form_fields;
916 }
917 }
918