Plugin Directory

Changeset 3782284


Ignore:
Timestamp:
11/03/2026 04:58:22 PM (67 minutes ago)
Author:
jarvismaximum
Message:

Release SubmitSignal 0.4.0: simpler authorized setup and reviewed local-demo screenshots.

Location:
submitsignal
Files:
9 edited
1 copied

Legend:

Unmodified
Added
Removed
  • submitsignal/tags/0.4.0/includes/native-admin.php

    r3780656 r3782284  
    1313        add_action( 'admin_post_submitsignal_native_action', array( __CLASS__, 'handle' ) );
    1414        add_action( 'admin_post_submitsignal_native_return', array( __CLASS__, 'returned' ) );
     15        add_action( 'admin_init', array( __CLASS__, 'checkout_return' ) );
    1516        add_action( 'submitsignal_native_expire', array( __CLASS__, 'expire' ) );
    1617    }
    … …  
    131132        if ( ! is_string( $url ) || strlen( $url ) > 4096 || preg_match( '/[\x00-\x20\x7f\\\\]/', $url ) ) { return false; }
    132133        $parts = wp_parse_url( $url );
     134        // Stripe Checkout can include opaque fragment state. Keep its full URL;
     135        // the portal retains its separate host and fragment restrictions.
    133136        return is_array( $parts ) && isset( $parts['scheme'], $parts['host'] ) && 'https' === $parts['scheme'] &&
    134137            ( $portal ? 'billing.stripe.com' : 'checkout.stripe.com' ) === $parts['host'] &&
    135             ! isset( $parts['user'] ) && ! isset( $parts['pass'] ) && ! isset( $parts['port'] ) && ! isset( $parts['fragment'] );
     138            ! isset( $parts['user'] ) && ! isset( $parts['pass'] ) && ! isset( $parts['port'] ) && ( ! $portal || ! isset( $parts['fragment'] ) );
    136139    }
    137140    public static function page_url( $url ) {
    138141        if ( ! is_string( $url ) || strlen( $url ) > 2048 || preg_match( '/[\x00-\x20\x7f\\\\]/', $url ) ) { return false; }
    139142        $parts = wp_parse_url( $url ); $home = wp_parse_url( home_url( '/' ) );
    140         if ( ! is_array( $parts ) || ! is_array( $home ) || empty( $parts['host'] ) || 'https' !== ( isset( $parts['scheme'] ) ? $parts['scheme'] : '' ) || strtolower( $parts['host'] ) !== strtolower( $home['host'] ) || isset( $parts['user'] ) || isset( $parts['pass'] ) || isset( $parts['fragment'] ) || isset( $parts['query'] ) || ( isset( $parts['port'] ) ? $parts['port'] : 443 ) !== ( isset( $home['port'] ) ? $home['port'] : 443 ) ) { return false; }
     143        if ( ! is_array( $parts ) || ! is_array( $home ) || empty( $parts['host'] ) || 'https' !== ( isset( $parts['scheme'] ) ? $parts['scheme'] : '' ) || preg_replace( '/\.$/', '', strtolower( $parts['host'] ) ) !== preg_replace( '/\.$/', '', strtolower( $home['host'] ) ) || isset( $parts['user'] ) || isset( $parts['pass'] ) || isset( $parts['fragment'] ) || isset( $parts['query'] ) || ( isset( $parts['port'] ) ? $parts['port'] : 443 ) !== ( isset( $home['port'] ) ? $home['port'] : 443 ) ) { return false; }
    141144        $base = rtrim( isset( $home['path'] ) ? $home['path'] : '', '/' );
    142145        $path = isset( $parts['path'] ) ? $parts['path'] : '/';
    … …  
    144147        if ( preg_match( '#(?:^|/)(?:\.|\.\.)(?:/|$)#', $decoded ) || false !== strpos( $decoded, '\\' ) || preg_match( '/[\x00-\x20\x7f]/', $decoded ) || preg_match( '/%2f|%5c/i', $path ) ) { return false; }
    145148        return '' === $base || $path === $base || 0 === strpos( $path, $base . '/' );
     149    }
     150    private static function canonical_page_url( $url ) {
     151        // Validate the original first. Match only the service's known origin
     152        // normalization; never decode or rewrite the selected page path.
     153        if ( ! self::page_url( $url ) ) { return false; }
     154        $parts = wp_parse_url( $url );
     155        $host = preg_replace( '/\.$/', '', strtolower( $parts['host'] ) );
     156        $port = isset( $parts['port'] ) && 443 !== $parts['port'] ? ':' . $parts['port'] : '';
     157        return 'https://' . $host . $port . ( ! empty( $parts['path'] ) ? $parts['path'] : '/' );
    146158    }
    147159    private static function api( $path, $body, $token = '' ) {
    … …  
    209221        if ( get_option( SubmitSignal_Plugin::SETTINGS ) !== $settings ) { return new WP_Error( 'storage', __( 'Approved credentials could not be saved. The private pending request remains for explicit claim retry after WordPress storage is repaired.', 'submitsignal' ) ); }
    210222        delete_option( self::VIEW ); self::clear_pending( self::key() ); SubmitSignal_Plugin::upgrade();
     223        // This single read follows the explicit account approval. A failed read
     224        // never repeats the claim or turns a connection into a form test.
     225        self::refresh_status( $settings );
    211226        return true;
     227    }
     228    private static function connected_notice() {
     229        $view = self::cache();
     230        return empty( $view['data'] ) ? __( 'Website connected. Current status is unavailable; use Refresh status. Do not repeat the connection.', 'submitsignal' ) : __( 'Website connected. Choose your form below and authorize its first check.', 'submitsignal' );
    212231    }
    213232    public static function returned() {
    … …  
    218237        if ( ! empty( $pending['claim_attempted'] ) ) { self::notice( __( 'A claim was already attempted. Use the explicit Retry connection claim button if its outcome is still unknown.', 'submitsignal' ) ); }
    219238        $result = self::claim( $pending );
    220         self::notice( is_wp_error( $result ) ? $result->get_error_message() : __( 'Website connected. Refresh status below, then choose a form and authorize its first check.', 'submitsignal' ) );
     239        self::notice( is_wp_error( $result ) ? $result->get_error_message() : self::connected_notice() );
    221240    }
    222241    private static function cache() {
    … …  
    248267    private static function payload( $operation, $view ) {
    249268        $body = array( 'action' => $operation );
    250         if ( 'create_monitor' === $operation ) {
     269        if ( in_array( $operation, array( 'create_monitor', 'check_form' ), true ) ) {
    251270            $form = self::input( 'form_id' ); $url = self::input( 'page_url' ); $name = sanitize_text_field( self::input( 'name' ) );
    252             if ( '1' !== self::input( 'consent' ) || ! in_array( $form, array_column( SubmitSignal_Plugin::forms(), 'id' ), true ) || ! self::page_url( $url ) || '' === $name || strlen( $name ) > 100 ) { return new WP_Error( 'input', __( 'Choose a compatible form, its HTTPS page on this website, a short name and explicit test permission.', 'submitsignal' ) ); }
    253             return array( 'action' => $operation, 'formId' => $form, 'pageUrl' => $url, 'name' => $name, 'consent' => true, 'intervalHours' => 24 );
     271            $forms = SubmitSignal_Plugin::forms();
     272            if ( '' === $name ) { foreach ( $forms as $item ) { if ( $item['id'] === $form ) { $name = substr( sanitize_text_field( $item['name'] ), 0, 100 ); } } }
     273            if ( '1' !== self::input( 'consent' ) || ! in_array( $form, array_column( $forms, 'id' ), true ) || ! self::page_url( $url ) || '' === $name || strlen( $name ) > 100 ) { return new WP_Error( 'input', __( 'Choose a compatible form, its HTTPS page on this website and explicit test permission.', 'submitsignal' ) ); }
     274            if ( 'check_form' === $operation && empty( $view['data']['capabilities']['browser'] ) ) { return new WP_Error( 'capability', __( 'Browser checks are unavailable. You can create a paused monitor without running it.', 'submitsignal' ) ); }
     275            if ( 'check_form' === $operation ) { foreach ( isset( $view['data']['monitors'] ) ? $view['data']['monitors'] : array() as $item ) { if ( isset( $item['formId'] ) && $item['formId'] === $form && ! empty( $item['submissionReview'] ) ) { return new WP_Error( 'consent', __( 'This form has an uncertain submission. Review its existing attempt before any further inquiry.', 'submitsignal' ) ); } } }
     276            return array( 'action' => 'create_monitor', 'formId' => $form, 'pageUrl' => self::canonical_page_url( $url ), 'name' => $name, 'consent' => true, 'intervalHours' => 24 );
    254277        }
    255278        if ( in_array( $operation, array( 'run_monitor', 'pause_monitor', 'review_monitor' ), true ) ) {
    … …  
    272295        if ( 'checkout' === $operation ) {
    273296            $plan = self::input( 'plan' );
    274             if ( ! in_array( $plan, array( 'solo', 'agency' ), true ) || '1' !== self::input( 'commercial_consent' ) || self::TERMS !== self::input( 'terms_version' ) || '1' !== self::input( 'us_business' ) || '1' !== self::input( 'us_billing' ) || '1' !== self::input( 'supported_use' ) ) { return new WP_Error( 'consent', __( 'Choose a plan and explicitly confirm the current terms, US business, US billing and supported use.', 'submitsignal' ) ); }
     297            $eligible = '1' === self::input( 'eligibility_consent' ) || ( '1' === self::input( 'us_business' ) && '1' === self::input( 'us_billing' ) && '1' === self::input( 'supported_use' ) );
     298            if ( ! in_array( $plan, array( 'solo', 'agency' ), true ) || '1' !== self::input( 'commercial_consent' ) || self::TERMS !== self::input( 'terms_version' ) || ! $eligible ) { return new WP_Error( 'consent', __( 'Choose a plan and explicitly confirm the current terms, US business, US billing and supported use.', 'submitsignal' ) ); }
    275299            $body['plan'] = $plan; $body['consent'] = array( 'accepted' => true, 'version' => self::TERMS, 'plan' => $plan );
    276300            $body['eligibility'] = array( 'businessCountry' => 'US', 'billingCountry' => 'US', 'supportedUse' => true );
    … …  
    287311    public static function handle() {
    288312        self::guard(); $operation = self::input( 'operation' );
    289         $allowed = array( 'start', 'retry_authorization', 'retry_claim', 'cancel_authorization', 'refresh_inventory', 'status', 'create_monitor', 'run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' );
     313        $allowed = array( 'start', 'retry_authorization', 'retry_claim', 'cancel_authorization', 'refresh_inventory', 'status', 'create_monitor', 'check_form', 'run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' );
    290314        if ( ! in_array( $operation, $allowed, true ) ) { wp_die( esc_html__( 'Unknown action.', 'submitsignal' ), '', array( 'response' => 400 ) ); }
    291315        check_admin_referer( 'submitsignal_native_' . $operation );
    … …  
    296320            wp_redirect( $result ); exit; // Exact canonical service path validated above.
    297321        }
    298         if ( 'retry_claim' === $operation ) { $pending = self::pending(); $result = $pending ? self::claim( $pending ) : new WP_Error( 'expired', 'The approval expired. Start a new connection deliberately.' ); self::notice( is_wp_error( $result ) ? $result->get_error_message() : 'Website connected. Refresh status to choose a form.' ); }
     322        if ( 'retry_claim' === $operation ) { $pending = self::pending(); $result = $pending ? self::claim( $pending ) : new WP_Error( 'expired', 'The approval expired. Start a new connection deliberately.' ); self::notice( is_wp_error( $result ) ? $result->get_error_message() : self::connected_notice() ); }
    299323        if ( 'refresh_inventory' === $operation ) {
    300324            if ( get_transient( 'submitsignal_native_inventory_lock' ) ) { self::notice( __( 'Wait one minute before refreshing inventory again.', 'submitsignal' ) ); }
    … …  
    338362            return $result;
    339363        }
     364        if ( 'check_form' === $operation ) {
     365            $monitor = isset( $result['monitor'] ) && is_array( $result['monitor'] ) ? $result['monitor'] : array();
     366            $already_known = false; foreach ( isset( $view['data']['monitors'] ) ? $view['data']['monitors'] : array() as $item ) { if ( isset( $item['id'], $monitor['id'] ) && $item['id'] === $monitor['id'] ) { $already_known = true; } }
     367            if ( $already_known || empty( $monitor['id'] ) || ! self::id( $monitor['id'] ) || ! isset( $monitor['siteId'], $monitor['formId'], $monitor['pageUrl'], $monitor['paused'] ) || $monitor['siteId'] !== $settings['site_id'] || $monitor['formId'] !== $body['formId'] || $monitor['pageUrl'] !== $body['pageUrl'] || true !== $monitor['paused'] || ! empty( $monitor['lastRunAt'] ) || ! empty( $monitor['submissionReview'] ) ) { return new WP_Error( 'native_uncertain', __( 'The created monitor could not be verified. No check was requested. Refresh status and review the existing monitor; do not recreate it.', 'submitsignal' ) ); }
     368            // The consumed attempt and action lease span both calls. Persist a
     369            // second fence before the one authorized run; never resume this
     370            // sequence after a timeout, process interruption or rejected create.
     371            $view['one_check'] = array( 'monitor_id' => $monitor['id'], 'run_attempted' => true );
     372            update_option( self::VIEW, $view, false );
     373            if ( get_option( self::VIEW ) !== $view ) { return new WP_Error( 'storage', __( 'The paused monitor was created, but its check could not be safely recorded. No check was requested. Refresh status before an explicit decision.', 'submitsignal' ) ); }
     374            $result = self::api( '/api/plugin/native/manage', array( 'action' => 'run_monitor', 'monitorId' => $monitor['id'] ), $settings['management_token'] );
     375            if ( is_wp_error( $result ) ) { self::rejected_grant( $result ); return $result; }
     376            $run = isset( $result['run'] ) && is_array( $result['run'] ) ? $result['run'] : array();
     377            if ( empty( $run['id'] ) || ! self::id( $run['id'] ) || ! isset( $run['siteId'], $run['monitorId'] ) || $run['siteId'] !== $settings['site_id'] || $run['monitorId'] !== $monitor['id'] ) { return new WP_Error( 'native_uncertain', __( 'The check outcome could not be verified. Refresh status and review this attempt before any further submission.', 'submitsignal' ) ); }
     378        }
    340379        if ( in_array( $operation, array( 'checkout', 'billing_portal' ), true ) ) {
    341380            if ( empty( $result['url'] ) || ! self::billing_url( $result['url'], 'billing_portal' === $operation ) ) { $view['site_id'] = $settings['site_id']; $view['uncertain'] = true; update_option( self::VIEW, $view, false ); return new WP_Error( 'destination', __( 'The billing destination could not be verified. Refresh status before another attempt.', 'submitsignal' ) ); }
    … …  
    351390        echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="margin:12px 0"><input type="hidden" name="action" value="submitsignal_native_action"><input type="hidden" name="operation" value="' . esc_attr( $operation ) . '">';
    352391        wp_nonce_field( 'submitsignal_native_' . $operation );
    353         if ( in_array( $operation, array( 'create_monitor', 'run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' ), true ) ) { self::hidden( 'native_attempt', self::$render_attempt ); }
     392        if ( in_array( $operation, array( 'create_monitor', 'check_form', 'run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' ), true ) ) { self::hidden( 'native_attempt', self::$render_attempt ); }
    354393    }
    355394    private static function hidden( $name, $value ) { echo '<input type="hidden" name="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '">'; }
    … …  
    379418        return $fallback ? 'Other check: ' . $fallback : 'Other check';
    380419    }
    381     private static function evidence( $run ) {
     420    private static function evidence( $run, $show_daily_offer = false ) {
     421        $passed = isset( $run['status'] ) && 'passed' === $run['status'];
    382422        echo '<p><strong>' . esc_html( isset( $run['status'] ) ? $run['status'] : 'unverified' ) . '</strong> · ' . esc_html( isset( $run['startedAt'] ) ? $run['startedAt'] : '' ) . '</p>';
     423        if ( $passed && $show_daily_offer ) { echo '<p><a class="button button-primary" href="#submitsignal-daily-monitoring">' . esc_html__( 'Review daily monitoring — $19/month plus tax', 'submitsignal' ) . '</a></p>'; }
    383424        if ( ! empty( $run['summary'] ) && is_string( $run['summary'] ) ) { echo '<p>' . esc_html( substr( sanitize_text_field( $run['summary'] ), 0, 1000 ) ) . '</p>'; }
    384425        if ( ! empty( $run['stages'] ) && is_array( $run['stages'] ) ) {
     426            $stages = array_slice( $run['stages'], 0, 15 );
     427            if ( $passed ) {
     428                $successful = array(); $attention = array();
     429                foreach ( $stages as $stage ) { if ( is_array( $stage ) && isset( $stage['status'] ) && 'passed' === $stage['status'] ) { $successful[] = $stage; } else { $attention[] = $stage; } }
     430                if ( $successful ) { echo '<details><summary>' . esc_html__( 'View passed-check evidence', 'submitsignal' ) . '</summary>'; self::evidence_stages( $successful ); echo '</details>'; }
     431                // Unverified inbox/CRM results and any contradictory failure
     432                // remain visible; a passed browser result never upgrades them.
     433                if ( $attention ) { self::evidence_stages( $attention ); }
     434            } else { self::evidence_stages( $stages ); }
     435        }
     436    }
     437    private static function evidence_stages( $stages ) {
    385438            echo '<ul>';
    386             foreach ( array_slice( $run['stages'], 0, 15 ) as $stage ) {
     439            foreach ( $stages as $stage ) {
    387440                if ( ! is_array( $stage ) ) { continue; }
    388441                $status = isset( $stage['status'] ) && is_string( $stage['status'] ) && in_array( $stage['status'], array( 'passed', 'failed', 'unverified', 'inconclusive' ), true ) ? $stage['status'] : 'unverified';
    … …  
    392445            }
    393446            echo '</ul>';
     447    }
     448    private static function block_form_ids( $blocks, &$ids, &$remaining, $depth = 0 ) {
     449        if ( $depth > 20 ) { return; }
     450        foreach ( $blocks as $block ) {
     451            if ( --$remaining < 0 ) { return; }
     452            if ( isset( $block['blockName'], $block['attrs']['formId'] ) && 'wpforms/form-selector' === $block['blockName'] && is_scalar( $block['attrs']['formId'] ) ) { $ids[] = (string) $block['attrs']['formId']; }
     453            if ( ! empty( $block['innerBlocks'] ) ) { self::block_form_ids( $block['innerBlocks'], $ids, $remaining, $depth + 1 ); }
     454        }
     455    }
     456    /** Local suggestions only. They neither fetch a page nor change inventory. */
     457    public static function page_suggestions( $forms ) {
     458        $suggestions = array(); foreach ( $forms as $form ) { $suggestions[ $form['id'] ] = array(); }
     459        if ( ! $suggestions ) { return $suggestions; }
     460        $posts = get_posts( array( 'post_type' => array( 'page', 'post' ), 'post_status' => 'publish', 'has_password' => false, 'numberposts' => 100, 'orderby' => 'ID', 'order' => 'DESC', 'suppress_filters' => true ) );
     461        $pattern = '/' . get_shortcode_regex( array( 'wpforms' ) ) . '/s';
     462        foreach ( $posts as $post ) {
     463            if ( 'publish' !== $post->post_status || $post->post_password || ! is_post_publicly_viewable( $post ) || strlen( $post->post_content ) > 262144 ) { continue; }
     464            $url = get_permalink( $post ); if ( ! self::page_url( $url ) ) { continue; }
     465            $ids = array(); $remaining = 500;
     466            self::block_form_ids( parse_blocks( $post->post_content ), $ids, $remaining );
     467            if ( preg_match_all( $pattern, $post->post_content, $matches, PREG_SET_ORDER ) ) {
     468                foreach ( $matches as $match ) {
     469                    if ( ! empty( $match[1] ) || ! empty( $match[6] ) ) { continue; }
     470                    $attributes = shortcode_parse_atts( $match[3] );
     471                    if ( is_array( $attributes ) && isset( $attributes['id'] ) && is_string( $attributes['id'] ) ) { $ids[] = $attributes['id']; }
     472                }
     473            }
     474            foreach ( array_unique( $ids ) as $id ) {
     475                if ( isset( $suggestions[ $id ] ) && count( $suggestions[ $id ] ) < 10 ) { $suggestions[ $id ][] = array( 'url' => $url, 'title' => substr( sanitize_text_field( get_the_title( $post ) ), 0, 100 ) ); }
     476            }
     477        }
     478        return $suggestions;
     479    }
     480    private static function setup_fields( $forms, $pages, $operation ) {
     481        $single = 1 === count( $forms ) ? $forms[0] : null;
     482        $choices = $single && isset( $pages[ $single['id'] ] ) ? $pages[ $single['id'] ] : array();
     483        $url = 1 === count( $choices ) ? $choices[0]['url'] : '';
     484        $list = 'submitsignal-pages-' . $operation;
     485        echo '<div data-ss-setup><p><label>' . esc_html__( 'Contact form', 'submitsignal' ) . ' <select name="form_id" data-ss-form required><option value="">' . esc_html__( 'Choose your form', 'submitsignal' ) . '</option>';
     486        foreach ( $forms as $form ) { echo '<option value="' . esc_attr( $form['id'] ) . '" ' . selected( $single ? $single['id'] : '', $form['id'], false ) . '>' . esc_html( $form['name'] . ' (#' . $form['id'] . ')' ) . '</option>'; }
     487        echo '</select></label></p><p><label>' . esc_html__( 'Page containing this form', 'submitsignal' ) . ' <input class="regular-text" data-ss-url name="page_url" type="url" list="' . esc_attr( $list ) . '" value="' . esc_attr( $url ) . '" maxlength="2048" required></label><datalist id="' . esc_attr( $list ) . '">';
     488        foreach ( $choices as $choice ) { echo '<option value="' . esc_attr( $choice['url'] ) . '">' . esc_html( $choice['title'] ) . '</option>'; }
     489        echo '</datalist></p><p>' . esc_html__( 'Confirm the suggested public page, choose another suggestion, or paste its HTTPS URL. Theme-built and dynamically embedded forms may need a manual URL. Nothing is submitted until you choose an action below.', 'submitsignal' ) . '</p><details><summary>' . esc_html__( 'Monitor name (optional)', 'submitsignal' ) . '</summary><p><label>' . esc_html__( 'Name', 'submitsignal' ) . ' <input class="regular-text" data-ss-name name="name" maxlength="100" value="' . esc_attr( $single ? $single['name'] : '' ) . '"></label></p></details></div>';
     490    }
     491    private static function setup_defaults_script( $forms, $pages ) {
     492        $names = array(); foreach ( $forms as $form ) { $names[ $form['id'] ] = $form['name']; }
     493        $flags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
     494        echo '<script>(function(){var names=' . wp_json_encode( $names, $flags ) . ',pages=' . wp_json_encode( $pages, $flags ) . ';document.querySelectorAll("[data-ss-setup]").forEach(function(group){var form=group.querySelector("[data-ss-form]"),url=group.querySelector("[data-ss-url]"),name=group.querySelector("[data-ss-name]"),list=document.getElementById(url.getAttribute("list")),lastName=name.value;form.addEventListener("change",function(){var choices=pages[form.value]||[];if(name.value===lastName||!name.value){name.value=(names[form.value]||"").slice(0,100);}lastName=(names[form.value]||"").slice(0,100);url.value=choices.length===1?choices[0].url:"";while(list.firstChild){list.removeChild(list.firstChild);}choices.forEach(function(choice){var option=document.createElement("option");option.value=choice.url;option.label=choice.title;list.appendChild(option);});});});})();</script>';
     495    }
     496    private static function checkout_form( $plan ) {
     497        $label = 'agency' === $plan ? 'Agency' : 'Solo'; $price = 'agency' === $plan ? '99' : '19';
     498        self::open_form( 'checkout' ); self::hidden( 'plan', $plan ); self::hidden( 'terms_version', self::TERMS );
     499        self::checkbox( 'eligibility_consent', __( 'This is a US business with US billing. I will use only supported ordinary WPForms Lite contact forms that I own or am authorized to test.', 'submitsignal' ) );
     500        echo '<p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Read the current terms', 'submitsignal' ) . '</a> · ' . esc_html( self::TERMS ) . '</p>';
     501        self::checkbox( 'commercial_consent', sprintf( __( 'I accept the current terms and %1$s at US$%2$s per month plus applicable tax, renewing monthly until canceled. I will review the total and complete payment on Stripe.', 'submitsignal' ), $label, $price ) );
     502        self::close_form( sprintf( __( 'Continue with %1$s — $%2$s/month', 'submitsignal' ), $label, $price ), 'solo' === $plan );
     503    }
     504    /** Reconcile a checkout return before WordPress emits administrator headers. */
     505    public static function checkout_return() {
     506        if ( ! current_user_can( 'manage_options' ) || ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] || 'submitsignal' !== self::input( 'page', true ) || ! self::management() ) { return; }
     507        $checkout_return = self::input( 'checkout', true );
     508        if ( in_array( $checkout_return, array( 'success', 'canceled' ), true ) ) {
     509            $result = self::manage( 'status' );
     510            self::notice( is_wp_error( $result ) ? __( 'Returned from checkout, but current status is unavailable. Refresh status before another action. Returning never confirms payment.', 'submitsignal' ) : __( 'Returned from checkout. Your verified plan is shown below; the return link itself is not payment proof. Daily monitoring stays paused until you explicitly enable it.', 'submitsignal' ) );
    394511        }
    395512    }
    … …  
    404521        }
    405522        $notice = get_transient( 'submitsignal_notice_' . get_current_user_id() ); delete_transient( 'submitsignal_notice_' . get_current_user_id() );
    406         echo '<div class="wrap"><h1>SubmitSignal</h1><p>' . esc_html__( 'Set up and review contact-form checks here in WordPress. Connecting does not run a test or start a paid plan.', 'submitsignal' ) . '</p>';
     523        echo '<div class="wrap"><h1>SubmitSignal</h1><p>' . esc_html__( 'Connect your account, check one form for free, then choose whether to monitor it daily. No card is needed for your first check.', 'submitsignal' ) . '</p>';
    407524        if ( is_string( $notice ) && $notice ) { echo '<div class="notice notice-info"><p>' . esc_html( $notice ) . '</p></div>'; }
    408525        if ( $pending ) {
    … …  
    412529            self::open_form( 'cancel_authorization' ); self::close_form( __( 'Cancel local pending approval', 'submitsignal' ) );
    413530        } elseif ( ! $management ) {
    414             echo '<h2>' . esc_html( $settings ? __( 'Enable WordPress controls', 'submitsignal' ) : __( 'Connect your free account', 'submitsignal' ) ) . '</h2>';
     531            echo '<h2>' . esc_html( $settings ? __( '1. Enable WordPress controls', 'submitsignal' ) : __( '1. Connect your free account', 'submitsignal' ) ) . '</h2>';
    415532            echo '<p>' . esc_html__( 'Continue to SubmitSignal, sign in, approve this website, then return here. No pairing-code copying is needed. Existing metadata connections require this separate approval before WordPress can manage checks or billing.', 'submitsignal' ) . '</p>';
    416533            self::open_form( 'start' );
    … …  
    418535            self::close_form( $settings ? __( 'Enable WordPress controls', 'submitsignal' ) : __( 'Connect SubmitSignal', 'submitsignal' ), true );
    419536        } else {
    420             echo '<h2>' . esc_html__( 'Connected — WordPress controls enabled', 'submitsignal' ) . '</h2><p>' . esc_html__( 'Credentials are private server-side options. This page does not poll, submit forms or purchase anything automatically.', 'submitsignal' ) . '</p>';
    421             if ( empty( $view['data'] ) ) { echo '<p><strong>' . esc_html__( 'Next: choose Refresh status to load your form controls and current plan.', 'submitsignal' ) . '</strong> ' . esc_html__( 'Then select Form, enter Monitor name and Public HTTPS page containing this form, accept the test permission and choose Create paused monitor. No test runs until you separately authorize Run first check.', 'submitsignal' ) . '</p>'; }
     537            echo '<h2>' . esc_html__( '1. Connected — WordPress controls enabled', 'submitsignal' ) . '</h2>';
     538            if ( empty( $view['data'] ) ) { echo '<p><strong>' . esc_html__( 'Current status could not be loaded. Choose Refresh status to continue.', 'submitsignal' ) . '</strong> ' . esc_html__( 'Your connection is saved. Do not reconnect or repeat a check to load status.', 'submitsignal' ) . '</p>'; }
    422539            self::open_form( 'status' ); self::close_form( __( 'Refresh status', 'submitsignal' ), true );
    423540            if ( ! empty( $view['observed_at'] ) ) { echo '<p>' . esc_html( 'Last explicit status refresh: ' . gmdate( 'Y-m-d H:i:s', $view['observed_at'] ) . ' UTC. This is a saved snapshot.' ) . '</p>'; }
    … …  
    425542            if ( ! empty( $view['data'] ) ) { self::controls( $view['data'], $forms, ! empty( $view['uncertain'] ) ); }
    426543        }
    427         echo '<h2>' . esc_html__( 'Compatible form inventory', 'submitsignal' ) . '</h2>';
     544        echo '<details><summary>' . esc_html__( 'Form inventory and advanced settings', 'submitsignal' ) . '</summary><h2>' . esc_html__( 'Compatible form inventory', 'submitsignal' ) . '</h2>';
    428545        if ( ! function_exists( 'wpforms' ) ) { echo '<p>' . esc_html__( 'WPForms is not active. Activate WPForms Lite, then create and publish an ordinary contact form.', 'submitsignal' ) . '</p>'; }
    429546        elseif ( ! $forms ) { $published = get_posts( array( 'post_type' => 'wpforms', 'post_status' => 'publish', 'numberposts' => 1, 'fields' => 'ids' ) ); echo '<p>' . esc_html( $published ? __( 'Published WPForms forms were found, but their fields or settings are outside supported coverage. Use ordinary name, email, text and paragraph fields; do not remove CAPTCHA or other protections to force a test.', 'submitsignal' ) : __( 'No published WPForms forms were found. Create and publish an ordinary contact form, then refresh inventory.', 'submitsignal' ) ) . '</p>'; }
    … …  
    432549        if ( $settings ) { self::receipt_controls( $settings ); }
    433550        if ( ! $settings && ! $pending ) { self::legacy_form(); }
    434         echo '<p>' . esc_html__( 'Launch coverage is simple public WPForms Lite forms. CAPTCHA, custom validation and unsupported fields can make a check inconclusive. WordPress mail handoff does not prove customer-inbox, spam-folder or CRM delivery. Existing email and CRM automations may still run for labeled tests.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/start' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Setup guide', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard?setup=wordpress' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/privacy' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Privacy', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Terms', 'submitsignal' ) . '</a></p></div>';
     551        echo '</details><p>' . esc_html__( 'Launch coverage is simple public WPForms Lite forms. CAPTCHA, custom validation and unsupported fields can make a check inconclusive. WordPress mail handoff does not prove customer-inbox, spam-folder or CRM delivery. Existing email and CRM automations may still run for labeled tests.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/start' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Setup guide', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard?setup=wordpress' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/privacy' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Privacy', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Terms', 'submitsignal' ) . '</a></p></div>';
    435552    }
    436553    private static function controls( $data, $forms, $uncertain ) {
    437         echo '<p>' . esc_html( 'Plan: ' . ( isset( $data['account']['plan'] ) ? $data['account']['plan'] : 'unknown' ) . ' · Billing: ' . ( isset( $data['account']['billingStatus'] ) ? $data['account']['billingStatus'] : 'unknown' ) . ' · Browser checks: ' . ( ! empty( $data['capabilities']['browser'] ) ? 'available' : 'unavailable' ) . ' · Daily scheduler: ' . ( ! empty( $data['capabilities']['scheduler'] ) ? 'available' : 'unavailable' ) ) . '</p>';
    438         echo '<p>' . esc_html__( 'Free accounts have one website, one monitor and up to three manual checks per day. Daily monitoring and failure emails require a paid plan and available service capabilities. A checkout return alone does not confirm payment; refresh verified status.', 'submitsignal' ) . '</p>';
     554        echo '<section style="max-width:760px;padding:16px;background:#fff;border:1px solid #dcdcde;margin:16px 0"><h2>' . esc_html__( '2. Check one form for free', 'submitsignal' ) . '</h2><p>' . esc_html__( 'Choose the form and public page. One explicit permission creates a paused monitor and requests exactly one labeled check. It does not start daily monitoring or take payment.', 'submitsignal' ) . '</p>';
     555        if ( empty( $data['capabilities']['browser'] ) ) { echo '<p role="status">' . esc_html__( 'Browser checks are currently unavailable. You can save a paused monitor and return later; no inquiry will be submitted now.', 'submitsignal' ) . '</p>'; }
     556        $pages = self::page_suggestions( $forms );
    439557        foreach ( array_slice( $data['monitors'], 0, 100 ) as $monitor ) {
    440558            if ( empty( $monitor['id'] ) || ! self::id( $monitor['id'] ) ) { continue; }
    441559            echo '<hr><h3>' . esc_html( isset( $monitor['name'] ) ? $monitor['name'] : 'Form monitor' ) . '</h3><p>' . esc_html( ! empty( $monitor['paused'] ) ? 'Daily schedule paused' : 'Daily schedule enabled; paid access and scheduler availability still apply' ) . '</p>';
    442560            $latest = null; foreach ( $data['runs'] as $run ) { if ( isset( $run['monitorId'] ) && $run['monitorId'] === $monitor['id'] && ( ! $latest || strcmp( (string) $run['startedAt'], (string) $latest['startedAt'] ) > 0 ) ) { $latest = $run; } }
    443             if ( $latest ) { self::evidence( $latest ); }
     561            if ( $latest ) { self::evidence( $latest, empty( $data['account']['hasExistingSubscription'] ) ); }
    444562            $review = isset( $monitor['submissionReview'] ) ? $monitor['submissionReview'] : null;
    445563            if ( $review ) {
    … …  
    447565                if ( ! $uncertain && isset( $review['state'], $review['runId'] ) && 'needs_review' === $review['state'] ) { self::open_form( 'review_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::hidden( 'run_id', $review['runId'] ); self::checkbox( 'acknowledgement', __( 'I reviewed this exact attempt and its possible downstream effects. I authorize clearing its hold without submitting another inquiry.', 'submitsignal' ) ); self::close_form( __( 'Acknowledge reviewed attempt', 'submitsignal' ) ); }
    448566            } elseif ( ! $uncertain && ! empty( $data['capabilities']['browser'] ) && ( ! $latest || 'running' !== $latest['status'] ) ) {
     567                $repeat_passed = $latest && 'passed' === $latest['status'];
     568                if ( $repeat_passed ) { echo '<details><summary>' . esc_html__( 'Run another manual check', 'submitsignal' ) . '</summary>'; }
    449569                self::open_form( 'run_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::checkbox( 'test_permission', __( 'I authorize one labeled synthetic inquiry on this form. Its existing email and CRM automations may run.', 'submitsignal' ) ); self::close_form( $latest ? __( 'Run one check', 'submitsignal' ) : __( 'Run first check', 'submitsignal' ), true );
     570                if ( $repeat_passed ) { echo '</details>'; }
    450571            }
    451             if ( ! $uncertain && ( empty( $monitor['paused'] ) || self::may_resume( $data, $monitor ) ) ) { self::open_form( 'pause_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::hidden( 'paused', empty( $monitor['paused'] ) ? '1' : '0' ); if ( ! empty( $monitor['paused'] ) ) { self::checkbox( 'schedule_permission', __( 'I authorize one labeled check each day while paid access and scheduling are available.', 'submitsignal' ) ); } self::close_form( empty( $monitor['paused'] ) ? __( 'Pause daily monitoring', 'submitsignal' ) : __( 'Resume daily monitoring', 'submitsignal' ) ); }
    452572        }
    453573        $free_limit_reached = isset( $data['account']['plan'] ) && 'early_access' === $data['account']['plan'] && count( $data['monitors'] ) >= 1;
    454574        if ( ! $uncertain && $free_limit_reached ) {
    455             echo '<hr><h2>' . esc_html__( 'Your free monitor is already set up', 'submitsignal' ) . '</h2><p>' . esc_html__( 'The free plan includes one monitor. Use the existing monitor above instead of creating another.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> — ' . esc_html__( 'If you chose the wrong form or page and have not run a check, delete that untested monitor in the dashboard, return here, choose Refresh status, and recreate it correctly.', 'submitsignal' ) . '</p><p>' . esc_html__( 'Submission-review holds survive deletion. Never delete and recreate a monitor to evade a hold or repeat an uncertain inquiry; review the original attempt and its downstream effects first.', 'submitsignal' ) . '</p>';
     575            echo '<details><summary>' . esc_html__( 'Free monitor options and recovery', 'submitsignal' ) . '</summary><h2>' . esc_html__( 'Your free monitor is already set up', 'submitsignal' ) . '</h2><p>' . esc_html__( 'The free plan includes one monitor. Use the existing monitor above instead of creating another.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> — ' . esc_html__( 'If you chose the wrong form or page and have not run a check, delete that untested monitor in the dashboard, return here, choose Refresh status, and recreate it correctly.', 'submitsignal' ) . '</p><p>' . esc_html__( 'Submission-review holds survive deletion. Never delete and recreate a monitor to evade a hold or repeat an uncertain inquiry; review the original attempt and its downstream effects first.', 'submitsignal' ) . '</p></details>';
    456576        } elseif ( ! $uncertain && $forms ) {
    457             echo '<hr><h2>' . esc_html__( 'Choose a form for its first check', 'submitsignal' ) . '</h2>'; self::open_form( 'create_monitor' );
    458             echo '<p><label>' . esc_html__( 'Form', 'submitsignal' ) . ' <select name="form_id" required><option value="">' . esc_html__( 'Choose a compatible form', 'submitsignal' ) . '</option>'; foreach ( $forms as $form ) { echo '<option value="' . esc_attr( $form['id'] ) . '">' . esc_html( $form['name'] ) . '</option>'; } echo '</select></label></p><p><label>' . esc_html__( 'Monitor name', 'submitsignal' ) . ' <input class="regular-text" name="name" maxlength="100" required></label></p><p><label>' . esc_html__( 'Public HTTPS page containing this form', 'submitsignal' ) . ' <input class="regular-text" name="page_url" type="url" maxlength="2048" required></label></p>';
    459             self::checkbox( 'consent', __( 'I own or administer this website and authorize labeled synthetic tests of this form, including its existing email and CRM automations. The monitor starts paused; running a check and enabling a daily schedule are separate actions.', 'submitsignal' ) ); self::close_form( __( 'Create paused monitor', 'submitsignal' ), true );
    460         }
    461         echo '<hr><h2>' . esc_html__( 'Optional daily monitoring', 'submitsignal' ) . '</h2><p>' . esc_html__( 'Solo: $19/month plus tax. Agency: $99/month plus tax. Paid launch enrollment is limited to US businesses with US billing, subject to current capacity. Payment details are entered on Stripe; WordPress never receives them. Returning here does not by itself verify payment.', 'submitsignal' ) . '</p>';
     577            if ( ! empty( $data['capabilities']['browser'] ) ) {
     578                self::open_form( 'check_form' ); self::setup_fields( $forms, $pages, 'check_form' );
     579                self::checkbox( 'consent', __( 'I own or administer this website. Create a paused monitor for the form and page shown above and run exactly one labeled test inquiry now. Its existing email and CRM automations may run. Daily checks and payment need separate permission.', 'submitsignal' ) ); self::close_form( __( 'Check this form', 'submitsignal' ), true );
     580            }
     581            echo '<details><summary>' . esc_html__( 'Save a paused monitor without running a check', 'submitsignal' ) . '</summary>';
     582            self::open_form( 'create_monitor' ); self::setup_fields( $forms, $pages, 'create_monitor' );
     583            self::checkbox( 'consent', __( 'I own or administer this website and authorize monitoring setup for this selected form. Save it paused without submitting an inquiry. A check or daily schedule requires a later explicit choice.', 'submitsignal' ) ); self::close_form( __( 'Create paused monitor', 'submitsignal' ) ); echo '</details>';
     584        }
     585        self::setup_defaults_script( $forms, $pages );
     586        echo '<p>' . esc_html__( 'Free diagnostics include one website, one monitor and up to three manual checks per day. Read browser submission, WordPress acceptance and email handoff separately; they do not prove inbox or CRM delivery.', 'submitsignal' ) . '</p></section>';
     587        echo '<section id="submitsignal-daily-monitoring" style="max-width:760px;padding:16px;background:#fff;border:1px solid #dcdcde;margin:16px 0"><h2>' . esc_html__( '3. Optional daily monitoring', 'submitsignal' ) . '</h2><p>' . esc_html( 'Plan: ' . ( isset( $data['account']['plan'] ) && 'early_access' !== $data['account']['plan'] ? $data['account']['plan'] : 'Free diagnostics' ) . ' · Billing: ' . ( isset( $data['account']['billingStatus'] ) ? $data['account']['billingStatus'] : 'unknown' ) ) . '</p><p>' . esc_html__( 'We recommend reviewing a successful check before paying. Solo is $19/month plus tax for one website; Agency is $99/month plus tax for up to ten. US business and US billing are required, subject to current launch capacity. Stripe handles payment details. Buying a plan does not enable the schedule.', 'submitsignal' ) . '</p>';
    462588        if ( ! $uncertain && ! empty( $data['account']['publicCheckout'] ) && ! empty( $data['capabilities']['billing'] ) && empty( $data['account']['hasExistingSubscription'] ) ) {
    463             self::open_form( 'checkout' ); echo '<p><label>' . esc_html__( 'Plan', 'submitsignal' ) . ' <select name="plan" required><option value="">' . esc_html__( 'Choose a plan', 'submitsignal' ) . '</option><option value="solo">Solo — $19/month plus tax</option><option value="agency">Agency — $99/month plus tax</option></select></label></p>';
    464             self::hidden( 'terms_version', self::TERMS ); self::checkbox( 'us_business', __( 'This is a US business.', 'submitsignal' ) ); self::checkbox( 'us_billing', __( 'The billing country is the United States.', 'submitsignal' ) ); self::checkbox( 'supported_use', __( 'My use is limited to supported ordinary WPForms Lite contact forms and I have permission to run labeled checks.', 'submitsignal' ) );
    465             echo '<p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Read the current commercial terms', 'submitsignal' ) . '</a> · ' . esc_html( self::TERMS ) . '</p>'; self::checkbox( 'commercial_consent', __( 'I accept the current commercial terms and authorize the selected monthly subscription plus tax, renewing until canceled. I will review the total and complete payment on Stripe.', 'submitsignal' ) ); self::close_form( __( 'Continue to secure checkout', 'submitsignal' ), true );
     589            self::checkout_form( 'solo' );
     590            echo '<details><summary>' . esc_html__( 'Need up to ten websites? Choose Agency — $99/month plus tax', 'submitsignal' ) . '</summary>'; self::checkout_form( 'agency' ); echo '</details>';
    466591        }
    467592        if ( ! $uncertain && ! empty( $data['account']['canManageBilling'] ) ) { self::open_form( 'billing_portal' ); self::close_form( __( 'Manage billing on Stripe', 'submitsignal' ) ); }
    468         if ( ! $uncertain ) { echo '<hr><h2>' . esc_html__( 'Disconnect WordPress controls', 'submitsignal' ) . '</h2>'; self::open_form( 'revoke' ); self::checkbox( 'acknowledgement', __( 'Pause this website’s monitors, revoke these WordPress controls and remove local credentials and queued metadata. Billing and hosted history remain unchanged; this does not cancel a subscription.', 'submitsignal' ) ); self::close_form( __( 'Pause and disconnect this website', 'submitsignal' ) ); }
     593        if ( ! $uncertain ) { foreach ( $data['monitors'] as $monitor ) { if ( ! empty( $monitor['id'] ) && self::id( $monitor['id'] ) && ( empty( $monitor['paused'] ) || self::may_resume( $data, $monitor ) ) ) { echo '<p><strong>' . esc_html( isset( $monitor['name'] ) ? $monitor['name'] : 'Form monitor' ) . '</strong></p>'; self::open_form( 'pause_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::hidden( 'paused', empty( $monitor['paused'] ) ? '1' : '0' ); if ( ! empty( $monitor['paused'] ) ) { self::checkbox( 'schedule_permission', __( 'I authorize one labeled check each day while paid access and scheduling are available.', 'submitsignal' ) ); } self::close_form( empty( $monitor['paused'] ) ? __( 'Pause daily monitoring', 'submitsignal' ) : __( 'Resume daily monitoring', 'submitsignal' ) ); } } }
     594        echo '</section>';
     595        if ( ! $uncertain ) { echo '<details><summary>' . esc_html__( 'Pause and disconnect WordPress controls', 'submitsignal' ) . '</summary>'; self::open_form( 'revoke' ); self::checkbox( 'acknowledgement', __( 'Pause this website’s monitors, revoke these WordPress controls and remove local credentials and queued metadata. Billing and hosted history remain unchanged; this does not cancel a subscription.', 'submitsignal' ) ); self::close_form( __( 'Pause and disconnect this website', 'submitsignal' ) ); echo '</details>'; }
    469596    }
    470597    private static function receipt_controls( $settings ) {
  • submitsignal/tags/0.4.0/readme.txt

    r3780656 r3782284  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 0.3.1
     7Stable tag: 0.4.0
    88License: GPL-2.0-or-later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 Connect ordinary WPForms Lite contact forms to SubmitSignal reliability checks.
     11Check a WPForms Lite contact form free from WordPress. Choose optional daily monitoring after reviewing its result.
    1212
    1313== Description ==
    1414
     15Choose a form, review its public page and authorize one clearly labeled inquiry. SubmitSignal shows separate browser submission, WordPress acceptance and email-handoff evidence in WordPress. Start free with one website, one form and up to three manual checks per day. No card is required.
     16
     17Version 0.4.0 shortens setup: connected form controls load automatically, known public form pages are suggested locally, monitor names are supplied, and one explicit Check this form action can prepare a paused monitor and run its first authorized check. Daily monitoring and payment remain separate choices.
     18
    1519SubmitSignal connects an administrator-authorized WordPress website to the hosted SubmitSignal monitoring service. It reports metadata about successful WPForms processing and WordPress mail-transport outcomes. The service can submit visibly labeled synthetic contact inquiries to forms the account owner explicitly selects.
    1620
    17 This is the official SubmitSignal WordPress connector. SubmitSignal, this plugin and the hosted monitoring service are owned and operated by Axtrel AI LLC. Visit the [official service website](https://getsubmitsignal.com) or [setup guide](https://getsubmitsignal.com/start). This connector uses the canonical service API at https://getsubmitsignal.com. Version 0.3.0 adds administrator-only setup, explicit manual checks, evidence, submission-review controls and billing handoff inside Settings > SubmitSignal.
     21This is the official SubmitSignal WordPress connector. SubmitSignal, this plugin and the hosted monitoring service are owned and operated by Axtrel AI LLC. Visit the [official service website](https://getsubmitsignal.com) or [setup guide](https://getsubmitsignal.com/start). This connector uses the canonical service API at https://getsubmitsignal.com. Setup, individually authorized checks, evidence, submission-review controls and the billing handoff are available inside Settings > SubmitSignal.
    1822
    1923Supported launch coverage is ordinary WPForms Lite contact forms. WPForms Pro is not included in supported launch coverage until acceptance testing with a licensed Pro installation is complete.
    … …  
    2125The free plugin requires a SubmitSignal service account. The free service tier includes manual diagnostics for one website and one monitor, with up to three checks per day. Scheduled daily monitoring and automated email failure alerts require an active Solo or Agency subscription. Current availability and subscription pricing are shown on the service website.
    2226
    23 Paid launch enrollment is limited to US businesses with US billing. Solo monitoring is $19/month plus tax; Agency monitoring is $99/month plus tax. Start with a [free form review](https://getsubmitsignal.com/compatibility). This questionnaire evaluates your answers; it does not visit or submit your form. An actual test requires the website owner's authorization.
     27Paid launch enrollment is limited to US businesses with US billing. Solo monitoring is $19/month plus tax; Agency monitoring is $99/month plus tax. Install free and authorize your first check in WordPress. If you are unsure about fit, the optional [free form review](https://getsubmitsignal.com/compatibility) evaluates your answers without visiting or submitting your form. An actual test requires the website owner's authorization.
    2428
    2529Browser checks require the service's browser runner to be configured, and scheduled monitoring and email alerts also depend on the hosted scheduler and email service. The dashboard shows current availability. Installing this plugin alone does not start synthetic submissions or promise active scheduled checks.
    … …  
    4751The administrator signs in to SubmitSignal, approves this website and returns to WordPress. Private metadata and management tokens are stored only in non-autoloaded server options; they never appear in HTML, JavaScript or URLs. Existing older connections need a separate new approval to enable WordPress controls. An uncertain response retains the original private request for an explicit retry.
    4852
    49 Native management requests are sent only after an administrator action protected by a WordPress nonce. Loading settings makes no service request. Status is a saved snapshot until explicitly refreshed; an accepted management action may perform one bounded status refresh. New native monitors start paused. Creating a monitor, running one labeled check, acknowledging an uncertain prior attempt, resuming daily monitoring and starting paid checkout each require explicit choices. There is no anonymous WordPress telemetry or automatic polling.
     53Native management changes require an administrator action protected by a WordPress nonce and single-use action token. After the administrator-approved connection callback, an accepted action, or a checkout return, the plugin may perform one bounded read of verified service status. Ordinary settings visits do not poll or submit forms. A checkout return never proves payment. Public form-page suggestions are read locally from published, non-password-protected WordPress content; only the page selected for monitoring is sent with the authorized setup action. New monitors start paused. Check this form explicitly authorizes preparing that monitor and submitting one labeled inquiry. Acknowledging an uncertain attempt, enabling daily monitoring and starting paid checkout remain separate choices. There is no anonymous WordPress telemetry.
    5054
    5155While connected, the plugin sends compatible form inventories, heartbeat timestamps, random event IDs, form IDs, form-processing outcomes, mail-transport outcomes and synthetic run references. This default metadata stream excludes entered field values, visitor names, email addresses, recipients, message content, visitor IP addresses and cookies. Separately enabled synthetic notification copies are processed by Resend as described above. The service records only the receiving event ID, test/run identifiers, unique test destination and receipt timestamps; it does not fetch the received message body. The hosted browser checks visit publicly accessible form pages; the hosted service's privacy policy separately describes that processing.
    … …  
    5761== Installation ==
    5862
    59 1. Download the current [official connector ZIP](https://getsubmitsignal.com/downloads/submitsignal.zip) and use WordPress > Plugins > Add New Plugin > Upload Plugin. The [WordPress.org listing](https://wordpress.org/plugins/submitsignal/) also offers the latest directory-published release; confirm its version before installing. Check the installed version after updating.
    60 2. Activate SubmitSignal. Install and activate WPForms Lite and create an ordinary contact form if needed. WPForms Pro is not yet part of supported launch coverage.
    61 3. Open Settings > SubmitSignal, or choose Settings beside SubmitSignal on the Plugins page. Read and accept the connection disclosure, then choose Connect SubmitSignal. Sign in to the service and approve this website; you return to the same WordPress administrator session without copying a code. A separate legacy code option remains available for hosted-dashboard-only control.
    62 4. Choose Refresh status to load the controls. Select Form, enter Monitor name and Public HTTPS page containing this form, explicitly authorize labeled tests, then choose Create paused monitor. Creation does not submit the form. The free plan includes one monitor; if you chose the wrong form or page and have not run a check, use Open dashboard to delete that untested monitor, then return, refresh status and recreate it correctly. Submission-review holds survive deletion; never delete and recreate a monitor to evade a hold or repeat an uncertain inquiry.
    63 5. Choose Run first check and explicitly authorize one synthetic inquiry. Review browser, WordPress acceptance and mail-handoff evidence. Inspect your real inbox and existing CRM separately. If an attempt may already have submitted, review its downstream effects before acknowledging the hold; acknowledgment does not rerun it.
    64 6. Free manual diagnostics cover one website, one monitor and up to three checks per day. For daily monitoring, choose Solo ($19/month plus tax) or Agency ($99/month plus tax), confirm US business/billing and supported use, accept the current commercial terms, and continue to Stripe. Payment details never enter WordPress. Refresh verified service status after returning; the return alone is not proof of payment. Enable a daily schedule only after a useful first check and explicit schedule permission.
    65 7. Use Pause or Resume for daily monitoring, and Manage billing when available. Billing and hosted history remain separate from disconnecting WordPress controls. The [setup guide](https://getsubmitsignal.com/start) and [hosted dashboard](https://getsubmitsignal.com/dashboard?setup=wordpress) remain available.
     631. Install and activate SubmitSignal and WPForms Lite. Open Settings > SubmitSignal, accept the connection disclosure and choose Connect SubmitSignal. Sign in, review the website and account shown, approve and return to the same WordPress administrator session. No pairing code is needed and the form controls load automatically. The [official website ZIP](https://getsubmitsignal.com/downloads/submitsignal.zip) can be installed with Plugins > Add New Plugin > Upload Plugin.
     642. Review the form and its public HTTPS page. WordPress suggests published pages containing a recognized embed; dynamic or theme embeds may require entering the URL yourself. Authorize one labeled inquiry and choose Check this form. It creates a paused monitor and requests one check; existing email and CRM automations may run. Review each evidence stage. An uncertain submission must be reviewed before any later check; deleting or recreating the monitor does not bypass a hold.
     653. After a useful first check, choose optional daily monitoring: Solo $19/month plus applicable tax for one website, or Agency $99/month plus applicable tax for up to ten. Confirm US business/billing eligibility and supported use, accept the selected monthly terms, and complete payment securely on Stripe/Link. WordPress reads verified status on return. Choose to enable daily checks separately when paid access is confirmed. Cancel through Manage billing; uninstalling or disconnecting does not cancel a subscription.
     66
     67Free diagnostics include one website, one form and up to three manual checks per day. Daily monitoring and automated failure emails require an active paid plan and operational services. See the [short setup guide](https://getsubmitsignal.com/start) or ask for free setup help there. Existing older connections and subscriptions are preserved. Manual pairing remains an advanced option.
    6668
    6769WordPress must be reachable over HTTPS and expose its public REST API. Install per website on multisite; network-wide configuration is not provided.
    … …  
    8688== Screenshots ==
    8789
    88 1. After account approval, refresh status in WordPress. Local demonstration with simulated service responses.
    89 2. Choose a supported form and public page, then create a paused monitor. Local demonstration; no form submission occurs.
    90 3. Review separate browser, WordPress and mail-handoff evidence. Local demonstration with simulated results; customer inbox and CRM delivery remain unverified.
     901. Connect from WordPress and approve the account connection. Local demonstration with simulated service responses.
     912. Review a suggested form page and authorize one first check. Local demonstration with simulated service responses.
     923. Review separate browser, WordPress and mail-handoff evidence, then choose optional daily monitoring. Local demonstration with simulated results; customer inbox and CRM delivery remain unverified.
    9193
    9294== Changelog ==
     95
     96= 0.4.0 =
     97* Shorten setup to connect, check one form and choose optional daily monitoring.
     98* Load verified form controls after approval and read verified status on checkout return, without automatic form tests or purchases.
     99* Suggest recognized public form pages locally and supply monitor names; preserve manual page entry when an embed is unknown.
     100* Combine paused monitor preparation and one explicitly authorized first check, preserving single-use attempts and uncertainty holds.
     101* Simplify billing declarations while retaining US eligibility, supported-use limits, exact-plan monthly terms and separate schedule permission.
     102* Preserve complete Stripe Checkout destinations, including opaque fragment state, without weakening host validation.
    93103
    94104= 0.3.1 =
  • submitsignal/tags/0.4.0/submitsignal.php

    r3780656 r3782284  
    33 * Plugin Name: SubmitSignal
    44 * Description: Connect ordinary WPForms contact forms to SubmitSignal reliability monitoring, with explicit administrator consent.
    5  * Version: 0.3.1
     5 * Version: 0.4.0
    66 * Requires at least: 6.2
    77 * Requires PHP: 7.4
    … …  
    1616
    1717final class SubmitSignal_Plugin {
    18     const VERSION = '0.3.1';
     18    const VERSION = '0.4.0';
    1919    const SERVICE = 'https://getsubmitsignal.com';
    2020    const SETTINGS = 'submitsignal_settings';
  • submitsignal/trunk/includes/native-admin.php

    r3780656 r3782284  
    1313        add_action( 'admin_post_submitsignal_native_action', array( __CLASS__, 'handle' ) );
    1414        add_action( 'admin_post_submitsignal_native_return', array( __CLASS__, 'returned' ) );
     15        add_action( 'admin_init', array( __CLASS__, 'checkout_return' ) );
    1516        add_action( 'submitsignal_native_expire', array( __CLASS__, 'expire' ) );
    1617    }
    … …  
    131132        if ( ! is_string( $url ) || strlen( $url ) > 4096 || preg_match( '/[\x00-\x20\x7f\\\\]/', $url ) ) { return false; }
    132133        $parts = wp_parse_url( $url );
     134        // Stripe Checkout can include opaque fragment state. Keep its full URL;
     135        // the portal retains its separate host and fragment restrictions.
    133136        return is_array( $parts ) && isset( $parts['scheme'], $parts['host'] ) && 'https' === $parts['scheme'] &&
    134137            ( $portal ? 'billing.stripe.com' : 'checkout.stripe.com' ) === $parts['host'] &&
    135             ! isset( $parts['user'] ) && ! isset( $parts['pass'] ) && ! isset( $parts['port'] ) && ! isset( $parts['fragment'] );
     138            ! isset( $parts['user'] ) && ! isset( $parts['pass'] ) && ! isset( $parts['port'] ) && ( ! $portal || ! isset( $parts['fragment'] ) );
    136139    }
    137140    public static function page_url( $url ) {
    138141        if ( ! is_string( $url ) || strlen( $url ) > 2048 || preg_match( '/[\x00-\x20\x7f\\\\]/', $url ) ) { return false; }
    139142        $parts = wp_parse_url( $url ); $home = wp_parse_url( home_url( '/' ) );
    140         if ( ! is_array( $parts ) || ! is_array( $home ) || empty( $parts['host'] ) || 'https' !== ( isset( $parts['scheme'] ) ? $parts['scheme'] : '' ) || strtolower( $parts['host'] ) !== strtolower( $home['host'] ) || isset( $parts['user'] ) || isset( $parts['pass'] ) || isset( $parts['fragment'] ) || isset( $parts['query'] ) || ( isset( $parts['port'] ) ? $parts['port'] : 443 ) !== ( isset( $home['port'] ) ? $home['port'] : 443 ) ) { return false; }
     143        if ( ! is_array( $parts ) || ! is_array( $home ) || empty( $parts['host'] ) || 'https' !== ( isset( $parts['scheme'] ) ? $parts['scheme'] : '' ) || preg_replace( '/\.$/', '', strtolower( $parts['host'] ) ) !== preg_replace( '/\.$/', '', strtolower( $home['host'] ) ) || isset( $parts['user'] ) || isset( $parts['pass'] ) || isset( $parts['fragment'] ) || isset( $parts['query'] ) || ( isset( $parts['port'] ) ? $parts['port'] : 443 ) !== ( isset( $home['port'] ) ? $home['port'] : 443 ) ) { return false; }
    141144        $base = rtrim( isset( $home['path'] ) ? $home['path'] : '', '/' );
    142145        $path = isset( $parts['path'] ) ? $parts['path'] : '/';
    … …  
    144147        if ( preg_match( '#(?:^|/)(?:\.|\.\.)(?:/|$)#', $decoded ) || false !== strpos( $decoded, '\\' ) || preg_match( '/[\x00-\x20\x7f]/', $decoded ) || preg_match( '/%2f|%5c/i', $path ) ) { return false; }
    145148        return '' === $base || $path === $base || 0 === strpos( $path, $base . '/' );
     149    }
     150    private static function canonical_page_url( $url ) {
     151        // Validate the original first. Match only the service's known origin
     152        // normalization; never decode or rewrite the selected page path.
     153        if ( ! self::page_url( $url ) ) { return false; }
     154        $parts = wp_parse_url( $url );
     155        $host = preg_replace( '/\.$/', '', strtolower( $parts['host'] ) );
     156        $port = isset( $parts['port'] ) && 443 !== $parts['port'] ? ':' . $parts['port'] : '';
     157        return 'https://' . $host . $port . ( ! empty( $parts['path'] ) ? $parts['path'] : '/' );
    146158    }
    147159    private static function api( $path, $body, $token = '' ) {
    … …  
    209221        if ( get_option( SubmitSignal_Plugin::SETTINGS ) !== $settings ) { return new WP_Error( 'storage', __( 'Approved credentials could not be saved. The private pending request remains for explicit claim retry after WordPress storage is repaired.', 'submitsignal' ) ); }
    210222        delete_option( self::VIEW ); self::clear_pending( self::key() ); SubmitSignal_Plugin::upgrade();
     223        // This single read follows the explicit account approval. A failed read
     224        // never repeats the claim or turns a connection into a form test.
     225        self::refresh_status( $settings );
    211226        return true;
     227    }
     228    private static function connected_notice() {
     229        $view = self::cache();
     230        return empty( $view['data'] ) ? __( 'Website connected. Current status is unavailable; use Refresh status. Do not repeat the connection.', 'submitsignal' ) : __( 'Website connected. Choose your form below and authorize its first check.', 'submitsignal' );
    212231    }
    213232    public static function returned() {
    … …  
    218237        if ( ! empty( $pending['claim_attempted'] ) ) { self::notice( __( 'A claim was already attempted. Use the explicit Retry connection claim button if its outcome is still unknown.', 'submitsignal' ) ); }
    219238        $result = self::claim( $pending );
    220         self::notice( is_wp_error( $result ) ? $result->get_error_message() : __( 'Website connected. Refresh status below, then choose a form and authorize its first check.', 'submitsignal' ) );
     239        self::notice( is_wp_error( $result ) ? $result->get_error_message() : self::connected_notice() );
    221240    }
    222241    private static function cache() {
    … …  
    248267    private static function payload( $operation, $view ) {
    249268        $body = array( 'action' => $operation );
    250         if ( 'create_monitor' === $operation ) {
     269        if ( in_array( $operation, array( 'create_monitor', 'check_form' ), true ) ) {
    251270            $form = self::input( 'form_id' ); $url = self::input( 'page_url' ); $name = sanitize_text_field( self::input( 'name' ) );
    252             if ( '1' !== self::input( 'consent' ) || ! in_array( $form, array_column( SubmitSignal_Plugin::forms(), 'id' ), true ) || ! self::page_url( $url ) || '' === $name || strlen( $name ) > 100 ) { return new WP_Error( 'input', __( 'Choose a compatible form, its HTTPS page on this website, a short name and explicit test permission.', 'submitsignal' ) ); }
    253             return array( 'action' => $operation, 'formId' => $form, 'pageUrl' => $url, 'name' => $name, 'consent' => true, 'intervalHours' => 24 );
     271            $forms = SubmitSignal_Plugin::forms();
     272            if ( '' === $name ) { foreach ( $forms as $item ) { if ( $item['id'] === $form ) { $name = substr( sanitize_text_field( $item['name'] ), 0, 100 ); } } }
     273            if ( '1' !== self::input( 'consent' ) || ! in_array( $form, array_column( $forms, 'id' ), true ) || ! self::page_url( $url ) || '' === $name || strlen( $name ) > 100 ) { return new WP_Error( 'input', __( 'Choose a compatible form, its HTTPS page on this website and explicit test permission.', 'submitsignal' ) ); }
     274            if ( 'check_form' === $operation && empty( $view['data']['capabilities']['browser'] ) ) { return new WP_Error( 'capability', __( 'Browser checks are unavailable. You can create a paused monitor without running it.', 'submitsignal' ) ); }
     275            if ( 'check_form' === $operation ) { foreach ( isset( $view['data']['monitors'] ) ? $view['data']['monitors'] : array() as $item ) { if ( isset( $item['formId'] ) && $item['formId'] === $form && ! empty( $item['submissionReview'] ) ) { return new WP_Error( 'consent', __( 'This form has an uncertain submission. Review its existing attempt before any further inquiry.', 'submitsignal' ) ); } } }
     276            return array( 'action' => 'create_monitor', 'formId' => $form, 'pageUrl' => self::canonical_page_url( $url ), 'name' => $name, 'consent' => true, 'intervalHours' => 24 );
    254277        }
    255278        if ( in_array( $operation, array( 'run_monitor', 'pause_monitor', 'review_monitor' ), true ) ) {
    … …  
    272295        if ( 'checkout' === $operation ) {
    273296            $plan = self::input( 'plan' );
    274             if ( ! in_array( $plan, array( 'solo', 'agency' ), true ) || '1' !== self::input( 'commercial_consent' ) || self::TERMS !== self::input( 'terms_version' ) || '1' !== self::input( 'us_business' ) || '1' !== self::input( 'us_billing' ) || '1' !== self::input( 'supported_use' ) ) { return new WP_Error( 'consent', __( 'Choose a plan and explicitly confirm the current terms, US business, US billing and supported use.', 'submitsignal' ) ); }
     297            $eligible = '1' === self::input( 'eligibility_consent' ) || ( '1' === self::input( 'us_business' ) && '1' === self::input( 'us_billing' ) && '1' === self::input( 'supported_use' ) );
     298            if ( ! in_array( $plan, array( 'solo', 'agency' ), true ) || '1' !== self::input( 'commercial_consent' ) || self::TERMS !== self::input( 'terms_version' ) || ! $eligible ) { return new WP_Error( 'consent', __( 'Choose a plan and explicitly confirm the current terms, US business, US billing and supported use.', 'submitsignal' ) ); }
    275299            $body['plan'] = $plan; $body['consent'] = array( 'accepted' => true, 'version' => self::TERMS, 'plan' => $plan );
    276300            $body['eligibility'] = array( 'businessCountry' => 'US', 'billingCountry' => 'US', 'supportedUse' => true );
    … …  
    287311    public static function handle() {
    288312        self::guard(); $operation = self::input( 'operation' );
    289         $allowed = array( 'start', 'retry_authorization', 'retry_claim', 'cancel_authorization', 'refresh_inventory', 'status', 'create_monitor', 'run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' );
     313        $allowed = array( 'start', 'retry_authorization', 'retry_claim', 'cancel_authorization', 'refresh_inventory', 'status', 'create_monitor', 'check_form', 'run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' );
    290314        if ( ! in_array( $operation, $allowed, true ) ) { wp_die( esc_html__( 'Unknown action.', 'submitsignal' ), '', array( 'response' => 400 ) ); }
    291315        check_admin_referer( 'submitsignal_native_' . $operation );
    … …  
    296320            wp_redirect( $result ); exit; // Exact canonical service path validated above.
    297321        }
    298         if ( 'retry_claim' === $operation ) { $pending = self::pending(); $result = $pending ? self::claim( $pending ) : new WP_Error( 'expired', 'The approval expired. Start a new connection deliberately.' ); self::notice( is_wp_error( $result ) ? $result->get_error_message() : 'Website connected. Refresh status to choose a form.' ); }
     322        if ( 'retry_claim' === $operation ) { $pending = self::pending(); $result = $pending ? self::claim( $pending ) : new WP_Error( 'expired', 'The approval expired. Start a new connection deliberately.' ); self::notice( is_wp_error( $result ) ? $result->get_error_message() : self::connected_notice() ); }
    299323        if ( 'refresh_inventory' === $operation ) {
    300324            if ( get_transient( 'submitsignal_native_inventory_lock' ) ) { self::notice( __( 'Wait one minute before refreshing inventory again.', 'submitsignal' ) ); }
    … …  
    338362            return $result;
    339363        }
     364        if ( 'check_form' === $operation ) {
     365            $monitor = isset( $result['monitor'] ) && is_array( $result['monitor'] ) ? $result['monitor'] : array();
     366            $already_known = false; foreach ( isset( $view['data']['monitors'] ) ? $view['data']['monitors'] : array() as $item ) { if ( isset( $item['id'], $monitor['id'] ) && $item['id'] === $monitor['id'] ) { $already_known = true; } }
     367            if ( $already_known || empty( $monitor['id'] ) || ! self::id( $monitor['id'] ) || ! isset( $monitor['siteId'], $monitor['formId'], $monitor['pageUrl'], $monitor['paused'] ) || $monitor['siteId'] !== $settings['site_id'] || $monitor['formId'] !== $body['formId'] || $monitor['pageUrl'] !== $body['pageUrl'] || true !== $monitor['paused'] || ! empty( $monitor['lastRunAt'] ) || ! empty( $monitor['submissionReview'] ) ) { return new WP_Error( 'native_uncertain', __( 'The created monitor could not be verified. No check was requested. Refresh status and review the existing monitor; do not recreate it.', 'submitsignal' ) ); }
     368            // The consumed attempt and action lease span both calls. Persist a
     369            // second fence before the one authorized run; never resume this
     370            // sequence after a timeout, process interruption or rejected create.
     371            $view['one_check'] = array( 'monitor_id' => $monitor['id'], 'run_attempted' => true );
     372            update_option( self::VIEW, $view, false );
     373            if ( get_option( self::VIEW ) !== $view ) { return new WP_Error( 'storage', __( 'The paused monitor was created, but its check could not be safely recorded. No check was requested. Refresh status before an explicit decision.', 'submitsignal' ) ); }
     374            $result = self::api( '/api/plugin/native/manage', array( 'action' => 'run_monitor', 'monitorId' => $monitor['id'] ), $settings['management_token'] );
     375            if ( is_wp_error( $result ) ) { self::rejected_grant( $result ); return $result; }
     376            $run = isset( $result['run'] ) && is_array( $result['run'] ) ? $result['run'] : array();
     377            if ( empty( $run['id'] ) || ! self::id( $run['id'] ) || ! isset( $run['siteId'], $run['monitorId'] ) || $run['siteId'] !== $settings['site_id'] || $run['monitorId'] !== $monitor['id'] ) { return new WP_Error( 'native_uncertain', __( 'The check outcome could not be verified. Refresh status and review this attempt before any further submission.', 'submitsignal' ) ); }
     378        }
    340379        if ( in_array( $operation, array( 'checkout', 'billing_portal' ), true ) ) {
    341380            if ( empty( $result['url'] ) || ! self::billing_url( $result['url'], 'billing_portal' === $operation ) ) { $view['site_id'] = $settings['site_id']; $view['uncertain'] = true; update_option( self::VIEW, $view, false ); return new WP_Error( 'destination', __( 'The billing destination could not be verified. Refresh status before another attempt.', 'submitsignal' ) ); }
    … …  
    351390        echo '<form method="post" action="' . esc_url( admin_url( 'admin-post.php' ) ) . '" style="margin:12px 0"><input type="hidden" name="action" value="submitsignal_native_action"><input type="hidden" name="operation" value="' . esc_attr( $operation ) . '">';
    352391        wp_nonce_field( 'submitsignal_native_' . $operation );
    353         if ( in_array( $operation, array( 'create_monitor', 'run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' ), true ) ) { self::hidden( 'native_attempt', self::$render_attempt ); }
     392        if ( in_array( $operation, array( 'create_monitor', 'check_form', 'run_monitor', 'pause_monitor', 'review_monitor', 'checkout', 'billing_portal', 'revoke' ), true ) ) { self::hidden( 'native_attempt', self::$render_attempt ); }
    354393    }
    355394    private static function hidden( $name, $value ) { echo '<input type="hidden" name="' . esc_attr( $name ) . '" value="' . esc_attr( $value ) . '">'; }
    … …  
    379418        return $fallback ? 'Other check: ' . $fallback : 'Other check';
    380419    }
    381     private static function evidence( $run ) {
     420    private static function evidence( $run, $show_daily_offer = false ) {
     421        $passed = isset( $run['status'] ) && 'passed' === $run['status'];
    382422        echo '<p><strong>' . esc_html( isset( $run['status'] ) ? $run['status'] : 'unverified' ) . '</strong> · ' . esc_html( isset( $run['startedAt'] ) ? $run['startedAt'] : '' ) . '</p>';
     423        if ( $passed && $show_daily_offer ) { echo '<p><a class="button button-primary" href="#submitsignal-daily-monitoring">' . esc_html__( 'Review daily monitoring — $19/month plus tax', 'submitsignal' ) . '</a></p>'; }
    383424        if ( ! empty( $run['summary'] ) && is_string( $run['summary'] ) ) { echo '<p>' . esc_html( substr( sanitize_text_field( $run['summary'] ), 0, 1000 ) ) . '</p>'; }
    384425        if ( ! empty( $run['stages'] ) && is_array( $run['stages'] ) ) {
     426            $stages = array_slice( $run['stages'], 0, 15 );
     427            if ( $passed ) {
     428                $successful = array(); $attention = array();
     429                foreach ( $stages as $stage ) { if ( is_array( $stage ) && isset( $stage['status'] ) && 'passed' === $stage['status'] ) { $successful[] = $stage; } else { $attention[] = $stage; } }
     430                if ( $successful ) { echo '<details><summary>' . esc_html__( 'View passed-check evidence', 'submitsignal' ) . '</summary>'; self::evidence_stages( $successful ); echo '</details>'; }
     431                // Unverified inbox/CRM results and any contradictory failure
     432                // remain visible; a passed browser result never upgrades them.
     433                if ( $attention ) { self::evidence_stages( $attention ); }
     434            } else { self::evidence_stages( $stages ); }
     435        }
     436    }
     437    private static function evidence_stages( $stages ) {
    385438            echo '<ul>';
    386             foreach ( array_slice( $run['stages'], 0, 15 ) as $stage ) {
     439            foreach ( $stages as $stage ) {
    387440                if ( ! is_array( $stage ) ) { continue; }
    388441                $status = isset( $stage['status'] ) && is_string( $stage['status'] ) && in_array( $stage['status'], array( 'passed', 'failed', 'unverified', 'inconclusive' ), true ) ? $stage['status'] : 'unverified';
    … …  
    392445            }
    393446            echo '</ul>';
     447    }
     448    private static function block_form_ids( $blocks, &$ids, &$remaining, $depth = 0 ) {
     449        if ( $depth > 20 ) { return; }
     450        foreach ( $blocks as $block ) {
     451            if ( --$remaining < 0 ) { return; }
     452            if ( isset( $block['blockName'], $block['attrs']['formId'] ) && 'wpforms/form-selector' === $block['blockName'] && is_scalar( $block['attrs']['formId'] ) ) { $ids[] = (string) $block['attrs']['formId']; }
     453            if ( ! empty( $block['innerBlocks'] ) ) { self::block_form_ids( $block['innerBlocks'], $ids, $remaining, $depth + 1 ); }
     454        }
     455    }
     456    /** Local suggestions only. They neither fetch a page nor change inventory. */
     457    public static function page_suggestions( $forms ) {
     458        $suggestions = array(); foreach ( $forms as $form ) { $suggestions[ $form['id'] ] = array(); }
     459        if ( ! $suggestions ) { return $suggestions; }
     460        $posts = get_posts( array( 'post_type' => array( 'page', 'post' ), 'post_status' => 'publish', 'has_password' => false, 'numberposts' => 100, 'orderby' => 'ID', 'order' => 'DESC', 'suppress_filters' => true ) );
     461        $pattern = '/' . get_shortcode_regex( array( 'wpforms' ) ) . '/s';
     462        foreach ( $posts as $post ) {
     463            if ( 'publish' !== $post->post_status || $post->post_password || ! is_post_publicly_viewable( $post ) || strlen( $post->post_content ) > 262144 ) { continue; }
     464            $url = get_permalink( $post ); if ( ! self::page_url( $url ) ) { continue; }
     465            $ids = array(); $remaining = 500;
     466            self::block_form_ids( parse_blocks( $post->post_content ), $ids, $remaining );
     467            if ( preg_match_all( $pattern, $post->post_content, $matches, PREG_SET_ORDER ) ) {
     468                foreach ( $matches as $match ) {
     469                    if ( ! empty( $match[1] ) || ! empty( $match[6] ) ) { continue; }
     470                    $attributes = shortcode_parse_atts( $match[3] );
     471                    if ( is_array( $attributes ) && isset( $attributes['id'] ) && is_string( $attributes['id'] ) ) { $ids[] = $attributes['id']; }
     472                }
     473            }
     474            foreach ( array_unique( $ids ) as $id ) {
     475                if ( isset( $suggestions[ $id ] ) && count( $suggestions[ $id ] ) < 10 ) { $suggestions[ $id ][] = array( 'url' => $url, 'title' => substr( sanitize_text_field( get_the_title( $post ) ), 0, 100 ) ); }
     476            }
     477        }
     478        return $suggestions;
     479    }
     480    private static function setup_fields( $forms, $pages, $operation ) {
     481        $single = 1 === count( $forms ) ? $forms[0] : null;
     482        $choices = $single && isset( $pages[ $single['id'] ] ) ? $pages[ $single['id'] ] : array();
     483        $url = 1 === count( $choices ) ? $choices[0]['url'] : '';
     484        $list = 'submitsignal-pages-' . $operation;
     485        echo '<div data-ss-setup><p><label>' . esc_html__( 'Contact form', 'submitsignal' ) . ' <select name="form_id" data-ss-form required><option value="">' . esc_html__( 'Choose your form', 'submitsignal' ) . '</option>';
     486        foreach ( $forms as $form ) { echo '<option value="' . esc_attr( $form['id'] ) . '" ' . selected( $single ? $single['id'] : '', $form['id'], false ) . '>' . esc_html( $form['name'] . ' (#' . $form['id'] . ')' ) . '</option>'; }
     487        echo '</select></label></p><p><label>' . esc_html__( 'Page containing this form', 'submitsignal' ) . ' <input class="regular-text" data-ss-url name="page_url" type="url" list="' . esc_attr( $list ) . '" value="' . esc_attr( $url ) . '" maxlength="2048" required></label><datalist id="' . esc_attr( $list ) . '">';
     488        foreach ( $choices as $choice ) { echo '<option value="' . esc_attr( $choice['url'] ) . '">' . esc_html( $choice['title'] ) . '</option>'; }
     489        echo '</datalist></p><p>' . esc_html__( 'Confirm the suggested public page, choose another suggestion, or paste its HTTPS URL. Theme-built and dynamically embedded forms may need a manual URL. Nothing is submitted until you choose an action below.', 'submitsignal' ) . '</p><details><summary>' . esc_html__( 'Monitor name (optional)', 'submitsignal' ) . '</summary><p><label>' . esc_html__( 'Name', 'submitsignal' ) . ' <input class="regular-text" data-ss-name name="name" maxlength="100" value="' . esc_attr( $single ? $single['name'] : '' ) . '"></label></p></details></div>';
     490    }
     491    private static function setup_defaults_script( $forms, $pages ) {
     492        $names = array(); foreach ( $forms as $form ) { $names[ $form['id'] ] = $form['name']; }
     493        $flags = JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT;
     494        echo '<script>(function(){var names=' . wp_json_encode( $names, $flags ) . ',pages=' . wp_json_encode( $pages, $flags ) . ';document.querySelectorAll("[data-ss-setup]").forEach(function(group){var form=group.querySelector("[data-ss-form]"),url=group.querySelector("[data-ss-url]"),name=group.querySelector("[data-ss-name]"),list=document.getElementById(url.getAttribute("list")),lastName=name.value;form.addEventListener("change",function(){var choices=pages[form.value]||[];if(name.value===lastName||!name.value){name.value=(names[form.value]||"").slice(0,100);}lastName=(names[form.value]||"").slice(0,100);url.value=choices.length===1?choices[0].url:"";while(list.firstChild){list.removeChild(list.firstChild);}choices.forEach(function(choice){var option=document.createElement("option");option.value=choice.url;option.label=choice.title;list.appendChild(option);});});});})();</script>';
     495    }
     496    private static function checkout_form( $plan ) {
     497        $label = 'agency' === $plan ? 'Agency' : 'Solo'; $price = 'agency' === $plan ? '99' : '19';
     498        self::open_form( 'checkout' ); self::hidden( 'plan', $plan ); self::hidden( 'terms_version', self::TERMS );
     499        self::checkbox( 'eligibility_consent', __( 'This is a US business with US billing. I will use only supported ordinary WPForms Lite contact forms that I own or am authorized to test.', 'submitsignal' ) );
     500        echo '<p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Read the current terms', 'submitsignal' ) . '</a> · ' . esc_html( self::TERMS ) . '</p>';
     501        self::checkbox( 'commercial_consent', sprintf( __( 'I accept the current terms and %1$s at US$%2$s per month plus applicable tax, renewing monthly until canceled. I will review the total and complete payment on Stripe.', 'submitsignal' ), $label, $price ) );
     502        self::close_form( sprintf( __( 'Continue with %1$s — $%2$s/month', 'submitsignal' ), $label, $price ), 'solo' === $plan );
     503    }
     504    /** Reconcile a checkout return before WordPress emits administrator headers. */
     505    public static function checkout_return() {
     506        if ( ! current_user_can( 'manage_options' ) || ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] || 'submitsignal' !== self::input( 'page', true ) || ! self::management() ) { return; }
     507        $checkout_return = self::input( 'checkout', true );
     508        if ( in_array( $checkout_return, array( 'success', 'canceled' ), true ) ) {
     509            $result = self::manage( 'status' );
     510            self::notice( is_wp_error( $result ) ? __( 'Returned from checkout, but current status is unavailable. Refresh status before another action. Returning never confirms payment.', 'submitsignal' ) : __( 'Returned from checkout. Your verified plan is shown below; the return link itself is not payment proof. Daily monitoring stays paused until you explicitly enable it.', 'submitsignal' ) );
    394511        }
    395512    }
    … …  
    404521        }
    405522        $notice = get_transient( 'submitsignal_notice_' . get_current_user_id() ); delete_transient( 'submitsignal_notice_' . get_current_user_id() );
    406         echo '<div class="wrap"><h1>SubmitSignal</h1><p>' . esc_html__( 'Set up and review contact-form checks here in WordPress. Connecting does not run a test or start a paid plan.', 'submitsignal' ) . '</p>';
     523        echo '<div class="wrap"><h1>SubmitSignal</h1><p>' . esc_html__( 'Connect your account, check one form for free, then choose whether to monitor it daily. No card is needed for your first check.', 'submitsignal' ) . '</p>';
    407524        if ( is_string( $notice ) && $notice ) { echo '<div class="notice notice-info"><p>' . esc_html( $notice ) . '</p></div>'; }
    408525        if ( $pending ) {
    … …  
    412529            self::open_form( 'cancel_authorization' ); self::close_form( __( 'Cancel local pending approval', 'submitsignal' ) );
    413530        } elseif ( ! $management ) {
    414             echo '<h2>' . esc_html( $settings ? __( 'Enable WordPress controls', 'submitsignal' ) : __( 'Connect your free account', 'submitsignal' ) ) . '</h2>';
     531            echo '<h2>' . esc_html( $settings ? __( '1. Enable WordPress controls', 'submitsignal' ) : __( '1. Connect your free account', 'submitsignal' ) ) . '</h2>';
    415532            echo '<p>' . esc_html__( 'Continue to SubmitSignal, sign in, approve this website, then return here. No pairing-code copying is needed. Existing metadata connections require this separate approval before WordPress can manage checks or billing.', 'submitsignal' ) . '</p>';
    416533            self::open_form( 'start' );
    … …  
    418535            self::close_form( $settings ? __( 'Enable WordPress controls', 'submitsignal' ) : __( 'Connect SubmitSignal', 'submitsignal' ), true );
    419536        } else {
    420             echo '<h2>' . esc_html__( 'Connected — WordPress controls enabled', 'submitsignal' ) . '</h2><p>' . esc_html__( 'Credentials are private server-side options. This page does not poll, submit forms or purchase anything automatically.', 'submitsignal' ) . '</p>';
    421             if ( empty( $view['data'] ) ) { echo '<p><strong>' . esc_html__( 'Next: choose Refresh status to load your form controls and current plan.', 'submitsignal' ) . '</strong> ' . esc_html__( 'Then select Form, enter Monitor name and Public HTTPS page containing this form, accept the test permission and choose Create paused monitor. No test runs until you separately authorize Run first check.', 'submitsignal' ) . '</p>'; }
     537            echo '<h2>' . esc_html__( '1. Connected — WordPress controls enabled', 'submitsignal' ) . '</h2>';
     538            if ( empty( $view['data'] ) ) { echo '<p><strong>' . esc_html__( 'Current status could not be loaded. Choose Refresh status to continue.', 'submitsignal' ) . '</strong> ' . esc_html__( 'Your connection is saved. Do not reconnect or repeat a check to load status.', 'submitsignal' ) . '</p>'; }
    422539            self::open_form( 'status' ); self::close_form( __( 'Refresh status', 'submitsignal' ), true );
    423540            if ( ! empty( $view['observed_at'] ) ) { echo '<p>' . esc_html( 'Last explicit status refresh: ' . gmdate( 'Y-m-d H:i:s', $view['observed_at'] ) . ' UTC. This is a saved snapshot.' ) . '</p>'; }
    … …  
    425542            if ( ! empty( $view['data'] ) ) { self::controls( $view['data'], $forms, ! empty( $view['uncertain'] ) ); }
    426543        }
    427         echo '<h2>' . esc_html__( 'Compatible form inventory', 'submitsignal' ) . '</h2>';
     544        echo '<details><summary>' . esc_html__( 'Form inventory and advanced settings', 'submitsignal' ) . '</summary><h2>' . esc_html__( 'Compatible form inventory', 'submitsignal' ) . '</h2>';
    428545        if ( ! function_exists( 'wpforms' ) ) { echo '<p>' . esc_html__( 'WPForms is not active. Activate WPForms Lite, then create and publish an ordinary contact form.', 'submitsignal' ) . '</p>'; }
    429546        elseif ( ! $forms ) { $published = get_posts( array( 'post_type' => 'wpforms', 'post_status' => 'publish', 'numberposts' => 1, 'fields' => 'ids' ) ); echo '<p>' . esc_html( $published ? __( 'Published WPForms forms were found, but their fields or settings are outside supported coverage. Use ordinary name, email, text and paragraph fields; do not remove CAPTCHA or other protections to force a test.', 'submitsignal' ) : __( 'No published WPForms forms were found. Create and publish an ordinary contact form, then refresh inventory.', 'submitsignal' ) ) . '</p>'; }
    … …  
    432549        if ( $settings ) { self::receipt_controls( $settings ); }
    433550        if ( ! $settings && ! $pending ) { self::legacy_form(); }
    434         echo '<p>' . esc_html__( 'Launch coverage is simple public WPForms Lite forms. CAPTCHA, custom validation and unsupported fields can make a check inconclusive. WordPress mail handoff does not prove customer-inbox, spam-folder or CRM delivery. Existing email and CRM automations may still run for labeled tests.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/start' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Setup guide', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard?setup=wordpress' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/privacy' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Privacy', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Terms', 'submitsignal' ) . '</a></p></div>';
     551        echo '</details><p>' . esc_html__( 'Launch coverage is simple public WPForms Lite forms. CAPTCHA, custom validation and unsupported fields can make a check inconclusive. WordPress mail handoff does not prove customer-inbox, spam-folder or CRM delivery. Existing email and CRM automations may still run for labeled tests.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/start' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Setup guide', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard?setup=wordpress' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/privacy' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Privacy', 'submitsignal' ) . '</a> · <a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Terms', 'submitsignal' ) . '</a></p></div>';
    435552    }
    436553    private static function controls( $data, $forms, $uncertain ) {
    437         echo '<p>' . esc_html( 'Plan: ' . ( isset( $data['account']['plan'] ) ? $data['account']['plan'] : 'unknown' ) . ' · Billing: ' . ( isset( $data['account']['billingStatus'] ) ? $data['account']['billingStatus'] : 'unknown' ) . ' · Browser checks: ' . ( ! empty( $data['capabilities']['browser'] ) ? 'available' : 'unavailable' ) . ' · Daily scheduler: ' . ( ! empty( $data['capabilities']['scheduler'] ) ? 'available' : 'unavailable' ) ) . '</p>';
    438         echo '<p>' . esc_html__( 'Free accounts have one website, one monitor and up to three manual checks per day. Daily monitoring and failure emails require a paid plan and available service capabilities. A checkout return alone does not confirm payment; refresh verified status.', 'submitsignal' ) . '</p>';
     554        echo '<section style="max-width:760px;padding:16px;background:#fff;border:1px solid #dcdcde;margin:16px 0"><h2>' . esc_html__( '2. Check one form for free', 'submitsignal' ) . '</h2><p>' . esc_html__( 'Choose the form and public page. One explicit permission creates a paused monitor and requests exactly one labeled check. It does not start daily monitoring or take payment.', 'submitsignal' ) . '</p>';
     555        if ( empty( $data['capabilities']['browser'] ) ) { echo '<p role="status">' . esc_html__( 'Browser checks are currently unavailable. You can save a paused monitor and return later; no inquiry will be submitted now.', 'submitsignal' ) . '</p>'; }
     556        $pages = self::page_suggestions( $forms );
    439557        foreach ( array_slice( $data['monitors'], 0, 100 ) as $monitor ) {
    440558            if ( empty( $monitor['id'] ) || ! self::id( $monitor['id'] ) ) { continue; }
    441559            echo '<hr><h3>' . esc_html( isset( $monitor['name'] ) ? $monitor['name'] : 'Form monitor' ) . '</h3><p>' . esc_html( ! empty( $monitor['paused'] ) ? 'Daily schedule paused' : 'Daily schedule enabled; paid access and scheduler availability still apply' ) . '</p>';
    442560            $latest = null; foreach ( $data['runs'] as $run ) { if ( isset( $run['monitorId'] ) && $run['monitorId'] === $monitor['id'] && ( ! $latest || strcmp( (string) $run['startedAt'], (string) $latest['startedAt'] ) > 0 ) ) { $latest = $run; } }
    443             if ( $latest ) { self::evidence( $latest ); }
     561            if ( $latest ) { self::evidence( $latest, empty( $data['account']['hasExistingSubscription'] ) ); }
    444562            $review = isset( $monitor['submissionReview'] ) ? $monitor['submissionReview'] : null;
    445563            if ( $review ) {
    … …  
    447565                if ( ! $uncertain && isset( $review['state'], $review['runId'] ) && 'needs_review' === $review['state'] ) { self::open_form( 'review_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::hidden( 'run_id', $review['runId'] ); self::checkbox( 'acknowledgement', __( 'I reviewed this exact attempt and its possible downstream effects. I authorize clearing its hold without submitting another inquiry.', 'submitsignal' ) ); self::close_form( __( 'Acknowledge reviewed attempt', 'submitsignal' ) ); }
    448566            } elseif ( ! $uncertain && ! empty( $data['capabilities']['browser'] ) && ( ! $latest || 'running' !== $latest['status'] ) ) {
     567                $repeat_passed = $latest && 'passed' === $latest['status'];
     568                if ( $repeat_passed ) { echo '<details><summary>' . esc_html__( 'Run another manual check', 'submitsignal' ) . '</summary>'; }
    449569                self::open_form( 'run_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::checkbox( 'test_permission', __( 'I authorize one labeled synthetic inquiry on this form. Its existing email and CRM automations may run.', 'submitsignal' ) ); self::close_form( $latest ? __( 'Run one check', 'submitsignal' ) : __( 'Run first check', 'submitsignal' ), true );
     570                if ( $repeat_passed ) { echo '</details>'; }
    450571            }
    451             if ( ! $uncertain && ( empty( $monitor['paused'] ) || self::may_resume( $data, $monitor ) ) ) { self::open_form( 'pause_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::hidden( 'paused', empty( $monitor['paused'] ) ? '1' : '0' ); if ( ! empty( $monitor['paused'] ) ) { self::checkbox( 'schedule_permission', __( 'I authorize one labeled check each day while paid access and scheduling are available.', 'submitsignal' ) ); } self::close_form( empty( $monitor['paused'] ) ? __( 'Pause daily monitoring', 'submitsignal' ) : __( 'Resume daily monitoring', 'submitsignal' ) ); }
    452572        }
    453573        $free_limit_reached = isset( $data['account']['plan'] ) && 'early_access' === $data['account']['plan'] && count( $data['monitors'] ) >= 1;
    454574        if ( ! $uncertain && $free_limit_reached ) {
    455             echo '<hr><h2>' . esc_html__( 'Your free monitor is already set up', 'submitsignal' ) . '</h2><p>' . esc_html__( 'The free plan includes one monitor. Use the existing monitor above instead of creating another.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> — ' . esc_html__( 'If you chose the wrong form or page and have not run a check, delete that untested monitor in the dashboard, return here, choose Refresh status, and recreate it correctly.', 'submitsignal' ) . '</p><p>' . esc_html__( 'Submission-review holds survive deletion. Never delete and recreate a monitor to evade a hold or repeat an uncertain inquiry; review the original attempt and its downstream effects first.', 'submitsignal' ) . '</p>';
     575            echo '<details><summary>' . esc_html__( 'Free monitor options and recovery', 'submitsignal' ) . '</summary><h2>' . esc_html__( 'Your free monitor is already set up', 'submitsignal' ) . '</h2><p>' . esc_html__( 'The free plan includes one monitor. Use the existing monitor above instead of creating another.', 'submitsignal' ) . '</p><p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/dashboard' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Open dashboard', 'submitsignal' ) . '</a> — ' . esc_html__( 'If you chose the wrong form or page and have not run a check, delete that untested monitor in the dashboard, return here, choose Refresh status, and recreate it correctly.', 'submitsignal' ) . '</p><p>' . esc_html__( 'Submission-review holds survive deletion. Never delete and recreate a monitor to evade a hold or repeat an uncertain inquiry; review the original attempt and its downstream effects first.', 'submitsignal' ) . '</p></details>';
    456576        } elseif ( ! $uncertain && $forms ) {
    457             echo '<hr><h2>' . esc_html__( 'Choose a form for its first check', 'submitsignal' ) . '</h2>'; self::open_form( 'create_monitor' );
    458             echo '<p><label>' . esc_html__( 'Form', 'submitsignal' ) . ' <select name="form_id" required><option value="">' . esc_html__( 'Choose a compatible form', 'submitsignal' ) . '</option>'; foreach ( $forms as $form ) { echo '<option value="' . esc_attr( $form['id'] ) . '">' . esc_html( $form['name'] ) . '</option>'; } echo '</select></label></p><p><label>' . esc_html__( 'Monitor name', 'submitsignal' ) . ' <input class="regular-text" name="name" maxlength="100" required></label></p><p><label>' . esc_html__( 'Public HTTPS page containing this form', 'submitsignal' ) . ' <input class="regular-text" name="page_url" type="url" maxlength="2048" required></label></p>';
    459             self::checkbox( 'consent', __( 'I own or administer this website and authorize labeled synthetic tests of this form, including its existing email and CRM automations. The monitor starts paused; running a check and enabling a daily schedule are separate actions.', 'submitsignal' ) ); self::close_form( __( 'Create paused monitor', 'submitsignal' ), true );
    460         }
    461         echo '<hr><h2>' . esc_html__( 'Optional daily monitoring', 'submitsignal' ) . '</h2><p>' . esc_html__( 'Solo: $19/month plus tax. Agency: $99/month plus tax. Paid launch enrollment is limited to US businesses with US billing, subject to current capacity. Payment details are entered on Stripe; WordPress never receives them. Returning here does not by itself verify payment.', 'submitsignal' ) . '</p>';
     577            if ( ! empty( $data['capabilities']['browser'] ) ) {
     578                self::open_form( 'check_form' ); self::setup_fields( $forms, $pages, 'check_form' );
     579                self::checkbox( 'consent', __( 'I own or administer this website. Create a paused monitor for the form and page shown above and run exactly one labeled test inquiry now. Its existing email and CRM automations may run. Daily checks and payment need separate permission.', 'submitsignal' ) ); self::close_form( __( 'Check this form', 'submitsignal' ), true );
     580            }
     581            echo '<details><summary>' . esc_html__( 'Save a paused monitor without running a check', 'submitsignal' ) . '</summary>';
     582            self::open_form( 'create_monitor' ); self::setup_fields( $forms, $pages, 'create_monitor' );
     583            self::checkbox( 'consent', __( 'I own or administer this website and authorize monitoring setup for this selected form. Save it paused without submitting an inquiry. A check or daily schedule requires a later explicit choice.', 'submitsignal' ) ); self::close_form( __( 'Create paused monitor', 'submitsignal' ) ); echo '</details>';
     584        }
     585        self::setup_defaults_script( $forms, $pages );
     586        echo '<p>' . esc_html__( 'Free diagnostics include one website, one monitor and up to three manual checks per day. Read browser submission, WordPress acceptance and email handoff separately; they do not prove inbox or CRM delivery.', 'submitsignal' ) . '</p></section>';
     587        echo '<section id="submitsignal-daily-monitoring" style="max-width:760px;padding:16px;background:#fff;border:1px solid #dcdcde;margin:16px 0"><h2>' . esc_html__( '3. Optional daily monitoring', 'submitsignal' ) . '</h2><p>' . esc_html( 'Plan: ' . ( isset( $data['account']['plan'] ) && 'early_access' !== $data['account']['plan'] ? $data['account']['plan'] : 'Free diagnostics' ) . ' · Billing: ' . ( isset( $data['account']['billingStatus'] ) ? $data['account']['billingStatus'] : 'unknown' ) ) . '</p><p>' . esc_html__( 'We recommend reviewing a successful check before paying. Solo is $19/month plus tax for one website; Agency is $99/month plus tax for up to ten. US business and US billing are required, subject to current launch capacity. Stripe handles payment details. Buying a plan does not enable the schedule.', 'submitsignal' ) . '</p>';
    462588        if ( ! $uncertain && ! empty( $data['account']['publicCheckout'] ) && ! empty( $data['capabilities']['billing'] ) && empty( $data['account']['hasExistingSubscription'] ) ) {
    463             self::open_form( 'checkout' ); echo '<p><label>' . esc_html__( 'Plan', 'submitsignal' ) . ' <select name="plan" required><option value="">' . esc_html__( 'Choose a plan', 'submitsignal' ) . '</option><option value="solo">Solo — $19/month plus tax</option><option value="agency">Agency — $99/month plus tax</option></select></label></p>';
    464             self::hidden( 'terms_version', self::TERMS ); self::checkbox( 'us_business', __( 'This is a US business.', 'submitsignal' ) ); self::checkbox( 'us_billing', __( 'The billing country is the United States.', 'submitsignal' ) ); self::checkbox( 'supported_use', __( 'My use is limited to supported ordinary WPForms Lite contact forms and I have permission to run labeled checks.', 'submitsignal' ) );
    465             echo '<p><a href="' . esc_url( SubmitSignal_Plugin::SERVICE . '/terms' ) . '" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Read the current commercial terms', 'submitsignal' ) . '</a> · ' . esc_html( self::TERMS ) . '</p>'; self::checkbox( 'commercial_consent', __( 'I accept the current commercial terms and authorize the selected monthly subscription plus tax, renewing until canceled. I will review the total and complete payment on Stripe.', 'submitsignal' ) ); self::close_form( __( 'Continue to secure checkout', 'submitsignal' ), true );
     589            self::checkout_form( 'solo' );
     590            echo '<details><summary>' . esc_html__( 'Need up to ten websites? Choose Agency — $99/month plus tax', 'submitsignal' ) . '</summary>'; self::checkout_form( 'agency' ); echo '</details>';
    466591        }
    467592        if ( ! $uncertain && ! empty( $data['account']['canManageBilling'] ) ) { self::open_form( 'billing_portal' ); self::close_form( __( 'Manage billing on Stripe', 'submitsignal' ) ); }
    468         if ( ! $uncertain ) { echo '<hr><h2>' . esc_html__( 'Disconnect WordPress controls', 'submitsignal' ) . '</h2>'; self::open_form( 'revoke' ); self::checkbox( 'acknowledgement', __( 'Pause this website’s monitors, revoke these WordPress controls and remove local credentials and queued metadata. Billing and hosted history remain unchanged; this does not cancel a subscription.', 'submitsignal' ) ); self::close_form( __( 'Pause and disconnect this website', 'submitsignal' ) ); }
     593        if ( ! $uncertain ) { foreach ( $data['monitors'] as $monitor ) { if ( ! empty( $monitor['id'] ) && self::id( $monitor['id'] ) && ( empty( $monitor['paused'] ) || self::may_resume( $data, $monitor ) ) ) { echo '<p><strong>' . esc_html( isset( $monitor['name'] ) ? $monitor['name'] : 'Form monitor' ) . '</strong></p>'; self::open_form( 'pause_monitor' ); self::hidden( 'monitor_id', $monitor['id'] ); self::hidden( 'paused', empty( $monitor['paused'] ) ? '1' : '0' ); if ( ! empty( $monitor['paused'] ) ) { self::checkbox( 'schedule_permission', __( 'I authorize one labeled check each day while paid access and scheduling are available.', 'submitsignal' ) ); } self::close_form( empty( $monitor['paused'] ) ? __( 'Pause daily monitoring', 'submitsignal' ) : __( 'Resume daily monitoring', 'submitsignal' ) ); } } }
     594        echo '</section>';
     595        if ( ! $uncertain ) { echo '<details><summary>' . esc_html__( 'Pause and disconnect WordPress controls', 'submitsignal' ) . '</summary>'; self::open_form( 'revoke' ); self::checkbox( 'acknowledgement', __( 'Pause this website’s monitors, revoke these WordPress controls and remove local credentials and queued metadata. Billing and hosted history remain unchanged; this does not cancel a subscription.', 'submitsignal' ) ); self::close_form( __( 'Pause and disconnect this website', 'submitsignal' ) ); echo '</details>'; }
    469596    }
    470597    private static function receipt_controls( $settings ) {
  • submitsignal/trunk/readme.txt

    r3780656 r3782284  
    55Tested up to: 7.1
    66Requires PHP: 7.4
    7 Stable tag: 0.3.1
     7Stable tag: 0.4.0
    88License: GPL-2.0-or-later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
    1010
    11 Connect ordinary WPForms Lite contact forms to SubmitSignal reliability checks.
     11Check a WPForms Lite contact form free from WordPress. Choose optional daily monitoring after reviewing its result.
    1212
    1313== Description ==
    1414
     15Choose a form, review its public page and authorize one clearly labeled inquiry. SubmitSignal shows separate browser submission, WordPress acceptance and email-handoff evidence in WordPress. Start free with one website, one form and up to three manual checks per day. No card is required.
     16
     17Version 0.4.0 shortens setup: connected form controls load automatically, known public form pages are suggested locally, monitor names are supplied, and one explicit Check this form action can prepare a paused monitor and run its first authorized check. Daily monitoring and payment remain separate choices.
     18
    1519SubmitSignal connects an administrator-authorized WordPress website to the hosted SubmitSignal monitoring service. It reports metadata about successful WPForms processing and WordPress mail-transport outcomes. The service can submit visibly labeled synthetic contact inquiries to forms the account owner explicitly selects.
    1620
    17 This is the official SubmitSignal WordPress connector. SubmitSignal, this plugin and the hosted monitoring service are owned and operated by Axtrel AI LLC. Visit the [official service website](https://getsubmitsignal.com) or [setup guide](https://getsubmitsignal.com/start). This connector uses the canonical service API at https://getsubmitsignal.com. Version 0.3.0 adds administrator-only setup, explicit manual checks, evidence, submission-review controls and billing handoff inside Settings > SubmitSignal.
     21This is the official SubmitSignal WordPress connector. SubmitSignal, this plugin and the hosted monitoring service are owned and operated by Axtrel AI LLC. Visit the [official service website](https://getsubmitsignal.com) or [setup guide](https://getsubmitsignal.com/start). This connector uses the canonical service API at https://getsubmitsignal.com. Setup, individually authorized checks, evidence, submission-review controls and the billing handoff are available inside Settings > SubmitSignal.
    1822
    1923Supported launch coverage is ordinary WPForms Lite contact forms. WPForms Pro is not included in supported launch coverage until acceptance testing with a licensed Pro installation is complete.
    … …  
    2125The free plugin requires a SubmitSignal service account. The free service tier includes manual diagnostics for one website and one monitor, with up to three checks per day. Scheduled daily monitoring and automated email failure alerts require an active Solo or Agency subscription. Current availability and subscription pricing are shown on the service website.
    2226
    23 Paid launch enrollment is limited to US businesses with US billing. Solo monitoring is $19/month plus tax; Agency monitoring is $99/month plus tax. Start with a [free form review](https://getsubmitsignal.com/compatibility). This questionnaire evaluates your answers; it does not visit or submit your form. An actual test requires the website owner's authorization.
     27Paid launch enrollment is limited to US businesses with US billing. Solo monitoring is $19/month plus tax; Agency monitoring is $99/month plus tax. Install free and authorize your first check in WordPress. If you are unsure about fit, the optional [free form review](https://getsubmitsignal.com/compatibility) evaluates your answers without visiting or submitting your form. An actual test requires the website owner's authorization.
    2428
    2529Browser checks require the service's browser runner to be configured, and scheduled monitoring and email alerts also depend on the hosted scheduler and email service. The dashboard shows current availability. Installing this plugin alone does not start synthetic submissions or promise active scheduled checks.
    … …  
    4751The administrator signs in to SubmitSignal, approves this website and returns to WordPress. Private metadata and management tokens are stored only in non-autoloaded server options; they never appear in HTML, JavaScript or URLs. Existing older connections need a separate new approval to enable WordPress controls. An uncertain response retains the original private request for an explicit retry.
    4852
    49 Native management requests are sent only after an administrator action protected by a WordPress nonce. Loading settings makes no service request. Status is a saved snapshot until explicitly refreshed; an accepted management action may perform one bounded status refresh. New native monitors start paused. Creating a monitor, running one labeled check, acknowledging an uncertain prior attempt, resuming daily monitoring and starting paid checkout each require explicit choices. There is no anonymous WordPress telemetry or automatic polling.
     53Native management changes require an administrator action protected by a WordPress nonce and single-use action token. After the administrator-approved connection callback, an accepted action, or a checkout return, the plugin may perform one bounded read of verified service status. Ordinary settings visits do not poll or submit forms. A checkout return never proves payment. Public form-page suggestions are read locally from published, non-password-protected WordPress content; only the page selected for monitoring is sent with the authorized setup action. New monitors start paused. Check this form explicitly authorizes preparing that monitor and submitting one labeled inquiry. Acknowledging an uncertain attempt, enabling daily monitoring and starting paid checkout remain separate choices. There is no anonymous WordPress telemetry.
    5054
    5155While connected, the plugin sends compatible form inventories, heartbeat timestamps, random event IDs, form IDs, form-processing outcomes, mail-transport outcomes and synthetic run references. This default metadata stream excludes entered field values, visitor names, email addresses, recipients, message content, visitor IP addresses and cookies. Separately enabled synthetic notification copies are processed by Resend as described above. The service records only the receiving event ID, test/run identifiers, unique test destination and receipt timestamps; it does not fetch the received message body. The hosted browser checks visit publicly accessible form pages; the hosted service's privacy policy separately describes that processing.
    … …  
    5761== Installation ==
    5862
    59 1. Download the current [official connector ZIP](https://getsubmitsignal.com/downloads/submitsignal.zip) and use WordPress > Plugins > Add New Plugin > Upload Plugin. The [WordPress.org listing](https://wordpress.org/plugins/submitsignal/) also offers the latest directory-published release; confirm its version before installing. Check the installed version after updating.
    60 2. Activate SubmitSignal. Install and activate WPForms Lite and create an ordinary contact form if needed. WPForms Pro is not yet part of supported launch coverage.
    61 3. Open Settings > SubmitSignal, or choose Settings beside SubmitSignal on the Plugins page. Read and accept the connection disclosure, then choose Connect SubmitSignal. Sign in to the service and approve this website; you return to the same WordPress administrator session without copying a code. A separate legacy code option remains available for hosted-dashboard-only control.
    62 4. Choose Refresh status to load the controls. Select Form, enter Monitor name and Public HTTPS page containing this form, explicitly authorize labeled tests, then choose Create paused monitor. Creation does not submit the form. The free plan includes one monitor; if you chose the wrong form or page and have not run a check, use Open dashboard to delete that untested monitor, then return, refresh status and recreate it correctly. Submission-review holds survive deletion; never delete and recreate a monitor to evade a hold or repeat an uncertain inquiry.
    63 5. Choose Run first check and explicitly authorize one synthetic inquiry. Review browser, WordPress acceptance and mail-handoff evidence. Inspect your real inbox and existing CRM separately. If an attempt may already have submitted, review its downstream effects before acknowledging the hold; acknowledgment does not rerun it.
    64 6. Free manual diagnostics cover one website, one monitor and up to three checks per day. For daily monitoring, choose Solo ($19/month plus tax) or Agency ($99/month plus tax), confirm US business/billing and supported use, accept the current commercial terms, and continue to Stripe. Payment details never enter WordPress. Refresh verified service status after returning; the return alone is not proof of payment. Enable a daily schedule only after a useful first check and explicit schedule permission.
    65 7. Use Pause or Resume for daily monitoring, and Manage billing when available. Billing and hosted history remain separate from disconnecting WordPress controls. The [setup guide](https://getsubmitsignal.com/start) and [hosted dashboard](https://getsubmitsignal.com/dashboard?setup=wordpress) remain available.
     631. Install and activate SubmitSignal and WPForms Lite. Open Settings > SubmitSignal, accept the connection disclosure and choose Connect SubmitSignal. Sign in, review the website and account shown, approve and return to the same WordPress administrator session. No pairing code is needed and the form controls load automatically. The [official website ZIP](https://getsubmitsignal.com/downloads/submitsignal.zip) can be installed with Plugins > Add New Plugin > Upload Plugin.
     642. Review the form and its public HTTPS page. WordPress suggests published pages containing a recognized embed; dynamic or theme embeds may require entering the URL yourself. Authorize one labeled inquiry and choose Check this form. It creates a paused monitor and requests one check; existing email and CRM automations may run. Review each evidence stage. An uncertain submission must be reviewed before any later check; deleting or recreating the monitor does not bypass a hold.
     653. After a useful first check, choose optional daily monitoring: Solo $19/month plus applicable tax for one website, or Agency $99/month plus applicable tax for up to ten. Confirm US business/billing eligibility and supported use, accept the selected monthly terms, and complete payment securely on Stripe/Link. WordPress reads verified status on return. Choose to enable daily checks separately when paid access is confirmed. Cancel through Manage billing; uninstalling or disconnecting does not cancel a subscription.
     66
     67Free diagnostics include one website, one form and up to three manual checks per day. Daily monitoring and automated failure emails require an active paid plan and operational services. See the [short setup guide](https://getsubmitsignal.com/start) or ask for free setup help there. Existing older connections and subscriptions are preserved. Manual pairing remains an advanced option.
    6668
    6769WordPress must be reachable over HTTPS and expose its public REST API. Install per website on multisite; network-wide configuration is not provided.
    … …  
    8688== Screenshots ==
    8789
    88 1. After account approval, refresh status in WordPress. Local demonstration with simulated service responses.
    89 2. Choose a supported form and public page, then create a paused monitor. Local demonstration; no form submission occurs.
    90 3. Review separate browser, WordPress and mail-handoff evidence. Local demonstration with simulated results; customer inbox and CRM delivery remain unverified.
     901. Connect from WordPress and approve the account connection. Local demonstration with simulated service responses.
     912. Review a suggested form page and authorize one first check. Local demonstration with simulated service responses.
     923. Review separate browser, WordPress and mail-handoff evidence, then choose optional daily monitoring. Local demonstration with simulated results; customer inbox and CRM delivery remain unverified.
    9193
    9294== Changelog ==
     95
     96= 0.4.0 =
     97* Shorten setup to connect, check one form and choose optional daily monitoring.
     98* Load verified form controls after approval and read verified status on checkout return, without automatic form tests or purchases.
     99* Suggest recognized public form pages locally and supply monitor names; preserve manual page entry when an embed is unknown.
     100* Combine paused monitor preparation and one explicitly authorized first check, preserving single-use attempts and uncertainty holds.
     101* Simplify billing declarations while retaining US eligibility, supported-use limits, exact-plan monthly terms and separate schedule permission.
     102* Preserve complete Stripe Checkout destinations, including opaque fragment state, without weakening host validation.
    93103
    94104= 0.3.1 =
  • submitsignal/trunk/submitsignal.php

    r3780656 r3782284  
    33 * Plugin Name: SubmitSignal
    44 * Description: Connect ordinary WPForms contact forms to SubmitSignal reliability monitoring, with explicit administrator consent.
    5  * Version: 0.3.1
     5 * Version: 0.4.0
    66 * Requires at least: 6.2
    77 * Requires PHP: 7.4
    … …  
    1616
    1717final class SubmitSignal_Plugin {
    18     const VERSION = '0.3.1';
     18    const VERSION = '0.4.0';
    1919    const SERVICE = 'https://getsubmitsignal.com';
    2020    const SETTINGS = 'submitsignal_settings';
Note: See TracChangeset for help on using the changeset viewer.