Plugin Directory

Changeset 3692533


Ignore:
Timestamp:
09/12/2026 09:48:37 AM (6 weeks ago)
Author:
vayapin
Message:

Release 2.3.2: mobile bottom sheet for the checkout widget, VayaConnect, address mapping and pin creation fixes

Location:
vayapin/trunk
Files:
14 added
16 edited

Legend:

Unmodified
Added
Removed
  • vayapin/trunk/README.md

    r3676127 r3692533  
    88| **WooCommerce** | 3.0+ (tested up to 11.x) |
    99| **Requires PHP** | 7.4+ |
    10 | **Stable tag** | 2.1.2 |
     10| **Stable tag** | 2.3.2 |
    1111| **License** | GPLv2 or later |
    1212
  • vayapin/trunk/README.txt

    r3676127 r3692533  
    44Requires at least: 6.0
    55Tested up to: 7.1
    6 Stable tag: 2.1.2
     6Stable tag: 2.3.2
    77Requires PHP: 7.4
    88License: GPLv2 or later
    … …  
    140140== Changelog ==
    141141
    142 = 2.1.2 =
    143 * Compatibility: tested against WordPress 7.1 and WooCommerce 11.0.
    144 * Compatibility: declares support for WooCommerce High-Performance Order Storage (HPOS), so stores can switch order storage with the plugin active.
    145 * Housekeeping: added the "WC requires at least" and "WC tested up to" plugin headers.
     142= 2.3.2 =
     143* Change: the mobile sheet takes the widget's own background colour instead of white, so the panel and the pin field read as one surface. The colour follows the merchant's accent setting.
     144* Fix: the sheet's back arrow disappeared under storefront button styling (themes match plain buttons through [type=button], which ties on specificity and loads last). The sheet's own controls are now insulated from it.
     145
     146= 2.3.1 =
     147* Fix: inside the mobile sheet the pin row lost its styling (oversized GO button, collapsed input) because the widget styles hang off a class that stayed behind on the original container. The row now matches the desktop layout exactly.
     148* Fix: the sheet's back button and search field no longer inherit storefront button and input styling.
     149
     150= 2.3.0 =
     151* New: on phones the whole widget flow opens in a bottom sheet: pin entry, country picking with search and full-size rows, drag or flick down to dismiss. Desktop keeps the existing popup.
     152* Fix: the widget scripts relied on jQuery's removed `.load()` shortcut and only ran on sites that still load jQuery Migrate.
     153
     154= 2.2.9 =
     155* Fix: typing and backspace were blocked in the VayaPin field after the country list had been opened. The list was hidden by class only, which left the country plugin's key handler bound and it cancelled every key press.
     156* Fix: the "Not found. Please enter a valid VayaPin." message stayed on screen after closing the popup. It is cleared on close, on reopen, and as soon as the code is edited.
     157
     158= 2.2.8 =
     159* Fix: checkout autofill left "Street address" and "Apartment" empty for pins that store the street number or the building name in separate fields (JP pins among them). The street line is now built from road + house number, the second line from building name + addition.
     160* Fix: city and state fall back to their "_part" counterparts when the primary field is empty.
     161
     162= 2.2.7 =
     163* Fix: checkout autofill now clears the "required field" validation errors on the fields it fills, instead of leaving them red until the field is touched manually.
     164* Fix: new VayaPin creation sent only the last name in the `name` field, which the API rejected with HTTP 422 ("name: must include a last name"). Full first + last name is sent now, per the /v1/pins contract.
     165* Fix: API validation errors were logged with an empty message, so a failed pin creation showed a bare HTTP_422 with no reason. The error code, message and per-field errors are parsed and stored now.
     166* Change: Pin Creation History CSV export includes the raw API response for failed attempts.
     167
     168= 2.2.0 =
     169* New: VayaConnect — request a VayaPin address from any customer. Adds a Partners tab listing WooCommerce customers with their VayaPin status (verified / pending / none) and a "Request VayaPin address" button.
     170* New: inbound `pin.address_updated` webhook receiver (REST: /wp-json/vayapin/v1/webhooks) with HMAC-SHA256 verification and replay idempotency. The verified record (VayaPin ID, every script, GPS, QR) is written back to the customer/order automatically.
     171* New: VayaConnect record surface on the order detail screen and customer profile.
     172* New: carrier-ready international label (dual-script + QR) at /vayapin-intl-label/{order_id}.
     173* New: VayaConnect on/off toggle (off by default). The outbound request verb is behind a single contract seam pending the VayaCore backend.
    146174
    147175= 2.1.1 =
    … …  
    161189== Upgrade Notice ==
    162190
    163 = 2.1.2 =
    164 Compatibility release for WordPress 7.1 and WooCommerce 11. No functional changes; safe update for all stores.
     191= 2.3.2 =
     192Mobile sheet visual fixes: single tinted surface and a visible back arrow.
     193
     194= 2.3.1 =
     195Fixes the pin row layout inside the mobile sheet.
     196
     197= 2.3.0 =
     198Adds the mobile bottom sheet and fixes empty street fields after autofill. Test the checkout widget on a phone after updating.
     199
     200= 2.2.9 =
     201Fixes the VayaPin input becoming uneditable after opening the country list, and the stale "Not found" message.
     202
     203= 2.2.8 =
     204Fixes empty street address fields after autofill for pins with a separate house number or building name.
     205
     206= 2.2.7 =
     207Fixes checkout validation errors sticking after autofill and VayaPin creation failing with HTTP 422. Recommended for all stores.
     208
     209= 2.2.0 =
     210Adds VayaConnect (Partners tab + inbound address webhook). Disabled by default — enable it under the Partners tab and set a webhook secret. Re-save permalinks if the international label URL 404s.
    165211
    166212= 2.1.1 =
  • vayapin/trunk/framework/ajax-methods.php

    r3531799 r3692533  
    171171    }
    172172   
     173    // A pin splits its address across more fields than WooCommerce has lines:
     174    // road + house_number + house (building) + addition, and settlement/region each
     175    // have a "_part" sibling. Reading only address_road/address_addition drops the
     176    // street entirely for pins that carry the number or the building name instead
     177    // (e.g. JP pins: "Marunouchi 1 Chome 9-1" + "Gran Tokyo North Tower").
     178    $road            = $get_field_value($data_source['address_road'] ?? '');
     179    $house_number    = $get_field_value($data_source['address_house_number'] ?? '');
     180    $house           = $get_field_value($data_source['address_house'] ?? '');
     181    $addition        = $get_field_value($data_source['address_addition'] ?? '');
     182    $settlement      = $get_field_value($data_source['address_settlement'] ?? '');
     183    $settlement_part = $get_field_value($data_source['address_settlement_part'] ?? '');
     184    $region          = $get_field_value($data_source['address_region'] ?? '');
     185    $region_part     = $get_field_value($data_source['address_region_part'] ?? '');
     186
     187    $join = function( $parts, $glue ) {
     188        $parts = array_filter( array_map( 'trim', $parts ), function ( $part ) {
     189            return '' !== $part;
     190        } );
     191        return implode( $glue, $parts );
     192    };
     193
     194    $address_1 = $join( array( $road, $house_number ), ' ' );
     195    $address_2 = $join( array( $house, $addition ), ', ' );
     196
     197    // Never leave line 1 empty while line 2 has the only street data.
     198    if ( '' === $address_1 && '' !== $address_2 ) {
     199        $address_1 = $address_2;
     200        $address_2 = '';
     201    }
     202
     203    $city_value  = '' !== $settlement ? $settlement : $settlement_part;
     204    $state_value = '' !== $region ? $region : $region_part;
     205
    173206    $data = [
    174207        'company'    => sanitize_text_field($title_value),
    … …  
    180213        'email'      => !empty($email_value) ? sanitize_email($email_value) : '',
    181214        'country'    => sanitize_text_field(strtoupper($data_source['country'] ?? '')),
    182         'city'       => sanitize_text_field($get_field_value($data_source['address_settlement'] ?? '')),
    183         'state'      => sanitize_text_field($get_field_value($data_source['address_region'] ?? '')),
     215        'city'       => sanitize_text_field($city_value),
     216        'state'      => sanitize_text_field($state_value),
    184217        'postcode'   => sanitize_text_field($get_field_value($data_source['address_postcode'] ?? '')),
    185218        'phone'      => sanitize_text_field($get_field_value($data_source['phone'] ?? '')),
    186         'address_1'  => sanitize_text_field($get_field_value($data_source['address_road'] ?? '')),
    187         'address_2'  => sanitize_text_field($get_field_value($data_source['address_addition'] ?? '')),
     219        'address_1'  => sanitize_text_field($address_1),
     220        'address_2'  => sanitize_text_field($address_2),
    188221        'debug'      => $debug_data
    189222    ];
    … …  
    353386   
    354387    // Generate CSV content
    355     $csv_content = "Timestamp,Order ID,Activity Type,Status,Error Code,Error Message,VayaPin ID\n";
     388    $csv_content = "Timestamp,Order ID,Activity Type,Status,Error Code,Error Message,VayaPin ID,API Response\n";
    356389
    357390    foreach ($logs as $log) {
    … …  
    370403        $vayapin_id    = $log->vayapin_id ? $log->vayapin_id : '';
    371404
     405        // Raw body of the failed call: without it a 422 cannot be diagnosed outside the DB.
     406        $raw_response = '';
     407        if ( 'failed' === $log->status && $log->api_response ) {
     408            $raw_response = str_replace(
     409                array( "\r", "\n", '"' ),
     410                array( ' ', ' ', '""' ),
     411                mb_substr( $log->api_response, 0, 2000 )
     412            );
     413        }
     414
    372415        $csv_content .= sprintf(
    373             '"%s","%s","%s","%s","%s","%s","%s"' . "\n",
     416            '"%s","%s","%s","%s","%s","%s","%s","%s"' . "\n",
    374417            $timestamp,
    375418            $order_id,
    … …  
    378421            $error_code,
    379422            $error_message,
    380             $vayapin_id
     423            $vayapin_id,
     424            $raw_response
    381425        );
    382426    }
    … …  
    465509    $logs = $wpdb->get_results( $query );
    466510
    467     $csv_content = "Timestamp,Event Type,Webhook URL,Status,Response Code,Error Message\n";
     511    $csv_content = "Timestamp,Event Type,Webhook URL,Status,Response Code,Error Message,Payload\n";
    468512    foreach ( $logs as $log ) {
    469513        $timestamp   = date_i18n( 'Y-m-d H:i:s', strtotime( $log->timestamp ) );
    … …  
    473517        $resp_code   = $log->response_code ? (string) $log->response_code : '';
    474518        $err_message = $log->error_message ? str_replace( array( "\r", "\n", '"' ), array( ' ', ' ', '""' ), (string) $log->error_message ) : '';
     519        $payload     = isset( $log->payload ) && '' !== (string) $log->payload ? str_replace( array( "\r", "\n", '"' ), array( ' ', ' ', '""' ), (string) $log->payload ) : '';
    475520
    476521        $csv_content .= sprintf(
    477             '"%s","%s","%s","%s","%s","%s"' . "\n",
     522            '"%s","%s","%s","%s","%s","%s","%s"' . "\n",
    478523            $timestamp,
    479524            $event_type,
    … …  
    481526            $status,
    482527            $resp_code,
    483             $err_message
     528            $err_message,
     529            $payload
    484530        );
    485531    }
  • vayapin/trunk/framework/backend-ui/admin-ui.php

    r3531799 r3692533  
    5757        'analytics_enabled'       => '1',
    5858        'analytics_weekly_email'  => '1',
     59        'vayaconnect_enabled'     => '0',
    5960        'label_generation'        => '1',
    6061        'order_confirmation_notice' => '1',
    … …  
    16061607    $new_input['analytics_enabled']     = $vp_on( 'analytics_enabled' ) ? '1' : '0';
    16071608    $new_input['analytics_weekly_email'] = $vp_on( 'analytics_weekly_email' ) ? '1' : '0';
     1609    $new_input['vayaconnect_enabled']    = $vp_on( 'vayaconnect_enabled' ) ? '1' : '0';
    16081610
    16091611    $allowed_widget_positions = array( 'above_billing', 'below_billing', 'above_shipping', 'custom' );
    … …  
    16871689            $new_input['webhook_secret'] = $options['webhook_secret'];
    16881690        }
     1691    }
     1692
     1693    // VayaConnect inbound webhook secret (separate from the outbound Notifications secret).
     1694    if ( isset( $input['vayaconnect_webhook_secret'] ) ) {
     1695        $vc_secret = sanitize_text_field( $input['vayaconnect_webhook_secret'] );
     1696
     1697        $existing_encrypted = $options['vayaconnect_webhook_secret'] ?? '';
     1698        $existing_decrypted = vayapin_decrypt_api_key( $existing_encrypted );
     1699        $masked_existing    = vayapin_mask_api_key( $existing_decrypted );
     1700
     1701        if ( empty( $vc_secret ) ) {
     1702            $new_input['vayaconnect_webhook_secret'] = '';
     1703        } elseif ( $vc_secret === $masked_existing && ! empty( $existing_encrypted ) ) {
     1704            // Unchanged masked value — keep the stored encrypted secret.
     1705            $new_input['vayaconnect_webhook_secret'] = $existing_encrypted;
     1706        } else {
     1707            $new_input['vayaconnect_webhook_secret'] = vayapin_encrypt_api_key( $vc_secret );
     1708        }
     1709    } elseif ( isset( $options['vayaconnect_webhook_secret'] ) ) {
     1710        $new_input['vayaconnect_webhook_secret'] = $options['vayaconnect_webhook_secret'];
    16891711    }
    16901712
    … …  
    21702192                        </td>
    21712193                    </tr>
     2194                    <?php if ( isset( $log->payload ) && '' !== (string) $log->payload ) : ?>
     2195                        <tr class="vp-log-payload-row">
     2196                            <td colspan="6">
     2197                                <details>
     2198                                    <summary><?php esc_html_e( 'Payload', 'vayapin-woocommerce' ); ?></summary>
     2199                                    <pre class="vp-log-payload"><?php echo esc_html( (string) $log->payload ); ?></pre>
     2200                                </details>
     2201                            </td>
     2202                        </tr>
     2203                    <?php endif; ?>
    21722204                <?php endforeach; ?>
    21732205                </tbody>
    … …  
    22582290                'resetAction' => 'vayapin_reset_settings_defaults',
    22592291                'iconBaseUrl' => plugins_url( 'icon/', __FILE__ ),
     2292                'connect'     => array(
     2293                    'nonce'           => wp_create_nonce( 'vayapin_connect' ),
     2294                    'listAction'      => 'vayapin_connect_partners_list',
     2295                    'requestAction'   => 'vayapin_connect_send_request',
     2296                    'createAction'    => 'vayapin_connect_add_recipient',
     2297                    'deleteAction'    => 'vayapin_connect_delete_request',
     2298                    'recordAction'    => 'vayapin_connect_get_record',
     2299                    'i18n'            => array(
     2300                        'requesting'   => __( 'Sending…', 'vayapin-woocommerce' ),
     2301                        'removing'     => __( 'Removing…', 'vayapin-woocommerce' ),
     2302                        'failed'       => __( 'Failed. Try again.', 'vayapin-woocommerce' ),
     2303                        'emailInvalid' => __( 'Enter a valid email address.', 'vayapin-woocommerce' ),
     2304                    ),
     2305                ),
    22602306                'i18n'        => array(
    22612307                    'resetError' => __( 'Something went wrong. Please try again.', 'vayapin-woocommerce' ),
    … …  
    22652311                ),
    22662312            )
     2313        );
     2314        wp_enqueue_script(
     2315            'vayapin-connect-partners',
     2316            plugins_url( 'js/connect-partners.js', __FILE__ ),
     2317            array( 'jquery', 'vayapin-admin-redesign' ),
     2318            defined( 'VAYAPIN_VERSION' ) ? VAYAPIN_VERSION : '1.0.0',
     2319            true
    22672320        );
    22682321    }
  • vayapin/trunk/framework/backend-ui/css/admin-redesign.css

    r3531799 r3692533  
    31753175  }
    31763176}
     3177
     3178/* ── VayaConnect · Partners tab ─────────────────────────────────── */
     3179.status-pill.vp-pill-none {
     3180  background: #f3f4f6;
     3181  color: #6b7280;
     3182}
     3183.status-pill.vp-pill-none .status-pill-dot {
     3184  background: #9ca3af;
     3185}
     3186
     3187.vp-partners-toolbar {
     3188  display: flex;
     3189  gap: 0.625rem;
     3190  flex-wrap: wrap;
     3191  align-items: center;
     3192}
     3193.vp-partners-toolbar #vp_partners_search {
     3194  flex: 1 1 220px;
     3195  min-width: 180px;
     3196}
     3197.vp-partners-toolbar #vp_partners_filter {
     3198  flex: 0 0 auto;
     3199  min-width: 160px;
     3200}
     3201.vp-partners-toolbar .vp-partners-add-toggle {
     3202  flex: 0 0 auto;
     3203  margin-left: auto;
     3204}
     3205
     3206.vp-partners-add-form {
     3207  margin-top: 0.75rem;
     3208  padding: 1rem;
     3209  border: 1px solid var(--border, #e5e7eb);
     3210  border-radius: 0.625rem;
     3211  background: var(--surface-subtle, #f9fafb);
     3212}
     3213.vp-partners-add-grid {
     3214  display: grid;
     3215  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
     3216  gap: 0.75rem;
     3217}
     3218.vp-partners-add-field {
     3219  display: flex;
     3220  flex-direction: column;
     3221  gap: 0.25rem;
     3222  font-size: 0.75rem;
     3223  color: #6b7280;
     3224}
     3225.vp-partners-add-field > span {
     3226  font-weight: 600;
     3227}
     3228.vp-req {
     3229  color: #dc2626;
     3230}
     3231.vp-partners-add-actions {
     3232  display: flex;
     3233  justify-content: flex-end;
     3234  gap: 0.5rem;
     3235  margin-top: 0.75rem;
     3236}
     3237
     3238.vp-partner-check-cell {
     3239  width: 2.5rem;
     3240  text-align: center;
     3241}
     3242.vp-partner-check,
     3243#vp_partners_check_all {
     3244  width: 1rem;
     3245  height: 1rem;
     3246  cursor: pointer;
     3247}
     3248.vp-partners-bulkbar {
     3249  display: flex;
     3250  align-items: center;
     3251  gap: 0.625rem;
     3252  margin-top: 0.75rem;
     3253  padding: 0.5rem 0.75rem;
     3254  border: 1px solid var(--border, #e5e7eb);
     3255  border-radius: 0.625rem;
     3256  background: var(--surface-subtle, #f9fafb);
     3257}
     3258.vp-partners-bulkbar #vp_partners_bulk_count {
     3259  margin-right: auto;
     3260}
     3261
     3262.vp-log-payload-row > td {
     3263  background: var(--surface-subtle, #f9fafb);
     3264}
     3265.vp-log-payload-row summary {
     3266  cursor: pointer;
     3267  font-size: 0.75rem;
     3268  color: #6b7280;
     3269}
     3270.vp-log-payload {
     3271  margin: 0.25rem 0 0;
     3272  padding: 0.5rem 0.75rem;
     3273  max-height: 20rem;
     3274  overflow: auto;
     3275  white-space: pre-wrap;
     3276  word-break: break-all;
     3277  font-size: 0.75rem;
     3278  font-family: ui-monospace, Menlo, monospace;
     3279  border: 1px solid var(--border, #e5e7eb);
     3280  border-radius: 0.375rem;
     3281  background: #fff;
     3282}
     3283
     3284.vp-partners-table th,
     3285.vp-partners-table td {
     3286  vertical-align: middle;
     3287}
     3288.vp-partner-name {
     3289  font-weight: 600;
     3290  color: var(--graphite-dark, #454545);
     3291}
     3292.vp-partner-email {
     3293  font-size: 0.75rem;
     3294  color: #6b7280;
     3295}
     3296.vp-partner-action-cell {
     3297  white-space: nowrap;
     3298  text-align: right;
     3299}
     3300.vp-partners-verified-note {
     3301  font-family: ui-monospace, Menlo, monospace;
     3302  font-size: 0.75rem;
     3303  color: var(--green-dark, #0d9488);
     3304}
     3305.vp-partners-empty {
     3306  text-align: center;
     3307  color: #9ca3af;
     3308  padding: 1.5rem 1rem !important;
     3309}
     3310.vp-muted {
     3311  color: #9ca3af;
     3312}
     3313
     3314.vp-partners-footer {
     3315  display: flex;
     3316  align-items: center;
     3317  justify-content: space-between;
     3318  margin-top: 0.75rem;
     3319  gap: 0.625rem;
     3320  flex-wrap: wrap;
     3321}
     3322.vp-partners-pager {
     3323  display: flex;
     3324  gap: 0.375rem;
     3325}
     3326
     3327.vp-dev-badge {
     3328  display: inline-block;
     3329  font-size: 0.625rem;
     3330  font-weight: 700;
     3331  letter-spacing: 0.05em;
     3332  color: #92400e;
     3333  background: #fff8eb;
     3334  border: 1px solid #f0deb6;
     3335  border-radius: 4px;
     3336  padding: 0.05rem 0.35rem;
     3337  margin-right: 0.35rem;
     3338  vertical-align: middle;
     3339}
     3340
     3341/* ── VayaConnect · record modal + toast ─────────────────────────── */
     3342.vayapin-settings-redesign .vp-record-modal__dialog {
     3343  max-width: 30rem;
     3344}
     3345.vayapin-settings-redesign .vp-record-modal__head {
     3346  display: flex;
     3347  align-items: flex-start;
     3348  justify-content: space-between;
     3349  gap: 0.75rem;
     3350  margin-bottom: 0.875rem;
     3351}
     3352.vayapin-settings-redesign .vp-record-modal__eyebrow {
     3353  font-size: 0.625rem;
     3354  font-weight: 700;
     3355  letter-spacing: 0.06em;
     3356  text-transform: uppercase;
     3357  color: var(--green-dark, #0d9488);
     3358  margin-bottom: 0.125rem;
     3359}
     3360.vayapin-settings-redesign .vp-record-modal__title {
     3361  margin: 0;
     3362  font-size: 1.125rem;
     3363}
     3364.vayapin-settings-redesign .vp-record-modal__body {
     3365  margin-bottom: 1rem;
     3366}
     3367.vayapin-settings-redesign .vp-rec-table {
     3368  border: 1px solid var(--border);
     3369  border-radius: 0.625rem;
     3370  overflow: hidden;
     3371}
     3372.vayapin-settings-redesign .vp-rec-row {
     3373  display: flex;
     3374  align-items: baseline;
     3375  gap: 0.75rem;
     3376  padding: 0.625rem 0.875rem;
     3377  border-bottom: 1px solid var(--border);
     3378}
     3379.vayapin-settings-redesign .vp-rec-row:last-child {
     3380  border-bottom: 0;
     3381}
     3382.vayapin-settings-redesign .vp-rec-key {
     3383  flex: 0 0 8.5rem;
     3384  font-size: 0.75rem;
     3385  font-weight: 600;
     3386  color: var(--graphite-mid, #6b7280);
     3387}
     3388.vayapin-settings-redesign .vp-rec-val {
     3389  flex: 1;
     3390  font-size: 0.8125rem;
     3391  color: var(--graphite-dark, #454545);
     3392  text-align: right;
     3393  word-break: break-word;
     3394}
     3395.vayapin-settings-redesign .vp-rec-mono {
     3396  font-family: ui-monospace, Menlo, monospace;
     3397  font-size: 0.78rem;
     3398}
     3399.vayapin-settings-redesign .vp-rec-script {
     3400  font-size: 0.85rem;
     3401}
     3402.vayapin-settings-redesign .vp-rec-qr {
     3403  display: flex;
     3404  justify-content: center;
     3405  margin-top: 0.875rem;
     3406}
     3407.vayapin-settings-redesign .vp-rec-qr img {
     3408  width: 7.5rem;
     3409  height: 7.5rem;
     3410  object-fit: contain;
     3411  border: 1px solid var(--border);
     3412  border-radius: 0.5rem;
     3413  padding: 0.25rem;
     3414  background: #fff;
     3415}
     3416.vayapin-settings-redesign .vp-rec-foot {
     3417  margin: 0.875rem 0 0;
     3418  font-size: 0.72rem;
     3419  color: var(--graphite-light, #9ca3af);
     3420  text-align: center;
     3421}
     3422.vayapin-settings-redesign .vp-toast-modal__dialog {
     3423  max-width: 22rem;
     3424}
     3425
     3426/* ── VayaConnect · verified action spacing + secret gate ────────── */
     3427.vayapin-settings-redesign .vp-partner-action-cell .vp-partners-view-btn {
     3428  margin-right: 0.75rem;
     3429}
     3430.vayapin-settings-redesign .vp-partner-action-cell .vp-partners-verified-note {
     3431  margin-left: 0.25rem;
     3432  white-space: nowrap;
     3433}
     3434
     3435.vayapin-settings-redesign .vp-partners-gate {
     3436  text-align: center;
     3437  padding: 2rem 1.25rem;
     3438  border: 1px dashed var(--border);
     3439  border-radius: 0.75rem;
     3440  background: var(--green-light, #f0fcf6);
     3441}
     3442.vayapin-settings-redesign .vp-partners-gate__icon {
     3443  display: inline-flex;
     3444  align-items: center;
     3445  justify-content: center;
     3446  width: 2.5rem;
     3447  height: 2.5rem;
     3448  border-radius: 50%;
     3449  background: #fff;
     3450  border: 1px solid var(--border);
     3451  color: var(--green-dark, #0d9488);
     3452  margin-bottom: 0.625rem;
     3453}
     3454.vayapin-settings-redesign .vp-partners-gate__icon svg {
     3455  width: 1.125rem;
     3456  height: 1.125rem;
     3457}
     3458.vayapin-settings-redesign .vp-partners-gate__title {
     3459  font-size: 0.95rem;
     3460  font-weight: 700;
     3461  color: var(--graphite-dark, #454545);
     3462  margin-bottom: 0.375rem;
     3463}
     3464.vayapin-settings-redesign .vp-partners-gate__text {
     3465  margin: 0 auto;
     3466  max-width: 28rem;
     3467  font-size: 0.8125rem;
     3468  color: var(--graphite-mid, #6b7280);
     3469  line-height: 1.5;
     3470}
     3471
     3472/* VayaConnect · pending "Request sent" as text, not a button */
     3473.vayapin-settings-redesign .vp-partner-action-cell .vp-partners-sent-note {
     3474  font-size: 0.8125rem;
     3475  color: var(--graphite-light, #9ca3af);
     3476  margin-right: 0.75rem;
     3477}
  • vayapin/trunk/framework/backend-ui/js/admin-redesign.js

    r3531799 r3692533  
    1414                analytics: 1,
    1515                badge: 1,
     16                partners: 1,
    1617        };
    1718        var VP_TAB_PREF_CLASSES = [
    … …  
    2122                'vp-tab-pref-analytics',
    2223                'vp-tab-pref-badge',
     24                'vp-tab-pref-partners',
    2325        ];
    2426
  • vayapin/trunk/framework/backend-ui/partials/settings-page-redesign.php

    r3578216 r3692533  
    174174                        }
    175175                        var t = localStorage.getItem('vayapin_settings_active_tab');
    176                         var ok = { connection: 1, checkout: 1, delivery: 1, notifications: 1, analytics: 1, badge: 1 };
     176                        var ok = { connection: 1, checkout: 1, delivery: 1, notifications: 1, analytics: 1, badge: 1, partners: 1 };
    177177                        if (!t || !ok[t] || t === 'connection') {
    178178                                return;
    … …  
    258258                                        <button type="button" class="tab-btn" data-vtab="notifications"><?php echo vayapin_admin_inline_tab_icon_svg( 'mail', 'tab-icon' ); ?> <?php esc_html_e( 'Notifications', 'vayapin-woocommerce' ); ?></button>
    259259                                        <button type="button" class="tab-btn" data-vtab="analytics"><?php echo vayapin_admin_inline_tab_icon_svg( 'stats-report', 'tab-icon' ); ?> <?php esc_html_e( 'Analytics', 'vayapin-woocommerce' ); ?></button>
     260                                        <button type="button" class="tab-btn" data-vtab="partners"><?php echo vayapin_admin_inline_tab_icon_svg( 'group', 'tab-icon' ); ?> <?php esc_html_e( 'Partners', 'vayapin-woocommerce' ); ?></button>
    260261                                </div>
    261262
    … …  
    871872                                </div>
    872873
     874                                <?php
     875                                $vayaconnect_enabled = '1' === vayapin_get_option( 'vayaconnect_enabled', '0' );
     876                                $vc_receiver_url     = function_exists( 'vayapin_connect_webhook_receiver_url' ) ? vayapin_connect_webhook_receiver_url() : '';
     877                                $vc_secret_encrypted = vayapin_get_option( 'vayaconnect_webhook_secret', '' );
     878                                $vc_secret_decrypted = function_exists( 'vayapin_connect_get_inbound_secret' ) ? vayapin_connect_get_inbound_secret() : '';
     879                                $vc_secret_masked    = ( '' !== $vc_secret_decrypted && function_exists( 'vayapin_mask_api_key' ) ) ? vayapin_mask_api_key( $vc_secret_decrypted ) : '';
     880                                $vc_secret_display   = '' !== $vc_secret_decrypted ? $vc_secret_masked : '';
     881                                $vc_initial          = ( $vayaconnect_enabled && '' !== (string) $vc_secret_decrypted && function_exists( 'vayapin_connect_get_partners' ) )
     882                                        ? vayapin_connect_get_partners( array( 'paged' => 1, 'per_page' => 15 ) )
     883                                        : array( 'rows' => array(), 'total' => 0, 'paged' => 1, 'per_page' => 15 );
     884                                ?>
     885                                <div id="tab-partners" class="tab-panel">
     886                                        <div class="section-card">
     887                                                <div class="section-header">
     888                                                        <div class="section-icon"><?php echo vayapin_admin_inline_tab_icon_svg( 'group' ); ?></div>
     889                                                        <div class="section-title-wrap">
     890                                                                <div class="section-title"><?php esc_html_e( 'VayaConnect', 'vayapin-woocommerce' ); ?></div>
     891                                                                <div class="section-desc"><?php esc_html_e( 'Request a VayaPin address from any customer. They drop one GPS pin in their own language; the verified address returns to you automatically — no foreign characters typed.', 'vayapin-woocommerce' ); ?></div>
     892                                                        </div>
     893                                                </div>
     894                                                <div class="section-body">
     895                                                        <div class="vp-checkbox-wrap">
     896                                                                <input type="checkbox" class="vp-checkbox" name="vayapin_woocommerce_options[vayaconnect_enabled]" value="1" id="vp_chk_vayaconnect" <?php checked( $vayaconnect_enabled ); ?>>
     897                                                                <label class="vp-checkbox-text" for="vp_chk_vayaconnect">
     898                                                                        <div class="vp-checkbox-title"><?php esc_html_e( 'Enable VayaConnect', 'vayapin-woocommerce' ); ?></div>
     899                                                                        <div class="vp-checkbox-desc"><?php esc_html_e( 'Turns on the partner list, the "Request VayaPin address" button, and the inbound address webhook receiver. Save after changing.', 'vayapin-woocommerce' ); ?></div>
     900                                                                </label>
     901                                                        </div>
     902                                                        <?php if ( $vc_receiver_url ) : ?>
     903                                                        <div class="field-row" style="margin-top:1rem;">
     904                                                                <div>
     905                                                                        <div class="field-label"><?php esc_html_e( 'Inbound webhook URL', 'vayapin-woocommerce' ); ?></div>
     906                                                                        <div class="field-hint"><?php esc_html_e( 'Give this URL and the VayaConnect secret below to VayaPin so address updates are pushed back to your store.', 'vayapin-woocommerce' ); ?></div>
     907                                                                </div>
     908                                                                <div class="field-control">
     909                                                                        <input class="vp-input mono" type="text" value="<?php echo esc_attr( $vc_receiver_url ); ?>" readonly onclick="this.select();">
     910                                                                </div>
     911                                                        </div>
     912                                                        <?php endif; ?>
     913                                                        <div class="field-row">
     914                                                                <div>
     915                                                                        <div class="field-label"><?php esc_html_e( 'VayaConnect webhook secret', 'vayapin-woocommerce' ); ?></div>
     916                                                                        <div class="field-hint"><?php esc_html_e( 'Verifies HMAC-SHA256 signatures on inbound pin.address_updated payloads. Separate from the outbound Notifications secret. Required for the inbound webhook receiver.', 'vayapin-woocommerce' ); ?></div>
     917                                                                </div>
     918                                                                <div class="field-control">
     919                                                                        <div class="input-wrap">
     920                                                                                <input class="vp-input mono vp-input--with-actions" type="password" name="vayapin_woocommerce_options[vayaconnect_webhook_secret]" id="vp_vc_secret_input" value="<?php echo esc_attr( $vc_secret_display ); ?>" data-real="<?php echo esc_attr( $vc_secret_decrypted ); ?>" data-masked="<?php echo esc_attr( $vc_secret_masked ); ?>" placeholder="<?php esc_attr_e( 'Paste the secret from VayaPin', 'vayapin-woocommerce' ); ?>" autocomplete="off">
     921                                                                        </div>
     922                                                                        <div class="vp-mono-helper" style="margin-top:0.5rem;">
     923                                                                                <span class="field-hint"><?php esc_html_e( 'Paste the signing secret VayaPin gave you for this endpoint, then click Save settings.', 'vayapin-woocommerce' ); ?></span>
     924                                                                                                                                                        </div>
     925                                                                </div>
     926                                                        </div>
     927                                                </div>
     928                                        </div>
     929
     930                                        <div class="section-card" style="margin-top:1rem;">
     931                                                <div class="section-header">
     932                                                        <div class="section-icon"><?php echo vayapin_admin_inline_tab_icon_svg( 'package' ); ?></div>
     933                                                        <div class="section-title-wrap">
     934                                                                <div class="section-title"><?php esc_html_e( 'Partners', 'vayapin-woocommerce' ); ?></div>
     935                                                                <div class="section-desc"><?php esc_html_e( 'Your WooCommerce customers and their VayaPin status. Send an address request to anyone who has no VayaPin yet.', 'vayapin-woocommerce' ); ?></div>
     936                                                        </div>
     937                                                </div>
     938                                                <div class="section-body">
     939                                                        <?php
     940                                                        $vc_has_secret = '' !== (string) $vc_secret_decrypted;
     941                                                        if ( ! $vayaconnect_enabled ) :
     942                                                        ?>
     943                                                                <p class="vp-muted"><?php esc_html_e( 'Enable VayaConnect above and save to load your partner list.', 'vayapin-woocommerce' ); ?></p>
     944                                                        <?php elseif ( ! $vc_has_secret ) : ?>
     945                                                                <div class="vp-partners-gate">
     946                                                                        <div class="vp-partners-gate__icon"><?php echo vayapin_admin_inline_tab_icon_svg( 'lock' ); ?></div>
     947                                                                        <div class="vp-partners-gate__title"><?php esc_html_e( 'Set a webhook secret to continue', 'vayapin-woocommerce' ); ?></div>
     948                                                                        <p class="vp-partners-gate__text"><?php esc_html_e( 'Generate the VayaConnect webhook secret above and click Save settings. The partner list and address requests unlock once the inbound receiver can verify signatures.', 'vayapin-woocommerce' ); ?></p>
     949                                                                </div>
     950                                                        <?php else : ?>
     951                                                                <div class="vp-partners-toolbar">
     952                                                                        <input type="search" id="vp_partners_search" class="vp-input" placeholder="<?php esc_attr_e( 'Search name or email…', 'vayapin-woocommerce' ); ?>" autocomplete="off">
     953                                                                        <select id="vp_partners_filter" class="vp-select">
     954                                                                                <option value="all"><?php esc_html_e( 'All statuses', 'vayapin-woocommerce' ); ?></option>
     955                                                                                <option value="none"><?php esc_html_e( 'No VayaPin', 'vayapin-woocommerce' ); ?></option>
     956                                                                                <option value="pending"><?php esc_html_e( 'Pending', 'vayapin-woocommerce' ); ?></option>
     957                                                                                <option value="verified"><?php esc_html_e( 'Verified', 'vayapin-woocommerce' ); ?></option>
     958                                                                        </select>
     959                                                                        <button type="button" class="btn btn-primary btn-sm vp-partners-add-toggle" id="vp_partners_add_toggle" aria-expanded="false" aria-controls="vp_partners_add_form">+ <?php esc_html_e( 'Add recipient', 'vayapin-woocommerce' ); ?></button>
     960                                                                </div>
     961                                                                <?php
     962                                                                $vc_base_country = '';
     963                                                                $vc_countries    = array();
     964                                                                if ( function_exists( 'WC' ) && WC()->countries ) {
     965                                                                        $vc_countries = WC()->countries->get_countries();
     966                                                                        $vc_base      = function_exists( 'wc_get_base_location' ) ? wc_get_base_location() : array();
     967                                                                        $vc_base_country = isset( $vc_base['country'] ) ? $vc_base['country'] : '';
     968                                                                }
     969                                                                ?>
     970                                                                <div id="vp_partners_add_form" class="vp-partners-add-form" hidden>
     971                                                                        <div class="vp-partners-add-grid">
     972                                                                                <label class="vp-partners-add-field">
     973                                                                                        <span><?php esc_html_e( 'Name', 'vayapin-woocommerce' ); ?></span>
     974                                                                                        <input type="text" id="vp_add_name" class="vp-input" autocomplete="off">
     975                                                                                </label>
     976                                                                                <label class="vp-partners-add-field">
     977                                                                                        <span><?php esc_html_e( 'Email', 'vayapin-woocommerce' ); ?> <span class="vp-req">*</span></span>
     978                                                                                        <input type="email" id="vp_add_email" class="vp-input" autocomplete="off">
     979                                                                                </label>
     980                                                                                <label class="vp-partners-add-field">
     981                                                                                        <span><?php esc_html_e( 'Phone', 'vayapin-woocommerce' ); ?></span>
     982                                                                                        <input type="text" id="vp_add_phone" class="vp-input" autocomplete="off" placeholder="+45…">
     983                                                                                </label>
     984                                                                                <label class="vp-partners-add-field">
     985                                                                                        <span><?php esc_html_e( 'Country', 'vayapin-woocommerce' ); ?></span>
     986                                                                                        <select id="vp_add_country" class="vp-select">
     987                                                                                                <?php foreach ( $vc_countries as $vc_code => $vc_label ) : ?>
     988                                                                                                        <option value="<?php echo esc_attr( $vc_code ); ?>" <?php selected( $vc_code, $vc_base_country ); ?>><?php echo esc_html( $vc_label ); ?></option>
     989                                                                                                <?php endforeach; ?>
     990                                                                                        </select>
     991                                                                                </label>
     992                                                                                <label class="vp-partners-add-field">
     993                                                                                        <span><?php esc_html_e( 'Address line 1', 'vayapin-woocommerce' ); ?></span>
     994                                                                                        <input type="text" id="vp_add_address_1" class="vp-input" autocomplete="off">
     995                                                                                </label>
     996                                                                                <label class="vp-partners-add-field">
     997                                                                                        <span><?php esc_html_e( 'City', 'vayapin-woocommerce' ); ?></span>
     998                                                                                        <input type="text" id="vp_add_city" class="vp-input" autocomplete="off">
     999                                                                                </label>
     1000                                                                                <label class="vp-partners-add-field">
     1001                                                                                        <span><?php esc_html_e( 'ZIP / Postcode', 'vayapin-woocommerce' ); ?></span>
     1002                                                                                        <input type="text" id="vp_add_zip" class="vp-input" autocomplete="off">
     1003                                                                                </label>
     1004                                                                        </div>
     1005                                                                        <p class="field-hint" style="margin-top:0.5rem;"><?php esc_html_e( 'VayaPin needs a rough address (varies by country) to start the request; the recipient refines it into a precise pin. For existing customers this is pulled from their order automatically.', 'vayapin-woocommerce' ); ?></p>
     1006                                                                        <div class="vp-partners-add-actions">
     1007                                                                                <button type="button" class="btn btn-ghost btn-sm" id="vp_partners_add_cancel"><?php esc_html_e( 'Cancel', 'vayapin-woocommerce' ); ?></button>
     1008                                                                                <button type="button" class="btn btn-primary btn-sm" id="vp_partners_add_submit"><?php esc_html_e( 'Send request', 'vayapin-woocommerce' ); ?></button>
     1009                                                                        </div>
     1010                                                                </div>
     1011                                                                <div class="vayapin-table-wrapper" style="margin-top:0.75rem;">
     1012                                                                        <div id="vp_partners_bulkbar" class="vp-partners-bulkbar" hidden><span id="vp_partners_bulk_count" class="vp-muted"></span><button type="button" class="btn btn-primary btn-sm" id="vp_partners_bulk_request"><?php esc_html_e( 'Request VayaPin address', 'vayapin-woocommerce' ); ?></button><button type="button" class="btn btn-ghost btn-sm" id="vp_partners_bulk_clear"><?php esc_html_e( 'Clear', 'vayapin-woocommerce' ); ?></button></div>
     1013                                                                                <table class="vayapin-logs-table vp-partners-table">
     1014                                                                                <thead>
     1015                                                                                        <tr>
     1016                                                                                                <th class="vp-partner-check-cell"><input type="checkbox" id="vp_partners_check_all" aria-label="<?php esc_attr_e( 'Select all', 'vayapin-woocommerce' ); ?>"></th><th><?php esc_html_e( 'Customer', 'vayapin-woocommerce' ); ?></th>
     1017                                                                                                <th><?php esc_html_e( 'Phone', 'vayapin-woocommerce' ); ?></th>
     1018                                                                                                <th><?php esc_html_e( 'Status', 'vayapin-woocommerce' ); ?></th>
     1019                                                                                                <th><?php esc_html_e( 'Action', 'vayapin-woocommerce' ); ?></th>
     1020                                                                                        </tr>
     1021                                                                                </thead>
     1022                                                                                <tbody id="vp_partners_tbody">
     1023                                                                                        <?php echo vayapin_connect_render_partner_rows( $vc_initial ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped within renderer. ?>
     1024                                                                                </tbody>
     1025                                                                        </table>
     1026                                                                </div>
     1027                                                                <div class="vp-partners-footer">
     1028                                                                        <span id="vp_partners_count" class="vp-muted"><?php echo esc_html( sprintf( /* translators: %d: total partner count */ _n( '%d customer', '%d customers', (int) $vc_initial['total'], 'vayapin-woocommerce' ), (int) $vc_initial['total'] ) ); ?></span>
     1029                                                                        <div class="vp-partners-pager">
     1030                                                                                <button type="button" class="btn btn-ghost btn-sm" id="vp_partners_prev" disabled>&larr; <?php esc_html_e( 'Prev', 'vayapin-woocommerce' ); ?></button>
     1031                                                                                <button type="button" class="btn btn-ghost btn-sm" id="vp_partners_next"><?php esc_html_e( 'Next', 'vayapin-woocommerce' ); ?> &rarr;</button>
     1032                                                                        </div>
     1033                                                                </div>
     1034                                                        <?php endif; ?>
     1035                                                </div>
     1036                                        </div>
     1037                                </div>
     1038
     1039                                <div id="vp_record_modal" class="vp-reset-modal vp-record-modal" aria-hidden="true">
     1040                                        <div class="vp-reset-modal__backdrop" tabindex="-1" data-vp-record-close></div>
     1041                                        <div class="vp-reset-modal__dialog vp-record-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="vp_record_modal_title">
     1042                                                <div class="vp-record-modal__head">
     1043                                                        <div>
     1044                                                                <div class="vp-record-modal__eyebrow"><?php esc_html_e( 'Verified contact record', 'vayapin-woocommerce' ); ?></div>
     1045                                                                <h2 id="vp_record_modal_title" class="vp-reset-modal__title vp-record-modal__title"></h2>
     1046                                                        </div>
     1047                                                        <span class="status-pill connected"><span class="status-pill-dot" aria-hidden="true"></span><?php esc_html_e( 'VayaPin verified', 'vayapin-woocommerce' ); ?></span>
     1048                                                </div>
     1049                                                <div class="vp-record-modal__body" id="vp_record_modal_body"></div>
     1050                                                <div class="vp-reset-modal__actions">
     1051                                                        <button type="button" class="btn btn-secondary vp-record-modal__close" data-vp-record-close><?php esc_html_e( 'Close', 'vayapin-woocommerce' ); ?></button>
     1052                                                </div>
     1053                                        </div>
     1054                                </div>
     1055
     1056                                <div id="vp_toast_modal" class="vp-reset-modal vp-toast-modal" aria-hidden="true">
     1057                                        <div class="vp-reset-modal__backdrop" tabindex="-1" data-vp-toast-close></div>
     1058                                        <div class="vp-reset-modal__dialog vp-toast-modal__dialog" role="alertdialog" aria-modal="true" aria-labelledby="vp_toast_modal_title">
     1059                                                <h2 id="vp_toast_modal_title" class="vp-reset-modal__title"></h2>
     1060                                                <p class="vp-reset-modal__text" id="vp_toast_modal_text"></p>
     1061                                                <div class="vp-reset-modal__actions">
     1062                                                        <button type="button" class="btn btn-primary vp-toast-modal__ok" data-vp-toast-close><?php esc_html_e( 'OK', 'vayapin-woocommerce' ); ?></button>
     1063                                                </div>
     1064                                        </div>
     1065                                </div>
     1066
    8731067                                <div class="save-footer-meta">
    8741068                                        <div class="save-footer-version"><?php
  • vayapin/trunk/framework/backend-ui/webhook-logs.php

    r3531799 r3692533  
    304304                                                </td>
    305305                                            </tr>
     306                                            <?php if ( isset( $log->payload ) && '' !== (string) $log->payload ) : ?>
     307                                                <tr class="vp-log-payload-row">
     308                                                    <td colspan="6">
     309                                                        <details>
     310                                                            <summary><?php esc_html_e( 'Payload', 'vayapin-woocommerce' ); ?></summary>
     311                                                            <pre class="vp-log-payload"><?php echo esc_html( (string) $log->payload ); ?></pre>
     312                                                        </details>
     313                                                    </td>
     314                                                </tr>
     315                                            <?php endif; ?>
    306316                                        <?php endforeach; ?>
    307317                                    </tbody>
  • vayapin/trunk/framework/frontend-ui/checkout-ui.php

    r3578216 r3692533  
    399399            $pin_fields = array(
    400400                'country'       => $order->get_billing_country(),
     401                // POST /v1/pins expects `name` as "first last"; a single token is rejected with
     402                // "name: must include a last name" (see openapi.json, /v1/pins request body).
    401403                'name'          => trim($order->get_billing_first_name() . ' ' . $order->get_billing_last_name()),
    402404                'email'         => $order->get_billing_email(),
  • vayapin/trunk/framework/styles-scripts/styles-scripts.php

    r3506381 r3692533  
    4141    );
    4242
     43    // Mobile sheet: hosts the whole widget flow on phone-width screens.
     44    $vayapin_sheet_css_path    = plugin_dir_path( dirname( __DIR__ ) ) . 'public/css/vayapin-sheet.css';
     45    $vayapin_sheet_css_version = file_exists( $vayapin_sheet_css_path ) ? filemtime( $vayapin_sheet_css_path ) : '1.0.0';
     46
     47    wp_enqueue_style(
     48        'vayapin-sheet-css',
     49        plugin_dir_url( dirname( __DIR__ ) ) . 'public/css/vayapin-sheet.css',
     50        array( 'vayapin-country-select-css' ),
     51        $vayapin_sheet_css_version
     52    );
     53
     54    $vayapin_sheet_js_path    = plugin_dir_path( dirname( __DIR__ ) ) . 'public/js/vayapin-sheet.js';
     55    $vayapin_sheet_js_version = file_exists( $vayapin_sheet_js_path ) ? filemtime( $vayapin_sheet_js_path ) : '1.0.0';
     56
     57    wp_enqueue_script(
     58        'vayapin-sheet',
     59        plugin_dir_url( dirname( __DIR__ ) ) . 'public/js/vayapin-sheet.js',
     60        array( 'jquery', 'vayapin-country-select-js' ),
     61        $vayapin_sheet_js_version,
     62        true
     63    );
     64
    4365    // Enqueue and localize custom JS for initializing Select2 on desired elements
    4466    $vayapin_select2_js_path    = plugin_dir_path( dirname( __DIR__ ) ) . 'public/js/vayapin-select2.js';
    … …  
    4870        'vayapin-select2-init',
    4971        plugin_dir_url( dirname( __DIR__ ) ) . 'public/js/vayapin-select2.js',
    50         array( 'jquery', 'select2-js' ),
     72        array( 'jquery', 'select2-js', 'vayapin-sheet' ),
    5173        $vayapin_select2_js_version,
    5274        true
  • vayapin/trunk/framework/vayapin/api-methods.php

    r3506381 r3692533  
    207207        error_log($e->getMessage());
    208208    }
     209}
     210
     211/**
     212 * Pull code and message out of a VayaPin API error body.
     213 *
     214 * The API answers a 422 with { error: { code, message }, data: { errors: { field: [...] } } },
     215 * older builds answered with a plain string in `error`. Reading only `$result['error']`
     216 * yields an array on the current format, which silently logs an empty message.
     217 *
     218 * @param mixed $result        Decoded response body.
     219 * @param int   $response_code HTTP status.
     220 * @return array { code, message }
     221 */
     222function vayapin_extract_api_error( $result, $response_code ) {
     223    $code    = '';
     224    $message = '';
     225
     226    if ( is_array( $result ) ) {
     227        if ( isset( $result['error'] ) && is_array( $result['error'] ) ) {
     228            $code    = isset( $result['error']['code'] ) ? (string) $result['error']['code'] : '';
     229            $message = isset( $result['error']['message'] ) ? (string) $result['error']['message'] : '';
     230        } elseif ( isset( $result['error'] ) && is_string( $result['error'] ) ) {
     231            $message = $result['error'];
     232        }
     233
     234        if ( '' === $code && isset( $result['error_code'] ) && is_string( $result['error_code'] ) ) {
     235            $code = $result['error_code'];
     236        }
     237        if ( '' === $message && isset( $result['message'] ) && is_string( $result['message'] ) ) {
     238            $message = $result['message'];
     239        }
     240
     241        // Per-field validation errors: { data: { errors: { phone_number: ["can't be blank"] } } }.
     242        $field_errors = isset( $result['data']['errors'] ) ? $result['data']['errors'] : null;
     243        if ( is_array( $field_errors ) ) {
     244            $parts = array();
     245            foreach ( $field_errors as $field => $messages ) {
     246                $messages = is_array( $messages ) ? $messages : array( $messages );
     247                $messages = array_filter( array_map( 'strval', $messages ) );
     248                if ( $messages ) {
     249                    $parts[] = $field . ': ' . implode( ', ', $messages );
     250                }
     251            }
     252            if ( $parts ) {
     253                $joined  = implode( '; ', $parts );
     254                $message = '' !== $message ? $message . ' (' . $joined . ')' : $joined;
     255            }
     256        }
     257    }
     258
     259    if ( '' === $code ) {
     260        $code = 'HTTP_' . (int) $response_code;
     261    }
     262    if ( '' === $message ) {
     263        $message = 'Unknown error (HTTP ' . (int) $response_code . ')';
     264    }
     265
     266    return array(
     267        'code'    => $code,
     268        'message' => $message,
     269    );
    209270}
    210271
    … …  
    293354    } else {
    294355        // Failed
    295         $error_code = isset($result['error_code']) ? $result['error_code'] : 'HTTP_' . $response_code;
    296         $error_message = isset($result['error']) ? $result['error'] : (isset($result['message']) ? $result['message'] : 'Unknown error');
    297         vayapin_log_pin_creation($order_id, 'failed', $error_code, $error_message, null, $result);
    298         return new WP_Error('api_error', $error_message, array('status' => $response_code, 'response' => $result));
    299     }
    300 }
     356        $error = vayapin_extract_api_error($result, $response_code);
     357        // Keep the raw body when it is not JSON, otherwise the reason is lost for good.
     358        $logged_response = is_array($result) ? $result : array('raw_body' => (string) $body, 'http_code' => (int) $response_code);
     359        vayapin_log_pin_creation($order_id, 'failed', $error['code'], $error['message'], null, $logged_response);
     360        return new WP_Error('api_error', $error['message'], array('status' => $response_code, 'response' => $result));
     361    }
     362}
  • vayapin/trunk/includes/class-vayapin-activator.php

    r3531799 r3692533  
    3333                Vayapin_Activator::create_pin_logs_table();
    3434                Vayapin_Activator::create_webhook_logs_table();
     35
     36                // VayaConnect request-state table (function loaded from framework/connect/request-store.php).
     37                if ( function_exists( 'vayapin_connect_create_requests_table' ) ) {
     38                        vayapin_connect_create_requests_table();
     39                }
     40
     41                // Flush rewrites so the /vayapin-intl-label/{id} route resolves.
     42                flush_rewrite_rules();
    3543        }
    3644
  • vayapin/trunk/public/js/vayapin-public.js

    r3578216 r3692533  
    44  var allowClose = true;
    55
    6   $(window).load(function () {
     6  /**
     7   * Close the country list through the plugin instance. Hiding the list by class
     8   * alone leaves countrySelect's document-level keydown handler bound, and that
     9   * handler calls preventDefault() on every key, which blocks typing and backspace
     10   * in the VayaPin input.
     11   */
     12  function closeCountryList() {
     13    var instance = $("#country_selector").data("plugin_countrySelect");
     14
     15    if (instance && typeof instance._closeDropdown === "function") {
     16      instance._closeDropdown();
     17      return;
     18    }
     19
     20    $(".vayapin-pin-country-wrapper .country-list")
     21      .addClass("hide")
     22      .removeClass("v-hide");
     23    $(".vayapin-pin-country-wrapper .selected-flag .arrow").removeClass("up");
     24  }
     25
     26  function closeVayapinPopup() {
     27    if (window.vayapinSheet && window.vayapinSheet.isOpen()) {
     28      window.vayapinSheet.close(0);
     29      $(".vayapin-checkout-button").attr("aria-expanded", "false");
     30      return;
     31    }
     32
     33    closeCountryList();
     34    $(".vayapin-overlay").fadeOut(100);
     35    $(".vayapin-checkout-dropdown").fadeOut(100);
     36    $(".vayapin-checkout-button").attr("aria-expanded", "false");
     37    // Stale "Not found" from the previous attempt must not greet the next one.
     38    $(".vayapin-error-text").hide();
     39  }
     40
     41  // jQuery 3 removed the .load() shortcut; it only still runs on sites that kept
     42  // jQuery Migrate, and silently kills this file on the ones that dropped it.
     43  $(window).on("load", function () {
    744    $(".vayapin-checkout-button").on("click", function (e) {
    845      e.stopPropagation();
     46      $(".vayapin-error-text").hide();
     47
     48      // Phones get the whole flow in a bottom sheet, not a popup over the form.
     49      if (window.vayapinSheet && window.vayapinSheet.shouldUse()) {
     50        $(this).attr("aria-expanded", "true");
     51        window.vayapinSheet.open();
     52        return;
     53      }
    954      $(".vayapin-overlay").fadeIn(100);
    1055      $(".vayapin-checkout-dropdown").fadeIn(100);
    … …  
    1459    $(".vayapin-checkout-dropdown-close").on("click", function (e) {
    1560      e.preventDefault();
    16       $(".vayapin-overlay").fadeOut(100);
    17       $(".vayapin-checkout-dropdown").fadeOut(100);
    18       $(".vayapin-checkout-button").attr("aria-expanded", "false");
     61      closeVayapinPopup();
    1962    });
    2063
    … …  
    2265      if (e.key === "Escape" || e.keyCode === 27) {
    2366        if ($(".vayapin-checkout-dropdown").is(":visible")) {
    24           $(".vayapin-overlay").fadeOut(100);
    25           $(".vayapin-checkout-dropdown").fadeOut(100);
    26           $(".vayapin-checkout-button").attr("aria-expanded", "false");
     67          closeVayapinPopup();
    2768        }
    2869      }
    … …  
    4081        // it bubbles up the country list is already hidden, so .has() misses it
    4182        // and the whole popup would wrongly close (notably on mobile/touch).
    42         !$(e.target).closest(".country-select, .country-list").length
     83        !$(e.target).closest(".country-select, .country-list").length &&
     84        // The mobile country sheet lives at body level, so any tap inside it
     85        // counts as "outside" the popup unless excluded here.
     86        !$(e.target).closest(".vp-sheet").length
    4387      ) {
    44         $(".vayapin-overlay").fadeOut(100);
    45         $dropdown.fadeOut(100);
    46         $(".vayapin-checkout-button").attr("aria-expanded", "false");
     88        closeVayapinPopup();
    4789      }
    4890    });
    … …  
    64106    $(document).on(
    65107      "touchstart mousedown",
    66       ".country-select, .country-list",
     108      ".country-select, .country-list, .vp-sheet",
    67109      function () {
    68110        allowClose = false;
    … …  
    75117    $(document).on(
    76118      "touchend mouseup",
    77       ".country-select, .country-list",
     119      ".country-select, .country-list, .vp-sheet",
    78120      function () {
    79121        if (countryCloseTimer) {
    … …  
    92134      // sees this click because we stopPropagation() below.
    93135      if (!$(e.target).closest(".country-select").length) {
    94         $(".vayapin-pin-country-wrapper .country-list")
    95           .addClass("hide")
    96           .removeClass("v-hide");
    97         $(".vayapin-pin-country-wrapper .selected-flag .arrow").removeClass(
    98           "up"
    99         );
     136        closeCountryList();
    100137      }
    101138      e.stopPropagation();
  • vayapin/trunk/public/js/vayapin-select2.js

    r3506381 r3692533  
    22  "use strict";
    33
    4   $(window).load(function () {
     4  // jQuery 3 removed the .load() shortcut; it only still runs on sites that kept
     5  // jQuery Migrate, and silently kills this file on the ones that dropped it.
     6  $(window).on("load", function () {
    57    /**
    68     * Initialize select2
    … …  
    161163        e.stopPropagation();
    162164
     165        // Phone width gets the sheet's country screen instead of the anchored dropdown.
     166        if (window.vayapinSheet && window.vayapinSheet.shouldUse()) {
     167          window.vayapinSheet.openCountryView();
     168          return;
     169        }
     170
    163171        var $list = instance.countryList;
    164172        if (!$list || !$list.length) return;
    … …  
    192200    });
    193201
     202    // countrySelect binds a document-level keydown that preventDefault()s every key
     203    // while its list is open. Without stopping the bubble here, typing and backspace
     204    // in the VayaPin field are swallowed and the text cannot be edited.
     205    $("#vayapin_input").on("keydown", function (e) {
     206      e.stopPropagation();
     207    });
     208
    194209    // Set the vaya pin in local storage and auto-set country
    195210    $("#vayapin_input").on("input", function (e) {
     211      // Editing the code means the previous "Not found" no longer applies.
     212      $(".vayapin-error-text").hide();
     213
    196214      var value = $(this).val();
    197215      $(this).val(value.toUpperCase());
    … …  
    253271        },
    254272        success: function (response) {
     273          if (window.vayapinSheet && window.vayapinSheet.isOpen()) {
     274            window.vayapinSheet.close(0);
     275          }
    255276          $(".vayapin-overlay").fadeOut(100);
    256277          $(".vayapin-checkout-dropdown").fadeOut(100);
    … …  
    281302
    282303  /**
     304   * Write a value into a checkout field and let WooCommerce re-run its own
     305   * validation for it. WooCommerce binds validate_field to "input validate change",
     306   * so without a trigger a "required field" error raised before the autofill keeps
     307   * showing on a field that now has a value.
     308   */
     309  function setCheckoutField($field, value) {
     310    if (!$field.length) return;
     311
     312    $field.val(value);
     313
     314    // Empty value: leave whatever validation state the field already had.
     315    if (value === undefined || value === null || value === "") return;
     316
     317    $field.trigger("input").trigger("change").trigger("validate");
     318  }
     319
     320  /**
     321   * Remove the summary block from a previous failed "Place order" attempt once no
     322   * field is flagged invalid anymore. Only the checkout notice group is touched,
     323   * so coupon/payment notices stay.
     324   */
     325  function clearStaleCheckoutNotices() {
     326    var $form = $("form.checkout");
     327    if (!$form.length) return;
     328    if ($form.find(".woocommerce-invalid").length) return;
     329
     330    $(".woocommerce-NoticeGroup-checkout").remove();
     331  }
     332
     333  /**
    283334   * Fill out fields in the checkout form
    284335   */
    285336  function fillOutFields(data) {
    286     // Fill first name field (from title)
    287     $("#billing_first_name").val(data.title);
    288 
    289     // Fill last name field (from subtitle)
    290     $("#billing_last_name").val(data.subtitle);
    291 
    292     // Fill company field (from description)
    293     $("#billing_company").val(data.description);
    294 
    295     // Fill email field
    296     $("#billing_email").val(data.email);
    297 
    298     // Fill phone field
    299     $("#billing_phone").val(data.phone);
    300 
    301     // Fill address fields
    302     $("#billing_address_1").val(data.address_1);
    303 
    304     $("#billing_address_2").val(data.address_2);
    305 
    306     // Fill city field
    307     $("#billing_city").val(data.city);
    308 
    309     // Fill postcode field
    310     $("#billing_postcode").val(data.postcode);
    311 
    312     // Set country first and wait for state field to update
    313     $("#billing_country").val(data.country).trigger("change");
     337    setCheckoutField($("#billing_first_name"), data.title);
     338    setCheckoutField($("#billing_last_name"), data.subtitle);
     339    setCheckoutField($("#billing_company"), data.description);
     340    setCheckoutField($("#billing_email"), data.email);
     341    setCheckoutField($("#billing_phone"), data.phone);
     342    setCheckoutField($("#billing_address_1"), data.address_1);
     343    setCheckoutField($("#billing_address_2"), data.address_2);
     344    setCheckoutField($("#billing_city"), data.city);
     345    setCheckoutField($("#billing_postcode"), data.postcode);
     346
     347    // Country rebuilds the state field, so it goes first and on its own.
     348    var $country = $("#billing_country");
     349    if ($country.length && data.country) {
     350      $country.val(data.country).trigger("change").trigger("validate");
     351    }
     352
     353    clearStaleCheckoutNotices();
    314354
    315355    // Wait for country change to complete and state field to reload
    316356    setTimeout(function () {
    317357      var $stateField = $("#billing_state");
    318       if (
    319         $stateField.length &&
    320         $stateField.hasClass("select2-hidden-accessible")
    321       ) {
    322         var stateValue = data.state;
    323         var stateText = data.state;
    324 
    325         // Create option if it doesn't exist
    326         var $existingOption = $stateField.find(
    327           "option[value='" + stateValue + "']"
    328         );
    329         if ($existingOption.length === 0) {
    330           var $newOption = new Option(stateText, stateValue, true, true);
    331           $stateField.append($newOption);
     358
     359      if ($stateField.length && data.state) {
     360        if ($stateField.is("select")) {
     361          var stateValue = data.state;
     362          var $existingOption = $stateField.find(
     363            "option[value='" + stateValue + "']"
     364          );
     365
     366          if ($existingOption.length === 0) {
     367            $stateField.append(new Option(stateValue, stateValue, true, true));
     368          } else {
     369            $existingOption.prop("selected", true);
     370          }
     371
     372          $stateField.val(stateValue).trigger("change").trigger("validate");
     373
     374          if ($stateField.hasClass("select2-hidden-accessible")) {
     375            $stateField.trigger("select2:select", {
     376              params: { data: { id: stateValue, text: stateValue } },
     377            });
     378          }
    332379        } else {
    333           $existingOption.prop("selected", true);
    334         }
    335 
    336         // Modern Select2 approach
    337         $stateField.val(stateValue).trigger("change");
    338 
    339         // Alternative: trigger select2:select event
    340         $stateField.trigger("select2:select", {
    341           params: { data: { id: stateValue, text: stateText } },
    342         });
    343       }
     380          setCheckoutField($stateField, data.state);
     381        }
     382      }
     383
     384      clearStaleCheckoutNotices();
    344385    }, 500);
    345386  }
  • vayapin/trunk/uninstall.php

    r3531799 r3692533  
    3636$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}vayapin_notification_logs" );
    3737
     38// Drop VayaConnect request-state table (added in 2.2.0).
     39$wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}vayapin_connect_requests" );
     40
    3841// Strip removed option keys from the main options array so re-installs start clean.
    3942$vp_options = get_option( 'vayapin_woocommerce_options' );
  • vayapin/trunk/vayapin.php

    r3676127 r3692533  
    1717 * Plugin URI:        https://vayapin.com
    1818 * Description:       Add VayaPin checkout autofill and delivery QR stickers to your WooCommerce store.
    19  * Version:           2.1.2
     19 * Version:           2.3.2
    2020 * Author:            Vayapin
    2121 * Author URI:        https://vayapin.com/
    … …  
    3838 * Rename this for your plugin and update it as you release new versions.
    3939 */
    40 define( 'VAYAPIN_VERSION', '2.1.2' );
     40define( 'VAYAPIN_VERSION', '2.3.2' );
    4141
    4242if ( ! defined( 'VAYAPIN_PLUGIN_FILE' ) ) {
    … …  
    102102
    103103/**
     104 * VayaConnect: outbound request + inbound webhook receiver + Partners surface.
     105 * Contract-dependent pieces are stubbed behind framework/connect/connect-config.php.
     106 */
     107require_once 'framework/connect/connect-config.php';
     108require_once 'framework/connect/request-store.php';
     109require_once 'framework/connect/partners.php';
     110require_once 'framework/connect/record.php';
     111require_once 'framework/connect/label-intl.php';
     112require_once 'framework/vayapin/connect-api.php';
     113require_once 'framework/vayapin/connect-webhook.php';
     114
     115/**
    104116 * Begins execution of the plugin.
    105117 *
Note: See TracChangeset for help on using the changeset viewer.