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
← All changes | includes/Classes/Actions.php +124 -363 2.1.1 → 2.4.1 View file →
@@ -27,11 +27,15 @@
27 27 add_action( 'admin_post_nopriv_paypal_form_handle', [ $this, 'paypal_form_handle' ] );
28 28
29 29 add_action( 'wp_ajax_better_payment_stripe_get_token', [ $this, 'better_payment_stripe_get_token' ] );
30 30 add_action( 'wp_ajax_nopriv_better_payment_stripe_get_token', [ $this, 'better_payment_stripe_get_token' ] );
31 -
31 +
32 32 add_action( 'wp_ajax_better_payment_paystack_get_token', [ $this, 'better_payment_paystack_get_token' ] );
33 33 add_action( 'wp_ajax_nopriv_better_payment_paystack_get_token', [ $this, 'better_payment_paystack_get_token' ] );
34 +
35 + // NOTE: PayPal IPN + verification-poll hooks are registered unconditionally
36 + // in better-payment.php (init_plugin), not here — they must run even when
37 + // Elementor is inactive (block/campaign payment flow).
34 38 }
35 39
36 40
37 41 /**
@@ -42,11 +46,11 @@
42 46 public function paypal_form_handle() {
43 47
44 48 check_admin_referer( 'better-payment-paypal', 'security' );
45 49
46 - if ( !empty( $_POST[ 'better_payment_page_id' ] ) ) {
47 - $page_id = intval( $_POST[ 'better_payment_page_id' ], 10 );
48 - } else {
50 + $page_id = ! empty( $_POST[ 'better_payment_page_id' ] ) ? intval( $_POST[ 'better_payment_page_id' ], 10 ) : 0;
51 +
52 + if ( ! $page_id ) {
49 53 $this->redirect_previous_page();
50 54 }
51 55
52 56 if ( !empty( $_POST[ 'better_payment_widget_id' ] ) ) {
@@ -75,36 +79,32 @@
75 79 $woo_product_id = !empty($el_settings["better_payment_form_woocommerce_product_id"]) ? intval($el_settings["better_payment_form_woocommerce_product_id"]) : 0;
76 80 $woo_product_ids = !empty($el_settings["better_payment_form_woocommerce_product_ids"]) ? $el_settings["better_payment_form_woocommerce_product_ids"] : [0];
77 81 $fluentcart_product_id = !empty($el_settings["better_payment_form_fluentcart_product_id"]) ? intval($el_settings["better_payment_form_fluentcart_product_id"]) : 0;
78 82 $fluentcart_product_ids = !empty($el_settings["better_payment_form_fluentcart_product_ids"]) ? $el_settings["better_payment_form_fluentcart_product_ids"] : [0];
79 - $is_layout_6 = ! empty( $el_settings[ 'better_payment_form_layout' ] ) && 'layout-6-pro' === $el_settings[ 'better_payment_form_layout' ];
80 - $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' ];
81 - $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' ] );
82 83
83 - if(!empty($el_settings['better_payment_form_currency_use_woocommerce']) && 'yes' === $el_settings['better_payment_form_currency_use_woocommerce'] &&
84 - !empty($el_settings['better_payment_form_currency_woocommerce'])){
85 - $el_settings_currency = $el_settings['better_payment_form_currency_woocommerce'];
84 + // Amount, quantity, currency and campaign come from the stored settings, never
85 + // from the request — see PaymentRequestGuard.
86 + $guarded = ( new PaymentRequestGuard() )->resolve_form_payment(
87 + $el_settings,
88 + wp_unslash( $_POST ),
89 + [
90 + 'page_id' => $page_id,
91 + 'gateway' => 'paypal',
92 + ]
93 + );
94 +
95 + if ( is_wp_error( $guarded ) ) {
96 + PaymentRequestGuard::reject_form_post( $guarded );
97 + return;
86 98 }
87 - if( !empty($_POST[ 'campaign_currency' ]) ) {
88 - $el_settings_currency = sanitize_text_field( $_POST[ 'campaign_currency' ] );
89 - }
90 99
100 + $el_settings_currency = $guarded['currency'];
91 101 $el_settings_currency_symbol = $this->get_currency_symbol( esc_html($el_settings_currency) );
92 102
93 - $primary_payment_amount = isset( $_POST[ 'primary_payment_amount' ] ) ? floatval( $_POST[ 'primary_payment_amount' ] ) : 0;
103 + $primary_payment_amount = $guarded['amount'];
104 + $primary_payment_amount_quantity = $guarded['quantity'];
94 105
95 - if( empty( $_POST['primary_payment_amount'] ) && ! empty( $_POST['primary_payment_amount_radio'] ) ){
96 - $primary_payment_amount = floatval( $_POST['primary_payment_amount_radio'] );
97 - }
98 -
99 - $primary_payment_amount_quantity = ! empty( $_POST['payment_amount_quantity'] ) ? intval( $_POST[ 'payment_amount_quantity' ] ) : '';
100 - if ( $is_woo_layout ) {
101 - $primary_payment_amount_quantity = 1;
102 - }
103 -
104 - $primary_payment_amount = ! empty( $primary_payment_amount_quantity ) ? $primary_payment_amount * $primary_payment_amount_quantity : $primary_payment_amount;
105 -
106 - $order_id = 'paypal_' . uniqid();
106 + $order_id = Handler::new_order_id( 'paypal' );
107 107 $request_data = [
108 108 'business' => $el_settings[ 'better_payment_paypal_business_email' ],
109 109 'currency_code' => $el_settings_currency,
110 110 'rm' => '2',
@@ -113,8 +113,9 @@
113 113 'item_number' => $order_id,
114 114 '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'),
115 115 'amount' => $primary_payment_amount,
116 116 'cmd' => $el_settings[ 'better_payment_paypal_button_type' ],
117 + 'notify_url' => admin_url( 'admin-post.php?action=better_payment_paypal_ipn' ),
117 118 ];
118 119
119 120 $product_ids = [
120 121 'woo_product_ids' => $woo_product_ids,
@@ -133,13 +134,19 @@
133 134 'fluentcart_product_id' => $fluentcart_product_id,
134 135 'fluentcart_product_ids' => maybe_serialize( $fluentcart_product_ids ),
135 136 'source' => 'paypal',
136 137 'amount_quantity' => ! empty( $primary_payment_amount_quantity ) ? intval( $primary_payment_amount_quantity ) : '',
137 - 'is_woo_layout' => $is_woo_layout,
138 - 'is_fluentcart_layout' => $is_fluentcart_layout,
139 138 'detailed_product_info' => maybe_serialize( $detailed_product_info ),
139 + 'paypal_business_email' => sanitize_email( $el_settings['better_payment_paypal_business_email'] ),
140 + // The PayPal environment the order was sent to; the IPN must come from the same one.
141 + 'paypal_mode' => 'yes' === $el_settings['better_payment_paypal_live_mode'] ? 'live' : 'sandbox',
142 + // Persisted so the async PayPal IPN can rebuild the e-mail body + on/off toggle.
143 + 'email_settings' => Handler::extract_email_settings( $el_settings ),
140 144 ];
141 145
146 + // Fields only Pro's forms store (layout 6's platform flags) — see PaymentEntitlement.
147 + $better_form_fields = PaymentEntitlement::transaction_fields( $better_form_fields, $el_settings );
148 +
142 149 $better_form_fields = array_merge( $better_form_fields, $this->fetch_better_form_fields($el_settings, $_POST) );
143 150
144 151 if ( !empty( $better_form_fields[ 'primary_first_name' ] ) ) {
145 152 $request_data[ 'primary_first_name' ] = sanitize_text_field( $better_form_fields[ 'primary_first_name' ] );
@@ -156,9 +163,9 @@
156 163 if ( !empty( $better_form_fields[ 'primary_reference_number' ] ) ) {
157 164 $request_data[ 'invoice' ] = sanitize_text_field( $better_form_fields[ 'primary_reference_number' ] );
158 165 }
159 166
160 - $campaign_id = ! empty( $_POST['campaign_id'] ) ? sanitize_text_field( $_POST['campaign_id'] ) : '';
167 + $campaign_id = $guarded['campaign_id'];
161 168
162 169 Handler::payment_create(
163 170 [
164 171 'amount' => floatval( $primary_payment_amount ),
@@ -203,38 +210,39 @@
203 210 wp_send_json_error( esc_html($err_msg) );
204 211 }
205 212
206 213 $el_settings = $this->better_payment_widget_settings( $page_id, $widget_id );
207 - $is_layout_6 = ! empty( $el_settings[ 'better_payment_form_layout' ] ) && 'layout-6-pro' === $el_settings[ 'better_payment_form_layout' ];
208 - $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' ];
209 - $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' ] );
210 214
215 + // Before any setting is read: an unknown, or unpublished, form has none.
216 + if ( empty( $el_settings ) ) {
217 + wp_send_json_error( esc_html(__( 'Setting Data is missing', 'better-payment' )) );
218 + }
219 +
211 220 $better_payment_keys = [
212 221 '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' ] ),
213 222 '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' ] ),
214 223 ];
215 224
216 - if ( empty( $el_settings ) ) {
217 - wp_send_json_error( esc_html(__( 'Setting Data is missing', 'better-payment' )) );
218 - }
225 + // Amount, quantity, currency, price ids, coupon and campaign come from the stored
226 + // settings, never from the request — see PaymentRequestGuard.
227 + $guard = new PaymentRequestGuard();
228 + $guarded = $guard->resolve_form_payment(
229 + $el_settings,
230 + isset( $_POST['fields'] ) && is_array( $_POST['fields'] ) ? wp_unslash( $_POST['fields'] ) : [],
231 + [
232 + 'page_id' => $page_id,
233 + 'gateway' => 'stripe',
234 + ]
235 + );
219 236
220 - $is_payment_split_payment = ! empty( $el_settings["better_payment_form_payment_type"] ) && 'split-payment' === $el_settings["better_payment_form_payment_type"];
221 -
222 - $amount = isset($_POST['fields']['primary_payment_amount']) ? floatval($_POST['fields']['primary_payment_amount']) : 0;
223 -
224 - if ( empty( $_POST['fields']['primary_payment_amount'] ) && ! empty( $_POST['fields']['primary_payment_amount_radio'] ) ) {
225 - $amount = floatval($_POST['fields']['primary_payment_amount_radio']);
237 + if ( is_wp_error( $guarded ) ) {
238 + wp_send_json_error( esc_html( $guarded->get_error_message() ) );
226 239 }
227 240
228 - $amount_quantity = ! empty( $_POST['fields']['payment_amount_quantity'] ) ? intval( $_POST['fields']['payment_amount_quantity'] ) : '';
229 - $product_quantities = ! empty( $_POST['fields']['payment_amount_quantity'] ) && is_array( $_POST['fields']['payment_amount_quantity'] ) ? array_map( 'intval', $_POST['fields']['payment_amount_quantity'] ) : [];
230 -
231 - if ( $is_woo_layout || $is_fluentcart_layout ) {
232 - $amount_quantity = 1;
233 - }
241 + $amount = $guarded['amount'];
242 + $amount_quantity = $guarded['quantity'];
243 + $product_quantities = $guarded['product_quantities'];
234 244
235 - $amount = ! empty( $amount_quantity ) ? $amount * $amount_quantity : $amount;
236 -
237 245 if ( empty( $better_payment_keys['public_key'] ) || empty( $better_payment_keys['secret_key'] ) ) {
238 246 wp_send_json_error( esc_html(__( 'Stripe Key missing', 'better-payment' )) );
239 247 }
240 248
@@ -242,9 +250,9 @@
242 250 'Authorization' => 'Basic ' . base64_encode( sanitize_text_field( $better_payment_keys['secret_key'] ) . ':' ),
243 251 'Stripe-Version' => '2019-05-16'
244 252 );
245 253
246 - $order_id = 'stripe_' . uniqid();
254 + $order_id = Handler::new_order_id( 'stripe' );
247 255
248 256 $el_settings_currency = $el_settings[ 'better_payment_form_currency' ];
249 257 $woo_product_id = !empty($el_settings["better_payment_form_woocommerce_product_id"]) ? intval($el_settings["better_payment_form_woocommerce_product_id"]) : 0;
250 258 $woo_product_ids = !empty($el_settings["better_payment_form_woocommerce_product_ids"]) ? $el_settings["better_payment_form_woocommerce_product_ids"] : [0];
@@ -250,15 +258,9 @@
250 258 $woo_product_ids = !empty($el_settings["better_payment_form_woocommerce_product_ids"]) ? $el_settings["better_payment_form_woocommerce_product_ids"] : [0];
251 259 $fluentcart_product_id = !empty($el_settings["better_payment_form_fluentcart_product_id"]) ? intval($el_settings["better_payment_form_fluentcart_product_id"]) : 0;
252 260 $fluentcart_product_ids = !empty($el_settings["better_payment_form_fluentcart_product_ids"]) ? $el_settings["better_payment_form_fluentcart_product_ids"] : [0];
253 261
254 - if(!empty($el_settings['better_payment_form_currency_use_woocommerce']) && 'yes' === $el_settings['better_payment_form_currency_use_woocommerce'] &&
255 - !empty($el_settings['better_payment_form_currency_woocommerce'])){
256 - $el_settings_currency = $el_settings['better_payment_form_currency_woocommerce'];
257 - }
258 - if( !empty($_POST['fields'][ 'campaign_currency' ]) ) {
259 - $el_settings_currency = sanitize_text_field( $_POST['fields'][ 'campaign_currency' ] );
260 - }
262 + $el_settings_currency = $guarded['currency'];
261 263
262 264 $el_settings_currency_symbol = $this->get_currency_symbol( esc_html($el_settings_currency) );
263 265
264 266 $request = [
@@ -295,46 +297,21 @@
295 297 ]
296 298 ]
297 299 ];
298 300
299 - $coupon_code = ! empty( $_POST['fields']['primary_coupon_code'] ) ? sanitize_text_field( $_POST['fields']['primary_coupon_code'] ) : '';
300 - if ( !empty( $coupon_code ) ) {
301 - $request['discounts'] = [
302 - [
303 - 'coupon' => $coupon_code
304 - ]
305 - ];
306 - }
301 + // The typed code is kept for the receipt; what Stripe applies is decided by the guard —
302 + // an active promotion code, or a coupon the merchant tagged public. A bare coupon id
303 + // no longer works, because every coupon in the account (internal 100%-off ones
304 + // included) would be one guess away.
305 + $coupon_code = $guarded['coupon_code'];
306 + $discount = $guard->resolve_stripe_discount( $coupon_code, $better_payment_keys['secret_key'] );
307 307
308 - $is_payment_recurring = ! empty( $_POST['fields']['better_payment_recurring_mode'] ) && 'subscription' === sanitize_text_field( $_POST['fields']['better_payment_recurring_mode'] );
309 - $recurring_price_id = ! empty( $_POST['fields']['better_payment_recurring_price_id'] ) ? sanitize_text_field( $_POST['fields']['better_payment_recurring_price_id'] ) : '';
310 - $mixed_payment_selection_data = $this->get_mixed_payment_selection_data( $el_settings, $_POST['fields'] );
311 -
312 - if ( is_wp_error( $mixed_payment_selection_data ) ) {
313 - wp_send_json_error( $mixed_payment_selection_data->get_error_message() );
308 + if ( is_wp_error( $discount ) ) {
309 + wp_send_json_error( esc_html( $discount->get_error_message() ) );
314 310 }
315 311
316 - $is_payment_mixed_recurring = ! empty( $mixed_payment_selection_data['is_mixed_recurring'] );
317 - $mixed_dynamic_recurring_price = [];
318 -
319 - if ( $is_payment_mixed_recurring ) {
320 - $mixed_dynamic_recurring_price = $this->create_mixed_recurring_stripe_price(
321 - $header_info,
322 - $amount,
323 - $el_settings_currency,
324 - $order_id,
325 - $widget_id,
326 - $mixed_payment_selection_data,
327 - $el_settings
328 - );
329 -
330 - if ( is_wp_error( $mixed_dynamic_recurring_price ) ) {
331 - wp_send_json_error( $mixed_dynamic_recurring_price->get_error_message() );
332 - }
333 -
334 - // Reuse existing recurring checkout flow with a runtime-generated recurring Stripe Price.
335 - $is_payment_recurring = true;
336 - $recurring_price_id = ! empty( $mixed_dynamic_recurring_price['price_id'] ) ? sanitize_text_field( $mixed_dynamic_recurring_price['price_id'] ) : '';
312 + if ( ! empty( $discount ) ) {
313 + $request['discounts'] = [ $discount ];
337 314 }
338 315
339 316 $product_ids = [
340 317 'woo_product_ids' => $woo_product_ids,
@@ -353,29 +330,17 @@
353 330 'fluentcart_product_id' => $fluentcart_product_id,
354 331 'fluentcart_product_ids' => maybe_serialize( $fluentcart_product_ids ),
355 332 'source' => 'stripe',
356 333 'amount_quantity' => ! empty( $amount_quantity ) ? $amount_quantity : '',
357 - 'is_woo_layout' => $is_woo_layout,
358 - 'is_fluentcart_layout' => $is_fluentcart_layout,
359 334 'stripe_coupon_code' => $coupon_code,
360 335 'detailed_product_info' => maybe_serialize( $detailed_product_info ),
361 336 ];
362 337
338 + // Fields only Pro's forms store (layout 6's platform flags) — see PaymentEntitlement.
339 + $better_form_fields = PaymentEntitlement::transaction_fields( $better_form_fields, $el_settings );
340 +
363 341 $better_form_fields = array_merge( $better_form_fields, $this->fetch_better_form_fields($el_settings, $_POST['fields']) );
364 342
365 - if ( ! empty( $mixed_payment_selection_data['is_payment_mixed'] ) ) {
366 - $better_form_fields['is_payment_mixed'] = 1;
367 - $better_form_fields['mixed_payment_selected_type'] = ! empty( $mixed_payment_selection_data['selected_type'] ) ? sanitize_text_field( $mixed_payment_selection_data['selected_type'] ) : 'one-time';
368 - }
369 -
370 - if ( $is_payment_mixed_recurring ) {
371 - $better_form_fields['mixed_payment_selected_type'] = 'recurring';
372 - $better_form_fields['mixed_payment_recurring_interval_count'] = ! empty( $mixed_payment_selection_data['interval_count'] ) ? absint( $mixed_payment_selection_data['interval_count'] ) : 1;
373 - $better_form_fields['mixed_payment_recurring_interval'] = ! empty( $mixed_payment_selection_data['interval'] ) ? sanitize_text_field( $mixed_payment_selection_data['interval'] ) : 'month';
374 - $better_form_fields['mixed_payment_dynamic_price_id'] = ! empty( $mixed_dynamic_recurring_price['price_id'] ) ? sanitize_text_field( $mixed_dynamic_recurring_price['price_id'] ) : '';
375 - $better_form_fields['mixed_payment_dynamic_product_id'] = ! empty( $mixed_dynamic_recurring_price['product_id'] ) ? sanitize_text_field( $mixed_dynamic_recurring_price['product_id'] ) : '';
376 - }
377 -
378 343 if ( !empty( $better_form_fields['primary_first_name'] ) ) {
379 344 $request[ 'customer_name' ] = $better_form_fields['primary_first_name'];
380 345 }
381 346
@@ -395,41 +360,34 @@
395 360 $request[ 'payment_intent_data' ][ 'metadata' ][ 'customer_email' ] = $request[ 'customer_email' ];
396 361
397 362 }
398 363
399 - if ( $is_payment_split_payment ) {
400 - $installment_price_id = ! empty($_POST['fields']['split_payment_installment']) ? sanitize_text_field($_POST['fields']['split_payment_installment']) : '';
401 - $split_payment_installments_data = ! empty( $el_settings['better_payment_split_installment_price_ids'] ) ? $el_settings['better_payment_split_installment_price_ids'] : [];
364 + // Recurring, split-payment and mixed checkouts are Pro's: refused without Pro, built by
365 + // Pro (or, for an older Pro, by Lite's transitional copy) — see PaymentEntitlement.
366 + if ( PaymentEntitlement::is_pro_form( $el_settings ) ) {
367 + $checkout = PaymentEntitlement::stripe_checkout(
368 + [
369 + 'request' => $request,
370 + 'form_fields' => $better_form_fields,
371 + ],
372 + [
373 + 'settings' => $el_settings,
374 + 'fields' => isset( $_POST['fields'] ) && is_array( $_POST['fields'] ) ? wp_unslash( $_POST['fields'] ) : [],
375 + 'guarded' => $guarded,
376 + 'order_id' => $order_id,
377 + 'widget_id' => $widget_id,
378 + 'header_info' => $header_info,
379 + ]
380 + );
402 381
403 - if ( is_array( $split_payment_installments_data ) && count( $split_payment_installments_data ) ){
404 - foreach( $split_payment_installments_data as $split_payment_installment_data ){
405 - $item_price_id = ! empty( $split_payment_installment_data[ 'better_payment_split_installment_price_id' ] ) ? sanitize_text_field( $split_payment_installment_data[ 'better_payment_split_installment_price_id' ] ) : '';
406 -
407 - if ( $installment_price_id === $item_price_id ) {
408 - $installment_price_iteration = ! empty( $split_payment_installment_data[ 'better_payment_split_installment_iteration' ] ) ? sanitize_text_field( $split_payment_installment_data[ 'better_payment_split_installment_iteration' ] ) : '';
409 - break;
410 - }
411 - }
382 + if ( is_wp_error( $checkout ) ) {
383 + wp_send_json_error( esc_html( $checkout->get_error_message() ) );
412 384 }
413 385
414 - $better_form_fields['is_payment_split_payment'] = 1;
415 - $better_form_fields['split_payment_installment_price_id'] = $installment_price_id;
416 - $better_form_fields['split_payment_total_amount_price_id'] = $recurring_price_id;
417 - $better_form_fields['split_payment_installment_iteration'] = ( !empty( $installment_price_id ) && !empty( $installment_price_iteration ) ) ? $installment_price_iteration : 1;
418 - $better_form_fields['split_payment_total_amount'] = $amount * $better_form_fields['split_payment_installment_iteration'];
386 + $request = $checkout['request'];
387 + $better_form_fields = $checkout['form_fields'];
419 388 }
420 -
421 - if ( $is_payment_recurring || $is_payment_split_payment ) {
422 - $request['mode'] = 'subscription';
423 - unset($request['line_items'][0]);
424 - unset($request['payment_intent_data']);
425 - $request['line_items'][0]['price'] = $is_payment_split_payment && ! empty( $installment_price_id ) ? $installment_price_id : $recurring_price_id;
426 - $request['line_items'][0]['quantity'] = 1;
427 389
428 - $better_form_fields['mode'] = $request['mode'];
429 - $better_form_fields['recurring_price_id'] = $request['line_items'][0]['price'];
430 - }
431 -
432 390 #ToDo: Need to prefill name field on stripe checkout
433 391 $response = wp_safe_remote_post(
434 392 'https://api.stripe.com/v1/checkout/sessions',
435 393 array(
@@ -442,9 +400,9 @@
442 400
443 401 $response_ar = json_decode( $response[ 'body' ] );
444 402
445 403 if ( ! empty( $response_ar->payment_intent ) || ( ! empty( $response_ar->mode ) && 'subscription' === $response_ar->mode ) ) {
446 - $campaign_id = ! empty( $_POST['fields']['campaign_id'] ) ? sanitize_text_field( $_POST['fields']['campaign_id'] ) : '';
404 + $campaign_id = $guarded['campaign_id'];
447 405
448 406 Handler::payment_create(
449 407 [
450 408 'amount' => $amount,
@@ -478,193 +436,8 @@
478 436 }
479 437 }
480 438
481 439 /**
482 - * Parse mixed payment selection payload.
483 - *
484 - * @param array $el_settings
485 - * @param array $fields
486 - *
487 - * @return array|\WP_Error
488 - */
489 - private function get_mixed_payment_selection_data( $el_settings = [], $fields = [] ) {
490 - $selection_data = [
491 - 'is_payment_mixed' => 0,
492 - 'selected_type' => 'one-time',
493 - 'is_mixed_recurring' => 0,
494 - 'interval_count' => 1,
495 - 'interval' => 'month',
496 - ];
497 -
498 - $is_payment_mixed = ! empty( $el_settings['better_payment_form_payment_type'] ) && 'mixed' === sanitize_text_field( $el_settings['better_payment_form_payment_type'] );
499 - if ( ! $is_payment_mixed ) {
500 - return $selection_data;
501 - }
502 -
503 - $selection_data['is_payment_mixed'] = 1;
504 - $selected_type = ! empty( $fields['better_payment_mixed_selected_type'] ) ? sanitize_text_field( $fields['better_payment_mixed_selected_type'] ) : '';
505 - if ( ! empty( $fields['payment_type'] ) && in_array( $fields['payment_type'], [ 'one-time', 'recurring' ], true ) ) {
506 - $selected_type = sanitize_text_field( $fields['payment_type'] );
507 - }
508 -
509 - $selected_type = in_array( $selected_type, [ 'one-time', 'recurring' ], true ) ? $selected_type : 'one-time';
510 - $selection_data['selected_type'] = $selected_type;
511 - $selection_data['is_mixed_recurring'] = 'recurring' === $selected_type ? 1 : 0;
512 -
513 - if ( ! $selection_data['is_mixed_recurring'] ) {
514 - return $selection_data;
515 - }
516 -
517 - $allowed_intervals = [ 'day', 'week', 'month', 'year' ];
518 - $mixed_interval_value = ! empty( $fields['better_payment_mixed_interval'] ) ? sanitize_text_field( $fields['better_payment_mixed_interval'] ) : '';
519 -
520 - if ( empty( $mixed_interval_value ) ) {
521 - $interval_type = ! empty( $fields['better_payment_mixed_interval_type'] ) ? sanitize_key( $fields['better_payment_mixed_interval_type'] ) : '';
522 -
523 - if ( empty( $interval_type ) ) {
524 - if ( ! empty( $fields['better_payment_mixed_interval_day'] ) ) {
525 - $interval_type = 'day';
526 - } elseif ( ! empty( $fields['better_payment_mixed_interval_week'] ) ) {
527 - $interval_type = 'week';
528 - } elseif ( ! empty( $fields['better_payment_mixed_interval_month'] ) ) {
529 - $interval_type = 'month';
530 - } elseif ( ! empty( $fields['better_payment_mixed_interval_year'] ) ) {
531 - $interval_type = 'year';
532 - }
533 - }
534 -
535 - $interval_type = in_array( $interval_type, $allowed_intervals, true ) ? $interval_type : '';
536 - $interval_count = 0;
537 -
538 - switch ( $interval_type ) {
539 - case 'day':
540 - $interval_count = ! empty( $fields['better_payment_mixed_interval_day'] ) ? absint( $fields['better_payment_mixed_interval_day'] ) : 0;
541 - break;
542 - case 'week':
543 - $interval_count = ! empty( $fields['better_payment_mixed_interval_week'] ) ? absint( $fields['better_payment_mixed_interval_week'] ) : 0;
544 - break;
545 - case 'year':
546 - $interval_count = ! empty( $fields['better_payment_mixed_interval_year'] ) ? absint( $fields['better_payment_mixed_interval_year'] ) : 0;
547 - break;
548 - case 'month':
549 - default:
550 - $interval_count = ! empty( $fields['better_payment_mixed_interval_month'] ) ? absint( $fields['better_payment_mixed_interval_month'] ) : 0;
551 - break;
552 - }
553 -
554 - if ( ! empty( $interval_count ) && ! empty( $interval_type ) ) {
555 - $mixed_interval_value = "{$interval_count}|{$interval_type}";
556 - }
557 - }
558 -
559 - if ( empty( $mixed_interval_value ) || false === strpos( $mixed_interval_value, '|' ) ) {
560 - return new \WP_Error( 'mixed_interval_missing', __( 'Recurring time period is required for mixed recurring payment.', 'better-payment' ) );
561 - }
562 -
563 - list( $interval_count_raw, $interval_raw ) = array_pad( explode( '|', $mixed_interval_value, 2 ), 2, '' );
564 - $interval_count = absint( $interval_count_raw );
565 - $interval = sanitize_key( $interval_raw );
566 -
567 - if ( empty( $interval_count ) || ! in_array( $interval, $allowed_intervals, true ) ) {
568 - return new \WP_Error( 'mixed_interval_invalid', __( 'Invalid recurring time period selected for mixed payment.', 'better-payment' ) );
569 - }
570 -
571 - $selection_data['interval_count'] = $interval_count;
572 - $selection_data['interval'] = $interval;
573 -
574 - return $selection_data;
575 - }
576 -
577 - /**
578 - * Create a runtime Stripe recurring Price (and Product) for mixed recurring payments.
579 - *
580 - * @param array $header_info
581 - * @param float $amount
582 - * @param string $currency
583 - * @param string $order_id
584 - * @param string $widget_id
585 - * @param array $selection_data
586 - * @param array $el_settings
587 - *
588 - * @return array|\WP_Error
589 - */
590 - private function create_mixed_recurring_stripe_price( $header_info = [], $amount = 0, $currency = '', $order_id = '', $widget_id = '', $selection_data = [], $el_settings = [] ) {
591 - $unit_amount = (int) round( floatval( $amount ) * 100 );
592 - if ( $unit_amount < 1 ) {
593 - return new \WP_Error( 'mixed_amount_invalid', __( 'Payment Amount field is required!', 'better-payment' ) );
594 - }
595 -
596 - $product_id = ! empty( $el_settings['better_payment_one_time_recurring_product_id'] )
597 - ? sanitize_text_field( $el_settings['better_payment_one_time_recurring_product_id'] )
598 - : '';
599 -
600 - if ( empty( $product_id ) ) {
601 - return new \WP_Error(
602 - 'mixed_product_id_missing',
603 - __( 'Stripe Product ID is required for mixed recurring payments. Please provide it in the widget settings.', 'better-payment' )
604 - );
605 - }
606 -
607 - $interval = ! empty( $selection_data['interval'] ) ? sanitize_key( $selection_data['interval'] ) : 'month';
608 - $interval_count = ! empty( $selection_data['interval_count'] ) ? absint( $selection_data['interval_count'] ) : 1;
609 - $currency = ! empty( $currency ) ? strtolower( sanitize_text_field( $currency ) ) : 'usd';
610 -
611 - $form_name = ! empty( $el_settings['better_payment_form_title'] ) ? sanitize_text_field( $el_settings['better_payment_form_title'] ) : __( 'Better Payment', 'better-payment' );
612 -
613 - $interval_label = $interval;
614 - if ( $interval_count > 1 ) {
615 - $interval_label = $interval_count . ' ' . $interval . 's';
616 - }
617 -
618 - $common_metadata = [
619 - 'order_id' => sanitize_text_field( $order_id ),
620 - 'widget_id' => sanitize_text_field( $widget_id ),
621 - 'is_mixed_payment' => '1',
622 - 'mixed_interval' => $interval,
623 - 'mixed_interval_count' => strval( $interval_count ),
624 - ];
625 -
626 - $price_request = [
627 - 'currency' => $currency,
628 - 'unit_amount' => $unit_amount,
629 - 'recurring' => [
630 - 'interval' => $interval,
631 - 'interval_count' => $interval_count,
632 - ],
633 - 'product' => $product_id,
634 - 'nickname' => sprintf( '%s - %s', $form_name, $interval_label ),
635 - 'metadata' => $common_metadata,
636 - ];
637 -
638 - $response = wp_safe_remote_post(
639 - 'https://api.stripe.com/v1/prices',
640 - [
641 - 'method' => 'POST',
642 - 'headers' => $header_info,
643 - 'body' => $price_request,
644 - 'timeout' => 70,
645 - ]
646 - );
647 -
648 - if ( is_wp_error( $response ) ) {
649 - return new \WP_Error( 'mixed_price_request_failed', $response->get_error_message() );
650 - }
651 -
652 - $response_body = wp_remote_retrieve_body( $response );
653 - $response_ar = ! empty( $response_body ) ? json_decode( $response_body ) : null;
654 - $error_message = ! empty( $response_ar->error->message ) ? sanitize_text_field( $response_ar->error->message ) : __( 'Unable to create recurring Stripe price for mixed payment.', 'better-payment' );
655 -
656 - if ( empty( $response_ar ) || empty( $response_ar->id ) ) {
657 - return new \WP_Error( 'mixed_price_create_failed', $error_message );
658 - }
659 -
660 - return [
661 - 'price_id' => sanitize_text_field( $response_ar->id ),
662 - 'product_id' => $product_id,
663 - ];
664 - }
665 -
666 - /**
667 440 * Get the paystack token
668 441 *
669 442 * @since 0.0.1
670 443 */
@@ -689,11 +462,8 @@
689 462
690 463 $page_id = intval( $_POST[ 'setting_data' ]['page_id'] );
691 464 $widget_id = sanitize_text_field( $_POST[ 'setting_data' ]['widget_id'] );
692 465 $el_settings = $this->better_payment_widget_settings( $page_id, $widget_id );
693 - $is_layout_6 = ! empty( $el_settings[ 'better_payment_form_layout' ] ) && 'layout-6-pro' === $el_settings[ 'better_payment_form_layout' ];
694 - $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' ];
695 - $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' ] );
696 466
697 467 if ( empty( $el_settings ) ) {
698 468 wp_send_json_error( esc_html(__( 'Setting Data is missing', 'better-payment' )) );
699 469 }
@@ -701,27 +471,32 @@
701 471 if ( empty( $el_settings[ 'better_payment_paystack_public_key' ] ) || empty( $el_settings[ 'better_payment_paystack_secret_key' ] ) ) {
702 472 wp_send_json_error( esc_html(__( 'Paystack Key missing', 'better-payment' )) );
703 473 }
704 474
705 - $amount = isset($_POST[ 'fields' ][ 'primary_payment_amount' ]) ? floatval($_POST[ 'fields' ][ 'primary_payment_amount' ]) : 0;
475 + // Amount, quantity, currency and campaign come from the stored settings, never
476 + // from the request — see PaymentRequestGuard.
477 + $guarded = ( new PaymentRequestGuard() )->resolve_form_payment(
478 + $el_settings,
479 + isset( $_POST['fields'] ) && is_array( $_POST['fields'] ) ? wp_unslash( $_POST['fields'] ) : [],
480 + [
481 + 'page_id' => $page_id,
482 + 'gateway' => 'paystack',
483 + ]
484 + );
706 485
707 - if ( empty( $_POST[ 'fields' ][ 'primary_payment_amount' ] ) && ! empty( $_POST[ 'fields' ][ 'primary_payment_amount_radio' ] ) ) {
708 - $amount = floatval($_POST[ 'fields' ][ 'primary_payment_amount_radio' ]);
486 + if ( is_wp_error( $guarded ) ) {
487 + wp_send_json_error( esc_html( $guarded->get_error_message() ) );
709 488 }
710 489
711 - $amount_quantity = ! empty( $_POST['fields']['payment_amount_quantity'] ) ? intval( $_POST['fields']['payment_amount_quantity'] ) : '';
712 - if ( $is_woo_layout ) {
713 - $amount_quantity = 1;
714 - }
490 + $amount = $guarded['amount'];
491 + $amount_quantity = $guarded['quantity'];
715 492
716 - $amount = ! empty( $amount_quantity ) ? $amount * $amount_quantity : $amount;
717 -
718 493 $header_info = array(
719 494 'Authorization' => 'Bearer ' . sanitize_text_field( $el_settings[ 'better_payment_paystack_secret_key' ] ),
720 495 "Cache-Control: no-cache",
721 496 );
722 497
723 - $order_id = 'paystack_' . uniqid();
498 + $order_id = Handler::new_order_id( 'paystack' );
724 499
725 500 $el_settings_currency = $el_settings[ 'better_payment_form_currency' ];
726 501 $woo_product_id = !empty($el_settings["better_payment_form_woocommerce_product_id"]) ? intval($el_settings["better_payment_form_woocommerce_product_id"]) : 0;
727 502 $woo_product_ids = !empty($el_settings["better_payment_form_woocommerce_product_ids"]) ? $el_settings["better_payment_form_woocommerce_product_ids"] : [0];
@@ -727,15 +502,9 @@
727 502 $woo_product_ids = !empty($el_settings["better_payment_form_woocommerce_product_ids"]) ? $el_settings["better_payment_form_woocommerce_product_ids"] : [0];
728 503 $fluentcart_product_id = !empty($el_settings["better_payment_form_fluentcart_product_id"]) ? intval($el_settings["better_payment_form_fluentcart_product_id"]) : 0;
729 504 $fluentcart_product_ids = !empty($el_settings["better_payment_form_fluentcart_product_ids"]) ? $el_settings["better_payment_form_fluentcart_product_ids"] : [0];
730 505
731 - if(!empty($settings['better_payment_form_currency_use_woocommerce']) && 'yes' === $el_settings['better_payment_form_currency_use_woocommerce'] &&
732 - !empty($settings['better_payment_form_currency_woocommerce'])){
733 - $el_settings_currency = $el_settings['better_payment_form_currency_woocommerce'];
734 - }
735 - if( !empty($_POST['fields'][ 'campaign_currency' ]) ) {
736 - $el_settings_currency = sanitize_text_field( $_POST['fields'][ 'campaign_currency' ] );
737 - }
506 + $el_settings_currency = $guarded['currency'];
738 507
739 508 $el_settings_currency_symbol = $this->get_currency_symbol( esc_html($el_settings_currency) );
740 509
741 510 $redirection_url_success = get_permalink( $page_id );
@@ -776,13 +545,14 @@
776 545 'fluentcart_product_id' => $fluentcart_product_id,
777 546 'fluentcart_product_ids' => maybe_serialize( $fluentcart_product_ids ),
778 547 'source' => 'paystack',
779 548 'amount_quantity' => ! empty( $amount_quantity ) ? $amount_quantity : '',
780 - 'is_woo_layout' => $is_woo_layout,
781 - 'is_fluentcart_layout' => $is_fluentcart_layout,
782 549 'detailed_product_info' => maybe_serialize( $detailed_product_info ),
783 550 ];
784 551
552 + // Fields only Pro's forms store (layout 6's platform flags) — see PaymentEntitlement.
553 + $better_form_fields = PaymentEntitlement::transaction_fields( $better_form_fields, $el_settings );
554 +
785 555 $better_form_fields = array_merge( $better_form_fields, $this->fetch_better_form_fields($el_settings, $_POST['fields']) );
786 556
787 557 if ( !empty( $better_form_fields['primary_email'] ) ) {
788 558 $request[ 'email' ] = $better_form_fields['primary_email'];
@@ -822,9 +592,9 @@
822 592
823 593 wp_send_json_error( $error_message );
824 594 }
825 595
826 - $campaign_id = ! empty( $_POST['fields']['campaign_id'] ) ? sanitize_text_field( $_POST['fields']['campaign_id'] ) : '';
596 + $campaign_id = $guarded['campaign_id'];
827 597
828 598 Handler::payment_create(
829 599 [
830 600 'amount' => $amount,
@@ -830,9 +600,10 @@
830 600 'amount' => $amount,
831 601 'order_id' => $order_id,
832 602 'payment_date' => date( 'Y-m-d H:i:s' ),
833 603 'source' => 'paystack',
834 - 'transaction_id' => '',
604 + // Paystack's reference, so verification can bind it to this order.
605 + 'transaction_id' => ! empty( $response_ar->data->reference ) ? sanitize_text_field( $response_ar->data->reference ) : '',
835 606 'customer_info' => maybe_serialize( $response_ar ),
836 607 'form_fields_info' => maybe_serialize( $better_form_fields ),
837 608 'status' => 'unpaid',
838 609 'currency' => $el_settings_currency,
@@ -936,8 +707,14 @@
936 707 *
937 708 * @since 1.0.0
938 709 */
939 710 public function better_payment_widget_settings( $page_id, $widget_id ) {
711 + // page_id is the visitor's to choose. A form on a page they cannot see — draft,
712 + // private, scheduled, password-protected — must be neither payable nor probeable.
713 + if ( ! $this->is_payable_form_page( $page_id ) ) {
714 + return [];
715 + }
716 +
940 717 $settings = $this->get_elementor_widget_settings( $page_id, $widget_id );
941 718
942 719 return $settings;
943 720 }
@@ -968,26 +745,10 @@
968 745 $post_data_primary_email = '';
969 746
970 747 $post_fields = $post_data_form_fields;
971 748
972 - $layout = ! empty( $el_settings['better_payment_form_layout'] ) ? sanitize_text_field( $el_settings['better_payment_form_layout'] ) : 'layout-1';
973 -
974 - switch( $layout ) {
975 - case 'layout-4-pro':
976 - $el_settings['better_payment_form_fields'] = $el_settings['better_payment_form_fields_layout_4_5_6'];
977 - break;
978 -
979 - case 'layout-5-pro':
980 - $el_settings['better_payment_form_fields'] = $el_settings['better_payment_form_fields_layout_4_5_6_desc'];
981 - break;
982 -
983 - case 'layout-6-pro':
984 - $el_settings['better_payment_form_fields'] = $el_settings['better_payment_form_fields_layout_4_5_6_woo'];
985 - break;
986 -
987 - default:
988 - break;
989 - }
749 + // The fields repeater a Pro form renders is Pro's to name — see PaymentEntitlement.
750 + $el_settings['better_payment_form_fields'] = PaymentEntitlement::form_fields( is_array( $el_settings ) ? $el_settings : [] );
990 751
991 752 //in case of duplicate post name using the first one
992 753 if(isset($el_settings['better_payment_form_fields']) && count($el_settings['better_payment_form_fields'])){
993 754 foreach ($el_settings['better_payment_form_fields'] as $item) {