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 +115 -362 2.2.2 → 2.4.1 View file →
@@ -46,11 +46,11 @@
46 46 public function paypal_form_handle() {
47 47
48 48 check_admin_referer( 'better-payment-paypal', 'security' );
49 49
50 - if ( !empty( $_POST[ 'better_payment_page_id' ] ) ) {
51 - $page_id = intval( $_POST[ 'better_payment_page_id' ], 10 );
52 - } else {
50 + $page_id = ! empty( $_POST[ 'better_payment_page_id' ] ) ? intval( $_POST[ 'better_payment_page_id' ], 10 ) : 0;
51 +
52 + if ( ! $page_id ) {
53 53 $this->redirect_previous_page();
54 54 }
55 55
56 56 if ( !empty( $_POST[ 'better_payment_widget_id' ] ) ) {
@@ -79,36 +79,32 @@
79 79 $woo_product_id = !empty($el_settings["better_payment_form_woocommerce_product_id"]) ? intval($el_settings["better_payment_form_woocommerce_product_id"]) : 0;
80 80 $woo_product_ids = !empty($el_settings["better_payment_form_woocommerce_product_ids"]) ? $el_settings["better_payment_form_woocommerce_product_ids"] : [0];
81 81 $fluentcart_product_id = !empty($el_settings["better_payment_form_fluentcart_product_id"]) ? intval($el_settings["better_payment_form_fluentcart_product_id"]) : 0;
82 82 $fluentcart_product_ids = !empty($el_settings["better_payment_form_fluentcart_product_ids"]) ? $el_settings["better_payment_form_fluentcart_product_ids"] : [0];
83 - $is_layout_6 = ! empty( $el_settings[ 'better_payment_form_layout' ] ) && 'layout-6-pro' === $el_settings[ 'better_payment_form_layout' ];
84 - $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' ];
85 - $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' ] );
86 83
87 - if(!empty($el_settings['better_payment_form_currency_use_woocommerce']) && 'yes' === $el_settings['better_payment_form_currency_use_woocommerce'] &&
88 - !empty($el_settings['better_payment_form_currency_woocommerce'])){
89 - $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;
90 98 }
91 - if( !empty($_POST[ 'campaign_currency' ]) ) {
92 - $el_settings_currency = sanitize_text_field( $_POST[ 'campaign_currency' ] );
93 - }
94 99
100 + $el_settings_currency = $guarded['currency'];
95 101 $el_settings_currency_symbol = $this->get_currency_symbol( esc_html($el_settings_currency) );
96 102
97 - $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'];
98 105
99 - if( empty( $_POST['primary_payment_amount'] ) && ! empty( $_POST['primary_payment_amount_radio'] ) ){
100 - $primary_payment_amount = floatval( $_POST['primary_payment_amount_radio'] );
101 - }
102 -
103 - $primary_payment_amount_quantity = ! empty( $_POST['payment_amount_quantity'] ) ? intval( $_POST[ 'payment_amount_quantity' ] ) : '';
104 - if ( $is_woo_layout ) {
105 - $primary_payment_amount_quantity = 1;
106 - }
107 -
108 - $primary_payment_amount = ! empty( $primary_payment_amount_quantity ) ? $primary_payment_amount * $primary_payment_amount_quantity : $primary_payment_amount;
109 -
110 - $order_id = 'paypal_' . uniqid();
106 + $order_id = Handler::new_order_id( 'paypal' );
111 107 $request_data = [
112 108 'business' => $el_settings[ 'better_payment_paypal_business_email' ],
113 109 'currency_code' => $el_settings_currency,
114 110 'rm' => '2',
@@ -138,16 +134,19 @@
138 134 'fluentcart_product_id' => $fluentcart_product_id,
139 135 'fluentcart_product_ids' => maybe_serialize( $fluentcart_product_ids ),
140 136 'source' => 'paypal',
141 137 'amount_quantity' => ! empty( $primary_payment_amount_quantity ) ? intval( $primary_payment_amount_quantity ) : '',
142 - 'is_woo_layout' => $is_woo_layout,
143 - 'is_fluentcart_layout' => $is_fluentcart_layout,
144 138 'detailed_product_info' => maybe_serialize( $detailed_product_info ),
145 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',
146 142 // Persisted so the async PayPal IPN can rebuild the e-mail body + on/off toggle.
147 143 'email_settings' => Handler::extract_email_settings( $el_settings ),
148 144 ];
149 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 +
150 149 $better_form_fields = array_merge( $better_form_fields, $this->fetch_better_form_fields($el_settings, $_POST) );
151 150
152 151 if ( !empty( $better_form_fields[ 'primary_first_name' ] ) ) {
153 152 $request_data[ 'primary_first_name' ] = sanitize_text_field( $better_form_fields[ 'primary_first_name' ] );
@@ -164,9 +163,9 @@
164 163 if ( !empty( $better_form_fields[ 'primary_reference_number' ] ) ) {
165 164 $request_data[ 'invoice' ] = sanitize_text_field( $better_form_fields[ 'primary_reference_number' ] );
166 165 }
167 166
168 - $campaign_id = ! empty( $_POST['campaign_id'] ) ? sanitize_text_field( $_POST['campaign_id'] ) : '';
167 + $campaign_id = $guarded['campaign_id'];
169 168
170 169 Handler::payment_create(
171 170 [
172 171 'amount' => floatval( $primary_payment_amount ),
@@ -211,38 +210,39 @@
211 210 wp_send_json_error( esc_html($err_msg) );
212 211 }
213 212
214 213 $el_settings = $this->better_payment_widget_settings( $page_id, $widget_id );
215 - $is_layout_6 = ! empty( $el_settings[ 'better_payment_form_layout' ] ) && 'layout-6-pro' === $el_settings[ 'better_payment_form_layout' ];
216 - $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' ];
217 - $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' ] );
218 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 +
219 220 $better_payment_keys = [
220 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' ] ),
221 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' ] ),
222 223 ];
223 224
224 - if ( empty( $el_settings ) ) {
225 - wp_send_json_error( esc_html(__( 'Setting Data is missing', 'better-payment' )) );
226 - }
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 + );
227 236
228 - $is_payment_split_payment = ! empty( $el_settings["better_payment_form_payment_type"] ) && 'split-payment' === $el_settings["better_payment_form_payment_type"];
229 -
230 - $amount = isset($_POST['fields']['primary_payment_amount']) ? floatval($_POST['fields']['primary_payment_amount']) : 0;
231 -
232 - if ( empty( $_POST['fields']['primary_payment_amount'] ) && ! empty( $_POST['fields']['primary_payment_amount_radio'] ) ) {
233 - $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() ) );
234 239 }
235 240
236 - $amount_quantity = ! empty( $_POST['fields']['payment_amount_quantity'] ) ? intval( $_POST['fields']['payment_amount_quantity'] ) : '';
237 - $product_quantities = ! empty( $_POST['fields']['payment_amount_quantity'] ) && is_array( $_POST['fields']['payment_amount_quantity'] ) ? array_map( 'intval', $_POST['fields']['payment_amount_quantity'] ) : [];
238 -
239 - if ( $is_woo_layout || $is_fluentcart_layout ) {
240 - $amount_quantity = 1;
241 - }
241 + $amount = $guarded['amount'];
242 + $amount_quantity = $guarded['quantity'];
243 + $product_quantities = $guarded['product_quantities'];
242 244
243 - $amount = ! empty( $amount_quantity ) ? $amount * $amount_quantity : $amount;
244 -
245 245 if ( empty( $better_payment_keys['public_key'] ) || empty( $better_payment_keys['secret_key'] ) ) {
246 246 wp_send_json_error( esc_html(__( 'Stripe Key missing', 'better-payment' )) );
247 247 }
248 248
@@ -250,9 +250,9 @@
250 250 'Authorization' => 'Basic ' . base64_encode( sanitize_text_field( $better_payment_keys['secret_key'] ) . ':' ),
251 251 'Stripe-Version' => '2019-05-16'
252 252 );
253 253
254 - $order_id = 'stripe_' . uniqid();
254 + $order_id = Handler::new_order_id( 'stripe' );
255 255
256 256 $el_settings_currency = $el_settings[ 'better_payment_form_currency' ];
257 257 $woo_product_id = !empty($el_settings["better_payment_form_woocommerce_product_id"]) ? intval($el_settings["better_payment_form_woocommerce_product_id"]) : 0;
258 258 $woo_product_ids = !empty($el_settings["better_payment_form_woocommerce_product_ids"]) ? $el_settings["better_payment_form_woocommerce_product_ids"] : [0];
@@ -258,15 +258,9 @@
258 258 $woo_product_ids = !empty($el_settings["better_payment_form_woocommerce_product_ids"]) ? $el_settings["better_payment_form_woocommerce_product_ids"] : [0];
259 259 $fluentcart_product_id = !empty($el_settings["better_payment_form_fluentcart_product_id"]) ? intval($el_settings["better_payment_form_fluentcart_product_id"]) : 0;
260 260 $fluentcart_product_ids = !empty($el_settings["better_payment_form_fluentcart_product_ids"]) ? $el_settings["better_payment_form_fluentcart_product_ids"] : [0];
261 261
262 - if(!empty($el_settings['better_payment_form_currency_use_woocommerce']) && 'yes' === $el_settings['better_payment_form_currency_use_woocommerce'] &&
263 - !empty($el_settings['better_payment_form_currency_woocommerce'])){
264 - $el_settings_currency = $el_settings['better_payment_form_currency_woocommerce'];
265 - }
266 - if( !empty($_POST['fields'][ 'campaign_currency' ]) ) {
267 - $el_settings_currency = sanitize_text_field( $_POST['fields'][ 'campaign_currency' ] );
268 - }
262 + $el_settings_currency = $guarded['currency'];
269 263
270 264 $el_settings_currency_symbol = $this->get_currency_symbol( esc_html($el_settings_currency) );
271 265
272 266 $request = [
@@ -303,46 +297,21 @@
303 297 ]
304 298 ]
305 299 ];
306 300
307 - $coupon_code = ! empty( $_POST['fields']['primary_coupon_code'] ) ? sanitize_text_field( $_POST['fields']['primary_coupon_code'] ) : '';
308 - if ( !empty( $coupon_code ) ) {
309 - $request['discounts'] = [
310 - [
311 - 'coupon' => $coupon_code
312 - ]
313 - ];
314 - }
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'] );
315 307
316 - $is_payment_recurring = ! empty( $_POST['fields']['better_payment_recurring_mode'] ) && 'subscription' === sanitize_text_field( $_POST['fields']['better_payment_recurring_mode'] );
317 - $recurring_price_id = ! empty( $_POST['fields']['better_payment_recurring_price_id'] ) ? sanitize_text_field( $_POST['fields']['better_payment_recurring_price_id'] ) : '';
318 - $mixed_payment_selection_data = $this->get_mixed_payment_selection_data( $el_settings, $_POST['fields'] );
319 -
320 - if ( is_wp_error( $mixed_payment_selection_data ) ) {
321 - 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() ) );
322 310 }
323 311
324 - $is_payment_mixed_recurring = ! empty( $mixed_payment_selection_data['is_mixed_recurring'] );
325 - $mixed_dynamic_recurring_price = [];
326 -
327 - if ( $is_payment_mixed_recurring ) {
328 - $mixed_dynamic_recurring_price = $this->create_mixed_recurring_stripe_price(
329 - $header_info,
330 - $amount,
331 - $el_settings_currency,
332 - $order_id,
333 - $widget_id,
334 - $mixed_payment_selection_data,
335 - $el_settings
336 - );
337 -
338 - if ( is_wp_error( $mixed_dynamic_recurring_price ) ) {
339 - wp_send_json_error( $mixed_dynamic_recurring_price->get_error_message() );
340 - }
341 -
342 - // Reuse existing recurring checkout flow with a runtime-generated recurring Stripe Price.
343 - $is_payment_recurring = true;
344 - $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 ];
345 314 }
346 315
347 316 $product_ids = [
348 317 'woo_product_ids' => $woo_product_ids,
@@ -361,29 +330,17 @@
361 330 'fluentcart_product_id' => $fluentcart_product_id,
362 331 'fluentcart_product_ids' => maybe_serialize( $fluentcart_product_ids ),
363 332 'source' => 'stripe',
364 333 'amount_quantity' => ! empty( $amount_quantity ) ? $amount_quantity : '',
365 - 'is_woo_layout' => $is_woo_layout,
366 - 'is_fluentcart_layout' => $is_fluentcart_layout,
367 334 'stripe_coupon_code' => $coupon_code,
368 335 'detailed_product_info' => maybe_serialize( $detailed_product_info ),
369 336 ];
370 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 +
371 341 $better_form_fields = array_merge( $better_form_fields, $this->fetch_better_form_fields($el_settings, $_POST['fields']) );
372 342
373 - if ( ! empty( $mixed_payment_selection_data['is_payment_mixed'] ) ) {
374 - $better_form_fields['is_payment_mixed'] = 1;
375 - $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';
376 - }
377 -
378 - if ( $is_payment_mixed_recurring ) {
379 - $better_form_fields['mixed_payment_selected_type'] = 'recurring';
380 - $better_form_fields['mixed_payment_recurring_interval_count'] = ! empty( $mixed_payment_selection_data['interval_count'] ) ? absint( $mixed_payment_selection_data['interval_count'] ) : 1;
381 - $better_form_fields['mixed_payment_recurring_interval'] = ! empty( $mixed_payment_selection_data['interval'] ) ? sanitize_text_field( $mixed_payment_selection_data['interval'] ) : 'month';
382 - $better_form_fields['mixed_payment_dynamic_price_id'] = ! empty( $mixed_dynamic_recurring_price['price_id'] ) ? sanitize_text_field( $mixed_dynamic_recurring_price['price_id'] ) : '';
383 - $better_form_fields['mixed_payment_dynamic_product_id'] = ! empty( $mixed_dynamic_recurring_price['product_id'] ) ? sanitize_text_field( $mixed_dynamic_recurring_price['product_id'] ) : '';
384 - }
385 -
386 343 if ( !empty( $better_form_fields['primary_first_name'] ) ) {
387 344 $request[ 'customer_name' ] = $better_form_fields['primary_first_name'];
388 345 }
389 346
@@ -403,41 +360,34 @@
403 360 $request[ 'payment_intent_data' ][ 'metadata' ][ 'customer_email' ] = $request[ 'customer_email' ];
404 361
405 362 }
406 363
407 - if ( $is_payment_split_payment ) {
408 - $installment_price_id = ! empty($_POST['fields']['split_payment_installment']) ? sanitize_text_field($_POST['fields']['split_payment_installment']) : '';
409 - $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 + );
410 381
411 - if ( is_array( $split_payment_installments_data ) && count( $split_payment_installments_data ) ){
412 - foreach( $split_payment_installments_data as $split_payment_installment_data ){
413 - $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' ] ) : '';
414 -
415 - if ( $installment_price_id === $item_price_id ) {
416 - $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' ] ) : '';
417 - break;
418 - }
419 - }
382 + if ( is_wp_error( $checkout ) ) {
383 + wp_send_json_error( esc_html( $checkout->get_error_message() ) );
420 384 }
421 385
422 - $better_form_fields['is_payment_split_payment'] = 1;
423 - $better_form_fields['split_payment_installment_price_id'] = $installment_price_id;
424 - $better_form_fields['split_payment_total_amount_price_id'] = $recurring_price_id;
425 - $better_form_fields['split_payment_installment_iteration'] = ( !empty( $installment_price_id ) && !empty( $installment_price_iteration ) ) ? $installment_price_iteration : 1;
426 - $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'];
427 388 }
428 -
429 - if ( $is_payment_recurring || $is_payment_split_payment ) {
430 - $request['mode'] = 'subscription';
431 - unset($request['line_items'][0]);
432 - unset($request['payment_intent_data']);
433 - $request['line_items'][0]['price'] = $is_payment_split_payment && ! empty( $installment_price_id ) ? $installment_price_id : $recurring_price_id;
434 - $request['line_items'][0]['quantity'] = 1;
435 389
436 - $better_form_fields['mode'] = $request['mode'];
437 - $better_form_fields['recurring_price_id'] = $request['line_items'][0]['price'];
438 - }
439 -
440 390 #ToDo: Need to prefill name field on stripe checkout
441 391 $response = wp_safe_remote_post(
442 392 'https://api.stripe.com/v1/checkout/sessions',
443 393 array(
@@ -450,9 +400,9 @@
450 400
451 401 $response_ar = json_decode( $response[ 'body' ] );
452 402
453 403 if ( ! empty( $response_ar->payment_intent ) || ( ! empty( $response_ar->mode ) && 'subscription' === $response_ar->mode ) ) {
454 - $campaign_id = ! empty( $_POST['fields']['campaign_id'] ) ? sanitize_text_field( $_POST['fields']['campaign_id'] ) : '';
404 + $campaign_id = $guarded['campaign_id'];
455 405
456 406 Handler::payment_create(
457 407 [
458 408 'amount' => $amount,
@@ -486,193 +436,8 @@
486 436 }
487 437 }
488 438
489 439 /**
490 - * Parse mixed payment selection payload.
491 - *
492 - * @param array $el_settings
493 - * @param array $fields
494 - *
495 - * @return array|\WP_Error
496 - */
497 - private function get_mixed_payment_selection_data( $el_settings = [], $fields = [] ) {
498 - $selection_data = [
499 - 'is_payment_mixed' => 0,
500 - 'selected_type' => 'one-time',
501 - 'is_mixed_recurring' => 0,
502 - 'interval_count' => 1,
503 - 'interval' => 'month',
504 - ];
505 -
506 - $is_payment_mixed = ! empty( $el_settings['better_payment_form_payment_type'] ) && 'mixed' === sanitize_text_field( $el_settings['better_payment_form_payment_type'] );
507 - if ( ! $is_payment_mixed ) {
508 - return $selection_data;
509 - }
510 -
511 - $selection_data['is_payment_mixed'] = 1;
512 - $selected_type = ! empty( $fields['better_payment_mixed_selected_type'] ) ? sanitize_text_field( $fields['better_payment_mixed_selected_type'] ) : '';
513 - if ( ! empty( $fields['payment_type'] ) && in_array( $fields['payment_type'], [ 'one-time', 'recurring' ], true ) ) {
514 - $selected_type = sanitize_text_field( $fields['payment_type'] );
515 - }
516 -
517 - $selected_type = in_array( $selected_type, [ 'one-time', 'recurring' ], true ) ? $selected_type : 'one-time';
518 - $selection_data['selected_type'] = $selected_type;
519 - $selection_data['is_mixed_recurring'] = 'recurring' === $selected_type ? 1 : 0;
520 -
521 - if ( ! $selection_data['is_mixed_recurring'] ) {
522 - return $selection_data;
523 - }
524 -
525 - $allowed_intervals = [ 'day', 'week', 'month', 'year' ];
526 - $mixed_interval_value = ! empty( $fields['better_payment_mixed_interval'] ) ? sanitize_text_field( $fields['better_payment_mixed_interval'] ) : '';
527 -
528 - if ( empty( $mixed_interval_value ) ) {
529 - $interval_type = ! empty( $fields['better_payment_mixed_interval_type'] ) ? sanitize_key( $fields['better_payment_mixed_interval_type'] ) : '';
530 -
531 - if ( empty( $interval_type ) ) {
532 - if ( ! empty( $fields['better_payment_mixed_interval_day'] ) ) {
533 - $interval_type = 'day';
534 - } elseif ( ! empty( $fields['better_payment_mixed_interval_week'] ) ) {
535 - $interval_type = 'week';
536 - } elseif ( ! empty( $fields['better_payment_mixed_interval_month'] ) ) {
537 - $interval_type = 'month';
538 - } elseif ( ! empty( $fields['better_payment_mixed_interval_year'] ) ) {
539 - $interval_type = 'year';
540 - }
541 - }
542 -
543 - $interval_type = in_array( $interval_type, $allowed_intervals, true ) ? $interval_type : '';
544 - $interval_count = 0;
545 -
546 - switch ( $interval_type ) {
547 - case 'day':
548 - $interval_count = ! empty( $fields['better_payment_mixed_interval_day'] ) ? absint( $fields['better_payment_mixed_interval_day'] ) : 0;
549 - break;
550 - case 'week':
551 - $interval_count = ! empty( $fields['better_payment_mixed_interval_week'] ) ? absint( $fields['better_payment_mixed_interval_week'] ) : 0;
552 - break;
553 - case 'year':
554 - $interval_count = ! empty( $fields['better_payment_mixed_interval_year'] ) ? absint( $fields['better_payment_mixed_interval_year'] ) : 0;
555 - break;
556 - case 'month':
557 - default:
558 - $interval_count = ! empty( $fields['better_payment_mixed_interval_month'] ) ? absint( $fields['better_payment_mixed_interval_month'] ) : 0;
559 - break;
560 - }
561 -
562 - if ( ! empty( $interval_count ) && ! empty( $interval_type ) ) {
563 - $mixed_interval_value = "{$interval_count}|{$interval_type}";
564 - }
565 - }
566 -
567 - if ( empty( $mixed_interval_value ) || false === strpos( $mixed_interval_value, '|' ) ) {
568 - return new \WP_Error( 'mixed_interval_missing', __( 'Recurring time period is required for mixed recurring payment.', 'better-payment' ) );
569 - }
570 -
571 - list( $interval_count_raw, $interval_raw ) = array_pad( explode( '|', $mixed_interval_value, 2 ), 2, '' );
572 - $interval_count = absint( $interval_count_raw );
573 - $interval = sanitize_key( $interval_raw );
574 -
575 - if ( empty( $interval_count ) || ! in_array( $interval, $allowed_intervals, true ) ) {
576 - return new \WP_Error( 'mixed_interval_invalid', __( 'Invalid recurring time period selected for mixed payment.', 'better-payment' ) );
577 - }
578 -
579 - $selection_data['interval_count'] = $interval_count;
580 - $selection_data['interval'] = $interval;
581 -
582 - return $selection_data;
583 - }
584 -
585 - /**
586 - * Create a runtime Stripe recurring Price (and Product) for mixed recurring payments.
587 - *
588 - * @param array $header_info
589 - * @param float $amount
590 - * @param string $currency
591 - * @param string $order_id
592 - * @param string $widget_id
593 - * @param array $selection_data
594 - * @param array $el_settings
595 - *
596 - * @return array|\WP_Error
597 - */
598 - private function create_mixed_recurring_stripe_price( $header_info = [], $amount = 0, $currency = '', $order_id = '', $widget_id = '', $selection_data = [], $el_settings = [] ) {
599 - $unit_amount = (int) round( floatval( $amount ) * 100 );
600 - if ( $unit_amount < 1 ) {
601 - return new \WP_Error( 'mixed_amount_invalid', __( 'Payment Amount field is required!', 'better-payment' ) );
602 - }
603 -
604 - $product_id = ! empty( $el_settings['better_payment_one_time_recurring_product_id'] )
605 - ? sanitize_text_field( $el_settings['better_payment_one_time_recurring_product_id'] )
606 - : '';
607 -
608 - if ( empty( $product_id ) ) {
609 - return new \WP_Error(
610 - 'mixed_product_id_missing',
611 - __( 'Stripe Product ID is required for mixed recurring payments. Please provide it in the widget settings.', 'better-payment' )
612 - );
613 - }
614 -
615 - $interval = ! empty( $selection_data['interval'] ) ? sanitize_key( $selection_data['interval'] ) : 'month';
616 - $interval_count = ! empty( $selection_data['interval_count'] ) ? absint( $selection_data['interval_count'] ) : 1;
617 - $currency = ! empty( $currency ) ? strtolower( sanitize_text_field( $currency ) ) : 'usd';
618 -
619 - $form_name = ! empty( $el_settings['better_payment_form_title'] ) ? sanitize_text_field( $el_settings['better_payment_form_title'] ) : __( 'Better Payment', 'better-payment' );
620 -
621 - $interval_label = $interval;
622 - if ( $interval_count > 1 ) {
623 - $interval_label = $interval_count . ' ' . $interval . 's';
624 - }
625 -
626 - $common_metadata = [
627 - 'order_id' => sanitize_text_field( $order_id ),
628 - 'widget_id' => sanitize_text_field( $widget_id ),
629 - 'is_mixed_payment' => '1',
630 - 'mixed_interval' => $interval,
631 - 'mixed_interval_count' => strval( $interval_count ),
632 - ];
633 -
634 - $price_request = [
635 - 'currency' => $currency,
636 - 'unit_amount' => $unit_amount,
637 - 'recurring' => [
638 - 'interval' => $interval,
639 - 'interval_count' => $interval_count,
640 - ],
641 - 'product' => $product_id,
642 - 'nickname' => sprintf( '%s - %s', $form_name, $interval_label ),
643 - 'metadata' => $common_metadata,
644 - ];
645 -
646 - $response = wp_safe_remote_post(
647 - 'https://api.stripe.com/v1/prices',
648 - [
649 - 'method' => 'POST',
650 - 'headers' => $header_info,
651 - 'body' => $price_request,
652 - 'timeout' => 70,
653 - ]
654 - );
655 -
656 - if ( is_wp_error( $response ) ) {
657 - return new \WP_Error( 'mixed_price_request_failed', $response->get_error_message() );
658 - }
659 -
660 - $response_body = wp_remote_retrieve_body( $response );
661 - $response_ar = ! empty( $response_body ) ? json_decode( $response_body ) : null;
662 - $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' );
663 -
664 - if ( empty( $response_ar ) || empty( $response_ar->id ) ) {
665 - return new \WP_Error( 'mixed_price_create_failed', $error_message );
666 - }
667 -
668 - return [
669 - 'price_id' => sanitize_text_field( $response_ar->id ),
670 - 'product_id' => $product_id,
671 - ];
672 - }
673 -
674 - /**
675 440 * Get the paystack token
676 441 *
677 442 * @since 0.0.1
678 443 */
@@ -697,11 +462,8 @@
697 462
698 463 $page_id = intval( $_POST[ 'setting_data' ]['page_id'] );
699 464 $widget_id = sanitize_text_field( $_POST[ 'setting_data' ]['widget_id'] );
700 465 $el_settings = $this->better_payment_widget_settings( $page_id, $widget_id );
701 - $is_layout_6 = ! empty( $el_settings[ 'better_payment_form_layout' ] ) && 'layout-6-pro' === $el_settings[ 'better_payment_form_layout' ];
702 - $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' ];
703 - $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' ] );
704 466
705 467 if ( empty( $el_settings ) ) {
706 468 wp_send_json_error( esc_html(__( 'Setting Data is missing', 'better-payment' )) );
707 469 }
@@ -709,27 +471,32 @@
709 471 if ( empty( $el_settings[ 'better_payment_paystack_public_key' ] ) || empty( $el_settings[ 'better_payment_paystack_secret_key' ] ) ) {
710 472 wp_send_json_error( esc_html(__( 'Paystack Key missing', 'better-payment' )) );
711 473 }
712 474
713 - $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 + );
714 485
715 - if ( empty( $_POST[ 'fields' ][ 'primary_payment_amount' ] ) && ! empty( $_POST[ 'fields' ][ 'primary_payment_amount_radio' ] ) ) {
716 - $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() ) );
717 488 }
718 489
719 - $amount_quantity = ! empty( $_POST['fields']['payment_amount_quantity'] ) ? intval( $_POST['fields']['payment_amount_quantity'] ) : '';
720 - if ( $is_woo_layout ) {
721 - $amount_quantity = 1;
722 - }
490 + $amount = $guarded['amount'];
491 + $amount_quantity = $guarded['quantity'];
723 492
724 - $amount = ! empty( $amount_quantity ) ? $amount * $amount_quantity : $amount;
725 -
726 493 $header_info = array(
727 494 'Authorization' => 'Bearer ' . sanitize_text_field( $el_settings[ 'better_payment_paystack_secret_key' ] ),
728 495 "Cache-Control: no-cache",
729 496 );
730 497
731 - $order_id = 'paystack_' . uniqid();
498 + $order_id = Handler::new_order_id( 'paystack' );
732 499
733 500 $el_settings_currency = $el_settings[ 'better_payment_form_currency' ];
734 501 $woo_product_id = !empty($el_settings["better_payment_form_woocommerce_product_id"]) ? intval($el_settings["better_payment_form_woocommerce_product_id"]) : 0;
735 502 $woo_product_ids = !empty($el_settings["better_payment_form_woocommerce_product_ids"]) ? $el_settings["better_payment_form_woocommerce_product_ids"] : [0];
@@ -735,15 +502,9 @@
735 502 $woo_product_ids = !empty($el_settings["better_payment_form_woocommerce_product_ids"]) ? $el_settings["better_payment_form_woocommerce_product_ids"] : [0];
736 503 $fluentcart_product_id = !empty($el_settings["better_payment_form_fluentcart_product_id"]) ? intval($el_settings["better_payment_form_fluentcart_product_id"]) : 0;
737 504 $fluentcart_product_ids = !empty($el_settings["better_payment_form_fluentcart_product_ids"]) ? $el_settings["better_payment_form_fluentcart_product_ids"] : [0];
738 505
739 - if(!empty($settings['better_payment_form_currency_use_woocommerce']) && 'yes' === $el_settings['better_payment_form_currency_use_woocommerce'] &&
740 - !empty($settings['better_payment_form_currency_woocommerce'])){
741 - $el_settings_currency = $el_settings['better_payment_form_currency_woocommerce'];
742 - }
743 - if( !empty($_POST['fields'][ 'campaign_currency' ]) ) {
744 - $el_settings_currency = sanitize_text_field( $_POST['fields'][ 'campaign_currency' ] );
745 - }
506 + $el_settings_currency = $guarded['currency'];
746 507
747 508 $el_settings_currency_symbol = $this->get_currency_symbol( esc_html($el_settings_currency) );
748 509
749 510 $redirection_url_success = get_permalink( $page_id );
@@ -784,13 +545,14 @@
784 545 'fluentcart_product_id' => $fluentcart_product_id,
785 546 'fluentcart_product_ids' => maybe_serialize( $fluentcart_product_ids ),
786 547 'source' => 'paystack',
787 548 'amount_quantity' => ! empty( $amount_quantity ) ? $amount_quantity : '',
788 - 'is_woo_layout' => $is_woo_layout,
789 - 'is_fluentcart_layout' => $is_fluentcart_layout,
790 549 'detailed_product_info' => maybe_serialize( $detailed_product_info ),
791 550 ];
792 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 +
793 555 $better_form_fields = array_merge( $better_form_fields, $this->fetch_better_form_fields($el_settings, $_POST['fields']) );
794 556
795 557 if ( !empty( $better_form_fields['primary_email'] ) ) {
796 558 $request[ 'email' ] = $better_form_fields['primary_email'];
@@ -830,9 +592,9 @@
830 592
831 593 wp_send_json_error( $error_message );
832 594 }
833 595
834 - $campaign_id = ! empty( $_POST['fields']['campaign_id'] ) ? sanitize_text_field( $_POST['fields']['campaign_id'] ) : '';
596 + $campaign_id = $guarded['campaign_id'];
835 597
836 598 Handler::payment_create(
837 599 [
838 600 'amount' => $amount,
@@ -838,9 +600,10 @@
838 600 'amount' => $amount,
839 601 'order_id' => $order_id,
840 602 'payment_date' => date( 'Y-m-d H:i:s' ),
841 603 'source' => 'paystack',
842 - '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 ) : '',
843 606 'customer_info' => maybe_serialize( $response_ar ),
844 607 'form_fields_info' => maybe_serialize( $better_form_fields ),
845 608 'status' => 'unpaid',
846 609 'currency' => $el_settings_currency,
@@ -944,8 +707,14 @@
944 707 *
945 708 * @since 1.0.0
946 709 */
947 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 +
948 717 $settings = $this->get_elementor_widget_settings( $page_id, $widget_id );
949 718
950 719 return $settings;
951 720 }
@@ -976,26 +745,10 @@
976 745 $post_data_primary_email = '';
977 746
978 747 $post_fields = $post_data_form_fields;
979 748
980 - $layout = ! empty( $el_settings['better_payment_form_layout'] ) ? sanitize_text_field( $el_settings['better_payment_form_layout'] ) : 'layout-1';
981 -
982 - switch( $layout ) {
983 - case 'layout-4-pro':
984 - $el_settings['better_payment_form_fields'] = $el_settings['better_payment_form_fields_layout_4_5_6'];
985 - break;
986 -
987 - case 'layout-5-pro':
988 - $el_settings['better_payment_form_fields'] = $el_settings['better_payment_form_fields_layout_4_5_6_desc'];
989 - break;
990 -
991 - case 'layout-6-pro':
992 - $el_settings['better_payment_form_fields'] = $el_settings['better_payment_form_fields_layout_4_5_6_woo'];
993 - break;
994 -
995 - default:
996 - break;
997 - }
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 : [] );
998 751
999 752 //in case of duplicate post name using the first one
1000 753 if(isset($el_settings['better_payment_form_fields']) && count($el_settings['better_payment_form_fields'])){
1001 754 foreach ($el_settings['better_payment_form_fields'] as $item) {