id = 'griffnode'; $this->method_title = 'GriffNode'; $this->method_description = 'Accept BTC, ETH, LTC, DOGE, DASH and USDT/USDC/DAI stablecoins via GriffNode. Funds settle directly to your own wallet - GriffNode never holds them. No chargebacks.'; $this->has_fields = true; // we render a crypto selector on checkout $this->init_form_fields(); $this->init_settings(); $this->title = $this->get_option( 'title' ); $this->description = $this->get_option( 'description' ); add_action( 'woocommerce_update_options_payment_gateways_' . $this->id, [ $this, 'process_admin_options' ] ); add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); } // ── Admin settings ──────────────────────────────────────────────────────── public function init_form_fields() { $this->form_fields = [ 'enabled' => [ 'title' => 'Enable / Disable', 'type' => 'checkbox', 'label' => 'Enable GriffNode payments', 'default' => 'yes', ], 'title' => [ 'title' => 'Title', 'type' => 'text', 'default' => 'Pay with Cryptocurrency', 'desc_tip' => 'Label shown to the customer on the checkout page.', ], 'description' => [ 'title' => 'Description', 'type' => 'textarea', 'default' => 'Pay securely with Bitcoin, Ethereum, Litecoin, Dogecoin, Dash or stablecoins. You will be redirected to a hosted payment page.', ], 'publishable_key' => [ 'title' => 'Publishable Key (pk_live_ / pk_test_)', 'type' => 'text', 'desc_tip' => 'Used to fetch your supported cryptos on the checkout page. Safe to expose in the browser.', ], 'secret_key' => [ 'title' => 'Secret Key (sk_live_ / sk_test_)', 'type' => 'password', 'desc_tip' => 'Used server-side to create transactions. Never exposed to the browser.', ], 'webhook_secret' => [ 'title' => 'Webhook Secret', 'type' => 'password', 'description' => __( 'Found in your GriffNode dashboard under Webhooks (Business plan and up): orders update the moment a payment confirms. Leave empty on the free Starter plan: the plugin checks every open payment every 30 seconds by itself.', 'griffnode-for-woocommerce' ), ], ]; } // ── Checkout field (crypto selector) ───────────────────────────────────── public function payment_fields() { if ( $this->description ) { echo '

' . esc_html( $this->description ) . '

'; } ?>

'failure' ]; } // Idempotency: a double-click on "Place order", a back-button resubmit, or a // WooCommerce checkout retry would otherwise call /transactions/create again and // overwrite the order's transaction id/payment URL, orphaning the first one. If // this order already has a live GriffNode transaction (created within the last // 55 minutes — transactions expire after 60), send the customer back to it // instead of minting a new deposit address. $existing_url = $order->get_meta( '_griffnode_payment_url' ); $existing_created = (int) $order->get_meta( '_griffnode_created_at' ); if ( $existing_url && $order->has_status( 'pending' ) && $existing_created && ( time() - $existing_created ) < 55 * MINUTE_IN_SECONDS ) { return [ 'result' => 'success', 'redirect' => $existing_url, ]; } // Nonce is verified by WooCommerce core during checkout before process_payment() runs. // phpcs:ignore WordPress.Security.NonceVerification.Missing $crypto = isset( $_POST['griffnode_crypto'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_POST['griffnode_crypto'] ) ) ) : ''; if ( ! $crypto ) { wc_add_notice( __( 'Please select a cryptocurrency.', 'griffnode-for-woocommerce' ), 'error' ); return [ 'result' => 'failure' ]; } $amount = (float) $order->get_total(); $currency = strtoupper( get_woocommerce_currency() ); // Fall back to USD if the store currency is not supported by GriffNode. $supported_fiat = [ 'USD', 'PLN', 'EUR', 'GBP' ]; if ( ! in_array( $currency, $supported_fiat, true ) ) { $currency = 'USD'; } $secret_key = $this->get_option( 'secret_key' ); $payload = [ 'crypto' => $crypto, 'amount' => $amount, // NOTE: the API's request field is "currency_fiat", not "currency" — see // CreateTransactionRequest in hephaestus/internal/handlers/api/transaction.go. // Sending the wrong key doesn't error, it silently defaults server-side to // USD, which mis-bills every non-USD store. Keep this key exactly as-is. 'currency_fiat' => $currency, 'customer_email' => $order->get_billing_email(), 'success_url' => $this->get_return_url( $order ), 'cancel_url' => wc_get_cart_url(), // The basic /transactions/create endpoint has no top-level order_id field // (only /transactions/create-detailed does) — metadata is the only channel // that survives the round trip into the webhook payload, so wc_order_id // belongs here, not as a top-level field. 'metadata' => [ 'wc_order_id' => (string) $order_id, 'wc_order_key' => $order->get_order_key(), 'customer' => $order->get_billing_email(), ], ]; $response = wp_remote_post( GRIFFNODE_API_BASE . '/transactions/create', [ 'headers' => [ 'Authorization' => 'Bearer ' . $secret_key, 'Content-Type' => 'application/json', 'Accept' => 'application/json', ], 'body' => wp_json_encode( $payload ), 'timeout' => 30, ] ); if ( is_wp_error( $response ) ) { wc_add_notice( __( 'Payment error: could not reach GriffNode. Please try again.', 'griffnode-for-woocommerce' ), 'error' ); return [ 'result' => 'failure' ]; } $body = json_decode( wp_remote_retrieve_body( $response ), true ); $code = wp_remote_retrieve_response_code( $response ); if ( $code !== 201 || empty( $body['data']['payment_url'] ) ) { $msg = $body['message'] ?? 'Unknown error'; /* translators: %s: error message returned by the GriffNode API. */ wc_add_notice( sprintf( __( 'Payment error: %s', 'griffnode-for-woocommerce' ), esc_html( $msg ) ), 'error' ); return [ 'result' => 'failure' ]; } // Store the GriffNode transaction ID on the order for webhook matching. // NOTE: the API's create response field is "transaction_id" (the legacy "txid" // key was retired when the backend moved to its canonical field names — see // docs/sdk/api-inconsistencies.md, item B1). Keep this key exactly as-is. $order->update_meta_data( '_griffnode_txid', $body['data']['transaction_id'] ?? '' ); $order->update_meta_data( '_griffnode_payment_url', $body['data']['payment_url'] ); $order->update_meta_data( '_griffnode_created_at', time() ); $order->update_status( 'pending', __( 'Awaiting GriffNode payment.', 'griffnode-for-woocommerce' ) ); $order->save(); GriffNode_Poller::opened(); return [ 'result' => 'success', 'redirect' => $body['data']['payment_url'], ]; } public function enqueue_scripts() { if ( 'yes' !== $this->enabled || ! is_checkout() || is_order_received_page() ) { return; } wp_enqueue_script( 'griffnode-checkout', GRIFFNODE_PLUGIN_URL . 'js/checkout.js', [ 'jquery' ], GRIFFNODE_VERSION, true ); wp_localize_script( 'griffnode-checkout', 'griffnodeCheckout', [ 'pk' => $this->get_option( 'publishable_key' ), 'api' => esc_url_raw( GRIFFNODE_API_BASE ), 'i18n' => [ 'noKey' => __( 'No publishable key configured', 'griffnode-for-woocommerce' ), 'none' => __( 'No cryptos configured, check your GriffNode dashboard', 'griffnode-for-woocommerce' ), 'failed' => __( 'Failed to load, try refreshing', 'griffnode-for-woocommerce' ), ], ] ); } }