| @@ -109,8 +109,173 @@ | ||
| 109 | 109 | return $fields; |
| 110 | 110 | } |
| 111 | 111 | |
| 112 | 112 | /** |
| 113 | + * Label a settings group for its tab. | |
| 114 | + * | |
| 115 | + * The label is the group's `heading` field, which is also what the panel | |
| 116 | + * shows, so a tab and its panel can never disagree. An add-on that adds a | |
| 117 | + * group without a heading still gets a usable tab rather than a blank one: | |
| 118 | + * `live_qr_settings` reads as "Live Qr Settings", which is wrong-ish but | |
| 119 | + * findable, and the fix is for that add-on to add a heading. | |
| 120 | + * | |
| 121 | + * `main` is the exception. It is what a field with no `group` falls back to, | |
| 122 | + * so it holds whatever nobody placed rather than anything named "Main". No | |
| 123 | + * field ships in it — this plugin has no settings that are merely general — | |
| 124 | + * and it only becomes a tab when something lands there uninvited. | |
| 125 | + * | |
| 126 | + * @param string $group_id Group key. | |
| 127 | + * @param array $group Group data: `fields`, `priority`. | |
| 128 | + * @return string Unescaped label. | |
| 129 | + */ | |
| 130 | + public static function group_label( $group_id, array $group ) { | |
| 131 | + foreach ( $group['fields'] ?? array() as $field ) { | |
| 132 | + if ( 'heading' === ( $field['type'] ?? '' ) && ! empty( $field['field_data']['title'] ) ) { | |
| 133 | + return $field['field_data']['title']; | |
| 134 | + } | |
| 135 | + } | |
| 136 | + | |
| 137 | + if ( 'main' === $group_id ) { | |
| 138 | + return __( 'General', 'subscription' ); | |
| 139 | + } | |
| 140 | + | |
| 141 | + return ucwords( str_replace( array( '_', '-' ), ' ', (string) $group_id ) ); | |
| 142 | + } | |
| 143 | + | |
| 144 | + /** | |
| 145 | + * Whether every field in a group is locked behind Pro. | |
| 146 | + * | |
| 147 | + * Drives the "Pro" marker on the tab, so the whole panel does not have to be | |
| 148 | + * opened to find out that none of it can be changed yet. | |
| 149 | + * | |
| 150 | + * @param array $group Group data. | |
| 151 | + * @return bool | |
| 152 | + */ | |
| 153 | + public static function group_is_pro_locked( array $group ) { | |
| 154 | + $has_field = false; | |
| 155 | + | |
| 156 | + foreach ( $group['fields'] ?? array() as $field ) { | |
| 157 | + if ( 'heading' === ( $field['type'] ?? '' ) ) { | |
| 158 | + continue; | |
| 159 | + } | |
| 160 | + $has_field = true; | |
| 161 | + if ( empty( $field['field_data']['pro_locked'] ) ) { | |
| 162 | + return false; | |
| 163 | + } | |
| 164 | + } | |
| 165 | + | |
| 166 | + return $has_field; | |
| 167 | + } | |
| 168 | + | |
| 169 | + /** | |
| 170 | + * The settings sections, in display order. | |
| 171 | + * | |
| 172 | + * One level, named for the job a merchant came to do rather than for the | |
| 173 | + * plugin's internals. Each section is one rail item and one panel; the | |
| 174 | + * groups inside it stack, so nothing is ever two clicks deep. | |
| 175 | + * | |
| 176 | + * There is deliberately no "All settings" entry. It duplicated every panel | |
| 177 | + * on one page, which made the rail beside it look like decoration and gave | |
| 178 | + * every setting two addresses. | |
| 179 | + * | |
| 180 | + * @return array<string,string> Section key => label. | |
| 181 | + */ | |
| 182 | + public static function categories() { | |
| 183 | + return array( | |
| 184 | + 'renewals' => __( 'Renewals', 'subscription' ), | |
| 185 | + 'payments' => __( 'Payments', 'subscription' ), | |
| 186 | + 'switching' => __( 'Switching & Upgrades', 'subscription' ), | |
| 187 | + 'customers' => __( 'Customers', 'subscription' ), | |
| 188 | + 'advanced' => __( 'Advanced', 'subscription' ), | |
| 189 | + ); | |
| 190 | + } | |
| 191 | + | |
| 192 | + /** | |
| 193 | + * Which section a settings group belongs to. | |
| 194 | + * | |
| 195 | + * Groups are merged rather than mapped one-to-one: a section holding a | |
| 196 | + * single option is a wasted click, so `health_queue` sits with the other | |
| 197 | + * plumbing in Advanced, and everything the customer meets — the role they | |
| 198 | + * are given, checking out as a guest, what their subscription's quick view | |
| 199 | + * shows — is in Customers. | |
| 200 | + * | |
| 201 | + * Unmapped groups, including any an add-on registers without knowing | |
| 202 | + * sections exist, fall into `advanced`, so a new group is always reachable. | |
| 203 | + * | |
| 204 | + * @param string $group_id Group key. | |
| 205 | + * @return string Section key. | |
| 206 | + */ | |
| 207 | + public static function group_category( $group_id ) { | |
| 208 | + $map = array( | |
| 209 | + 'renewals' => 'renewals', | |
| 210 | + 'payment_gateways' => 'payments', | |
| 211 | + 'payment_failure' => 'payments', | |
| 212 | + 'grace_period' => 'payments', | |
| 213 | + 'switching' => 'switching', | |
| 214 | + 'role_based_settings' => 'customers', | |
| 215 | + 'guest_checkout' => 'customers', | |
| 216 | + 'live_qr_settings' => 'customers', | |
| 217 | + 'api_settings' => 'advanced', | |
| 218 | + 'health_queue' => 'advanced', | |
| 219 | + ); | |
| 220 | + | |
| 221 | + return $map[ $group_id ] ?? 'advanced'; | |
| 222 | + } | |
| 223 | + | |
| 224 | + /** | |
| 225 | + * Group keys bucketed by section, each list in the order the groups already | |
| 226 | + * sort in. | |
| 227 | + * | |
| 228 | + * Empty sections are dropped. Most of them are filled by Pro, and free | |
| 229 | + * alone would otherwise show rail items that open onto nothing. | |
| 230 | + * | |
| 231 | + * @param array $settings_fields Grouped, sorted settings fields. | |
| 232 | + * @return array<string,string[]> Section key => ordered group keys. | |
| 233 | + */ | |
| 234 | + public static function category_groups( array $settings_fields ) { | |
| 235 | + $out = array(); | |
| 236 | + foreach ( array_keys( self::categories() ) as $cat ) { | |
| 237 | + $out[ $cat ] = array(); | |
| 238 | + } | |
| 239 | + | |
| 240 | + foreach ( array_keys( $settings_fields ) as $group_id ) { | |
| 241 | + $cat = self::group_category( $group_id ); | |
| 242 | + $out[ $cat ][] = $group_id; | |
| 243 | + } | |
| 244 | + | |
| 245 | + return array_filter( | |
| 246 | + $out, | |
| 247 | + function ( $group_ids ) { | |
| 248 | + return ! empty( $group_ids ); | |
| 249 | + } | |
| 250 | + ); | |
| 251 | + } | |
| 252 | + | |
| 253 | + /** | |
| 254 | + * Whether every group in a section is locked behind Pro. | |
| 255 | + * | |
| 256 | + * Drives the "Pro" marker on the rail item, so a section none of which can | |
| 257 | + * be changed yet says so before it is opened. | |
| 258 | + * | |
| 259 | + * @param string[] $group_ids Group keys in the section. | |
| 260 | + * @param array $settings_fields Grouped settings fields. | |
| 261 | + * @return bool | |
| 262 | + */ | |
| 263 | + public static function category_is_pro_locked( array $group_ids, array $settings_fields ) { | |
| 264 | + if ( empty( $group_ids ) ) { | |
| 265 | + return false; | |
| 266 | + } | |
| 267 | + | |
| 268 | + foreach ( $group_ids as $group_id ) { | |
| 269 | + if ( ! self::group_is_pro_locked( $settings_fields[ $group_id ] ?? array() ) ) { | |
| 270 | + return false; | |
| 271 | + } | |
| 272 | + } | |
| 273 | + | |
| 274 | + return true; | |
| 275 | + } | |
| 276 | + | |
| 277 | + /** | |
| 113 | 278 | * Render specified settings field. |
| 114 | 279 | * |
| 115 | 280 | * @param string $field Field type. |
| 116 | 281 | * @param array $args Field arguments. |
| @@ -583,9 +748,9 @@ | ||
| 583 | 748 | * |
| 584 | 749 | * Generic and reusable: a sortable list of text items with per-row remove/move |
| 585 | 750 | * controls and an inline input + add button. The ordered list is serialized as |
| 586 | 751 | * JSON (`[{ key, label }]`) into a hidden input so it submits with the form; |
| 587 | - * behaviour is wired by `WPSubsEditList` (admin-components.js). All user-facing | |
| 752 | + * behaviour is wired by `WPSubsEditList` (admin-components/editlist.js). All user-facing | |
| 588 | 753 | * strings are overridable so the field carries no feature-specific text. |
| 589 | 754 | * |
| 590 | 755 | * When `modal` is true the list lives inside a `wpsubs-modal` (via the |
| 591 | 756 | * `WPSubsModal` component) and the settings row only shows a trigger button with |