Changeset 3794297
- Timestamp:
- 11/10/2026 11:43:22 AM (18 hours ago)
- Location:
- hustlepay-payment-connector-for-woocommerce
- Files:
-
- 12 deleted
- 20 edited
- 1 copied
-
tags/0.10.0 (copied) (copied from hustlepay-payment-connector-for-woocommerce/trunk )
-
tags/0.10.0/assets/admin/hustlepay-admin.js (modified) (2 diffs)
-
tags/0.10.0/assets/hustlepay/985.js (deleted)
-
tags/0.10.0/assets/hustlepay/payment-rtl.css (deleted)
-
tags/0.10.0/assets/hustlepay/payment.asset.php (deleted)
-
tags/0.10.0/assets/hustlepay/payment.css (deleted)
-
tags/0.10.0/assets/hustlepay/payment.js (deleted)
-
tags/0.10.0/changelog.txt (modified) (1 diff)
-
tags/0.10.0/hustlepay-payment.php (modified) (3 diffs)
-
tags/0.10.0/includes/bootstrap.php (modified) (1 diff)
-
tags/0.10.0/includes/crypto/class-hustlepay-crypto.php (modified) (1 diff)
-
tags/0.10.0/includes/gateway/class-wc-hustlepay-payment-connector.php (modified) (12 diffs)
-
tags/0.10.0/includes/gateway/trait-assets.php (modified) (1 diff)
-
tags/0.10.0/includes/gateway/trait-settings.php (modified) (2 diffs)
-
tags/0.10.0/includes/rest/class-hustlepay-rest-session.php (deleted)
-
tags/0.10.0/includes/rest/rest-loader.php (modified) (2 diffs)
-
tags/0.10.0/readme.txt (modified) (5 diffs)
-
trunk/assets/admin/hustlepay-admin.js (modified) (2 diffs)
-
trunk/assets/hustlepay/985.js (deleted)
-
trunk/assets/hustlepay/payment-rtl.css (deleted)
-
trunk/assets/hustlepay/payment.asset.php (deleted)
-
trunk/assets/hustlepay/payment.css (deleted)
-
trunk/assets/hustlepay/payment.js (deleted)
-
trunk/changelog.txt (modified) (1 diff)
-
trunk/hustlepay-payment.php (modified) (3 diffs)
-
trunk/includes/bootstrap.php (modified) (1 diff)
-
trunk/includes/crypto/class-hustlepay-crypto.php (modified) (1 diff)
-
trunk/includes/gateway/class-wc-hustlepay-payment-connector.php (modified) (12 diffs)
-
trunk/includes/gateway/trait-assets.php (modified) (1 diff)
-
trunk/includes/gateway/trait-settings.php (modified) (2 diffs)
-
trunk/includes/rest/class-hustlepay-rest-session.php (deleted)
-
trunk/includes/rest/rest-loader.php (modified) (2 diffs)
-
trunk/readme.txt (modified) (5 diffs)
Legend:
- Unmodified
- Added
- Removed
-
hustlepay-payment-connector-for-woocommerce/tags/0.10.0/assets/admin/hustlepay-admin.js
r3702360 r3794297 51 51 52 52 /** 53 * Toggle readonly API URLs unless debug enabled53 * Toggle readonly API/checkout URLs unless debug enabled 54 54 */ 55 55 function toggleApiUrlReadonly() { … … 59 59 : String($debug.val() || '').toLowerCase() === 'yes'; 60 60 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 ); 63 65 64 66 if (debugOn) { 65 $live.removeAttr('readonly'); 66 $test.removeAttr('readonly'); 67 $urls.removeAttr('readonly'); 67 68 } else { 68 $live.attr('readonly', 'readonly'); 69 $test.attr('readonly', 'readonly'); 69 $urls.attr('readonly', 'readonly'); 70 70 } 71 71 } -
hustlepay-payment-connector-for-woocommerce/tags/0.10.0/changelog.txt
r3767343 r3794297 1 1 *** HustlePay Payment Connector Changelog *** 2 2026-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 2 9 2026-10-26 - version 0.9.11 3 10 * 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 7 7 * Author URI: https://hustlepay.me 8 8 * 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.119 * Version: 0.10.0 10 10 * Text Domain: hustlepay-payment-connector-for-woocommerce 11 11 * … … 26 26 27 27 if (!defined('HUSTLE_PAY_PAYMENT_VERSION')) { 28 define('HUSTLE_PAY_PAYMENT_VERSION', '0. 9.11');28 define('HUSTLE_PAY_PAYMENT_VERSION', '0.10.0'); 29 29 } 30 30 … … 52 52 if (!defined('HUSTLE_PAY_DEFAULT_LIVE_API_URL')) { 53 53 define('HUSTLE_PAY_DEFAULT_LIVE_API_URL', 'https://api.hustlepay.net'); 54 } 55 if (!defined('HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL')) { 56 define('HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL', 'https://checkout.hustlepay.dev'); 57 } 58 if (!defined('HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL')) { 59 define('HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL', 'https://checkout.hustlepay.net'); 54 60 } 55 61 if (!defined('HUSTLE_PAY_ICON_URL')) { -
hustlepay-payment-connector-for-woocommerce/tags/0.10.0/includes/bootstrap.php
r3732199 r3794297 19 19 20 20 require_once __DIR__ . '/rest/class-hustlepay-rest-webhook.php'; 21 require_once __DIR__ . '/rest/class-hustlepay-rest-session.php';22 21 require_once __DIR__ . '/rest/rest-loader.php'; 23 22 -
hustlepay-payment-connector-for-woocommerce/tags/0.10.0/includes/crypto/class-hustlepay-crypto.php
r3732199 r3794297 89 89 * Order-bound token derived from WordPress's own nonce salt — ties a 90 90 * 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. 93 92 */ 94 93 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 53 53 add_action('woocommerce_api_hustlepay_payment_processed', array($this, 'hustlepay_payment_processed')); 54 54 55 add_action('wp_enqueue_scripts', array($this, 'payment_scripts'));56 55 add_action('admin_enqueue_scripts', array($this, 'admin_scripts')); 57 56 add_action('woocommerce_available_payment_gateways', array($this, 'available_payment_gateways')); … … 136 135 } 137 136 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 138 163 public function validate_test_api_auth_key_field($key, $value): string 139 164 { … … 517 542 } elseif (in_array($status, array('failed', 'canceled', 'expired', 'error'), true)) { 518 543 544 $order->update_meta_data('_hustlepay_exhausted_uuid', $payment_uuid ?: (string) $order->get_meta('_hustlepay_payment_uuid', true)); 519 545 if ('failed' !== $current) { 520 546 $order->update_status('failed', 'HustlePay Webhook: Payment failed'); … … 662 688 } 663 689 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; 725 712 } 726 713 … … 750 737 $created_at = (int) $order->get_meta('_hustlepay_session_created_at', true); 751 738 $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)) { 753 742 return (string) $existing_uuid; 754 743 } … … 788 777 } 789 778 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 ); 843 820 } 844 821 … … 941 918 942 919 if (null === $result) { 943 // Fail closed, unlike handle_create_session()'s lock — an unlocked920 // Fail closed, unlike start_session()'s lock — an unlocked 944 921 // double refund risks two real API calls against HustlePay for the 945 922 // same payment, which this plugin can't make idempotent on its own. … … 1249 1226 $this->log('Payment process initiated for order ID: ' . $order_id . ', Amount: ' . $order->get_total() . ', Currency: ' . $order->get_currency()); 1250 1227 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 1251 1243 return array( 1252 1244 'result' => 'success', 1253 'redirect' => $ order->get_checkout_payment_url(true),1245 'redirect' => $this->hosted_checkout_redirect_url($payment_uuid), 1254 1246 ); 1255 1247 } 1256 1248 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. 1259 1279 * 1260 1280 * @param int $order_id … … 1262 1282 public function receipt_page($order_id): void 1263 1283 { 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 ); 1266 1294 } 1267 1295 … … 1299 1327 if ($token === '' || !hash_equals(HustlePay_Crypto::order_token($order), $token)) { 1300 1328 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; 1301 1338 } 1302 1339 … … 1447 1484 'custom_attributes' => array('readonly' => 'readonly'), 1448 1485 ), 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 ), 1449 1502 'test_api_publishable_key' => array( 1450 1503 'title' => __('Publishable Key (Test)', 'hustlepay-payment-connector-for-woocommerce'), 1451 1504 'type' => 'password', 1452 'description' => __(' Used only by the checkout widgetin 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'), 1453 1506 'default' => '', 1454 1507 'desc_tip' => true, … … 1457 1510 'title' => __('Publishable Key (Live)', 'hustlepay-payment-connector-for-woocommerce'), 1458 1511 'type' => 'password', 1459 'description' => __(' Used only by the checkout widgetin 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'), 1460 1513 'default' => '', 1461 1514 'desc_tip' => true, -
hustlepay-payment-connector-for-woocommerce/tags/0.10.0/includes/gateway/trait-assets.php
r3690308 r3794297 8 8 */ 9 9 trait 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 — this25 // hook fires for every request to this endpoint, so it must happen26 // 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:enable30 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 true50 );51 52 wp_enqueue_style(53 'hustlepay-payment-style',54 HUSTLE_PAY_PAYMENT_PLUGIN_URL . 'assets/hustlepay/payment.css',55 array(),56 $version57 );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 }72 10 73 11 /** -
hustlepay-payment-connector-for-woocommerce/tags/0.10.0/includes/gateway/trait-settings.php
r3732199 r3794297 35 35 public string $test_api_url; 36 36 public string $live_api_url; 37 38 public string $checkout_url; 37 39 38 40 /* ===================================================== … … 77 79 ? $this->test_api_url 78 80 : $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 ); 79 85 } 80 86 -
hustlepay-payment-connector-for-woocommerce/tags/0.10.0/includes/rest/rest-loader.php
r3538030 r3794297 16 16 17 17 $webhook = new HustlePay_REST_Webhook($gateway); 18 $session = new HustlePay_REST_Session($gateway);19 18 20 19 register_rest_route( … … 27 26 ) 28 27 ); 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 );39 28 } 40 29 ); -
hustlepay-payment-connector-for-woocommerce/tags/0.10.0/readme.txt
r3767343 r3794297 4 4 Requires at least: 6.1 5 5 Tested up to: 7.1 6 Stable tag: 0. 9.116 Stable tag: 0.10.0 7 7 WC requires at least: 8.0.0 8 8 WC tested up to: 11.0.0 … … 31 31 * Live API: https://api.hustlepay.net 32 32 * Test API: https://api.hustlepay.dev 33 * Live hosted checkout page: https://checkout.hustlepay.net 34 * Test hosted checkout page: https://checkout.hustlepay.dev 33 35 * Service website: https://www.hustlepay.me/ 34 36 * Terms of Service: https://www.hustlepay.me/terms-of-service/ … … 40 42 * Order line items, fees, shipping lines, and tax lines. 41 43 * 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. 43 45 44 46 The 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. … … 83 85 = Which API keys does the plugin need? = 84 86 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.87 Three 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. 86 88 87 89 = What happens to my data if I uninstall the plugin? = … … 94 96 95 97 == 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 96 105 = 0.9.11 = 97 106 * 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 51 51 52 52 /** 53 * Toggle readonly API URLs unless debug enabled53 * Toggle readonly API/checkout URLs unless debug enabled 54 54 */ 55 55 function toggleApiUrlReadonly() { … … 59 59 : String($debug.val() || '').toLowerCase() === 'yes'; 60 60 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 ); 63 65 64 66 if (debugOn) { 65 $live.removeAttr('readonly'); 66 $test.removeAttr('readonly'); 67 $urls.removeAttr('readonly'); 67 68 } else { 68 $live.attr('readonly', 'readonly'); 69 $test.attr('readonly', 'readonly'); 69 $urls.attr('readonly', 'readonly'); 70 70 } 71 71 } -
hustlepay-payment-connector-for-woocommerce/trunk/changelog.txt
r3767343 r3794297 1 1 *** HustlePay Payment Connector Changelog *** 2 2026-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 2 9 2026-10-26 - version 0.9.11 3 10 * 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 7 7 * Author URI: https://hustlepay.me 8 8 * 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.119 * Version: 0.10.0 10 10 * Text Domain: hustlepay-payment-connector-for-woocommerce 11 11 * … … 26 26 27 27 if (!defined('HUSTLE_PAY_PAYMENT_VERSION')) { 28 define('HUSTLE_PAY_PAYMENT_VERSION', '0. 9.11');28 define('HUSTLE_PAY_PAYMENT_VERSION', '0.10.0'); 29 29 } 30 30 … … 52 52 if (!defined('HUSTLE_PAY_DEFAULT_LIVE_API_URL')) { 53 53 define('HUSTLE_PAY_DEFAULT_LIVE_API_URL', 'https://api.hustlepay.net'); 54 } 55 if (!defined('HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL')) { 56 define('HUSTLE_PAY_DEFAULT_TEST_CHECKOUT_URL', 'https://checkout.hustlepay.dev'); 57 } 58 if (!defined('HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL')) { 59 define('HUSTLE_PAY_DEFAULT_LIVE_CHECKOUT_URL', 'https://checkout.hustlepay.net'); 54 60 } 55 61 if (!defined('HUSTLE_PAY_ICON_URL')) { -
hustlepay-payment-connector-for-woocommerce/trunk/includes/bootstrap.php
r3732199 r3794297 19 19 20 20 require_once __DIR__ . '/rest/class-hustlepay-rest-webhook.php'; 21 require_once __DIR__ . '/rest/class-hustlepay-rest-session.php';22 21 require_once __DIR__ . '/rest/rest-loader.php'; 23 22 -
hustlepay-payment-connector-for-woocommerce/trunk/includes/crypto/class-hustlepay-crypto.php
r3732199 r3794297 89 89 * Order-bound token derived from WordPress's own nonce salt — ties a 90 90 * 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. 93 92 */ 94 93 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 53 53 add_action('woocommerce_api_hustlepay_payment_processed', array($this, 'hustlepay_payment_processed')); 54 54 55 add_action('wp_enqueue_scripts', array($this, 'payment_scripts'));56 55 add_action('admin_enqueue_scripts', array($this, 'admin_scripts')); 57 56 add_action('woocommerce_available_payment_gateways', array($this, 'available_payment_gateways')); … … 136 135 } 137 136 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 138 163 public function validate_test_api_auth_key_field($key, $value): string 139 164 { … … 517 542 } elseif (in_array($status, array('failed', 'canceled', 'expired', 'error'), true)) { 518 543 544 $order->update_meta_data('_hustlepay_exhausted_uuid', $payment_uuid ?: (string) $order->get_meta('_hustlepay_payment_uuid', true)); 519 545 if ('failed' !== $current) { 520 546 $order->update_status('failed', 'HustlePay Webhook: Payment failed'); … … 662 688 } 663 689 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; 725 712 } 726 713 … … 750 737 $created_at = (int) $order->get_meta('_hustlepay_session_created_at', true); 751 738 $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)) { 753 742 return (string) $existing_uuid; 754 743 } … … 788 777 } 789 778 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 ); 843 820 } 844 821 … … 941 918 942 919 if (null === $result) { 943 // Fail closed, unlike handle_create_session()'s lock — an unlocked920 // Fail closed, unlike start_session()'s lock — an unlocked 944 921 // double refund risks two real API calls against HustlePay for the 945 922 // same payment, which this plugin can't make idempotent on its own. … … 1249 1226 $this->log('Payment process initiated for order ID: ' . $order_id . ', Amount: ' . $order->get_total() . ', Currency: ' . $order->get_currency()); 1250 1227 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 1251 1243 return array( 1252 1244 'result' => 'success', 1253 'redirect' => $ order->get_checkout_payment_url(true),1245 'redirect' => $this->hosted_checkout_redirect_url($payment_uuid), 1254 1246 ); 1255 1247 } 1256 1248 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. 1259 1279 * 1260 1280 * @param int $order_id … … 1262 1282 public function receipt_page($order_id): void 1263 1283 { 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 ); 1266 1294 } 1267 1295 … … 1299 1327 if ($token === '' || !hash_equals(HustlePay_Crypto::order_token($order), $token)) { 1300 1328 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; 1301 1338 } 1302 1339 … … 1447 1484 'custom_attributes' => array('readonly' => 'readonly'), 1448 1485 ), 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 ), 1449 1502 'test_api_publishable_key' => array( 1450 1503 'title' => __('Publishable Key (Test)', 'hustlepay-payment-connector-for-woocommerce'), 1451 1504 'type' => 'password', 1452 'description' => __(' Used only by the checkout widgetin 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'), 1453 1506 'default' => '', 1454 1507 'desc_tip' => true, … … 1457 1510 'title' => __('Publishable Key (Live)', 'hustlepay-payment-connector-for-woocommerce'), 1458 1511 'type' => 'password', 1459 'description' => __(' Used only by the checkout widgetin 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'), 1460 1513 'default' => '', 1461 1514 'desc_tip' => true, -
hustlepay-payment-connector-for-woocommerce/trunk/includes/gateway/trait-assets.php
r3690308 r3794297 8 8 */ 9 9 trait 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 — this25 // hook fires for every request to this endpoint, so it must happen26 // 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:enable30 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 true50 );51 52 wp_enqueue_style(53 'hustlepay-payment-style',54 HUSTLE_PAY_PAYMENT_PLUGIN_URL . 'assets/hustlepay/payment.css',55 array(),56 $version57 );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 }72 10 73 11 /** -
hustlepay-payment-connector-for-woocommerce/trunk/includes/gateway/trait-settings.php
r3732199 r3794297 35 35 public string $test_api_url; 36 36 public string $live_api_url; 37 38 public string $checkout_url; 37 39 38 40 /* ===================================================== … … 77 79 ? $this->test_api_url 78 80 : $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 ); 79 85 } 80 86 -
hustlepay-payment-connector-for-woocommerce/trunk/includes/rest/rest-loader.php
r3538030 r3794297 16 16 17 17 $webhook = new HustlePay_REST_Webhook($gateway); 18 $session = new HustlePay_REST_Session($gateway);19 18 20 19 register_rest_route( … … 27 26 ) 28 27 ); 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 );39 28 } 40 29 ); -
hustlepay-payment-connector-for-woocommerce/trunk/readme.txt
r3767343 r3794297 4 4 Requires at least: 6.1 5 5 Tested up to: 7.1 6 Stable tag: 0. 9.116 Stable tag: 0.10.0 7 7 WC requires at least: 8.0.0 8 8 WC tested up to: 11.0.0 … … 31 31 * Live API: https://api.hustlepay.net 32 32 * Test API: https://api.hustlepay.dev 33 * Live hosted checkout page: https://checkout.hustlepay.net 34 * Test hosted checkout page: https://checkout.hustlepay.dev 33 35 * Service website: https://www.hustlepay.me/ 34 36 * Terms of Service: https://www.hustlepay.me/terms-of-service/ … … 40 42 * Order line items, fees, shipping lines, and tax lines. 41 43 * 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. 43 45 44 46 The 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. … … 83 85 = Which API keys does the plugin need? = 84 86 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.87 Three 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. 86 88 87 89 = What happens to my data if I uninstall the plugin? = … … 94 96 95 97 == 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 96 105 = 0.9.11 = 97 106 * 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.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)