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 / Classes / PaymentEntitlement.php

PaymentEntitlement.php in Better Payment – Instant Payments, Donations, Fundraising with Subscriptions & More 2.4.0, at includes/Classes/PaymentEntitlement.php

389 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Better_Payment\Lite\Classes;
4
5 use WP_Error;
6
7 if ( ! defined( 'ABSPATH' ) ) {
8 exit;
9 }
10
11 /**
12 * Which payment requests need Better Payment Pro, and who handles them.
13 *
14 * Pro has no payment handlers: its Payment Form layouts 4, 5 and 6 and its recurring,
15 * split-payment and mixed payment types post to Lite's handlers. Those handlers are
16 * `nopriv` and their nonce is printed on every public form, so a Pro form saved on a site
17 * without Pro — or a forged request naming one — would otherwise run Pro's payment types.
18 * This class is where that stops, for all nine handlers at once:
19 *
20 * - Without Pro, PaymentRequestGuard refuses a Pro form's payment, or a request posting a
21 * field only a Pro form renders (`bp_pro_required`), before any gateway is contacted or any
22 * transaction row is stored. A free layout is a one-time form on every install, whatever
23 * payment type its saved settings still hold.
24 * - With a Pro that ships the payment types itself (declares FEATURE on
25 * `better_payment/pro_provides`), each step is asked of Pro through a hook. A step that
26 * decides what is charged or how the checkout is built is refused
27 * (`bp_pro_payment_unhandled`) when no callback answers it; every other step keeps what
28 * Lite built.
29 * - With an older Pro that does not, Lite's transitional copy, ProPaymentTypes, answers.
30 *
31 * Hooks (each fires only while `better_payment/pro_enabled` is true):
32 * - `better_payment/payment_guard/resolve_pro_payment` (null, $base, $settings, $fields, $context, $guard)
33 * → resolution array | WP_Error. Nothing returned refuses the payment.
34 * - `better_payment/stripe_checkout/pro_request` (null, $checkout, $context)
35 * → [ 'request' => array, 'form_fields' => array ] | WP_Error. Nothing returned refuses it.
36 * - `better_payment/payment_form/pro_form_fields` ([], $settings) → the fields repeater a Pro form renders.
37 * - `better_payment/payment_form/pro_transaction_fields` ($form_fields_info, $settings) → the stored fields.
38 * - `better_payment/stripe_payment/pro_verification_args` ($args, $form_fields_info) → `expand` + recurring/split flags.
39 * - `better_payment/stripe_payment/pro_verified_session` ($details, $session, $args) → stored + returned details.
40 * - `better_payment/success_page/pro_rows` ([], $position, $settings, $payment, $context) → thank-you page rows.
41 * - `better_payment/email/pro_form_field_rows` ($rows, $context, $form_fields_info) → e-mail field rows.
42 *
43 * This class is permanent. When ProCompatibility::MIN_PRO_VERSION reaches 1.4.4, delete
44 * ProPaymentTypes and the lite_copy_applies() branch in answer(); the entitlement check
45 * and the hooks stay.
46 *
47 * @since 2.4.0
48 */
49 final class PaymentEntitlement {
50
51 /**
52 * Feature slug Pro declares on `better_payment/pro_provides` once it ships the payment types itself.
53 *
54 * One slug for every step: they share the markers written at checkout (`mode`,
55 * `is_payment_split_payment`, `is_payment_mixed`) and read back on return, so a Pro that
56 * built the checkout but not the verification would leave split payments uncapped.
57 */
58 const FEATURE = 'pro_payment_types';
59
60 /**
61 * Payment Form layouts only Pro renders.
62 */
63 const PRO_LAYOUTS = [ 'layout-4-pro', 'layout-5-pro', 'layout-6-pro' ];
64
65 /**
66 * Request fields only a Pro payment form renders: a subscription mode, a plan, an
67 * installment, and a mixed form's one-time/recurring choice and period.
68 */
69 const PRO_REQUEST_FIELDS = [
70 'better_payment_recurring_mode',
71 'better_payment_recurring_price_id',
72 'split_payment_installment',
73 'better_payment_mixed_selected_type',
74 'better_payment_mixed_interval',
75 'better_payment_mixed_interval_type',
76 'better_payment_mixed_interval_day',
77 'better_payment_mixed_interval_week',
78 'better_payment_mixed_interval_month',
79 'better_payment_mixed_interval_year',
80 ];
81
82 /**
83 * Whether a form is a Pro payment form: one of Pro's layouts (layout 6 is the product basket).
84 *
85 * Only the layout counts. The recurring, split-payment and mixed payment types exist only
86 * on Pro's layouts 4 and 5, while a free layout (1-3, or a block) can still carry a saved
87 * payment type from an earlier layout choice or from when Pro was active — Elementor keeps
88 * the saved value of a control nobody registers. That leftover means nothing: a free layout
89 * renders a one-time form and is charged as one.
90 *
91 * @param array $settings Stored form settings.
92 * @return bool
93 */
94 public static function is_pro_form( array $settings ): bool {
95 return in_array( PaymentRequestGuard::setting( $settings, 'better_payment_form_layout', '' ), self::PRO_LAYOUTS, true );
96 }
97
98 /**
99 * Whether a request posts a field only a Pro payment form renders.
100 *
101 * No free layout, block or Lite script posts these. On a free layout such a request is
102 * forged; on a free install it asks for a Pro payment.
103 *
104 * @param array $fields Submitted fields, unslashed.
105 * @return bool
106 */
107 public static function posts_pro_fields( array $fields ): bool {
108 foreach ( self::PRO_REQUEST_FIELDS as $key ) {
109 if ( '' !== PaymentRequestGuard::field( $fields, $key ) ) {
110 return true;
111 }
112 }
113
114 return false;
115 }
116
117 /**
118 * Whether Lite's transitional copy answers: an active Pro that does not ship the payment types.
119 *
120 * @return bool
121 */
122 public static function lite_copy_applies(): bool {
123 return ProCompatibility::lite_copy_applies( self::FEATURE );
124 }
125
126 /**
127 * The refusal of a Pro form's payment on a site without Pro.
128 *
129 * @return WP_Error
130 */
131 public static function refusal(): WP_Error {
132 return new WP_Error( 'bp_pro_required', __( 'This payment form requires Better Payment Pro, which is not active on this site.', 'better-payment' ) );
133 }
134
135 /**
136 * The refusal of a Pro form's payment nothing handled.
137 *
138 * @return WP_Error
139 */
140 public static function unhandled(): WP_Error {
141 return new WP_Error( 'bp_pro_payment_unhandled', __( 'This payment form cannot take payments right now. Please try again later.', 'better-payment' ) );
142 }
143
144 /**
145 * What a Pro form's request may charge. Called by PaymentRequestGuard::resolve_form_payment().
146 *
147 * The resolver answers the amount, quantity, price ids and coupon. The verified campaign
148 * and the currency are Lite's and are kept whatever it returns.
149 *
150 * @param PaymentRequestGuard $guard The guard, whose checks the resolver uses.
151 * @param array $base The guard's resolution so far (all keys, nothing charged).
152 * @param array $settings Stored form settings.
153 * @param array $fields Submitted fields, unslashed.
154 * @param array $context `page_id`, `gateway`, `campaign` (verified campaign or null).
155 * @return array|WP_Error
156 */
157 public static function resolve_form_payment( PaymentRequestGuard $guard, array $base, array $settings, array $fields, array $context ) {
158 if ( ! ProCompatibility::pro_enabled() ) {
159 return self::refusal();
160 }
161
162 $resolution = self::answer( 'better_payment/payment_guard/resolve_pro_payment', 'resolve_form_payment', [ null, $base, $settings, $fields, $context, $guard ] );
163
164 if ( is_wp_error( $resolution ) ) {
165 return $resolution;
166 }
167
168 if ( ! is_array( $resolution ) || ! isset( $resolution['amount'] ) || ! is_numeric( $resolution['amount'] )
169 || ! is_finite( (float) $resolution['amount'] ) || (float) $resolution['amount'] < 0 ) {
170 return self::unhandled();
171 }
172
173 $resolved = array_merge( $base, $resolution );
174
175 $resolved['campaign_id'] = $base['campaign_id'];
176 $resolved['currency'] = $base['currency'];
177
178 return $resolved;
179 }
180
181 /**
182 * The Stripe Checkout Session request for a Pro form. Called by the widget's Stripe handler.
183 *
184 * @param array $checkout `request` (the Checkout Session body) and `form_fields` (the transaction's stored fields).
185 * @param array $context `settings`, `fields` (unslashed), `guarded` (the guard's resolution),
186 * `order_id`, `widget_id`, `header_info` (Stripe request headers).
187 * @return array|WP_Error `request` and `form_fields`.
188 */
189 public static function stripe_checkout( array $checkout, array $context ) {
190 if ( ! ProCompatibility::pro_enabled() ) {
191 return self::refusal();
192 }
193
194 $result = self::answer( 'better_payment/stripe_checkout/pro_request', 'stripe_checkout', [ null, $checkout, $context ] );
195
196 if ( is_wp_error( $result ) ) {
197 return $result;
198 }
199
200 if ( ! is_array( $result ) || ! isset( $result['request'], $result['form_fields'] )
201 || ! is_array( $result['request'] ) || ! is_array( $result['form_fields'] ) ) {
202 return self::unhandled();
203 }
204
205 return [
206 'request' => $result['request'],
207 'form_fields' => $result['form_fields'],
208 ];
209 }
210
211 /**
212 * The fields repeater whose submitted values a transaction records.
213 *
214 * A free form's is `better_payment_form_fields`. A Pro form records none on a site
215 * without Pro (its payment was refused), and whatever Pro answers otherwise.
216 *
217 * @param array $settings Stored form settings.
218 * @return array
219 */
220 public static function form_fields( array $settings ): array {
221 if ( ! self::is_pro_form( $settings ) ) {
222 return PaymentRequestGuard::repeater( $settings, 'better_payment_form_fields' );
223 }
224
225 if ( ! ProCompatibility::pro_enabled() ) {
226 return [];
227 }
228
229 $form_fields = self::answer( 'better_payment/payment_form/pro_form_fields', 'form_fields', [ [], $settings ] );
230
231 return is_array( $form_fields ) ? $form_fields : [];
232 }
233
234 /**
235 * The fields a transaction stores, with any Pro adds (layout 6's platform flags).
236 *
237 * @param array $form_fields_info Fields the handler built.
238 * @param array $settings Stored form settings.
239 * @return array
240 */
241 public static function transaction_fields( array $form_fields_info, array $settings ): array {
242 if ( ! ProCompatibility::pro_enabled() ) {
243 return $form_fields_info;
244 }
245
246 $result = self::answer( 'better_payment/payment_form/pro_transaction_fields', 'transaction_fields', [ $form_fields_info, $settings ] );
247
248 return is_array( $result ) ? $result : $form_fields_info;
249 }
250
251 /**
252 * How a Stripe Checkout Session is retrieved on return, and whether it is a subscription.
253 *
254 * @param array $args `expand` (string[]), `is_payment_recurring`, `is_payment_split_payment`.
255 * @param array $form_fields_info The transaction's stored fields.
256 * @return array
257 */
258 public static function stripe_verification_args( array $args, array $form_fields_info ): array {
259 if ( ! ProCompatibility::pro_enabled() ) {
260 return $args;
261 }
262
263 $result = self::answer( 'better_payment/stripe_payment/pro_verification_args', 'stripe_verification_args', [ $args, $form_fields_info ] );
264
265 if ( ! is_array( $result ) || ! isset( $result['expand'] ) || ! is_array( $result['expand'] ) ) {
266 return $args;
267 }
268
269 $result['expand'] = array_values( array_filter( $result['expand'], 'is_string' ) );
270 $result['is_payment_recurring'] = ! empty( $result['is_payment_recurring'] );
271 $result['is_payment_split_payment'] = ! empty( $result['is_payment_split_payment'] );
272
273 return $result;
274 }
275
276 /**
277 * What a verified Stripe Checkout Session stores and returns beyond a one-time payment.
278 *
279 * @param array $details `form_fields_info`, `transaction_id`, `email`, `frontend_data` (extra keys for the thank-you page).
280 * @param object $session The retrieved Checkout Session.
281 * @param array $args stripe_verification_args().
282 * @return array
283 */
284 public static function stripe_verified_session( array $details, $session, array $args ): array {
285 if ( ! ProCompatibility::pro_enabled() ) {
286 return $details;
287 }
288
289 $result = self::answer( 'better_payment/stripe_payment/pro_verified_session', 'stripe_verified_session', [ $details, $session, $args ] );
290
291 if ( ! is_array( $result ) || ! array_key_exists( 'form_fields_info', $result )
292 || ! isset( $result['frontend_data'] ) || ! is_array( $result['frontend_data'] ) ) {
293 return $details;
294 }
295
296 return [
297 'form_fields_info' => $result['form_fields_info'],
298 'transaction_id' => isset( $result['transaction_id'] ) && is_scalar( $result['transaction_id'] ) ? (string) $result['transaction_id'] : $details['transaction_id'],
299 'email' => isset( $result['email'] ) && is_scalar( $result['email'] ) ? (string) $result['email'] : $details['email'],
300 'frontend_data' => $result['frontend_data'],
301 ];
302 }
303
304 /**
305 * Thank-you page rows only Pro's payment types show — asked only for a Pro form, so a free
306 * layout's leftover payment type never labels its thank-you page.
307 *
308 * @param string $position 'after_payment_method' | 'after_merchant_details'.
309 * @param mixed $settings Form settings.
310 * @param mixed $payment The verified payment (a `*_payment_success()` result).
311 * @param array $context `currency_symbol` (escaped).
312 * @return array[] `label` and `value`, both HTML.
313 */
314 public static function success_rows( $position, $settings, $payment, array $context ): array {
315 if ( ! ProCompatibility::pro_enabled() || ! is_array( $settings ) || ! self::is_pro_form( $settings ) ) {
316 return [];
317 }
318
319 $rows = self::answer(
320 'better_payment/success_page/pro_rows',
321 'success_rows',
322 [ [], (string) $position, $settings, is_array( $payment ) ? $payment : [], $context ]
323 );
324
325 $valid = [];
326
327 foreach ( is_array( $rows ) ? $rows : [] as $row ) {
328 if ( is_array( $row ) && isset( $row['label'], $row['value'] ) && is_scalar( $row['label'] ) && is_scalar( $row['value'] ) ) {
329 $valid[] = [
330 'label' => (string) $row['label'],
331 'value' => (string) $row['value'],
332 ];
333 }
334 }
335
336 return $valid;
337 }
338
339 /**
340 * The form-field rows a payment e-mail lists, after Pro hides or relabels its own fields.
341 *
342 * @param array[] $rows `key` (stored field key), `label`, `value`, in stored order.
343 * @param string $context 'summary' (the e-mail's From section) | 'all_fields' (`[bp-all-fields]`).
344 * @param mixed $form_fields_info The transaction's stored fields.
345 * @return array[]
346 */
347 public static function email_rows( array $rows, $context, $form_fields_info ): array {
348 if ( ! ProCompatibility::pro_enabled() ) {
349 return $rows;
350 }
351
352 $result = self::answer( 'better_payment/email/pro_form_field_rows', 'email_rows', [ $rows, (string) $context, is_array( $form_fields_info ) ? $form_fields_info : [] ] );
353
354 if ( ! is_array( $result ) ) {
355 return $rows;
356 }
357
358 $valid = [];
359
360 foreach ( $result as $row ) {
361 if ( is_array( $row ) && array_key_exists( 'key', $row ) && array_key_exists( 'label', $row ) && array_key_exists( 'value', $row ) ) {
362 $valid[] = $row;
363 }
364 }
365
366 return $valid;
367 }
368
369 /**
370 * One step of a Pro payment: Lite's transitional copy for an older Pro, otherwise the hook.
371 *
372 * Every caller checks ProCompatibility::pro_enabled() first, so a free install never
373 * reaches the copy or the hook.
374 *
375 * @param string $hook Filter name.
376 * @param string $method ProPaymentTypes method with the filter's arguments.
377 * @param array $args Filter arguments, the filtered value first.
378 * @return mixed
379 */
380 private static function answer( $hook, $method, array $args ) {
381 // Transitional: delete this branch with ProPaymentTypes (MIN_PRO_VERSION >= 1.4.4).
382 if ( self::lite_copy_applies() ) {
383 return call_user_func_array( [ ProPaymentTypes::class, $method ], $args );
384 }
385
386 return apply_filters_ref_array( $hook, $args );
387 }
388 }
389