PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / utils / checkout-fields.php

checkout-fields.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/utils/checkout-fields.php

223 lines 11.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Checkout Fields helper.
4 *
5 * Single source of truth for the checkout-form schema. Reads the
6 * `checkout_fields` setting (managed from StoreEngine → Settings →
7 * Checkout Fields) and exposes a normalised list both the traditional
8 * PHP templates and the React embedded checkout consume.
9 *
10 * $fields = CheckoutFields::all(); // every field with metadata
11 * $fields = CheckoutFields::for_group('billing');
12 * $rule = CheckoutFields::get( 'billing_state' );
13 * $req = CheckoutFields::required_payload_keys();
14 * $key = CheckoutFields::payload_key( 'billing_address_line' ); // 'billing_address_1'
15 */
16
17 namespace StoreEngine\Utils;
18
19 if ( ! defined( 'ABSPATH' ) ) {
20 exit;
21 }
22
23 class CheckoutFields {
24
25 const SETTING = 'checkout_fields';
26
27 /**
28 * Schema for every supported field. Defines:
29 * - id settings key
30 * - payload_key key used in the place_order POST/REST payload
31 * - group contact|billing|shipping
32 * - label translated label
33 * - system true → user can't disable; always required
34 * - default_enabled default enabled state when admin hasn't configured this field
35 * - default_required default required state when admin hasn't configured this field
36 *
37 * System fields are always enabled & required regardless of defaults below.
38 * Non-system fields fall back to these defaults when the admin hasn't saved a
39 * preference yet — without this, every non-system field would silently default
40 * to disabled, causing Address line 2 / State / Postcode / Phone to vanish
41 * from the rendered form (which also breaks shipping calc because
42 * has_full_shipping_address() requires state + postcode to be present).
43 */
44 public static function schema(): array {
45 /**
46 * Filter the raw checkout-field schema. Addons can flip the `system`
47 * flag or adjust per-field defaults per store mode (e.g. demote email
48 * and require phone in phone-only checkout).
49 *
50 * @param array $schema
51 */
52 return apply_filters( 'storeengine/checkout/fields_schema', [
53 // Contact
54 'email' => [
55 'payload_key' => 'user_email',
56 'group' => 'contact',
57 'label' => __( 'Email', 'storeengine' ),
58 'system' => true,
59 ],
60 // Billing
61 'billing_first_name' => [ 'payload_key' => 'billing_first_name', 'group' => 'billing', 'label' => __( 'First name', 'storeengine' ), 'default_enabled' => true, 'default_required' => true ],
62 'billing_last_name' => [ 'payload_key' => 'billing_last_name', 'group' => 'billing', 'label' => __( 'Last name', 'storeengine' ), 'default_enabled' => true, 'default_required' => true ],
63 'billing_address_line' => [ 'payload_key' => 'billing_address_1', 'group' => 'billing', 'label' => __( 'Address line', 'storeengine' ), 'default_enabled' => true, 'default_required' => true ],
64 'billing_address_line_2' => [ 'payload_key' => 'billing_address_2', 'group' => 'billing', 'label' => __( 'Address line 2', 'storeengine' ), 'default_enabled' => true, 'default_required' => false ],
65 'billing_country' => [ 'payload_key' => 'billing_country', 'group' => 'billing', 'label' => __( 'Country', 'storeengine' ), 'default_enabled' => true, 'default_required' => true ],
66 'billing_city' => [ 'payload_key' => 'billing_city', 'group' => 'billing', 'label' => __( 'City', 'storeengine' ), 'default_enabled' => true, 'default_required' => true ],
67 'billing_state' => [ 'payload_key' => 'billing_state', 'group' => 'billing', 'label' => __( 'State / Region', 'storeengine' ), 'default_enabled' => true, 'default_required' => false ],
68 'billing_apt' => [ 'payload_key' => 'billing_apt', 'group' => 'billing', 'label' => __( 'Apt / Suite', 'storeengine' ), 'default_enabled' => false, 'default_required' => false ],
69 'billing_post_code' => [ 'payload_key' => 'billing_postcode', 'group' => 'billing', 'label' => __( 'Postcode', 'storeengine' ), 'default_enabled' => true, 'default_required' => true ],
70 'billing_phone' => [ 'payload_key' => 'billing_phone', 'group' => 'billing', 'label' => __( 'Phone', 'storeengine' ), 'default_enabled' => true, 'default_required' => false ],
71 // Shipping
72 'shipping_first_name' => [ 'payload_key' => 'shipping_first_name', 'group' => 'shipping', 'label' => __( 'First name', 'storeengine' ), 'system' => true ],
73 'shipping_last_name' => [ 'payload_key' => 'shipping_last_name', 'group' => 'shipping', 'label' => __( 'Last name', 'storeengine' ), 'system' => true ],
74 'shipping_address_line' => [ 'payload_key' => 'shipping_address_1', 'group' => 'shipping', 'label' => __( 'Address line', 'storeengine' ), 'default_enabled' => true, 'default_required' => true ],
75 'shipping_address_line_2' => [ 'payload_key' => 'shipping_address_2', 'group' => 'shipping', 'label' => __( 'Address line 2', 'storeengine' ), 'default_enabled' => true, 'default_required' => false ],
76 'shipping_country' => [ 'payload_key' => 'shipping_country', 'group' => 'shipping', 'label' => __( 'Country', 'storeengine' ), 'default_enabled' => true, 'default_required' => true ],
77 'shipping_city' => [ 'payload_key' => 'shipping_city', 'group' => 'shipping', 'label' => __( 'City', 'storeengine' ), 'default_enabled' => true, 'default_required' => true ],
78 'shipping_state' => [ 'payload_key' => 'shipping_state', 'group' => 'shipping', 'label' => __( 'State / Region', 'storeengine' ), 'default_enabled' => true, 'default_required' => false ],
79 'shipping_apt' => [ 'payload_key' => 'shipping_apt', 'group' => 'shipping', 'label' => __( 'Apt / Suite', 'storeengine' ), 'default_enabled' => false, 'default_required' => false ],
80 'shipping_post_code' => [ 'payload_key' => 'shipping_postal_code', 'group' => 'shipping', 'label' => __( 'Postcode', 'storeengine' ), 'default_enabled' => true, 'default_required' => true ],
81 'shipping_phone' => [ 'payload_key' => 'shipping_phone', 'group' => 'shipping', 'label' => __( 'Phone', 'storeengine' ), 'default_enabled' => true, 'default_required' => false ],
82 ] );
83 }
84
85 /**
86 * Resolved list with the saved enabled/required flags merged in.
87 * Returns array<string $id, array> with keys: id, payload_key, group, label, system, enabled, required.
88 */
89 public static function all(): array {
90 $schema = self::schema();
91 $saved = (array) Helper::get_settings( self::SETTING, [] );
92
93 // Helper::get_settings can return objects when settings are JSON-decoded
94 // without assoc; coerce to array for predictable iteration.
95 $saved = json_decode( wp_json_encode( $saved ), true ) ?: [];
96
97 // When shipping is live, the address details the rate calculator relies on
98 // must be mandatory — otherwise a shopper can leave them blank and silently
99 // get no shipping options (zone matching + has_full_shipping_address() both
100 // need them). Promote the *enabled* shipping location fields to required.
101 // Memoised per request; the field list is filterable for stores with
102 // unusual locale needs. The truly-impossible case (a destination country
103 // with no states) is relaxed in required_payload_keys() / validate().
104 static $force_required_ids = null;
105 if ( null === $force_required_ids ) {
106 $shipping_live = ShippingUtils::is_shipping_enabled() && ShippingUtils::get_shipping_methods_count( true ) > 0;
107 $force_required_ids = $shipping_live
108 ? (array) apply_filters( 'storeengine/checkout/required_shipping_fields', [ 'shipping_city', 'shipping_state', 'shipping_post_code' ] )
109 : [];
110 }
111
112 $out = [];
113 foreach ( $schema as $id => $meta ) {
114 $saved_row = is_array( $saved[ $id ] ?? null ) ? $saved[ $id ] : [];
115 $system = ! empty( $meta['system'] );
116 // Per-field defaults from the schema; fall back to false only when the
117 // schema itself didn't declare a preference. System fields ignore both.
118 $default_enabled = ! empty( $meta['default_enabled'] );
119 $default_required = ! empty( $meta['default_required'] );
120 // System fields are always enabled & always required. For the rest,
121 // use a tolerant boolean coercer because form-encoded payloads send
122 // `false` as the literal string `"false"`, and `(bool) "false" === true`.
123 // When the admin hasn't saved a preference yet, fall back to the
124 // per-field schema default rather than blanket-disabling everything.
125 $enabled = $system ? true : self::to_bool( $saved_row['enabled'] ?? $default_enabled );
126 $required = $system ? true : self::to_bool( $saved_row['required'] ?? $default_required );
127
128 // Force-require the shipping fields shipping calc depends on, but only
129 // when they're actually shown — never resurrect a disabled field.
130 if ( $enabled && in_array( $id, $force_required_ids, true ) ) {
131 $required = true;
132 }
133
134 $out[ $id ] = [
135 'id' => $id,
136 'payload_key' => $meta['payload_key'],
137 'group' => $meta['group'],
138 'label' => $meta['label'],
139 'system' => $system,
140 'enabled' => $enabled,
141 'required' => $required,
142 ];
143 }
144
145 return $out;
146 }
147
148 /**
149 * Tolerant boolean coercer. Treats "false", "0", "", "no", "off" as false.
150 * Standard `(bool)` casts treat any non-empty string as true, which silently
151 * breaks settings that come from form-encoded POST data.
152 */
153 public static function to_bool( $value ): bool {
154 if ( is_bool( $value ) ) {
155 return $value;
156 }
157 if ( is_numeric( $value ) ) {
158 return (int) $value > 0;
159 }
160 if ( is_string( $value ) ) {
161 $v = strtolower( trim( $value ) );
162 if ( in_array( $v, [ '', '0', 'false', 'no', 'off', 'null' ], true ) ) {
163 return false;
164 }
165 return true;
166 }
167
168 return (bool) $value;
169 }
170
171 public static function for_group( string $group ): array {
172 return array_filter( self::all(), static fn( $f ) => $f['group'] === $group );
173 }
174
175 public static function get( string $id ): ?array {
176 $all = self::all();
177
178 return $all[ $id ] ?? null;
179 }
180
181 /**
182 * Map a settings id (e.g. 'billing_address_line') to the payload key the
183 * place_order endpoint understands (e.g. 'billing_address_1').
184 */
185 public static function payload_key( string $id ): ?string {
186 $row = self::get( $id );
187
188 return $row['payload_key'] ?? null;
189 }
190
191 /**
192 * Required payload keys, accounting for the contextual `needs_shipping` flag:
193 * shipping fields are only required when the cart actually needs shipping.
194 */
195 public static function required_payload_keys( bool $needs_shipping = true, string $shipping_country = '' ): array {
196 // Don't demand a State for a destination that simply has none — otherwise
197 // the force-required promotion in all() would make those orders
198 // un-completable. get_states() returns an empty array for stateless
199 // countries (and a populated map for US/BD/etc.).
200 $skip_shipping_state = false;
201 if ( $shipping_country ) {
202 $states = \StoreEngine\Classes\Countries::init()->get_states( $shipping_country );
203 $skip_shipping_state = is_array( $states ) && empty( $states );
204 }
205
206 $keys = [];
207 foreach ( self::all() as $row ) {
208 if ( ! $row['enabled'] || ! $row['required'] ) {
209 continue;
210 }
211 if ( ! $needs_shipping && 'shipping' === $row['group'] ) {
212 continue;
213 }
214 if ( $skip_shipping_state && 'shipping_state' === $row['id'] ) {
215 continue;
216 }
217 $keys[] = $row['payload_key'];
218 }
219
220 return array_values( array_unique( $keys ) );
221 }
222 }
223