Plugin Directory

Changeset 3794297


Ignore:
Timestamp:
11/10/2026 11:43:22 AM (18 hours ago)
Author:
owlvano
Message:

Update to version 0.10.0 from GitHub

Location:
hustlepay-payment-connector-for-woocommerce
Files:
12 deleted
20 edited
1 copied

Legend:

Unmodified
Added
Removed
  • hustlepay-payment-connector-for-woocommerce/tags/0.10.0/assets/admin/hustlepay-admin.js

    r3702360 r3794297  
    5151
    5252        /**
    53          * Toggle readonly API URLs unless debug enabled
     53         * Toggle readonly API/checkout URLs unless debug enabled
    5454         */
    5555        function toggleApiUrlReadonly() {
    … …  
    5959                        : String($debug.val() || '').toLowerCase() === 'yes';
    6060
    61                 var $live = $('#woocommerce_hustlepay_payment_live_api_url');
    62                 var $test = $('#woocommerce_hustlepay_payment_test_api_url');
     61                var $urls = $(
     62                        '#woocommerce_hustlepay_payment_live_api_url, #woocommerce_hustlepay_payment_test_api_url, ' +
     63                                '#woocommerce_hustlepay_payment_live_checkout_url, #woocommerce_hustlepay_payment_test_checkout_url'
     64                );
    6365
    6466                if (debugOn) {
    65                         $live.removeAttr('readonly');
    66                         $test.removeAttr('readonly');
     67                        $urls.removeAttr('readonly');
    6768                } else {
    68                         $live.attr('readonly', 'readonly');
    69                         $test.attr('readonly', 'readonly');
     69                        $urls.attr('readonly', 'readonly');
    7070                }
    7171        }
  • hustlepay-payment-connector-for-woocommerce/tags/0.10.0/changelog.txt

    r3767343 r3794297  
    11*** HustlePay Payment Connector Changelog ***
     22026-11-09 - version 0.10.0
     3* Checkout now redirects to HustlePay's hosted checkout page instead of embedding the checkout widget in the store's pay page.
     4* Settings: new Checkout URL (Test/Live) fields; only https URLs are accepted.
     5* After a cancelled payment, the hosted checkout's Return button brings the shopper back to the order's payment page to retry or choose another method.
     6* Checkout shows an error instead of leaving the store when the Publishable Key for the active mode is missing.
     7* Resubmitting an already-paid order goes straight to the order-received page.
     8
    292026-10-26 - version 0.9.11
    310* Checkout widget: the tip control shows the merchant's own tip options (percentages or fixed amounts, an optional starred suggestion that is never added on its own, and an optional custom amount field). Falls back to 10% / 15% / 20% when the merchant has none.
  • hustlepay-payment-connector-for-woocommerce/tags/0.10.0/hustlepay-payment.php

    r3767343 r3794297  
    77 * Author URI:  https://hustlepay.me
    88 * Description: Adds a WooCommerce checkout connector for HustlePay payment orchestration. Payment processing remains with the merchant's configured payment service providers.
    9  * Version:     0.9.11
     9 * Version:     0.10.0
    1010 * Text Domain: hustlepay-payment-connector-for-woocommerce
    1111 *
    … …  
    2626
    2727if (!defined('HUSTLE_PAY_PAYMENT_VERSION')) {
    28   define('HUSTLE_PAY_PAYMENT_VERSION', '0.9.11');
     28  define('HUSTLE_PAY_PAYMENT_VERSION', '0.10.0');
    2929}
    3030
    … …  
    5252if (!defined('HUSTLE_PAY_DEFAULT_LIVE_API_URL')) {
    5353  define('HUSTLE_PAY_DEFAULT_LIVE_API_URL', 'https://api.hustlepay.net');
     54}
     55if (!defined('HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL')) {
     56  define('HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL', 'https://checkout.hustlepay.dev');
     57}
     58if (!defined('HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL')) {
     59  define('HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL', 'https://checkout.hustlepay.net');
    5460}
    5561if (!defined('HUSTLE_PAY_ICON_URL')) {
  • hustlepay-payment-connector-for-woocommerce/tags/0.10.0/includes/bootstrap.php

    r3732199 r3794297  
    1919
    2020require_once __DIR__ . '/rest/class-hustlepay-rest-webhook.php';
    21 require_once __DIR__ . '/rest/class-hustlepay-rest-session.php';
    2221require_once __DIR__ . '/rest/rest-loader.php';
    2322
  • hustlepay-payment-connector-for-woocommerce/tags/0.10.0/includes/crypto/class-hustlepay-crypto.php

    r3732199 r3794297  
    8989         * Order-bound token derived from WordPress's own nonce salt — ties a
    9090         * URL/request to one specific order without needing a logged-in user
    91          * session. Same formula used for the create-session nonce and the
    92          * payment return-flow redirect; keep both in sync via this method.
     91         * session. Guards the payment return-flow redirect.
    9392         */
    9493        public static function order_token( WC_Order $order ): string {
  • hustlepay-payment-connector-for-woocommerce/tags/0.10.0/includes/gateway/class-wc-hustlepay-payment-connector.php

    r3732199 r3794297  
    5353    add_action('woocommerce_api_hustlepay_payment_processed', array($this, 'hustlepay_payment_processed'));
    5454
    55     add_action('wp_enqueue_scripts', array($this, 'payment_scripts'));
    5655    add_action('admin_enqueue_scripts', array($this, 'admin_scripts'));
    5756    add_action('woocommerce_available_payment_gateways', array($this, 'available_payment_gateways'));
    … …  
    136135  }
    137136
     137  public function validate_test_checkout_url_field($key, $value): string
     138  {
     139    return $this->validate_checkout_url($value, HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL, __('Checkout URL (Test)', 'hustlepay-payment-connector-for-woocommerce'));
     140  }
     141
     142  public function validate_live_checkout_url_field($key, $value): string
     143  {
     144    return $this->validate_checkout_url($value, HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL, __('Checkout URL (Live)', 'hustlepay-payment-connector-for-woocommerce'));
     145  }
     146
     147  private function validate_checkout_url($value, string $default, string $label): string
     148  {
     149    $value = trim(sanitize_text_field((string) $value));
     150
     151    if ('' === $value) {
     152      return $default;
     153    }
     154
     155    if (!self::is_valid_checkout_url($value)) {
     156      /* translators: %s: settings field label. */
     157      $this->reject_setting(sprintf(__('%s must be an https URL. The previous value was kept.', 'hustlepay-payment-connector-for-woocommerce'), $label));
     158    }
     159
     160    return esc_url_raw($value);
     161  }
     162
    138163  public function validate_test_api_auth_key_field($key, $value): string
    139164  {
    … …  
    517542    } elseif (in_array($status, array('failed', 'canceled', 'expired', 'error'), true)) {
    518543
     544      $order->update_meta_data('_hustlepay_exhausted_uuid', $payment_uuid ?: (string) $order->get_meta('_hustlepay_payment_uuid', true));
    519545      if ('failed' !== $current) {
    520546        $order->update_status('failed', 'HustlePay Webhook: Payment failed');
    … …  
    662688  }
    663689
    664   /**
    665    * REST handler to create a HustlePay payment session for the frontend.
    666    * Expects POST with { order_id } or full order payload. Returns { transactionId, apiUrl, publishableKey }.
    667    */
    668   public function handle_create_session(\WP_REST_Request $request): \WP_REST_Response
    669   {
    670     // Only the gateway touches settings
    671     $this->load_settings_from_options();
    672 
    673     try {
    674       $payload = $this->build_session_payload($request);
    675       $order_id = !empty($payload['_order_id']) ? absint($payload['_order_id']) : 0;
    676 
    677       if ($order_id > 0) {
    678         // 25s comfortably exceeds HustlePay_Api_Client's own 20s timeout, so
    679         // a request that waits on this lock finds a just-stored uuid instead
    680         // of racing the API call itself.
    681         $payment_uuid = $this->with_order_lock($order_id, 25, function () use ($payload, $order_id) {
    682           return $this->create_or_reuse_session($payload, $order_id);
    683         });
    684 
    685         if (null === $payment_uuid) {
    686           // Lock contention: proceed unlocked rather than fail checkout — an
    687           // orphaned session is the lesser failure.
    688           if (!empty($this->debug_enabled)) {
    689             $this->log("Session-creation lock busy for order #{$order_id}, proceeding without dedup");
    690           }
    691           $payment_uuid = $this->create_or_reuse_session($payload, $order_id);
    692         }
    693       } else {
    694         $payment_uuid = $this->create_or_reuse_session($payload, 0);
    695       }
    696 
    697       return rest_ensure_response(
    698         array(
    699           'payment_uuid' => $payment_uuid,
    700         )
    701       );
    702 
    703     } catch (HustlePay_Api_Exception $e) {
    704       $this->log('Upstream error: ' . $e->getMessage(), 'error');
    705 
    706       return new WP_REST_Response(
    707         array(
    708           'code' => 'UPSTREAM_ERROR',
    709           'message' => __('Payment service temporarily unavailable. Please try again.', 'hustlepay-payment-connector-for-woocommerce'),
    710         ),
    711         502
    712       );
    713 
    714     } catch (\Throwable $e) {
    715       $this->log('Internal error: ' . $e->getMessage(), 'error');
    716 
    717       return new WP_REST_Response(
    718         array(
    719           'code' => 'INTERNAL_ERROR',
    720           'message' => __('Could not initialize payment. Please try again.', 'hustlepay-payment-connector-for-woocommerce'),
    721         ),
    722         500
    723       );
    724     }
     690  private function start_session(\WC_Order $order): string
     691  {
     692    $payload = $this->build_session_payload($order);
     693    $order_id = $order->get_id();
     694
     695    // 25s comfortably exceeds HustlePay_Api_Client's own 20s timeout, so
     696    // a request that waits on this lock finds a just-stored uuid instead
     697    // of racing the API call itself.
     698    $payment_uuid = $this->with_order_lock($order_id, 25, function () use ($payload, $order_id) {
     699      return $this->create_or_reuse_session($payload, $order_id);
     700    });
     701
     702    if (null === $payment_uuid) {
     703      // Lock contention: proceed unlocked rather than fail checkout — an
     704      // orphaned session is the lesser failure.
     705      if (!empty($this->debug_enabled)) {
     706        $this->log("Session-creation lock busy for order #{$order_id}, proceeding without dedup");
     707      }
     708      $payment_uuid = $this->create_or_reuse_session($payload, $order_id);
     709    }
     710
     711    return $payment_uuid;
    725712  }
    726713
    … …  
    750737        $created_at = (int) $order->get_meta('_hustlepay_session_created_at', true);
    751738        $reuse_window = (int) apply_filters('hustlepay_session_reuse_window_seconds', 15);
    752         if ($existing_uuid !== '' && $created_at > 0 && (time() - $created_at) < $reuse_window) {
     739        $same_environment = $order->get_meta('_hustlepay_environment', true) === ($this->test_mode ? 'test' : 'live');
     740        if ($existing_uuid !== '' && $created_at > 0 && (time() - $created_at) < $reuse_window
     741          && $same_environment && $existing_uuid !== $order->get_meta('_hustlepay_exhausted_uuid', true)) {
    753742          return (string) $existing_uuid;
    754743        }
    … …  
    788777  }
    789778
    790   private function build_session_payload(\WP_REST_Request $request): array
    791   {
    792     $params = $request->get_json_params();
    793 
    794     if (!empty($params['order_id'])) {
    795       $order_id = absint($params['order_id']);
    796       $order = wc_get_order($order_id);
    797       if (!$order) {
    798         throw new Exception('Invalid order_id');
    799       }
    800 
    801       return array(
    802         '_order_id' => $order_id,
    803         'order_id' => (string) $order_id,
    804         'amount_value' => (string) $order->get_total(),
    805         'amount_currency' => strtoupper($order->get_currency()),
    806         'description' => 'Order #' . $order_id,
    807         'callback_url' => rest_url('hustlepay/v1/callback'),
    808         'return_urls' => array(
    809           'default' => add_query_arg(
    810             array('order_id' => $order_id, 'token' => HustlePay_Crypto::order_token($order)),
    811             WC()->api_request_url('hustlepay_payment_processed')
    812           ),
    813         ),
    814         'customer_name' =>
    815           trim($order->get_billing_first_name()) . ' ' .
    816           trim($order->get_billing_last_name()),
    817         'customer_email' => $order->get_billing_email(),
    818         'billing_address' => array(
    819           'address_1' => $order->get_billing_address_1(),
    820           'address_2' => $order->get_billing_address_2(),
    821           'city' => $order->get_billing_city(),
    822           'company' => $order->get_billing_company(),
    823           'country' => $order->get_billing_country(),
    824           'email' => $order->get_billing_email(),
    825           'first_name' => $order->get_billing_first_name(),
    826           'last_name' => $order->get_billing_last_name(),
    827           'phone' => $order->get_billing_phone(),
    828           'state' => $order->get_billing_state(),
    829           'zip_code' => $order->get_billing_postcode(),
    830         ),
    831         'order_items' => $this->get_order_items($order),
    832         'fees' => $this->get_order_fees($order),
    833         'shipping' => $this->get_order_shipping($order),
    834         'taxes' => $this->get_order_taxes($order),
    835       );
    836     }
    837 
    838     if (!is_array($params)) {
    839       throw new Exception('Missing payload');
    840     }
    841 
    842     return $params;
     779  private function build_session_payload(\WC_Order $order): array
     780  {
     781    $order_id = $order->get_id();
     782
     783    $return_args = array('order_id' => $order_id, 'token' => HustlePay_Crypto::order_token($order));
     784    $return_endpoint = WC()->api_request_url('hustlepay_payment_processed');
     785
     786    return array(
     787      '_order_id' => $order_id,
     788      'order_id' => (string) $order_id,
     789      'amount_value' => (string) $order->get_total(),
     790      'amount_currency' => strtoupper($order->get_currency()),
     791      'description' => 'Order #' . $order_id,
     792      'callback_url' => rest_url('hustlepay/v1/callback'),
     793      'return_urls' => array(
     794        'default' => add_query_arg($return_args, $return_endpoint),
     795        // The order key stays out of anything HustlePay stores; the pay-form URL is resolved here after the token check.
     796        'cancel' => add_query_arg($return_args + array('cancelled' => '1'), $return_endpoint),
     797      ),
     798      'customer_name' =>
     799        trim($order->get_billing_first_name()) . ' ' .
     800        trim($order->get_billing_last_name()),
     801      'customer_email' => $order->get_billing_email(),
     802      'billing_address' => array(
     803        'address_1' => $order->get_billing_address_1(),
     804        'address_2' => $order->get_billing_address_2(),
     805        'city' => $order->get_billing_city(),
     806        'company' => $order->get_billing_company(),
     807        'country' => $order->get_billing_country(),
     808        'email' => $order->get_billing_email(),
     809        'first_name' => $order->get_billing_first_name(),
     810        'last_name' => $order->get_billing_last_name(),
     811        'phone' => $order->get_billing_phone(),
     812        'state' => $order->get_billing_state(),
     813        'zip_code' => $order->get_billing_postcode(),
     814      ),
     815      'order_items' => $this->get_order_items($order),
     816      'fees' => $this->get_order_fees($order),
     817      'shipping' => $this->get_order_shipping($order),
     818      'taxes' => $this->get_order_taxes($order),
     819    );
    843820  }
    844821
    … …  
    941918
    942919    if (null === $result) {
    943       // Fail closed, unlike handle_create_session()'s lock — an unlocked
     920      // Fail closed, unlike start_session()'s lock — an unlocked
    944921      // double refund risks two real API calls against HustlePay for the
    945922      // same payment, which this plugin can't make idempotent on its own.
    … …  
    12491226    $this->log('Payment process initiated for order ID: ' . $order_id . ', Amount: ' . $order->get_total() . ', Currency: ' . $order->get_currency());
    12501227
     1228    try {
     1229      if ('' === (string) $this->api_publishable_key || !self::is_valid_checkout_url((string) $this->checkout_url)) {
     1230        throw new Exception('Publishable key or hosted checkout URL is not configured for the current mode');
     1231      }
     1232      $payment_uuid = $this->start_session($order);
     1233    } catch (\Throwable $e) {
     1234      $this->log('Session creation failed for order ' . $order_id . ': ' . $e->getMessage(), 'error');
     1235      throw new Exception(esc_html__('Payment service temporarily unavailable. Please try again.', 'hustlepay-payment-connector-for-woocommerce'));
     1236    }
     1237
     1238    $order = wc_get_order($order_id);
     1239    if ($order && $order->is_paid()) {
     1240      return array('result' => 'success', 'redirect' => $this->get_return_url($order));
     1241    }
     1242
    12511243    return array(
    12521244      'result' => 'success',
    1253       'redirect' => $order->get_checkout_payment_url(true),
     1245      'redirect' => $this->hosted_checkout_redirect_url($payment_uuid),
    12541246    );
    12551247  }
    12561248
    1257   /**
    1258    * Receipt page
     1249  private function hosted_checkout_redirect_url(string $payment_uuid): string
     1250  {
     1251    // The hosted app reads both params from location.search, so a bare origin gets a "/" before the query.
     1252    $base = preg_replace('#^(https?://[^/?\#]+)(?=[?\#]|$)#i', '$1/', (string) $this->checkout_url);
     1253
     1254    return add_query_arg(
     1255      array(
     1256        'public_key' => rawurlencode((string) $this->api_publishable_key),
     1257        'payment_id' => rawurlencode($payment_uuid),
     1258      ),
     1259      $base
     1260    );
     1261  }
     1262
     1263  public static function is_valid_checkout_url(string $url): bool
     1264  {
     1265    $parts = wp_parse_url($url);
     1266    $scheme = strtolower((string) ($parts['scheme'] ?? ''));
     1267    $host = strtolower((string) ($parts['host'] ?? ''));
     1268
     1269    if ('' === $host) {
     1270      return false;
     1271    }
     1272
     1273    return 'https' === $scheme || ('http' === $scheme && in_array($host, array('localhost', '127.0.0.1'), true));
     1274  }
     1275
     1276  /**
     1277   * Only reachable from receipt URLs issued before checkout moved to the
     1278   * hosted page; process_payment() no longer redirects here.
    12591279   *
    12601280   * @param int $order_id
    … …  
    12621282  public function receipt_page($order_id): void
    12631283  {
    1264     $this->log('Receipt page loaded for order ID: ' . $order_id);
    1265     echo '<div class="hustlepay-plugin-root"></div>';
     1284    $order = wc_get_order($order_id);
     1285    if (!$order) {
     1286      return;
     1287    }
     1288
     1289    printf(
     1290      '<p><a class="button" href="%s">%s</a></p>',
     1291      esc_url($order->get_checkout_payment_url(false)),
     1292      esc_html__('Continue to payment', 'hustlepay-payment-connector-for-woocommerce')
     1293    );
    12661294  }
    12671295
    … …  
    12991327      if ($token === '' || !hash_equals(HustlePay_Crypto::order_token($order), $token)) {
    13001328        throw new Exception('Invalid or expired payment confirmation link.');
     1329      }
     1330
     1331      // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- order_token() verified above.
     1332      if (!empty($_GET['cancelled']) && !$order->is_paid()) {
     1333        // The cancel return can beat the failure webhook; without this the next Pay reuses the finished session.
     1334        $order->update_meta_data('_hustlepay_exhausted_uuid', (string) $order->get_meta('_hustlepay_payment_uuid', true));
     1335        $order->save();
     1336        wp_safe_redirect($order->get_checkout_payment_url(false));
     1337        exit;
    13011338      }
    13021339
    … …  
    14471484          'custom_attributes' => array('readonly' => 'readonly'),
    14481485        ),
     1486        'test_checkout_url' => array(
     1487          'title' => __('Checkout URL (Test)', 'hustlepay-payment-connector-for-woocommerce'),
     1488          'type' => 'text',
     1489          'description' => __('Hosted HustlePay checkout page shoppers are redirected to in test mode.', 'hustlepay-payment-connector-for-woocommerce'),
     1490          'default' => HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL,
     1491          'desc_tip' => true,
     1492          'custom_attributes' => array('readonly' => 'readonly'),
     1493        ),
     1494        'live_checkout_url' => array(
     1495          'title' => __('Checkout URL (Live)', 'hustlepay-payment-connector-for-woocommerce'),
     1496          'type' => 'text',
     1497          'description' => __('Hosted HustlePay checkout page shoppers are redirected to in live mode.', 'hustlepay-payment-connector-for-woocommerce'),
     1498          'default' => HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL,
     1499          'desc_tip' => true,
     1500          'custom_attributes' => array('readonly' => 'readonly'),
     1501        ),
    14491502        'test_api_publishable_key' => array(
    14501503          'title' => __('Publishable Key (Test)', 'hustlepay-payment-connector-for-woocommerce'),
    14511504          'type' => 'password',
    1452           'description' => __('Used only by the checkout widget in the shopper\'s browser. Safe to expose.', 'hustlepay-payment-connector-for-woocommerce'),
     1505          'description' => __('Sent to the hosted checkout page in the shopper\'s browser. Safe to expose.', 'hustlepay-payment-connector-for-woocommerce'),
    14531506          'default' => '',
    14541507          'desc_tip' => true,
    … …  
    14571510          'title' => __('Publishable Key (Live)', 'hustlepay-payment-connector-for-woocommerce'),
    14581511          'type' => 'password',
    1459           'description' => __('Used only by the checkout widget in the shopper\'s browser. Safe to expose.', 'hustlepay-payment-connector-for-woocommerce'),
     1512          'description' => __('Sent to the hosted checkout page in the shopper\'s browser. Safe to expose.', 'hustlepay-payment-connector-for-woocommerce'),
    14601513          'default' => '',
    14611514          'desc_tip' => true,
  • hustlepay-payment-connector-for-woocommerce/tags/0.10.0/includes/gateway/trait-assets.php

    r3690308 r3794297  
    88 */
    99trait HustlePay_Gateway_Assets {
    10 
    11         /**
    12          * Frontend payment scripts (checkout / blocks).
    13          */
    14         public function payment_scripts(): void {
    15                 if ( $this->enabled === 'no' ) {
    16                         return;
    17                 }
    18 
    19                 $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
    20                 if ( ! $order || $order->get_payment_method() !== $this->id ) {
    21                         return;
    22                 }
    23 
    24                 // Same order-key check WC's own pay-page template performs — this
    25                 // hook fires for every request to this endpoint, so it must happen
    26                 // here too before anything below is localized.
    27                 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- the order key itself *is* the credential being checked, via hash_equals() below.
    28                 $order_key = isset( $_GET['key'] ) ? sanitize_text_field( wp_unslash( $_GET['key'] ) ) : '';
    29                 // phpcs:enable
    30                 if ( '' === $order_key || ! hash_equals( $order->get_order_key(), $order_key ) ) {
    31                         return;
    32                 }
    33 
    34                 $asset_path   = plugin_dir_path( HUSTLE_PAY_PAYMENT_FILE ) . 'assets/hustlepay/payment.asset.php';
    35                 $version      = null;
    36                 $dependencies = array();
    37 
    38                 if ( file_exists( $asset_path ) ) {
    39                         $asset        = require $asset_path;
    40                         $version      = $asset['version'] ?? null;
    41                         $dependencies = $asset['dependencies'] ?? array();
    42                 }
    43 
    44                 wp_enqueue_script(
    45                         'hustlepay-payment-script',
    46                         HUSTLE_PAY_PAYMENT_PLUGIN_URL . 'assets/hustlepay/payment.js',
    47                         $dependencies,
    48                         $version,
    49                         true
    50                 );
    51 
    52                 wp_enqueue_style(
    53                         'hustlepay-payment-style',
    54                         HUSTLE_PAY_PAYMENT_PLUGIN_URL . 'assets/hustlepay/payment.css',
    55                         array(),
    56                         $version
    57                 );
    58 
    59                 wp_localize_script(
    60                         'hustlepay-payment-script',
    61                         'hustlepayConfig',
    62                         array(
    63                                 'api_url'            => $this->api_url,
    64                                 'public_key'         => $this->api_publishable_key,
    65                                 'order_id'           => $order->get_id(),
    66                                 'create_session_url' => rest_url( 'hustlepay/v1/create-session' ),
    67                                 'wp_rest_nonce'      => HustlePay_Crypto::order_token( $order ),
    68                                 'debug_enabled'          => $this->debug_enabled,
    69                         )
    70                 );
    71         }
    7210
    7311        /**
  • hustlepay-payment-connector-for-woocommerce/tags/0.10.0/includes/gateway/trait-settings.php

    r3732199 r3794297  
    3535        public string $test_api_url;
    3636        public string $live_api_url;
     37
     38        public string $checkout_url;
    3739
    3840        /* =====================================================
    … …  
    7779                ? $this->test_api_url
    7880                : $this->live_api_url;
     81
     82                $this->checkout_url = $this->test_mode
     83                ? $this->get_option( 'test_checkout_url', HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL )
     84                : $this->get_option( 'live_checkout_url', HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL );
    7985        }
    8086
  • hustlepay-payment-connector-for-woocommerce/tags/0.10.0/includes/rest/rest-loader.php

    r3538030 r3794297  
    1616
    1717    $webhook = new HustlePay_REST_Webhook($gateway);
    18     $session = new HustlePay_REST_Session($gateway);
    1918
    2019    register_rest_route(
    … …  
    2726      )
    2827    );
    29 
    30     register_rest_route(
    31       'hustlepay/v1',
    32       '/create-session',
    33       array(
    34         'methods' => 'POST',
    35         'callback' => [$session, 'handle'],
    36         'permission_callback' => [$session, 'permission'],
    37       )
    38     );
    3928  }
    4029);
  • hustlepay-payment-connector-for-woocommerce/tags/0.10.0/readme.txt

    r3767343 r3794297  
    44Requires at least: 6.1
    55Tested up to: 7.1
    6 Stable tag: 0.9.11
     6Stable tag: 0.10.0
    77WC requires at least: 8.0.0
    88WC tested up to: 11.0.0
    … …  
    3131* Live API: https://api.hustlepay.net
    3232* Test API: https://api.hustlepay.dev
     33* Live hosted checkout page: https://checkout.hustlepay.net
     34* Test hosted checkout page: https://checkout.hustlepay.dev
    3335* Service website: https://www.hustlepay.me/
    3436* Terms of Service: https://www.hustlepay.me/terms-of-service/
    … …  
    4042* Order line items, fees, shipping lines, and tax lines.
    4143* Customer name, billing email, billing phone, company, and billing address.
    42 * The merchant's HustlePay Secret API Key, sent as the `X-API-Key` header when creating checkout sessions and processing refunds. Until a Secret API Key is entered, the Publishable Key is sent in its place, for stores set up before that setting existed. The Publishable Key is also passed to the checkout widget in the customer's browser. The Webhook Signing Secret is stored in WordPress settings and used only to verify webhooks from HustlePay; it is never sent.
     44* The merchant's HustlePay Secret API Key, sent as the `X-API-Key` header when creating checkout sessions and processing refunds. Until a Secret API Key is entered, the Publishable Key is sent in its place, for stores set up before that setting existed. The customer is then redirected to HustlePay's hosted checkout page, with the Publishable Key and the checkout session ID in the page link. The Webhook Signing Secret is stored in WordPress settings and used only to verify webhooks from HustlePay; it is never sent.
    4345
    4446The plugin does not send this data to HustlePay merely by being installed or activated. The data is sent when the merchant has configured the connector and a customer starts a HustlePay checkout flow.
    … …  
    8385= Which API keys does the plugin need? =
    8486
    85 Three per mode, all from your HustlePay merchant portal. The Publishable Key (`pk_…`) is used only by the checkout widget in the shopper's browser. The Secret API Key (`sk_…`) authenticates your store's requests to HustlePay. It is sent only from your server to HustlePay, never to shoppers' browsers. The Webhook Signing Secret verifies the payment updates HustlePay sends back. Stores that were set up before the Secret API Key setting existed keep working with only a Publishable Key, and WP Admin shows a notice until the Secret API Key is entered. Enter it as soon as you have it, because without it the key authenticating your store is the one exposed to browsers.
     87Three per mode, all from your HustlePay merchant portal. The Publishable Key (`pk_…`) is included in the link that takes the shopper to HustlePay's hosted checkout page. The Secret API Key (`sk_…`) authenticates your store's requests to HustlePay. It is sent only from your server to HustlePay, never to shoppers' browsers. The Webhook Signing Secret verifies the payment updates HustlePay sends back. Stores that were set up before the Secret API Key setting existed keep working with only a Publishable Key, and WP Admin shows a notice until the Secret API Key is entered. Enter it as soon as you have it, because without it the key authenticating your store is the one exposed to browsers.
    8688
    8789= What happens to my data if I uninstall the plugin? =
    … …  
    9496
    9597== Changelog ==
     98= 0.10.0 =
     99* Checkout now redirects to HustlePay's hosted checkout page instead of embedding the checkout widget in the store's pay page.
     100* Settings: new Checkout URL (Test/Live) fields; only https URLs are accepted.
     101* After a cancelled payment, the hosted checkout's Return button brings the shopper back to the order's payment page to retry or choose another method.
     102* Checkout shows an error instead of leaving the store when the Publishable Key for the active mode is missing.
     103* Resubmitting an already-paid order goes straight to the order-received page.
     104
    96105= 0.9.11 =
    97106* Checkout widget: the tip control shows the merchant's own tip options (percentages or fixed amounts, an optional starred suggestion that is never added on its own, and an optional custom amount field). Falls back to 10% / 15% / 20% when the merchant has none.
  • hustlepay-payment-connector-for-woocommerce/trunk/assets/admin/hustlepay-admin.js

    r3702360 r3794297  
    5151
    5252        /**
    53          * Toggle readonly API URLs unless debug enabled
     53         * Toggle readonly API/checkout URLs unless debug enabled
    5454         */
    5555        function toggleApiUrlReadonly() {
    … …  
    5959                        : String($debug.val() || '').toLowerCase() === 'yes';
    6060
    61                 var $live = $('#woocommerce_hustlepay_payment_live_api_url');
    62                 var $test = $('#woocommerce_hustlepay_payment_test_api_url');
     61                var $urls = $(
     62                        '#woocommerce_hustlepay_payment_live_api_url, #woocommerce_hustlepay_payment_test_api_url, ' +
     63                                '#woocommerce_hustlepay_payment_live_checkout_url, #woocommerce_hustlepay_payment_test_checkout_url'
     64                );
    6365
    6466                if (debugOn) {
    65                         $live.removeAttr('readonly');
    66                         $test.removeAttr('readonly');
     67                        $urls.removeAttr('readonly');
    6768                } else {
    68                         $live.attr('readonly', 'readonly');
    69                         $test.attr('readonly', 'readonly');
     69                        $urls.attr('readonly', 'readonly');
    7070                }
    7171        }
  • hustlepay-payment-connector-for-woocommerce/trunk/changelog.txt

    r3767343 r3794297  
    11*** HustlePay Payment Connector Changelog ***
     22026-11-09 - version 0.10.0
     3* Checkout now redirects to HustlePay's hosted checkout page instead of embedding the checkout widget in the store's pay page.
     4* Settings: new Checkout URL (Test/Live) fields; only https URLs are accepted.
     5* After a cancelled payment, the hosted checkout's Return button brings the shopper back to the order's payment page to retry or choose another method.
     6* Checkout shows an error instead of leaving the store when the Publishable Key for the active mode is missing.
     7* Resubmitting an already-paid order goes straight to the order-received page.
     8
    292026-10-26 - version 0.9.11
    310* Checkout widget: the tip control shows the merchant's own tip options (percentages or fixed amounts, an optional starred suggestion that is never added on its own, and an optional custom amount field). Falls back to 10% / 15% / 20% when the merchant has none.
  • hustlepay-payment-connector-for-woocommerce/trunk/hustlepay-payment.php

    r3767343 r3794297  
    77 * Author URI:  https://hustlepay.me
    88 * Description: Adds a WooCommerce checkout connector for HustlePay payment orchestration. Payment processing remains with the merchant's configured payment service providers.
    9  * Version:     0.9.11
     9 * Version:     0.10.0
    1010 * Text Domain: hustlepay-payment-connector-for-woocommerce
    1111 *
    … …  
    2626
    2727if (!defined('HUSTLE_PAY_PAYMENT_VERSION')) {
    28   define('HUSTLE_PAY_PAYMENT_VERSION', '0.9.11');
     28  define('HUSTLE_PAY_PAYMENT_VERSION', '0.10.0');
    2929}
    3030
    … …  
    5252if (!defined('HUSTLE_PAY_DEFAULT_LIVE_API_URL')) {
    5353  define('HUSTLE_PAY_DEFAULT_LIVE_API_URL', 'https://api.hustlepay.net');
     54}
     55if (!defined('HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL')) {
     56  define('HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL', 'https://checkout.hustlepay.dev');
     57}
     58if (!defined('HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL')) {
     59  define('HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL', 'https://checkout.hustlepay.net');
    5460}
    5561if (!defined('HUSTLE_PAY_ICON_URL')) {
  • hustlepay-payment-connector-for-woocommerce/trunk/includes/bootstrap.php

    r3732199 r3794297  
    1919
    2020require_once __DIR__ . '/rest/class-hustlepay-rest-webhook.php';
    21 require_once __DIR__ . '/rest/class-hustlepay-rest-session.php';
    2221require_once __DIR__ . '/rest/rest-loader.php';
    2322
  • hustlepay-payment-connector-for-woocommerce/trunk/includes/crypto/class-hustlepay-crypto.php

    r3732199 r3794297  
    8989         * Order-bound token derived from WordPress's own nonce salt — ties a
    9090         * URL/request to one specific order without needing a logged-in user
    91          * session. Same formula used for the create-session nonce and the
    92          * payment return-flow redirect; keep both in sync via this method.
     91         * session. Guards the payment return-flow redirect.
    9392         */
    9493        public static function order_token( WC_Order $order ): string {
  • hustlepay-payment-connector-for-woocommerce/trunk/includes/gateway/class-wc-hustlepay-payment-connector.php

    r3732199 r3794297  
    5353    add_action('woocommerce_api_hustlepay_payment_processed', array($this, 'hustlepay_payment_processed'));
    5454
    55     add_action('wp_enqueue_scripts', array($this, 'payment_scripts'));
    5655    add_action('admin_enqueue_scripts', array($this, 'admin_scripts'));
    5756    add_action('woocommerce_available_payment_gateways', array($this, 'available_payment_gateways'));
    … …  
    136135  }
    137136
     137  public function validate_test_checkout_url_field($key, $value): string
     138  {
     139    return $this->validate_checkout_url($value, HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL, __('Checkout URL (Test)', 'hustlepay-payment-connector-for-woocommerce'));
     140  }
     141
     142  public function validate_live_checkout_url_field($key, $value): string
     143  {
     144    return $this->validate_checkout_url($value, HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL, __('Checkout URL (Live)', 'hustlepay-payment-connector-for-woocommerce'));
     145  }
     146
     147  private function validate_checkout_url($value, string $default, string $label): string
     148  {
     149    $value = trim(sanitize_text_field((string) $value));
     150
     151    if ('' === $value) {
     152      return $default;
     153    }
     154
     155    if (!self::is_valid_checkout_url($value)) {
     156      /* translators: %s: settings field label. */
     157      $this->reject_setting(sprintf(__('%s must be an https URL. The previous value was kept.', 'hustlepay-payment-connector-for-woocommerce'), $label));
     158    }
     159
     160    return esc_url_raw($value);
     161  }
     162
    138163  public function validate_test_api_auth_key_field($key, $value): string
    139164  {
    … …  
    517542    } elseif (in_array($status, array('failed', 'canceled', 'expired', 'error'), true)) {
    518543
     544      $order->update_meta_data('_hustlepay_exhausted_uuid', $payment_uuid ?: (string) $order->get_meta('_hustlepay_payment_uuid', true));
    519545      if ('failed' !== $current) {
    520546        $order->update_status('failed', 'HustlePay Webhook: Payment failed');
    … …  
    662688  }
    663689
    664   /**
    665    * REST handler to create a HustlePay payment session for the frontend.
    666    * Expects POST with { order_id } or full order payload. Returns { transactionId, apiUrl, publishableKey }.
    667    */
    668   public function handle_create_session(\WP_REST_Request $request): \WP_REST_Response
    669   {
    670     // Only the gateway touches settings
    671     $this->load_settings_from_options();
    672 
    673     try {
    674       $payload = $this->build_session_payload($request);
    675       $order_id = !empty($payload['_order_id']) ? absint($payload['_order_id']) : 0;
    676 
    677       if ($order_id > 0) {
    678         // 25s comfortably exceeds HustlePay_Api_Client's own 20s timeout, so
    679         // a request that waits on this lock finds a just-stored uuid instead
    680         // of racing the API call itself.
    681         $payment_uuid = $this->with_order_lock($order_id, 25, function () use ($payload, $order_id) {
    682           return $this->create_or_reuse_session($payload, $order_id);
    683         });
    684 
    685         if (null === $payment_uuid) {
    686           // Lock contention: proceed unlocked rather than fail checkout — an
    687           // orphaned session is the lesser failure.
    688           if (!empty($this->debug_enabled)) {
    689             $this->log("Session-creation lock busy for order #{$order_id}, proceeding without dedup");
    690           }
    691           $payment_uuid = $this->create_or_reuse_session($payload, $order_id);
    692         }
    693       } else {
    694         $payment_uuid = $this->create_or_reuse_session($payload, 0);
    695       }
    696 
    697       return rest_ensure_response(
    698         array(
    699           'payment_uuid' => $payment_uuid,
    700         )
    701       );
    702 
    703     } catch (HustlePay_Api_Exception $e) {
    704       $this->log('Upstream error: ' . $e->getMessage(), 'error');
    705 
    706       return new WP_REST_Response(
    707         array(
    708           'code' => 'UPSTREAM_ERROR',
    709           'message' => __('Payment service temporarily unavailable. Please try again.', 'hustlepay-payment-connector-for-woocommerce'),
    710         ),
    711         502
    712       );
    713 
    714     } catch (\Throwable $e) {
    715       $this->log('Internal error: ' . $e->getMessage(), 'error');
    716 
    717       return new WP_REST_Response(
    718         array(
    719           'code' => 'INTERNAL_ERROR',
    720           'message' => __('Could not initialize payment. Please try again.', 'hustlepay-payment-connector-for-woocommerce'),
    721         ),
    722         500
    723       );
    724     }
     690  private function start_session(\WC_Order $order): string
     691  {
     692    $payload = $this->build_session_payload($order);
     693    $order_id = $order->get_id();
     694
     695    // 25s comfortably exceeds HustlePay_Api_Client's own 20s timeout, so
     696    // a request that waits on this lock finds a just-stored uuid instead
     697    // of racing the API call itself.
     698    $payment_uuid = $this->with_order_lock($order_id, 25, function () use ($payload, $order_id) {
     699      return $this->create_or_reuse_session($payload, $order_id);
     700    });
     701
     702    if (null === $payment_uuid) {
     703      // Lock contention: proceed unlocked rather than fail checkout — an
     704      // orphaned session is the lesser failure.
     705      if (!empty($this->debug_enabled)) {
     706        $this->log("Session-creation lock busy for order #{$order_id}, proceeding without dedup");
     707      }
     708      $payment_uuid = $this->create_or_reuse_session($payload, $order_id);
     709    }
     710
     711    return $payment_uuid;
    725712  }
    726713
    … …  
    750737        $created_at = (int) $order->get_meta('_hustlepay_session_created_at', true);
    751738        $reuse_window = (int) apply_filters('hustlepay_session_reuse_window_seconds', 15);
    752         if ($existing_uuid !== '' && $created_at > 0 && (time() - $created_at) < $reuse_window) {
     739        $same_environment = $order->get_meta('_hustlepay_environment', true) === ($this->test_mode ? 'test' : 'live');
     740        if ($existing_uuid !== '' && $created_at > 0 && (time() - $created_at) < $reuse_window
     741          && $same_environment && $existing_uuid !== $order->get_meta('_hustlepay_exhausted_uuid', true)) {
    753742          return (string) $existing_uuid;
    754743        }
    … …  
    788777  }
    789778
    790   private function build_session_payload(\WP_REST_Request $request): array
    791   {
    792     $params = $request->get_json_params();
    793 
    794     if (!empty($params['order_id'])) {
    795       $order_id = absint($params['order_id']);
    796       $order = wc_get_order($order_id);
    797       if (!$order) {
    798         throw new Exception('Invalid order_id');
    799       }
    800 
    801       return array(
    802         '_order_id' => $order_id,
    803         'order_id' => (string) $order_id,
    804         'amount_value' => (string) $order->get_total(),
    805         'amount_currency' => strtoupper($order->get_currency()),
    806         'description' => 'Order #' . $order_id,
    807         'callback_url' => rest_url('hustlepay/v1/callback'),
    808         'return_urls' => array(
    809           'default' => add_query_arg(
    810             array('order_id' => $order_id, 'token' => HustlePay_Crypto::order_token($order)),
    811             WC()->api_request_url('hustlepay_payment_processed')
    812           ),
    813         ),
    814         'customer_name' =>
    815           trim($order->get_billing_first_name()) . ' ' .
    816           trim($order->get_billing_last_name()),
    817         'customer_email' => $order->get_billing_email(),
    818         'billing_address' => array(
    819           'address_1' => $order->get_billing_address_1(),
    820           'address_2' => $order->get_billing_address_2(),
    821           'city' => $order->get_billing_city(),
    822           'company' => $order->get_billing_company(),
    823           'country' => $order->get_billing_country(),
    824           'email' => $order->get_billing_email(),
    825           'first_name' => $order->get_billing_first_name(),
    826           'last_name' => $order->get_billing_last_name(),
    827           'phone' => $order->get_billing_phone(),
    828           'state' => $order->get_billing_state(),
    829           'zip_code' => $order->get_billing_postcode(),
    830         ),
    831         'order_items' => $this->get_order_items($order),
    832         'fees' => $this->get_order_fees($order),
    833         'shipping' => $this->get_order_shipping($order),
    834         'taxes' => $this->get_order_taxes($order),
    835       );
    836     }
    837 
    838     if (!is_array($params)) {
    839       throw new Exception('Missing payload');
    840     }
    841 
    842     return $params;
     779  private function build_session_payload(\WC_Order $order): array
     780  {
     781    $order_id = $order->get_id();
     782
     783    $return_args = array('order_id' => $order_id, 'token' => HustlePay_Crypto::order_token($order));
     784    $return_endpoint = WC()->api_request_url('hustlepay_payment_processed');
     785
     786    return array(
     787      '_order_id' => $order_id,
     788      'order_id' => (string) $order_id,
     789      'amount_value' => (string) $order->get_total(),
     790      'amount_currency' => strtoupper($order->get_currency()),
     791      'description' => 'Order #' . $order_id,
     792      'callback_url' => rest_url('hustlepay/v1/callback'),
     793      'return_urls' => array(
     794        'default' => add_query_arg($return_args, $return_endpoint),
     795        // The order key stays out of anything HustlePay stores; the pay-form URL is resolved here after the token check.
     796        'cancel' => add_query_arg($return_args + array('cancelled' => '1'), $return_endpoint),
     797      ),
     798      'customer_name' =>
     799        trim($order->get_billing_first_name()) . ' ' .
     800        trim($order->get_billing_last_name()),
     801      'customer_email' => $order->get_billing_email(),
     802      'billing_address' => array(
     803        'address_1' => $order->get_billing_address_1(),
     804        'address_2' => $order->get_billing_address_2(),
     805        'city' => $order->get_billing_city(),
     806        'company' => $order->get_billing_company(),
     807        'country' => $order->get_billing_country(),
     808        'email' => $order->get_billing_email(),
     809        'first_name' => $order->get_billing_first_name(),
     810        'last_name' => $order->get_billing_last_name(),
     811        'phone' => $order->get_billing_phone(),
     812        'state' => $order->get_billing_state(),
     813        'zip_code' => $order->get_billing_postcode(),
     814      ),
     815      'order_items' => $this->get_order_items($order),
     816      'fees' => $this->get_order_fees($order),
     817      'shipping' => $this->get_order_shipping($order),
     818      'taxes' => $this->get_order_taxes($order),
     819    );
    843820  }
    844821
    … …  
    941918
    942919    if (null === $result) {
    943       // Fail closed, unlike handle_create_session()'s lock — an unlocked
     920      // Fail closed, unlike start_session()'s lock — an unlocked
    944921      // double refund risks two real API calls against HustlePay for the
    945922      // same payment, which this plugin can't make idempotent on its own.
    … …  
    12491226    $this->log('Payment process initiated for order ID: ' . $order_id . ', Amount: ' . $order->get_total() . ', Currency: ' . $order->get_currency());
    12501227
     1228    try {
     1229      if ('' === (string) $this->api_publishable_key || !self::is_valid_checkout_url((string) $this->checkout_url)) {
     1230        throw new Exception('Publishable key or hosted checkout URL is not configured for the current mode');
     1231      }
     1232      $payment_uuid = $this->start_session($order);
     1233    } catch (\Throwable $e) {
     1234      $this->log('Session creation failed for order ' . $order_id . ': ' . $e->getMessage(), 'error');
     1235      throw new Exception(esc_html__('Payment service temporarily unavailable. Please try again.', 'hustlepay-payment-connector-for-woocommerce'));
     1236    }
     1237
     1238    $order = wc_get_order($order_id);
     1239    if ($order && $order->is_paid()) {
     1240      return array('result' => 'success', 'redirect' => $this->get_return_url($order));
     1241    }
     1242
    12511243    return array(
    12521244      'result' => 'success',
    1253       'redirect' => $order->get_checkout_payment_url(true),
     1245      'redirect' => $this->hosted_checkout_redirect_url($payment_uuid),
    12541246    );
    12551247  }
    12561248
    1257   /**
    1258    * Receipt page
     1249  private function hosted_checkout_redirect_url(string $payment_uuid): string
     1250  {
     1251    // The hosted app reads both params from location.search, so a bare origin gets a "/" before the query.
     1252    $base = preg_replace('#^(https?://[^/?\#]+)(?=[?\#]|$)#i', '$1/', (string) $this->checkout_url);
     1253
     1254    return add_query_arg(
     1255      array(
     1256        'public_key' => rawurlencode((string) $this->api_publishable_key),
     1257        'payment_id' => rawurlencode($payment_uuid),
     1258      ),
     1259      $base
     1260    );
     1261  }
     1262
     1263  public static function is_valid_checkout_url(string $url): bool
     1264  {
     1265    $parts = wp_parse_url($url);
     1266    $scheme = strtolower((string) ($parts['scheme'] ?? ''));
     1267    $host = strtolower((string) ($parts['host'] ?? ''));
     1268
     1269    if ('' === $host) {
     1270      return false;
     1271    }
     1272
     1273    return 'https' === $scheme || ('http' === $scheme && in_array($host, array('localhost', '127.0.0.1'), true));
     1274  }
     1275
     1276  /**
     1277   * Only reachable from receipt URLs issued before checkout moved to the
     1278   * hosted page; process_payment() no longer redirects here.
    12591279   *
    12601280   * @param int $order_id
    … …  
    12621282  public function receipt_page($order_id): void
    12631283  {
    1264     $this->log('Receipt page loaded for order ID: ' . $order_id);
    1265     echo '<div class="hustlepay-plugin-root"></div>';
     1284    $order = wc_get_order($order_id);
     1285    if (!$order) {
     1286      return;
     1287    }
     1288
     1289    printf(
     1290      '<p><a class="button" href="%s">%s</a></p>',
     1291      esc_url($order->get_checkout_payment_url(false)),
     1292      esc_html__('Continue to payment', 'hustlepay-payment-connector-for-woocommerce')
     1293    );
    12661294  }
    12671295
    … …  
    12991327      if ($token === '' || !hash_equals(HustlePay_Crypto::order_token($order), $token)) {
    13001328        throw new Exception('Invalid or expired payment confirmation link.');
     1329      }
     1330
     1331      // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- order_token() verified above.
     1332      if (!empty($_GET['cancelled']) && !$order->is_paid()) {
     1333        // The cancel return can beat the failure webhook; without this the next Pay reuses the finished session.
     1334        $order->update_meta_data('_hustlepay_exhausted_uuid', (string) $order->get_meta('_hustlepay_payment_uuid', true));
     1335        $order->save();
     1336        wp_safe_redirect($order->get_checkout_payment_url(false));
     1337        exit;
    13011338      }
    13021339
    … …  
    14471484          'custom_attributes' => array('readonly' => 'readonly'),
    14481485        ),
     1486        'test_checkout_url' => array(
     1487          'title' => __('Checkout URL (Test)', 'hustlepay-payment-connector-for-woocommerce'),
     1488          'type' => 'text',
     1489          'description' => __('Hosted HustlePay checkout page shoppers are redirected to in test mode.', 'hustlepay-payment-connector-for-woocommerce'),
     1490          'default' => HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL,
     1491          'desc_tip' => true,
     1492          'custom_attributes' => array('readonly' => 'readonly'),
     1493        ),
     1494        'live_checkout_url' => array(
     1495          'title' => __('Checkout URL (Live)', 'hustlepay-payment-connector-for-woocommerce'),
     1496          'type' => 'text',
     1497          'description' => __('Hosted HustlePay checkout page shoppers are redirected to in live mode.', 'hustlepay-payment-connector-for-woocommerce'),
     1498          'default' => HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL,
     1499          'desc_tip' => true,
     1500          'custom_attributes' => array('readonly' => 'readonly'),
     1501        ),
    14491502        'test_api_publishable_key' => array(
    14501503          'title' => __('Publishable Key (Test)', 'hustlepay-payment-connector-for-woocommerce'),
    14511504          'type' => 'password',
    1452           'description' => __('Used only by the checkout widget in the shopper\'s browser. Safe to expose.', 'hustlepay-payment-connector-for-woocommerce'),
     1505          'description' => __('Sent to the hosted checkout page in the shopper\'s browser. Safe to expose.', 'hustlepay-payment-connector-for-woocommerce'),
    14531506          'default' => '',
    14541507          'desc_tip' => true,
    … …  
    14571510          'title' => __('Publishable Key (Live)', 'hustlepay-payment-connector-for-woocommerce'),
    14581511          'type' => 'password',
    1459           'description' => __('Used only by the checkout widget in the shopper\'s browser. Safe to expose.', 'hustlepay-payment-connector-for-woocommerce'),
     1512          'description' => __('Sent to the hosted checkout page in the shopper\'s browser. Safe to expose.', 'hustlepay-payment-connector-for-woocommerce'),
    14601513          'default' => '',
    14611514          'desc_tip' => true,
  • hustlepay-payment-connector-for-woocommerce/trunk/includes/gateway/trait-assets.php

    r3690308 r3794297  
    88 */
    99trait HustlePay_Gateway_Assets {
    10 
    11         /**
    12          * Frontend payment scripts (checkout / blocks).
    13          */
    14         public function payment_scripts(): void {
    15                 if ( $this->enabled === 'no' ) {
    16                         return;
    17                 }
    18 
    19                 $order = wc_get_order( absint( get_query_var( 'order-pay' ) ) );
    20                 if ( ! $order || $order->get_payment_method() !== $this->id ) {
    21                         return;
    22                 }
    23 
    24                 // Same order-key check WC's own pay-page template performs — this
    25                 // hook fires for every request to this endpoint, so it must happen
    26                 // here too before anything below is localized.
    27                 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- the order key itself *is* the credential being checked, via hash_equals() below.
    28                 $order_key = isset( $_GET['key'] ) ? sanitize_text_field( wp_unslash( $_GET['key'] ) ) : '';
    29                 // phpcs:enable
    30                 if ( '' === $order_key || ! hash_equals( $order->get_order_key(), $order_key ) ) {
    31                         return;
    32                 }
    33 
    34                 $asset_path   = plugin_dir_path( HUSTLE_PAY_PAYMENT_FILE ) . 'assets/hustlepay/payment.asset.php';
    35                 $version      = null;
    36                 $dependencies = array();
    37 
    38                 if ( file_exists( $asset_path ) ) {
    39                         $asset        = require $asset_path;
    40                         $version      = $asset['version'] ?? null;
    41                         $dependencies = $asset['dependencies'] ?? array();
    42                 }
    43 
    44                 wp_enqueue_script(
    45                         'hustlepay-payment-script',
    46                         HUSTLE_PAY_PAYMENT_PLUGIN_URL . 'assets/hustlepay/payment.js',
    47                         $dependencies,
    48                         $version,
    49                         true
    50                 );
    51 
    52                 wp_enqueue_style(
    53                         'hustlepay-payment-style',
    54                         HUSTLE_PAY_PAYMENT_PLUGIN_URL . 'assets/hustlepay/payment.css',
    55                         array(),
    56                         $version
    57                 );
    58 
    59                 wp_localize_script(
    60                         'hustlepay-payment-script',
    61                         'hustlepayConfig',
    62                         array(
    63                                 'api_url'            => $this->api_url,
    64                                 'public_key'         => $this->api_publishable_key,
    65                                 'order_id'           => $order->get_id(),
    66                                 'create_session_url' => rest_url( 'hustlepay/v1/create-session' ),
    67                                 'wp_rest_nonce'      => HustlePay_Crypto::order_token( $order ),
    68                                 'debug_enabled'          => $this->debug_enabled,
    69                         )
    70                 );
    71         }
    7210
    7311        /**
  • hustlepay-payment-connector-for-woocommerce/trunk/includes/gateway/trait-settings.php

    r3732199 r3794297  
    3535        public string $test_api_url;
    3636        public string $live_api_url;
     37
     38        public string $checkout_url;
    3739
    3840        /* =====================================================
    … …  
    7779                ? $this->test_api_url
    7880                : $this->live_api_url;
     81
     82                $this->checkout_url = $this->test_mode
     83                ? $this->get_option( 'test_checkout_url', HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL )
     84                : $this->get_option( 'live_checkout_url', HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL );
    7985        }
    8086
  • hustlepay-payment-connector-for-woocommerce/trunk/includes/rest/rest-loader.php

    r3538030 r3794297  
    1616
    1717    $webhook = new HustlePay_REST_Webhook($gateway);
    18     $session = new HustlePay_REST_Session($gateway);
    1918
    2019    register_rest_route(
    … …  
    2726      )
    2827    );
    29 
    30     register_rest_route(
    31       'hustlepay/v1',
    32       '/create-session',
    33       array(
    34         'methods' => 'POST',
    35         'callback' => [$session, 'handle'],
    36         'permission_callback' => [$session, 'permission'],
    37       )
    38     );
    3928  }
    4029);
  • hustlepay-payment-connector-for-woocommerce/trunk/readme.txt

    r3767343 r3794297  
    44Requires at least: 6.1
    55Tested up to: 7.1
    6 Stable tag: 0.9.11
     6Stable tag: 0.10.0
    77WC requires at least: 8.0.0
    88WC tested up to: 11.0.0
    … …  
    3131* Live API: https://api.hustlepay.net
    3232* Test API: https://api.hustlepay.dev
     33* Live hosted checkout page: https://checkout.hustlepay.net
     34* Test hosted checkout page: https://checkout.hustlepay.dev
    3335* Service website: https://www.hustlepay.me/
    3436* Terms of Service: https://www.hustlepay.me/terms-of-service/
    … …  
    4042* Order line items, fees, shipping lines, and tax lines.
    4143* Customer name, billing email, billing phone, company, and billing address.
    42 * The merchant's HustlePay Secret API Key, sent as the `X-API-Key` header when creating checkout sessions and processing refunds. Until a Secret API Key is entered, the Publishable Key is sent in its place, for stores set up before that setting existed. The Publishable Key is also passed to the checkout widget in the customer's browser. The Webhook Signing Secret is stored in WordPress settings and used only to verify webhooks from HustlePay; it is never sent.
     44* The merchant's HustlePay Secret API Key, sent as the `X-API-Key` header when creating checkout sessions and processing refunds. Until a Secret API Key is entered, the Publishable Key is sent in its place, for stores set up before that setting existed. The customer is then redirected to HustlePay's hosted checkout page, with the Publishable Key and the checkout session ID in the page link. The Webhook Signing Secret is stored in WordPress settings and used only to verify webhooks from HustlePay; it is never sent.
    4345
    4446The plugin does not send this data to HustlePay merely by being installed or activated. The data is sent when the merchant has configured the connector and a customer starts a HustlePay checkout flow.
    … …  
    8385= Which API keys does the plugin need? =
    8486
    85 Three per mode, all from your HustlePay merchant portal. The Publishable Key (`pk_…`) is used only by the checkout widget in the shopper's browser. The Secret API Key (`sk_…`) authenticates your store's requests to HustlePay. It is sent only from your server to HustlePay, never to shoppers' browsers. The Webhook Signing Secret verifies the payment updates HustlePay sends back. Stores that were set up before the Secret API Key setting existed keep working with only a Publishable Key, and WP Admin shows a notice until the Secret API Key is entered. Enter it as soon as you have it, because without it the key authenticating your store is the one exposed to browsers.
     87Three per mode, all from your HustlePay merchant portal. The Publishable Key (`pk_…`) is included in the link that takes the shopper to HustlePay's hosted checkout page. The Secret API Key (`sk_…`) authenticates your store's requests to HustlePay. It is sent only from your server to HustlePay, never to shoppers' browsers. The Webhook Signing Secret verifies the payment updates HustlePay sends back. Stores that were set up before the Secret API Key setting existed keep working with only a Publishable Key, and WP Admin shows a notice until the Secret API Key is entered. Enter it as soon as you have it, because without it the key authenticating your store is the one exposed to browsers.
    8688
    8789= What happens to my data if I uninstall the plugin? =
    … …  
    9496
    9597== Changelog ==
     98= 0.10.0 =
     99* Checkout now redirects to HustlePay's hosted checkout page instead of embedding the checkout widget in the store's pay page.
     100* Settings: new Checkout URL (Test/Live) fields; only https URLs are accepted.
     101* After a cancelled payment, the hosted checkout's Return button brings the shopper back to the order's payment page to retry or choose another method.
     102* Checkout shows an error instead of leaving the store when the Publishable Key for the active mode is missing.
     103* Resubmitting an already-paid order goes straight to the order-received page.
     104
    96105= 0.9.11 =
    97106* Checkout widget: the tip control shows the merchant's own tip options (percentages or fixed amounts, an optional starred suggestion that is never added on its own, and an optional custom amount field). Falls back to 10% / 15% / 20% when the merchant has none.
Note: See TracChangeset for help on using the changeset viewer.