> */ public static function get_registered_templates() { /** * Filters the templates offered in the builder's Templates tab. Pro * adds its complex/AI-adjacent templates here (tier `pro`) only while * it's active; a Free user never receives Pro field data through this * filter — see get_locked_template_teasers() for what Free shows * instead when Pro is inactive. * * @param array> $templates Free's own templates. */ $templates = apply_filters( 'adminoptim_form_builder_templates', self::get_free_templates() ); foreach ( $templates as &$template ) { $fields = isset( $template['fields'] ) && is_array( $template['fields'] ) ? $template['fields'] : []; if ( ! self::tree_has_submit( $fields ) ) { $label = isset( $template['settings']['submit_label'] ) ? (string) $template['settings']['submit_label'] : ''; $fields[] = Form_Builder_Forms::make_submit_field( $label ); $template['fields'] = $fields; } } unset( $template ); return $templates; } /** Recursively determine whether a template already defines its action item. */ private static function tree_has_submit( array $fields ) { foreach ( $fields as $field ) { if ( 'submit' === (string) ( $field['type'] ?? '' ) ) { return true; } if ( 'row' === (string) ( $field['type'] ?? '' ) && ! empty( $field['children'] ) && is_array( $field['children'] ) && self::tree_has_submit( $field['children'] ) ) { return true; } } return false; } /** * Get the static, field-data-free teaser list shown as locked cards on * the Templates tab when Pro is inactive. Mirrors the shape of the old * `Form_Builder_Forms::get_form_presets()` teaser, extended with * `category`/`features` so the gallery can filter/group it like a real * template card. Every teaser's `id` must equal the real Pro template's * `id` registered in `Form_Builder_Pro_Presets::get_templates()` — see * `FormBuilderTemplatesTest::test_teaser_and_pro_template_ids_match()`, * which fails the build if either side adds a template without the * other. * * @return array> */ public static function get_locked_template_teasers() { return [ [ 'id' => 'review', 'name' => __( 'Product review', 'admin-optimizer' ), 'category' => 'feedback', 'tier' => 'pro', 'description' => __( 'Star rating and written review.', 'admin-optimizer' ), 'fields_label' => __( '11 fields', 'admin-optimizer' ), 'features' => [ __( 'star rating', 'admin-optimizer' ) ], ], [ 'id' => 'nps', 'name' => __( 'NPS survey', 'admin-optimizer' ), 'category' => 'feedback', 'tier' => 'pro', 'description' => __( '0-10 recommendation score with follow-up questions based on the score.', 'admin-optimizer' ), 'fields_label' => __( '5 fields', 'admin-optimizer' ), 'features' => [ __( 'conditional logic', 'admin-optimizer' ) ], ], [ 'id' => 'eventreg', 'name' => __( 'Event registration', 'admin-optimizer' ), 'category' => 'events', 'tier' => 'pro', 'description' => __( 'Attendee details with a submission limit and close date. Set the limit/close date in the form\'s Settings tab.', 'admin-optimizer' ), 'fields_label' => __( '9 fields', 'admin-optimizer' ), 'features' => [ __( 'submission limit', 'admin-optimizer' ), __( 'close date', 'admin-optimizer' ) ], ], [ 'id' => 'bugreport', 'name' => __( 'Bug report with screenshot', 'admin-optimizer' ), 'category' => 'contact', 'tier' => 'pro', 'description' => __( 'Issue details with a validated screenshot upload and a browser-specific follow-up.', 'admin-optimizer' ), 'fields_label' => __( '9 fields', 'admin-optimizer' ), 'features' => [ __( 'file upload', 'admin-optimizer' ), __( 'conditional logic', 'admin-optimizer' ) ], ], [ 'id' => 'job', 'name' => __( 'Job application', 'admin-optimizer' ), 'category' => 'jobs', 'tier' => 'pro', 'description' => __( 'Resume upload and role questions, with a close date. Set the close date in the form\'s Settings tab.', 'admin-optimizer' ), 'fields_label' => __( '9 fields', 'admin-optimizer' ), 'features' => [ __( 'file upload', 'admin-optimizer' ), __( 'close date', 'admin-optimizer' ) ], ], [ 'id' => 'intake', 'name' => __( 'Client intake', 'admin-optimizer' ), 'category' => 'sales', 'tier' => 'pro', 'description' => __( 'Project brief that branches into service-specific follow-up questions.', 'admin-optimizer' ), 'fields_label' => __( '13 fields', 'admin-optimizer' ), 'features' => [ __( 'conditional logic', 'admin-optimizer' ) ], ], [ 'id' => 'csat', 'name' => __( 'Customer satisfaction survey', 'admin-optimizer' ), 'category' => 'feedback', 'tier' => 'pro', 'description' => __( 'Star rating with a follow-up question based on the score.', 'admin-optimizer' ), 'fields_label' => __( '5 fields', 'admin-optimizer' ), 'features' => [ __( 'star rating', 'admin-optimizer' ), __( 'conditional logic', 'admin-optimizer' ) ], ], [ 'id' => 'testimonial', 'name' => __( 'Testimonial collection', 'admin-optimizer' ), 'category' => 'feedback', 'tier' => 'pro', 'description' => __( 'Star rating, written testimonial, and an optional photo.', 'admin-optimizer' ), 'fields_label' => __( '6 fields', 'admin-optimizer' ), 'features' => [ __( 'star rating', 'admin-optimizer' ), __( 'file upload', 'admin-optimizer' ) ], ], [ 'id' => 'members', 'name' => __( 'Members-only request', 'admin-optimizer' ), 'category' => 'contact', 'tier' => 'pro', 'description' => __( 'A request form restricted to logged-in members. Set Access to logged-in users in the form\'s Settings tab.', 'admin-optimizer' ), 'fields_label' => __( '4 fields', 'admin-optimizer' ), 'features' => [ __( 'access control', 'admin-optimizer' ) ], ], [ 'id' => 'download', 'name' => __( 'Download request', 'admin-optimizer' ), 'category' => 'sales', 'tier' => 'pro', 'description' => __( 'Collect a visitor\'s name and email, then send them straight to your download or destination page after they submit.', 'admin-optimizer' ), 'fields_label' => __( '3 fields', 'admin-optimizer' ), 'features' => [ __( 'custom redirect', 'admin-optimizer' ) ], ], ]; } /** * Get the categories offered as filter chips on the Templates tab. * * @return array */ public static function get_categories() { return [ 'contact' => __( 'Contact & support', 'admin-optimizer' ), 'feedback' => __( 'Feedback', 'admin-optimizer' ), 'events' => __( 'Events', 'admin-optimizer' ), 'sales' => __( 'Sales & marketing', 'admin-optimizer' ), 'jobs' => __( 'Jobs', 'admin-optimizer' ), ]; } /** * Free's own launch set of standard templates. Guardrail: every field * here must be a Free field type with no Pro-only settings — enforced by * a unit test (`FormBuilderTemplatesTest`) rather than left to review * alone, since a Free template that smuggled in a Pro-only rule would * give a Free user an unenforced restriction with no indication why. * * @return array> */ private static function get_free_templates() { return [ [ 'id' => 'contact', 'name' => __( 'Contact', 'admin-optimizer' ), 'category' => 'contact', 'tier' => 'free', 'description' => __( 'Name, email, and a message.', 'admin-optimizer' ), 'features' => [], 'fields' => [ self::field( 'text', __( 'Your name', 'admin-optimizer' ), 'name', 1 ), self::field( 'email', __( 'Your email', 'admin-optimizer' ), 'email', 1 ), self::field( 'text', __( 'Subject', 'admin-optimizer' ), 'subject', 0 ), self::field( 'textarea', __( 'Message', 'admin-optimizer' ), 'message', 1 ), ], 'settings' => [ 'success_message' => __( 'Thanks for reaching out. We will reply soon.', 'admin-optimizer' ), ], ], [ 'id' => 'support', 'name' => __( 'Support request', 'admin-optimizer' ), 'category' => 'contact', 'tier' => 'free', 'description' => __( 'Describe an issue and how to reach you.', 'admin-optimizer' ), 'features' => [], 'fields' => [ self::field( 'text', __( 'Your name', 'admin-optimizer' ), 'name', 1 ), self::field( 'email', __( 'Your email', 'admin-optimizer' ), 'email', 1 ), self::field( 'select', __( 'Topic', 'admin-optimizer' ), 'topic', 1, [ 'choices' => self::choices( [ __( 'Billing', 'admin-optimizer' ), __( 'Technical issue', 'admin-optimizer' ), __( 'Account', 'admin-optimizer' ), __( 'Other', 'admin-optimizer' ), ] ), ] ), self::field( 'textarea', __( 'What happened?', 'admin-optimizer' ), 'details', 1 ), ], 'settings' => [ 'success_message' => __( 'We have your request and will get back to you.', 'admin-optimizer' ), ], ], [ 'id' => 'callback', 'name' => __( 'Callback request', 'admin-optimizer' ), 'category' => 'contact', 'tier' => 'free', 'description' => __( 'Phone number and best time to call.', 'admin-optimizer' ), 'features' => [], 'fields' => [ self::field( 'text', __( 'Your name', 'admin-optimizer' ), 'name', 1 ), self::field( 'phone', __( 'Phone number', 'admin-optimizer' ), 'phone', 1 ), self::field( 'select', __( 'Best time to call', 'admin-optimizer' ), 'best_time', 0, [ 'choices' => self::choices( [ __( 'Morning', 'admin-optimizer' ), __( 'Afternoon', 'admin-optimizer' ), __( 'Evening', 'admin-optimizer' ), ] ), ] ), self::field( 'textarea', __( 'Anything we should know?', 'admin-optimizer' ), 'note', 0 ), ], ], [ 'id' => 'feedback', 'name' => __( 'Feedback', 'admin-optimizer' ), 'category' => 'feedback', 'tier' => 'free', 'description' => __( 'Satisfaction and suggestions.', 'admin-optimizer' ), 'features' => [], 'fields' => [ self::field( 'text', __( 'Your name', 'admin-optimizer' ), 'name', 0 ), self::field( 'email', __( 'Your email', 'admin-optimizer' ), 'email', 0 ), self::field( 'select', __( 'How satisfied are you?', 'admin-optimizer' ), 'satisfaction', 1, [ 'choices' => self::choices( [ __( 'Very satisfied', 'admin-optimizer' ), __( 'Satisfied', 'admin-optimizer' ), __( 'Neutral', 'admin-optimizer' ), __( 'Unsatisfied', 'admin-optimizer' ), ] ), ] ), self::field( 'textarea', __( 'What could we improve?', 'admin-optimizer' ), 'improve', 0 ), ], ], [ 'id' => 'newsletter', 'name' => __( 'Newsletter signup', 'admin-optimizer' ), 'category' => 'sales', 'tier' => 'free', 'description' => __( 'Email signup with interests.', 'admin-optimizer' ), 'features' => [], 'fields' => [ self::field( 'email', __( 'Email', 'admin-optimizer' ), 'email', 1 ), self::field( 'text', __( 'First name', 'admin-optimizer' ), 'first_name', 0 ), self::field( 'check', __( 'Interests', 'admin-optimizer' ), 'interests', 0, [ 'choices' => self::choices( [ __( 'Product news', 'admin-optimizer' ), __( 'Tips and guides', 'admin-optimizer' ), __( 'Offers', 'admin-optimizer' ), ] ), ] ), ], ], [ 'id' => 'quote', 'name' => __( 'Quote request', 'admin-optimizer' ), 'category' => 'sales', 'tier' => 'free', 'description' => __( 'Details for a price estimate.', 'admin-optimizer' ), 'features' => [], 'fields' => [ self::field( 'text', __( 'Your name', 'admin-optimizer' ), 'name', 1 ), self::field( 'email', __( 'Your email', 'admin-optimizer' ), 'email', 1 ), self::field( 'phone', __( 'Phone', 'admin-optimizer' ), 'phone', 0 ), self::field( 'select', __( 'Service needed', 'admin-optimizer' ), 'service', 1, [ 'choices' => self::choices( [ __( 'Design', 'admin-optimizer' ), __( 'Development', 'admin-optimizer' ), __( 'Consulting', 'admin-optimizer' ), ] ), ] ), self::field( 'number', __( 'Budget', 'admin-optimizer' ), 'budget', 0 ), self::field( 'textarea', __( 'Project details', 'admin-optimizer' ), 'details', 1 ), ], ], [ 'id' => 'rsvp', 'name' => __( 'Event RSVP', 'admin-optimizer' ), 'category' => 'events', 'tier' => 'free', 'description' => __( 'Attendance and guest count.', 'admin-optimizer' ), 'features' => [], 'fields' => [ self::field( 'text', __( 'Your name', 'admin-optimizer' ), 'name', 1 ), self::field( 'email', __( 'Your email', 'admin-optimizer' ), 'email', 1 ), self::field( 'radio', __( 'Will you attend?', 'admin-optimizer' ), 'attending', 1, [ 'choices' => self::choices( [ __( 'Yes', 'admin-optimizer' ), __( 'No', 'admin-optimizer' ), __( 'Maybe', 'admin-optimizer' ), ] ), ] ), self::field( 'number', __( 'Number of guests', 'admin-optimizer' ), 'guests', 0 ), self::field( 'textarea', __( 'Dietary needs', 'admin-optimizer' ), 'dietary', 0 ), ], ], [ 'id' => 'inquiry', 'name' => __( 'General inquiry', 'admin-optimizer' ), 'category' => 'contact', 'tier' => 'free', 'description' => __( 'A general question with a subject picker.', 'admin-optimizer' ), 'features' => [], 'fields' => [ self::field( 'text', __( 'Your name', 'admin-optimizer' ), 'name', 1 ), self::field( 'email', __( 'Your email', 'admin-optimizer' ), 'email', 1 ), self::field( 'select', __( 'Subject', 'admin-optimizer' ), 'subject', 1, [ 'choices' => self::choices( [ __( 'General question', 'admin-optimizer' ), __( 'Partnership', 'admin-optimizer' ), __( 'Press', 'admin-optimizer' ), __( 'Other', 'admin-optimizer' ), ] ), ] ), self::field( 'textarea', __( 'Message', 'admin-optimizer' ), 'message', 1 ), ], ], [ 'id' => 'appointment', 'name' => __( 'Appointment request', 'admin-optimizer' ), 'category' => 'contact', 'tier' => 'free', 'description' => __( 'Request a preferred date and time. This does not book a real appointment slot — follow up to confirm.', 'admin-optimizer' ), 'features' => [], 'fields' => [ self::field( 'text', __( 'Your name', 'admin-optimizer' ), 'name', 1 ), self::field( 'email', __( 'Your email', 'admin-optimizer' ), 'email', 1 ), self::field( 'phone', __( 'Phone', 'admin-optimizer' ), 'phone', 0 ), self::field( 'datepicker', __( 'Preferred date', 'admin-optimizer' ), 'preferred_date', 1 ), self::field( 'select', __( 'Preferred time', 'admin-optimizer' ), 'preferred_time', 1, [ 'choices' => self::choices( [ __( 'Morning', 'admin-optimizer' ), __( 'Afternoon', 'admin-optimizer' ), __( 'Evening', 'admin-optimizer' ), ] ), ] ), self::field( 'select', __( 'Reason for visit', 'admin-optimizer' ), 'reason', 0, [ 'choices' => self::choices( [ __( 'Consultation', 'admin-optimizer' ), __( 'Follow-up', 'admin-optimizer' ), __( 'Other', 'admin-optimizer' ), ] ), ] ), self::field( 'textarea', __( 'Notes', 'admin-optimizer' ), 'notes', 0 ), ], 'settings' => [ 'success_message' => __( 'Thanks, we received your appointment request and will confirm a time with you soon.', 'admin-optimizer' ), ], ], [ 'id' => 'waitlist', 'name' => __( 'Waitlist / early access', 'admin-optimizer' ), 'category' => 'sales', 'tier' => 'free', 'description' => __( 'Collect signups for a waitlist or early access.', 'admin-optimizer' ), 'features' => [], 'fields' => [ self::field( 'text', __( 'Your name', 'admin-optimizer' ), 'name', 1 ), self::field( 'email', __( 'Your email', 'admin-optimizer' ), 'email', 1 ), self::field( 'select', __( 'How did you hear about us?', 'admin-optimizer' ), 'referral_source', 0, [ 'choices' => self::choices( [ __( 'Search engine', 'admin-optimizer' ), __( 'Social media', 'admin-optimizer' ), __( 'Friend or colleague', 'admin-optimizer' ), __( 'Other', 'admin-optimizer' ), ] ), ] ), self::field( 'textarea', __( 'Anything else you would like us to know?', 'admin-optimizer' ), 'note', 0 ), self::field( 'check', __( 'Consent', 'admin-optimizer' ), 'consent', 1, // "yes" is the stored choice value/slug, not display text — left untranslated. [ 'choices' => 'yes|' . __( 'I agree to be contacted about early access', 'admin-optimizer' ) ] ), ], ], ]; } /** * Join a list of (already-translated) choice lines into the newline- * separated string Fields::parse_choices() expects. Kept as a separate * array per choice, rather than one translatable multi-line string, so * each choice is its own translatable string. * * @param array $lines Choice lines, each `value` or `value|label`. * * @return string */ private static function choices( array $lines ) { return implode( "\n", $lines ); } /** * Build one field definition in `_adminoptim_form_fields` shape. * * @param string $type Field type key. * @param string $label Field label. * @param string $name Field name (`^[a-z][a-z0-9_]*$`). * @param int $required 1 if required. * @param array $extra Extra field keys (e.g. `choices`). * * @return array */ private static function field( $type, $label, $name, $required = 0, array $extra = [] ) { return array_merge( [ 'type' => $type, 'label' => $label, 'name' => $name, 'required' => $required ? 1 : 0, ], $extra ); } }