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

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

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