'1nodes (Crypto)', 'checkout_label' => $checkout_label, ); return $gateways; } /** * Register settings section. * * @param array $sections Settings sections. * * @return array */ public static function register_settings_section( array $sections ): array { $sections[ self::SECTION_ID ] = '1nodes'; return $sections; } /** * Register gateway settings. * * @param array $settings Settings. * * @return array */ public static function register_gateway_settings( array $settings ): array { $settings[ self::SECTION_ID ] = array( 'nodes_edd_1nodes_header' => array( 'id' => 'nodes_edd_1nodes_header', 'name' => '1nodes Gateway', 'desc' => 'Configure the 1nodes payment gateway settings.', 'type' => 'header', ), 'nodes_edd_checkout_label' => array( 'id' => 'nodes_edd_checkout_label', 'name' => 'Checkout gateway label', 'desc' => 'Example: Pay with Crypto (1nodes)', 'type' => 'text', 'size' => 'regular', 'std' => 'Pay with 1nodes', ), 'nodes_edd_merchant_key' => array( 'id' => 'nodes_edd_merchant_key', 'name' => 'Merchant Key', 'desc' => 'Your 1nodes merchant key.', 'type' => 'text', 'size' => 'regular', ), 'nodes_edd_secret_key' => array( 'id' => 'nodes_edd_secret_key', 'name' => 'Secret Key', 'desc' => 'Used to verify callbacks/webhooks. Stored in the database.', 'type' => 'password', 'size' => 'regular', ), ); return $settings; } /** * Render gateway checkout information. */ public static function render_no_cc_form(): void { echo '

'; echo esc_html__( 'You will be redirected to 1nodes to complete your crypto payment.', '1nodes-crypto-payments-for-edd' ); echo '

'; } /** * Process payment. * * @param array $purchase_data Purchase data. */ public static function process_payment( array $purchase_data ): void { /** * 1. Create EDD payment. */ $payment_id = edd_insert_payment( array( 'price' => $purchase_data['price'], 'date' => $purchase_data['date'], 'user_email' => $purchase_data['user_email'], 'purchase_key' => $purchase_data['purchase_key'], 'currency' => edd_get_currency(), 'downloads' => $purchase_data['downloads'], 'cart_details' => $purchase_data['cart_details'], 'user_info' => $purchase_data['user_info'], 'gateway' => self::GATEWAY_ID, 'status' => 'pending', ) ); if ( ! $payment_id ) { edd_set_error( 'payment_not_created', __( 'Failed to create payment record.', '1nodes-crypto-payments-for-edd' ) ); edd_send_back_to_checkout(); return; } /** * 2. Get EDD payment. */ $payment = edd_get_payment( $payment_id ); if ( ! $payment ) { edd_set_error( 'payment_not_found', __( 'Payment record could not be found.', '1nodes-crypto-payments-for-edd' ) ); edd_send_back_to_checkout(); return; } /** * 3. Merchant key. */ $merchant_key = trim( (string) edd_get_option( 'nodes_edd_merchant_key', '' ) ); if ( empty( $merchant_key ) ) { edd_set_error( 'merchant_key_missing', __( 'Merchant key is not configured.', '1nodes-crypto-payments-for-edd' ) ); edd_send_back_to_checkout(); return; } /** * 4. Webhook URL. */ $callback_url = add_query_arg( array( 'edd-listener' => 'onenodes_webhook', 'payment_id' => $payment_id, ), home_url( '/' ) ); /** * 5. Return URL. * * We add payment ID and payment key so that * the confirmation page can check payment status. */ $return_url = add_query_arg( array( 'check_pay_status' => '1', 'payment_id' => $payment_id, 'payment_key' => $payment->key, ), edd_get_success_page_uri() ); /** * 6. Create payment payload. */ $payload = array( 'amount' => (string) $purchase_data['price'], 'order_id' => (string) $payment_id, 'callback' => $callback_url, 'return_url' => $return_url, 'meta' => array( 'edd_payment_id' => (string) $payment_id, 'customer_email' => $purchase_data['user_email'], ), ); /** * 7. Create payment request. */ $response = wp_remote_post( 'https://1nodes.com/wp-json/v1/api/create-payment', array( 'timeout' => 30, 'sslverify' => true, 'body' => wp_json_encode( $payload ), 'headers' => array( 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $merchant_key, ), ) ); /** * 8. Handle API connection error. */ if ( is_wp_error( $response ) ) { edd_set_error( 'api_error', __( 'Gateway connection error.', '1nodes-crypto-payments-for-edd' ) ); edd_send_back_to_checkout(); return; } /** * 9. Validate HTTP status. */ $response_code = wp_remote_retrieve_response_code( $response ); if ( 201 !== $response_code ) { edd_set_error( 'api_error', __( 'Gateway connection error.', '1nodes-crypto-payments-for-edd' ) ); edd_send_back_to_checkout(); return; } /** * 10. Decode response. */ $body = json_decode( wp_remote_retrieve_body( $response ), true ); /** * 11. Validate response. */ if ( empty( $body['success'] ) || empty( $body['data']['checkout_url'] ) ) { edd_set_error( 'invalid_response', __( 'Invalid response from gateway.', '1nodes-crypto-payments-for-edd' ) ); edd_send_back_to_checkout(); return; } /** * 12. Redirect to 1nodes checkout. */ $checkout_url = esc_url_raw( $body['data']['checkout_url'] ); $checkout_host = wp_parse_url( $checkout_url, PHP_URL_HOST ); if ( '1nodes.com' !== $checkout_host ) { edd_set_error( 'invalid_checkout_url', __( 'Invalid checkout URL received from gateway.', '1nodes-crypto-payments-for-edd' ) ); edd_send_back_to_checkout(); return; } // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- Checkout URL is received from the 1nodes API and restricted to the expected host above. wp_redirect( $checkout_url ); exit; } /** * Enqueue payment status assets. */ public static function enqueue_payment_status_assets(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public payment return URL parameter; payment key validation is performed below. if ( empty( $_GET['check_pay_status'] ) ) { return; } $check_pay_status = sanitize_text_field( // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public payment return URL parameter; validated as a payment flow parameter. wp_unslash( $_GET['check_pay_status'] ) ); if ( '1' !== $check_pay_status ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public payment return URL parameter; payment key validation is performed below. $payment_id = isset( $_GET['payment_id'] ) ? absint( $_GET['payment_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public payment return URL parameter; validated against the EDD payment key below. $payment_key = isset( $_GET['payment_key'] ) ? sanitize_text_field(wp_unslash( $_GET['payment_key'] )) : ''; if ( ! $payment_id || empty( $payment_key ) ) { return; } /** * Validate payment before loading assets. */ $payment = edd_get_payment( $payment_id ); if ( ! $payment ) { return; } if ( empty( $payment->key ) || ! hash_equals( (string) $payment->key, $payment_key ) ) { return; } /** * JS. */ wp_enqueue_script( 'onenodes-payment-status', plugins_url( 'assets/js/payment-status.js', __FILE__ ), array(), '1.0.0', true ); /** * CSS. */ wp_enqueue_style( 'onenodes-payment-status', plugins_url( 'assets/css/payment-status.css', __FILE__ ), array(), '1.0.0' ); /** * Pass data to JS. */ wp_localize_script( 'onenodes-payment-status', 'OneNodesPayment', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'payment_id' => $payment_id, 'payment_key' => $payment_key, 'interval' => 3000, 'nonce' => wp_create_nonce( 'onenodes_check_payment_status' ), 'timeout' => 120000, 'messages' => array( 'checking' => __( 'Please wait, we are checking your payment status...', '1nodes-crypto-payments-for-edd' ), 'timeout' => __( 'We could not confirm your payment yet. Please refresh the page in a moment.', '1nodes-crypto-payments-for-edd' ), ), ) ); } /** * AJAX: Check payment status. */ public static function check_payment_status(): void { $nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'onenodes_check_payment_status' ) ) { wp_send_json_error( array( 'message' => 'Invalid security token.', ), 403 ); } /** * Payment ID. */ $payment_id = isset( $_POST['payment_id'] ) ? absint( $_POST['payment_id'] ) : 0; /** * Payment key. */ $payment_key = isset( $_POST['payment_key'] ) ? sanitize_text_field( wp_unslash( $_POST['payment_key'] ) ) : ''; if ( ! $payment_id || empty( $payment_key ) ) { wp_send_json_error( array( 'message' => 'Invalid payment data.', ), 400 ); } /** * Get payment. */ $payment = edd_get_payment( $payment_id ); if ( ! $payment ) { wp_send_json_error( array( 'message' => 'Payment not found.', ), 404 ); } if ( self::GATEWAY_ID !== $payment->gateway ) { wp_send_json_error( array( 'message' => 'Invalid payment gateway.', ), 403 ); } /** * Validate payment key. */ if ( empty( $payment->key ) || ! hash_equals( (string) $payment->key, $payment_key ) ) { wp_send_json_error( array( 'message' => 'Invalid payment key.', ), 403 ); } /** * Return current EDD payment status. */ $status = strtolower( (string) $payment->status ); if ( 'complete' === $status ) { if ( function_exists( 'edd_empty_cart' ) ) { edd_empty_cart(); } } wp_send_json_success( array( 'payment_id' => $payment_id, 'status' => $status, ) ); } /** * Process 1nodes webhook. */ public static function process_webhook(): void { /** * 1. Only POST requests. */ $request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : ''; if ( 'POST' !== $request_method ) { wp_send_json( array( 'status' => 'error', 'message' => 'Method not allowed', ), 405 ); } /** * 2. Get raw body. */ $raw_body = file_get_contents( 'php://input' ); if ( empty( $raw_body ) ) { wp_send_json( array( 'status' => 'error', 'message' => 'Empty request body', ), 400 ); } /** * 3. Get webhook signature. */ $signature = isset( $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ) ? trim( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ) ) ) : ''; if ( empty( $signature ) && function_exists( 'getallheaders' ) ) { $headers = getallheaders(); $signature = $headers['X-Webhook-Signature'] ?? $headers['x-webhook-signature'] ?? ''; } if ( empty( $signature ) ) { wp_send_json( array( 'status' => 'error', 'message' => 'Missing signature', ), 400 ); } /** * 4. Get secret key. */ $secret_key = trim( (string) edd_get_option( 'nodes_edd_secret_key', '' ) ); if ( empty( $secret_key ) ) { wp_send_json( array( 'status' => 'error', 'message' => 'Webhook secret is not configured', ), 500 ); } /** * 5. Verify HMAC signature. */ $expected_signature = hash_hmac( 'sha256', $raw_body, $secret_key ); if ( ! hash_equals( $expected_signature, $signature ) ) { wp_send_json( array( 'status' => 'error', 'message' => 'Invalid signature', ), 401 ); } /** * 6. Decode JSON. */ $inputs = json_decode( $raw_body, true ); if ( ! is_array( $inputs ) ) { wp_send_json( array( 'status' => 'error', 'message' => 'Invalid JSON', ), 400 ); } /** * 7. Validate gateway. */ if ( empty( $inputs['gateway'] ) || '1nodes' !== $inputs['gateway'] ) { wp_send_json( array( 'status' => 'error', 'message' => 'Invalid gateway', ), 400 ); } /** * 8. Get local EDD payment ID. */ // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public webhook routing parameter; request is authenticated by HMAC signature. $payment_id = isset( $_GET['payment_id'] ) ? absint( $_GET['payment_id'] ) : 0; if ( ! $payment_id ) { wp_send_json( array( 'status' => 'error', 'message' => 'Invalid payment_id', ), 400 ); } /** * 9. Get EDD payment. */ $payment = edd_get_payment( $payment_id ); if ( ! $payment ) { wp_send_json( array( 'status' => 'error', 'message' => 'Payment not found', ), 404 ); } if ( self::GATEWAY_ID !== $payment->gateway ) { wp_send_json( array( 'status' => 'error', 'message' => 'Invalid payment gateway', ), 403 ); } /** * 10. Already completed. */ if ( 'complete' === $payment->status ) { wp_send_json( array( 'status' => 'success', 'message' => 'Already processed', ), 200 ); } /** * 11. Validate provider payment status. */ $input_status = strtolower( (string) ( $inputs['status'] ?? '' ) ); if ( 'paid' !== $input_status ) { wp_send_json( array( 'status' => 'success', 'message' => 'Ignored', ), 200 ); } /** * 12. Save 1nodes payment ID. */ if ( ! empty( $inputs['payment_id'] ) ) { $payment->update_meta( '_onenodes_payment_id', sanitize_text_field( $inputs['payment_id'] ) ); } /** * 13. Mark EDD payment as complete. * * IMPORTANT: * Do NOT use "publish". */ edd_update_payment_status( $payment_id, 'complete' ); /** * 14. Add note. */ $payment->add_note( __( 'Payment confirmed by 1nodes webhook.', '1nodes-crypto-payments-for-edd' ) ); /** * 15. Response. */ wp_send_json( array( 'status' => 'success', 'message' => 'OK', ), 200 ); } } Nodes_EDD_Crypto_Payments::init();