array( 'title' => __( 'Service estimates', 'killerconversions' ), 'message' => __( 'Show visitors a calculated estimate before they send their enquiry.', 'killerconversions' ), 'cta' => __( 'Open estimate guidance', 'killerconversions' ), 'alternative' => __( 'Open form builder', 'killerconversions' ), 'section' => 'build' ), 'application' => array( 'title' => __( 'Saved applications', 'killerconversions' ), 'message' => __( 'Pro lets visitors save their application and finish it later. Free includes single-page enquiry forms; multi-step forms require Pro.', 'killerconversions' ), 'cta' => __( 'Open application guidance', 'killerconversions' ), 'alternative' => __( 'Continue with a single-session form', 'killerconversions' ), 'section' => 'settings' ), 'connections' => array( 'title' => __( 'Connected tools', 'killerconversions' ), 'message' => __( 'Send selected answers to your connected tool without copying them manually.', 'killerconversions' ), 'cta' => __( 'Review connection choices', 'killerconversions' ), 'alternative' => __( 'Open email & newsletter settings', 'killerconversions' ), 'section' => 'connect' ), 'followup' => array( 'title' => __( 'Follow-up & teams', 'killerconversions' ), 'message' => __( 'Review connected destinations and give retained leads a clear next step with stages, notes and scoped staff access.', 'killerconversions' ), 'cta' => __( 'Review team follow-up', 'killerconversions' ), 'alternative' => __( 'Review staff notification settings', 'killerconversions' ), 'section' => 'connect' ), 'recovery' => array( 'title' => __( 'Delivery recovery', 'killerconversions' ), 'message' => __( 'This entry is saved. Free queues notifications with bounded retries and a guarded retry action for failed jobs.', 'killerconversions' ), 'cta' => __( 'Review delivery recovery', 'killerconversions' ), 'alternative' => __( 'Open delivery activity', 'killerconversions' ), 'section' => 'connect' ), ); } public static function key( $suffix ) { return 'kconv_workflow_' . $suffix . '_' . get_current_blog_id(); } public static function allowed() { return current_user_can( 'manage_options' ); } public static function pro() { return KCONV_Presentation::unlocked(); } public static function dismissed( $workflow ) { $value = get_user_meta( get_current_user_id(), self::key( 'dismissed' ), true ); return is_array( $value ) && ! empty( $value[ $workflow ] ); } private static function scalar( $raw, $key ) { return isset( $raw[ $key ] ) && is_string( $raw[ $key ] ) ? $raw[ $key ] : ''; } public static function context( $raw ) { if ( ! self::allowed() ) { return new WP_Error( 'forbidden', __( 'Site administrator access is required.', 'killerconversions' ) ); } $workflow = self::scalar( $raw, 'workflow' ); if ( ! isset( self::definitions()[ $workflow ] ) ) { return new WP_Error( 'workflow', __( 'Choose a supported workflow.', 'killerconversions' ) ); } $fid = self::scalar( $raw, 'form_id' ); $eid = self::scalar( $raw, 'entry_id' ); if ( ! preg_match( '/^[1-9][0-9]*$/D', $fid ) || ( '' !== $eid && ! preg_match( '/^[1-9][0-9]*$/D', $eid ) ) ) { return new WP_Error( 'form', __( 'Save a form before remembering a workflow.', 'killerconversions' ) ); } $fid = (string) KCONV_Authoring::public_id( (int) $fid ); if ( ! current_user_can( 'edit_post', (int) $fid ) ) { return new WP_Error( 'form', __( 'This form is no longer available to your account.', 'killerconversions' ) ); } $authoring = KCONV_Authoring::context( (int) $fid ); $form = $authoring['form']; if ( ! $form || ! in_array( $form['status'], array( 'draft', 'publish' ), true ) ) { return new WP_Error( 'form', __( 'This form is no longer available.', 'killerconversions' ) ); } $entry = null; if ( 'recovery' === $workflow ) { if ( ! $eid || ! KCONV_Core::can( 'kconv_view_entries' ) ) { return new WP_Error( 'entry', __( 'Open a retained entry to review recovery.', 'killerconversions' ) ); } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- The task hint uses a read-only count of retained rows in the fixed entries table. The aggregate has prepared identifiers/values and only controls administrator guidance. $entry = $wpdb->get_row( $wpdb->prepare( 'SELECT id,form_id,mail_status FROM %i WHERE id=%d AND form_id=%d AND expires_at>%s', KCONV_Core::table( 'entries' ), (int) $eid, (int) $fid, gmdate( 'Y-m-d H:i:s' ) ), ARRAY_A ); if ( ! $entry ) { return new WP_Error( 'entry', __( 'This entry was deleted or its retention period expired.', 'killerconversions' ) ); } } elseif ( '' !== $eid ) { return new WP_Error( 'entry', __( 'An entry reference is only used for delivery recovery.', 'killerconversions' ) ); } return array( 'workflow' => $workflow, 'form_id' => (int) $fid, 'entry_id' => $entry ? (int) $eid : 0, 'revision' => hash( 'sha256', wp_json_encode( $form ) ), 'form' => $form, 'entry' => $entry ); } public static function url( $workflow, $fid, $eid = 0 ) { $args = array( 'page' => 'kconv-workflows', 'workflow' => $workflow, 'form_id' => (int) $fid ); if ( $eid ) { $args['entry_id'] = (int) $eid; } return add_query_arg( $args, admin_url( 'admin.php' ) ); } public static function back( $context, $pro = false ) { if ( 'recovery' === $context['workflow'] ) { return admin_url( 'admin.php?page=kconv-pro-delivery&delivery_form=' . $context['form_id'] ); } if ( $pro && self::pro() && 'followup' === $context['workflow'] ) { return admin_url( 'admin.php?page=kconv-entries&form_id=' . $context['form_id'] ); } return add_query_arg( array( 'page' => 'killerconversions', 'edit' => $context['form_id'], 'kconv_section' => self::definitions()[ $context['workflow'] ]['section'], 'kconv_workflow' => $pro ? $context['workflow'] : '' ), admin_url( 'admin.php' ) ); } public static function task() { if ( ! self::allowed() ) { return null; } $task = get_user_meta( get_current_user_id(), self::key( 'task' ), true ); if ( ! is_array( $task ) || ! is_string( $task['workflow'] ?? null ) || ! is_int( $task['form_id'] ?? null ) || $task['form_id'] < 1 || ! is_int( $task['entry_id'] ?? null ) || $task['entry_id'] < 0 || ! is_int( $task['expires'] ?? null ) || $task['expires'] <= time() || ! is_string( $task['revision'] ?? null ) || ! preg_match( '/^[a-f0-9]{64}$/D', $task['revision'] ) ) { return null; } $raw = array( 'workflow' => $task['workflow'] ?? '', 'form_id' => (string) ( $task['form_id'] ?? '' ), 'entry_id' => empty( $task['entry_id'] ) ? '' : (string) $task['entry_id'] ); $context = self::context( $raw ); if ( is_wp_error( $context ) ) { return null; } $context['changed'] = ! isset( $task['revision'] ) || ! hash_equals( $context['revision'], (string) $task['revision'] ); return $context; } public static function remember( $context ) { $task = array_intersect_key( $context, array_flip( array( 'workflow', 'form_id', 'entry_id', 'revision' ) ) ); $task['expires'] = time() + 7 * DAY_IN_SECONDS; update_user_meta( get_current_user_id(), self::key( 'task' ), $task ); return get_user_meta( get_current_user_id(), self::key( 'task' ), true ) === $task; } /** Background cleanup uses a value precondition so a concurrently refreshed task survives. */ public static function cleanup() { $key = self::key( 'task' ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Daily bounded cleanup selects at most 1000 IDs by this plugin's exact task key; no visitor data or meta-value search is involved. $users = get_users( array( 'meta_key' => $key, 'fields' => 'ID', 'number' => 1000 ) ); foreach ( $users as $user_id ) { $task = get_user_meta( $user_id, $key, true ); if ( is_array( $task ) && ( empty( $task['expires'] ) || (int) $task['expires'] <= time() ) ) { delete_user_meta( $user_id, $key, $task ); } } } /** Explicit POST only. Rendering a preview never remembers a task. */ public static function remember_control( $workflow, $form_id ) { $context = self::context( array( 'workflow' => $workflow, 'form_id' => (string) $form_id ) ); if ( is_wp_error( $context ) ) { return; } echo '

' . esc_html__( 'Come back to this workflow', 'killerconversions' ) . '

' . esc_html__( 'Remember a local link to this form for seven days. No form changes, visitor answers or credentials are stored in the task.', 'killerconversions' ) . '

'; self::action( $context, 'remember', __( 'Remember this workflow', 'killerconversions' ) ); echo '
'; } public static function continuation_url() { $task = self::task(); return $task ? self::url( $task['workflow'], $task['form_id'], $task['entry_id'] ) : admin_url( 'admin.php?page=killerconversions' ); } public static function saved( $id, $form, $raw ) { // This runs after the complete authoring transaction committed; rollback never records intent. if ( ! self::allowed() || ! isset( self::definitions()[ self::scalar( $raw, 'kconv_continue_workflow' ) ] ) ) { return; } $workflow = self::scalar( $raw, 'kconv_continue_workflow' ); if ( 'recovery' === $workflow ) { return; } $context = self::context( array( 'workflow' => $workflow, 'form_id' => (string) $id ) ); if ( ! is_wp_error( $context ) ) { self::remember( $context ); } } public static function submit() { if ( ! self::allowed() ) { wp_die( esc_html__( 'Site administrator access is required.', 'killerconversions' ), '', array( 'response' => 403 ) ); } $method = isset( $_SERVER['REQUEST_METHOD'] ) && is_string( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : ''; if ( 'POST' !== $method ) { wp_die( esc_html__( 'Use the workflow form to make this change.', 'killerconversions' ), '', array( 'response' => 405 ) ); } if ( ! isset( $_POST['_wpnonce'] ) || ! is_string( $_POST['_wpnonce'] ) ) { wp_nonce_ays( 'kconv_workflow' ); } $nonce = sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ); if ( ! wp_verify_nonce( $nonce, 'kconv_workflow' ) ) { wp_nonce_ays( 'kconv_workflow' ); } $raw = wp_unslash( $_POST ); $operation = self::scalar( $raw, 'operation' ); if ( 'forget' === $operation ) { delete_user_meta( get_current_user_id(), self::key( 'task' ) ); wp_safe_redirect( admin_url( 'admin.php?page=killerconversions' ) ); exit; } $context = self::context( $raw ); if ( is_wp_error( $context ) ) { wp_die( esc_html( $context->get_error_message() ), '', array( 'response' => 400 ) ); } if ( 'remember' === $operation ) { if ( ! self::remember( $context ) ) { wp_die( esc_html__( 'The task could not be remembered. Your form is unchanged. Reload and try again.', 'killerconversions' ), '', array( 'response' => 500 ) ); } } elseif ( 'dismiss' === $operation ) { $dismissed = get_user_meta( get_current_user_id(), self::key( 'dismissed' ), true ); $dismissed = is_array( $dismissed ) ? array_intersect_key( $dismissed, self::definitions() ) : array(); $dismissed[ $context['workflow'] ] = true; update_user_meta( get_current_user_id(), self::key( 'dismissed' ), $dismissed ); } else { wp_die( esc_html__( 'Unknown workflow action.', 'killerconversions' ), '', array( 'response' => 400 ) ); } wp_safe_redirect( 'dismiss' === $operation ? self::back( $context ) : self::url( $context['workflow'], $context['form_id'], $context['entry_id'] ) ); exit; } public static function menu() { $hook = add_submenu_page( null, __( 'Form workflow', 'killerconversions' ), __( 'Form workflow', 'killerconversions' ), 'manage_options', 'kconv-workflows', array( __CLASS__, 'screen' ) ); if ( $hook ) { add_action( 'load-' . $hook, array( __CLASS__, 'page_title' ) ); } } public static function page_title() { // WordPress cannot infer the document title for this hidden, form-specific screen. global $title; $title = __( 'Form workflow', 'killerconversions' ); } public static function assets() { $page = KCONV_Admin_Scope::page(); if ( ! self::allowed() || ! in_array( $page, array( 'killerconversions', 'kconv-workflows', 'kconv-entries' ), true ) ) { return; } wp_enqueue_style( 'kconv-workflows', KCONV_URL . 'assets/workflows.css', array(), KCONV_VERSION ); if ( 'killerconversions' === $page ) { wp_enqueue_script( 'kconv-workflows', KCONV_URL . 'assets/workflows.js', array( 'kconv-workspace' , 'wp-i18n' ), KCONV_VERSION, true ); wp_set_script_translations( 'kconv-workflows', 'killerconversions', KCONV_DIR . 'languages' ); } } public static function palette( $form ) { if ( ! self::allowed() ) { return; } echo '

' . esc_html__( 'Estimate calculations · Pro', 'killerconversions' ) . '

'; } public static function editor( $form ) { if ( ! self::allowed() ) { return; } echo '

' . esc_html__( 'Check your published form', 'killerconversions' ) . '

  1. ' . esc_html__( 'Choose Publish form or Publish updates when ready, then add the form’s block or shortcode to a WordPress page.', 'killerconversions' ) . '
  2. ' . esc_html__( 'Open that page as a visitor and send a clearly marked test entry.', 'killerconversions' ) . '
  3. ' . esc_html__( 'Find the test in Inbox. Check any notification recipient separately; acceptance does not prove inbox delivery.', 'killerconversions' ) . '

' . esc_html__( 'Try form does not save entries or send notifications. The steps above use your actual page and create a real test entry.', 'killerconversions' ) . '

'; echo ''; } public static function entry( $entry ) { // Do not repeat contextual offers on every inbox row or for delegated operators. // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only entry() navigation/filter input; values are normalized or allowlisted and escaped at output. No state change occurs from this read; mutations use separate capability/nonce guards. if ( ! self::allowed() || empty( $_GET['entry_id'] ) || ! is_scalar( $_GET['entry_id'] ) || (int) $_GET['entry_id'] !== (int) $entry['id'] || 'failed' !== $entry['mail_status'] || $entry['expires_at'] <= gmdate( 'Y-m-d H:i:s' ) ) { return; } echo '

' . esc_html__( 'This entry is saved. WordPress reported that its notification could not be sent. The underlying mail error was not recorded.', 'killerconversions' ) . '

' . esc_html__( 'Troubleshoot sending', 'killerconversions' ) . '

' . esc_html__( 'Check the recipient in Connect, review your existing WordPress mail configuration, and send a clearly marked test from the published form. Keep using the retained entry while you investigate.', 'killerconversions' ) . '

' . esc_html__( 'Review email settings', 'killerconversions' ) . '
'; $dismissed = get_user_meta( get_current_user_id(), self::key( 'dismissed' ), true ); if ( ! self::dismissed( 'recovery' ) ) { echo '

' . esc_html( self::definitions()['recovery']['message'] ) . ' ' . esc_html__( 'Review delivery recovery', 'killerconversions' ) . '

'; } } private static function action( $context, $operation, $label, $class = 'button' ) { echo '
'; wp_nonce_field( 'kconv_workflow' ); foreach ( array( 'workflow', 'form_id', 'entry_id' ) as $key ) { if ( ! empty( $context[ $key ] ) ) { echo ''; } } echo '
'; } public static function continuation() { if ( KCONV_Admin_Scope::plugin( array( 'kconv-license', 'killerconversions-pro-updates' ) ) ) { return; } $screen = get_current_screen(); if ( ! KCONV_Admin_Scope::plugin( array( 'killerconversions', 'kconv-license', 'killerconversions-pro-updates' ) ) && ! ( $screen && 'plugins' === $screen->id && self::pro() ) ) { return; } $task = self::task(); if ( ! $task ) { return; } echo '

' . esc_html__( 'Your saved task:', 'killerconversions' ) . ' ' . esc_html( self::definitions()[ $task['workflow'] ]['title'] ) . '. ' . ( $task['changed'] ? esc_html__( 'The form has changed. Review its current settings before continuing.', 'killerconversions' ) : esc_html__( 'Continue when you are ready; no form settings were changed by remembering this task.', 'killerconversions' ) ) . ' ' . esc_html__( 'Continue saved task', 'killerconversions' ) . '

'; self::action( $task, 'forget', __( 'Forget this task', 'killerconversions' ), 'button-link' ); echo '
'; } public static function screen() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only screen() navigation/filter input; values are normalized or allowlisted and escaped at output. No state change occurs from this read; mutations use separate capability/nonce guards. $context = self::context( wp_unslash( $_GET ) ); echo '
'; KCONV_Admin::navigation( 'discover' ); echo '

' . esc_html__( 'Your next form workflow', 'killerconversions' ) . '

'; if ( is_wp_error( $context ) ) { echo '

' . esc_html( $context->get_error_message() ) . '

' . esc_html__( 'Back to forms', 'killerconversions' ) . '
'; return; } $d = self::definitions()[ $context['workflow'] ]; $task = self::task(); echo '

' . esc_html( self::pro() ? __( 'Continue with your installed tools', 'killerconversions' ) : __( 'Choose the edition for this workflow', 'killerconversions' ) ) . '

' . esc_html( $d['title'] ) . '

' . esc_html( 'recovery' === $context['workflow'] && 'failed' !== $context['entry']['mail_status'] ? __( 'Review the recorded status before deciding whether a new send is needed.', 'killerconversions' ) : $d['message'] ) . '

' . esc_html__( 'Form:', 'killerconversions' ) . ' ' . esc_html( $context['form']['title'] ) . '

'; if ( $task && $task['workflow'] === $context['workflow'] && $task['form_id'] === $context['form_id'] && $task['entry_id'] === $context['entry_id'] ) { echo '

' . esc_html( $task['changed'] ? __( 'This form changed after you remembered the task. Review the current form; nothing will be replaced automatically.', 'killerconversions' ) : __( 'This task link expires seven days after you remember it. Expired references are removed by scheduled cleanup or when you forget them. It is private to this administrator and contains no visitor answers or credentials.', 'killerconversions' ) ) . '

'; } if ( 'estimate' === $context['workflow'] ) { echo '

' . esc_html__( 'Illustrative example — not a calculation from your form', 'killerconversions' ) . '

' . esc_html__( '2 units × $125 + $75 priority service = $325 estimate.', 'killerconversions' ) . '

' . esc_html__( 'In Pro, add quantity × unit-price rows and fixed extras in Estimate settings, plus minimum/base charges, volume tiers and formula examples. Submitted totals are recalculated on the server. An estimate is not a payment or invoice.', 'killerconversions' ) . '

'; } elseif ( 'application' === $context['workflow'] ) { echo '

' . esc_html__( 'With Pro, visitors explicitly save and copy a private continuation link. Anyone with that link can access the draft. There is no automatic partial-answer capture or draft email.', 'killerconversions' ) . '

' . esc_html__( 'Drafts expire after 1, 3 or 7 days from first save. Pro supports private file uploads with 1/5/10 MiB choices and host-aware combined limits; hosting can reduce these limits. Pro can configure up to five files and 25 MiB combined. Supported PDF, PNG, JPEG and TXT only. Consent choices must be made again. Public sites need HTTPS; encrypted drafts need PHP Sodium. The storage panel shows the local budget.', 'killerconversions' ) . '

'; } elseif ( 'connections' === $context['workflow'] ) { echo '

' . esc_html__( 'Free includes basic Brevo and MailerLite subscriptions with separate optional visitor marketing consent. Pro adds HubSpot, Slack and signed HTTPS webhooks. Configure each destination, review selected fields and acknowledge the transfer before enabling.', 'killerconversions' ) . '

' . esc_html__( 'No connection or credentials have been checked on this page. Existing entries and CSV remain available in Free.', 'killerconversions' ) . '

'; } elseif ( 'followup' === $context['workflow'] ) { echo '

' . esc_html__( 'Free includes staff email, CSV, basic assignment and status. Pro adds connected tools, conditional routing, named stages, notes, due dates and scoped staff access. Review each destination and permission before enabling it.', 'killerconversions' ) . '

  1. ' . esc_html__( 'Review the form’s saved destinations and selected answers.', 'killerconversions' ) . '
  2. ' . esc_html__( 'Choose a stage and next step for a retained entry.', 'killerconversions' ) . '
  3. ' . esc_html__( 'Review staff access and verify any delivery separately.', 'killerconversions' ) . '
'; } else { echo '

' . esc_html__( 'Recorded notification status:', 'killerconversions' ) . ' ' . esc_html( $context['entry']['mail_status'] ) . '

' . esc_html__( 'Free queues future staff and requested visitor emails through your existing WordPress mail transport. Failed jobs have a guarded Retry action, limited to three manual retry cycles. Review the saved destination first; an uncertain prior send can cause a duplicate. Free also includes Review a new send to check the current destination before resending a retained entry. No historical message is sent automatically.', 'killerconversions' ) . '

' . esc_html__( 'The form must be published and its destination valid. Subscription recovery requires the original matching consent record. A copied site pauses outgoing work until an administrator reviews it. Provider acceptance is not inbox receipt.', 'killerconversions' ) . '

'; } if ( self::pro() && 'estimate' === $context['workflow'] ) { $pricing = ! empty( $context['form']['estimate'] ) ? $context['form']['estimate'] : null; $pricing_label = ! $pricing ? __( 'No saved pricing table', 'killerconversions' ) : ( 'advanced' === $pricing['mode'] ? __( 'Advanced pricing configured', 'killerconversions' ) : __( 'Simple pricing configured', 'killerconversions' ) ); echo '

' . esc_html__( 'Next steps for this estimate', 'killerconversions' ) . '

' . esc_html__( 'Saved configuration:', 'killerconversions' ) . ' ' . esc_html( $pricing_label ) . '

  1. ' . esc_html__( 'Review this form’s pricing. For advanced pricing, add expected totals for ordinary quantities, tier boundaries, minimum charges and extras.', 'killerconversions' ) . '
  2. ' . esc_html__( 'Use Review setup to check current examples, then review a fictional PDF sample with your branding.', 'killerconversions' ) . '
  3. ' . esc_html__( 'Save a draft, use Try form with representative answers, and publish when ready. Verify a clearly marked visitor test in Inbox and inspect its saved PDF separately.', 'killerconversions' ) . '

' . esc_html__( 'This page reads saved configuration only. It has not tested calculations, created a document or confirmed delivery.', 'killerconversions' ) . '

' . esc_html__( 'Start a separate tiered estimate example', 'killerconversions' ) . '

'; } elseif ( self::pro() && 'connections' === $context['workflow'] ) { echo '

' . esc_html__( 'From configured to verified', 'killerconversions' ) . '

  1. ' . esc_html__( 'Review the destination and selected fields on this form. Use Review setup before saving the intended configuration.', 'killerconversions' ) . '
  2. ' . esc_html__( 'Use Test saved connection to inspect synthetic data and explicitly send one test. A connection can stay off while you test it.', 'killerconversions' ) . '
  3. ' . esc_html__( 'Inspect the destination using the test reference, then verify a published form submission separately. Provider acceptance does not prove receipt or the final remote result.', 'killerconversions' ) . '

' . esc_html__( 'Configuration, a connection test and a published submission test are separate checks. Opening this guidance completes none of them.', 'killerconversions' ) . '

'; } echo '
'; if ( ! KCONV_Pro_Discovery::pro_ready() ) { $example = array( 'estimate' => 'quotes', 'application' => 'resume', 'connections' => 'followup', 'followup' => 'followup', 'recovery' => 'followup' ); KCONV_Pro_Discovery::link( $example[ $context['workflow'] ], __( 'See what Pro adds to this workflow', 'killerconversions' ) ); } if ( in_array( KCONV_Presentation::state(), array( 'locked', 'mismatch' ), true ) ) { $next = KCONV_Presentation::action(); echo '' . esc_html( $next['label'] ) . ''; } else { echo '' . esc_html( self::pro() ? __( 'Continue this task', 'killerconversions' ) : $d['alternative'] ) . ''; } self::action( $context, 'remember', __( 'Remember this task', 'killerconversions' ) ); self::action( $context, 'dismiss', __( 'Hide this guidance', 'killerconversions' ), 'button-link' ); if ( $task ) { self::action( $context, 'forget', __( 'Forget saved task', 'killerconversions' ), 'button-link' ); } echo '
'; } }