Initial working version.

This commit is contained in:
ndeet 2025-04-01 18:03:23 +02:00
commit 6131230042
8 changed files with 845 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.DS_Store
phpcs.xml
phpunit.xml
Thumbs.db
composer.lock
/vendor/

View File

@ -0,0 +1,44 @@
(() => {
let settings = {};
/**
* Example of rendering gateway fields (without jsx).
*
* This renders a simple div with a label and input.
*
* @see https://react.dev/reference/react/createElement
*/
function BtcpayGatewayFields() {
return window.wp.element.createElement(
"div",
{
className: 'btpcay-gateway-help-text'
},
window.wp.element.createElement(
"p",
{
style: {marginBottom: 0}
},
settings.message,
)
);
}
/**
* Example of a front-end gateway object.
*/
const BtcpayGateway = {
id: "btcpay-gateway",
initialize() {
settings = this.settings
},
Fields() {
return window.wp.element.createElement(BtcpayGatewayFields);
},
};
/**
* The final step is to register the front-end gateway with GiveWP.
*/
window.givewp.gateways.register(BtcpayGateway);
})();

136
btcpay-for-givewp.php Normal file
View File

@ -0,0 +1,136 @@
<?php
/**
* Plugin Name: BTCPay Server for GiveWP
* Plugin URI: https://docs.btcpayserver.org/GiveWP/
* Description: BTCPay Server Bitcoin / Lightning Network payment gateway integration for GiveWP
* Version: 1.0.0
* Author: BTCPay Server integrations team
* Author URI: https://btcpayserver.org
* Text Domain: btcpay-for-givewp
* Domain Path: /languages
* License: MIT
* License URI: https://opensource.org/licenses/MIT
* Requires PHP: 8.1
* Requires at least: 6.0
* Tested up to: 6.7
* Requires Plugins: give
* GiveWP tested up to: 3.22.2
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Define plugin constants
define('BTCPAY_FOR_GIVEWP_VERSION', '1.0.0');
define('BTCPAY_FOR_GIVEWP_DIR', plugin_dir_path(__FILE__));
define('BTCPAY_FOR_GIVEWP_URL', plugin_dir_url(__FILE__));
// Composer autoloader
require_once __DIR__ . '/vendor/autoload.php';
/**
* Main plugin class
*/
final class BTCPayForGiveWP {
/**
* @var BTCPayForGiveWP The single instance of this class
*/
private static $instance = null;
/**
* Main Plugin Instance
*
* Ensures only one instance of the plugin exists in memory at any one time.
*
* @return BTCPayForGiveWP
*/
public static function instance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Plugin Constructor
*/
private function __construct() {
// Load text domain
add_action('init', [$this, 'loadTextdomain']);
// Admin notice if Give is not active
add_action('admin_notices', [$this, 'adminNotice']);
}
/**
* Initialize the plugin on give_init
* This is the main initialization method that sets up the gateway
*/
public function setupGateway() {
// Check if Give is active
if (!$this->isGiveActive()) {
return;
}
// Register the payment gateway with GiveWP
add_action('givewp_register_payment_gateway', function($paymentGatewayRegister) {
$paymentGatewayRegister->registerGateway(BTCPayServer\Give\Gateway\BtcpayGateway::class);
});
// Initialize the settings class
if (is_admin()) {
BTCPayServer\Give\Admin\Settings::instance();
}
}
/**
* Check if Give is active
*
* @return bool
*/
private function isGiveActive() {
return class_exists('Give');
}
/**
* Load plugin text domain
*/
public function loadTextdomain() {
load_plugin_textdomain(
'btcpay-for-givewp',
false,
dirname(plugin_basename(__FILE__)) . '/languages'
);
}
/**
* Admin notice for when Give is not active
*/
public function adminNotice() {
if (!$this->is_give_active()) {
?>
<div class="error">
<p><?php _e('Give must be installed and activated for the BTCPay for GiveWP Gateway add-on to work.', 'btcpay-for-givewp'); ?></p>
</div>
<?php
}
}
}
/**
* Returns the main instance of BTCPayForGiveWP
*
* @return BTCPayForGiveWP
*/
function BTCPayForGiveWP() {
return BTCPayForGiveWP::instance();
}
// Initialize the plugin
BTCPayForGiveWP();
// Setup the gateway at the proper time
add_action('give_init', [BTCPayForGiveWP(), 'setupGateway']);

29
composer.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "btcpayserver/givewp",
"description": "BTCPay Server Bitcoin/Lightning Network payment gateway integration for Give-WP",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "BTCPay Server integrations team",
"email": "integrations@btcpayserver.org"
}
],
"require": {
"php": ">=8.1",
"composer/installers": "~1.0"
},
"autoload": {
"psr-4": {
"BTCPayServer\\Give\\": "src/"
}
},
"config": {
"sort-packages": true,
"allow-plugins": {
"composer/installers": true
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

21
license.txt Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 BTCPay Server
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

19
readme.txt Normal file
View File

@ -0,0 +1,19 @@
=== BTCPay Server for GiveWP ===
Contributors: ndeet
Tags: Bitcoin, BTCPay Server, cryptocurrency, GiveWP, donations, fundraising, gateway
Requires at least: 5.0
Tested up to: 6.7
Stable tag: 1.0.0
Requires Give: 2.24.0
Requires PHP: 8.1
License: MIT
License URI: https://opensource.org/licenses/MIT
A BTCPay Server Bitcoin / Lightning Network (and other cryptocurrencies) payment gateway for GiveWP.
== Description ==
This plugin requires the GiveWP core plugin activated to function properly.
== Installation ==

306
src/Admin/Settings.php Normal file
View File

@ -0,0 +1,306 @@
<?php
namespace BTCPayServer\Give\Admin;
use BTCPayServer\Client\Webhook;
use BTCPayServer\Give\Gateway\BtcpayGateway;
/**
* BTCPay Settings Class
*/
class Settings {
/**
* @var Settings
*/
private static $instance = null;
/**
* Get the singleton instance
*/
public static function instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
// Register the gateway settings section
add_filter('give_get_sections_gateways', [$this, 'registerSections']);
// Register the gateway settings fields
add_filter('give_get_settings_gateways', [$this, 'registerSettings']);
// Add validation for API credentials
add_action('admin_init', [$this, 'handleSettingsSave']);
add_action('admin_notices', [$this, 'displayApiValidationMessages']);
}
/**
* Register Gateway Section
*/
public function registerSections($sections) {
$sections['btcpay-gateway'] = __('BTCPay Gateway', 'btcpay-for-givewp');
return $sections;
}
/**
* Register Gateway Settings
*/
public function registerSettings($settings) {
$current_section = give_get_current_setting_section();
if ($current_section !== 'btcpay-gateway') {
return $settings;
}
return [
[
'id' => 'btcpay_settings_title',
'type' => 'title',
'title' => __('BTCPay Server Gateway Settings', 'btcpay-for-givewp'),
],
[
'id' => 'btcpay_url',
'name' => __('BTCPay Server URL', 'btcpay-for-givewp'),
'desc' => __('Enter the URL where you log into your BTCPay Server. e.g. https://mainnet.demo.btcpayserver.org', 'btcpay-for-givewp'),
'type' => 'text',
'default' => '',
'sanitize_callback' => 'esc_url_raw',
],
[
'id' => 'btcpay_store_id',
'name' => __('Store ID', 'btcpay-for-givewp'),
'desc' => __('Enter your BTCPay Server Store ID.', 'btcpay-for-givewp'),
'type' => 'text',
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
],
[
'id' => 'btcpay_api_key',
'name' => __('API Key', 'btcpay-for-givewp'),
'desc' => __('Enter your BTCPay API key.', 'btcpay-for-givewp'),
'type' => 'password',
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
],
[
'id' => 'btcpay_settings',
'type' => 'sectionend',
],
];
}
/**
* Handle settings save and API validation
*/
public function handleSettingsSave() {
if (!$this->isSettingsSaveRequest()) {
return;
}
// Verify nonce
if (!wp_verify_nonce($_POST['_give-save-settings'], 'give-save-settings')) {
return;
}
// Check if we're in the BTCPay settings section
if (empty($_GET['section']) || $_GET['section'] !== 'btcpay-gateway') {
return;
}
$btcpay_url = isset($_POST['btcpay_url']) ? esc_url_raw($_POST['btcpay_url']) : '';
$store_id = isset($_POST['btcpay_store_id']) ? sanitize_text_field($_POST['btcpay_store_id']) : '';
$api_key = isset($_POST['btcpay_api_key']) ? sanitize_text_field($_POST['btcpay_api_key']) : '';
// Only validate if all fields are filled
if ($btcpay_url && $store_id && $api_key) {
$validation_result = $this->validateApiCredentials($btcpay_url, $store_id, $api_key);
if (is_wp_error($validation_result)) {
set_transient('btcpay_api_validation_error', $validation_result->get_error_message(), 45);
} else {
set_transient('btcpay_api_validation_success', __('BTCPay for GiveWP: BTCPay Server API credentials verified successfully!', 'btcpay-for-givewp'), 45);
}
// Setup webhook if it does not exist.
if ($this->webhookExists($btcpay_url, $store_id, $api_key)) {
set_transient('btcpay_webhook_exists', __('BTCPay for GiveWP: Webhook already exists, no need to create it.', 'btcpay-for-givewp'), 45);
} else {
$webhook = $this->createWebhook($btcpay_url, $store_id, $api_key);
// Store webhook secret and other data for later use
give_update_option('btcpay_wh_id', $webhook->getId());
give_update_option('btcpay_wh_secret', $webhook->getSecret());
give_update_option('btcpay_wh_url', $webhook->getUrl());
set_transient('btcpay_webhook_created', __('BTCPay for GiveWP: Webhook successfully created.', 'btcpay-for-givewp'), 45);
}
}
}
/**
* Display API validation messages
*/
public function displayApiValidationMessages() {
$error = get_transient('btcpay_api_validation_error');
$whError = get_transient('btcpay_webhook_error');
$success = get_transient('btcpay_api_validation_success');
$whExists = get_transient('btcpay_webhook_exists');
$whCreated = get_transient('btcpay_webhook_created');
if ($error) {
?>
<div class="error">
<p><?php echo esc_html($error); ?></p>
</div>
<?php
delete_transient('btcpay_api_validation_error');
}
if ($whError) {
?>
<div class="error">
<p><?php echo esc_html($whError); ?></p>
</div>
<?php
delete_transient('btcpay_webhook_error');
}
if ($success) {
?>
<div class="updated">
<p><?php echo esc_html($success); ?></p>
</div>
<?php
delete_transient('btcpay_api_validation_success');
}
if ($whExists) {
?>
<div class="updated">
<p><?php echo esc_html($whExists); ?></p>
</div>
<?php
delete_transient('btcpay_webhook_exists');
}
if ($whCreated) {
?>
<div class="updated">
<p><?php echo esc_html($whCreated); ?></p>
</div>
<?php
delete_transient('btcpay_webhook_created');
}
}
/**
* Check if current request is for saving settings
*/
private function isSettingsSaveRequest(): bool {
return (
isset($_POST['_give-save-settings']) &&
isset($_GET['page']) &&
$_GET['page'] === 'give-settings'
);
}
/**
* Validate BTCPay API credentials
*/
private function validateApiCredentials(string $url, string $store_id, string $api_key) {
// Remove trailing slashes
$url = rtrim($url, '/');
// Test endpoint
$test_endpoint = "$url/api/v1/stores/$store_id";
$response = wp_remote_get($test_endpoint, [
'headers' => [
'Authorization' => "token $api_key",
'Content-Type' => 'application/json',
],
'timeout' => 30,
]);
if (is_wp_error($response)) {
return new \WP_Error(
'btcpay_api_error',
sprintf(
__('Failed to connect to BTCPay Server: %s', 'btcpay-for-givewp'),
$response->get_error_message()
)
);
}
$response_code = wp_remote_retrieve_response_code($response);
if ($response_code !== 200) {
return new \WP_Error(
'btcpay_api_error',
sprintf(
__('Invalid API credentials. BTCPay Server returned: %s', 'btcpay-for-givewp'),
wp_remote_retrieve_response_message($response)
)
);
}
return true;
}
/**
* Check if the webhook exists.
*/
public function webhookExists(string $btcpay_url, string $store_id, string $api_key) {
try {
$existingWebhook = give_get_option('btcpay_wh_id');
if (!$existingWebhook) {
return false;
}
$client = new Webhook($btcpay_url, $api_key);
$webhooks = $client->getStoreWebhooks($store_id);
foreach ($webhooks->all() as $webhook) {
if ($webhook->getId() === $existingWebhook) {
return true;
}
}
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
return false;
}
/**
* Create a webhook on BTCPay Server
*/
public function createWebhook(string $url, string $store_id, string $api_key) {
// Which webhook events to subscribe to
$events = [
'InvoiceReceivedPayment',
'InvoicePaymentSettled',
'InvoiceProcessing',
'InvoiceExpired',
'InvoiceSettled',
'InvoiceInvalid'
];
try {
// Create the webhook
$client = new Webhook($url, $api_key);
$webhook = $client->createWebhook(
$store_id,
BtcpayGateway::webhookUrl(),
$events,
null
);
return $webhook;
} catch (\Exception $e) {
throw new \Exception($e->getMessage());
}
}
}

View File

@ -0,0 +1,284 @@
<?php
namespace BTCPayServer\Give\Gateway;
use BTCPayServer\Client\Invoice;
use BTCPayServer\Client\InvoiceCheckoutOptions;
use BTCPayServer\Client\Webhook;
use BTCPayServer\Util\PreciseNumber;
use Give\Donations\Models\Donation;
use Give\Donations\Models\DonationNote;
use Give\Donations\ValueObjects\DonationStatus;
use Give\Framework\Http\Response\Types\RedirectResponse;
use Give\Framework\PaymentGateways\Commands\PaymentRefunded;
use Give\Framework\PaymentGateways\Commands\RedirectOffsite;
use Give\Framework\PaymentGateways\PaymentGateway;
/**
* BTCPay Gateway for GiveWP
*/
class BtcpayGateway extends PaymentGateway
{
/**
* Secure route methods for gateway callbacks
*/
public $secureRouteMethods = [
'handlePaymentRedirect'
];
/**
* Route methods for gateway callbacks
*/
public $routeMethods = [
'processWebhook'
];
/**
* @inheritDoc
*/
public static function id(): string
{
return 'btcpay-gateway';
}
/**
* @inheritDoc
*/
public function getId(): string
{
return self::id();
}
/**
* @inheritDoc
*/
public function getName(): string
{
return __('BTCPay Server Gateway', 'btcpay-for-givewp');
}
/**
* @inheritDoc
*/
public function getPaymentMethodLabel(): string
{
return __('Pay with Bitcoin / Lightning Network (BTCPay)', 'btcpay-for-givewp');
}
public static function webhookUrl(): string
{
$instance = new static();
return $instance->generateGatewayRouteUrl($instance->routeMethods[0]);
}
public static function redirectUrl(): string
{
$instance = new static();
return $instance->generateSecureGatewayRouteUrl($instance->secureRouteMethods[0]);
}
/**
* Register scripts for the gateway
*/
public function enqueueScript(int $formId)
{
wp_enqueue_script(
'btcpay-gateway',
BTCPAY_FOR_GIVEWP_URL . 'assets/js/btcpay-gateway.js',
['react', 'wp-element'],
BTCPAY_FOR_GIVEWP_VERSION,
true
);
}
/**
* Settings for the gateway form
*/
public function formSettings(int $formId): array
{
return [
'message' => __('You will be redirected to a payment page to complete the donation.', 'btcpay-for-givewp'),
];
}
/**
* Legacy form field markup
*/
public function getLegacyFormFieldMarkup(int $formId, array $args): string
{
return "<div class='btcpay-gateway-help-text'>
<p>" . __('You will be redirected to a payment page to complete the donation.', 'btcpay-for-givewp') . "</p>
</div>";
}
/**
* Create a payment
*/
public function createPayment(Donation $donation, $gatewayData)
{
// Get BTCPay Server credentials from settings
$btcpayUrl = give_get_option('btcpay_url');
$storeId = give_get_option('btcpay_store_id');
$apiKey = give_get_option('btcpay_api_key');
// Generate return URL for after payment
$returnUrl = $this->generateSecureGatewayRouteUrl(
'handlePaymentRedirect',
$donation->id,
[
'givewp-donation-id' => $donation->id,
'givewp-success-url' => urlencode(give_get_success_page_uri()),
]
);
// Create the invoice checkout options
$checkoutOptions = new InvoiceCheckoutOptions();
$checkoutOptions->setRedirectUrl($returnUrl);
try {
$client = new Invoice($btcpayUrl, $apiKey);
$invoice = $client->createInvoice(
$storeId,
$donation->amount->getCurrency()->getCode(),
PreciseNumber::ParseFloat($donation->amount->formatToDecimal()),
$donation->id,
null,
null,
$checkoutOptions
);
// Store the invoice ID in the donation as reference.
$donation->gatewayTransactionId = 'invoice id: ' . $invoice->getId();
$donation->save();
return new RedirectOffsite($invoice->getCheckoutLink());
} catch (\Exception $e) {
throw new \Exception('Failed to create invoice on BTCPay: ' . $e->getMessage());
}
}
/**
* Handle the return from BTCPay Server
*/
protected function handlePaymentRedirect(array $queryParams): RedirectResponse
{
$donationId = (int) $queryParams['givewp-donation-id'];
$successUrl = urldecode($queryParams['givewp-success-url']);
DonationNote::create([
'donationId' => $donationId,
'content' => 'Donor returned via redirect link from BTCPay invoice payment page.'
]);
return new RedirectResponse($successUrl);
}
public function processWebhook()
{
$rawData = file_get_contents('php://input');
$payload = json_decode($rawData, false);
// Validate webhook payload data
// Note: getallheaders() CamelCases all headers for PHP-FPM/Nginx but for others maybe not, so "BTCPay-Sig" may becomes "Btcpay-Sig".
$headers = getallheaders();
foreach ($headers as $key => $value) {
if (strtolower($key) === 'btcpay-sig') {
$signature = $value;
}
}
try {
$webhookClient = new Webhook(give_get_option('btcpay_url'), give_get_option('btcpay_api_key'));
// Validate the webhook request.
if (!$webhookClient->isIncomingWebhookRequestValid($rawData, $signature, give_get_option('btcpay_wh_secret'))) {
throw new \RuntimeException(
'Invalid BTCPay Server payment webhook message received - signature did not match.'
);
}
} catch (\Exception $e) {
error_log('BTCPay for GiveWP: ' . $e->getMessage());
wp_die('Webhook request validation failed.');
}
// Load the donation reference from the payload
$invoice = $this->loadInvoice($payload->invoiceId);
$donationId = (int) $invoice->getData()['metadata']['orderId'];
if ($donationId) {
$donation = Donation::find($donationId);
// Process webhook events
switch ($payload->type) {
case 'InvoiceReceivedPayment':
// As soon as we receive a payment, we update donation status.
$donation->status = DonationStatus::PROCESSING();
DonationNote::create([
'donationId' => $donationId,
'content' => 'BTCPay Webhook: Payment received but not confirmed. Invoice ID: ' . $payload->invoiceId
]);
break;
case 'InvoiceSettled':
// Handle invoice settled event
$donation->status = DonationStatus::COMPLETE();
DonationNote::create([
'donationId' => $donationId,
'content' => 'BTCPay Webhook: Payment complete (settled).'
]);
break;
case 'InvoiceExpired':
// Handle invoice expired event
$donation->status = DonationStatus::ABANDONED();
DonationNote::create([
'donationId' => $donationId,
'content' => 'BTCPay Webhook: Invoice expired without any payment.'
]);
break;
case 'InvoiceInvalid':
// Handle invoice invalid event
$donation->status = DonationStatus::FAILED();
DonationNote::create([
'donationId' => $donationId,
'content' => 'Payment was set invalid manually on BTCPay.'
]);
break;
default:
error_log('Unhandled BTCPay Server webhook event: ' . $payload->eventType);
return true;
}
// Save the donation
$donation->save();
} else {
// Do not throw error here as we don't want to break the webhook delivery on BTCPay Server side.
error_log('Invalid donation ID in webhook payload.');
}
}
/**
* Load invoice from BTCPay Server
*/
public function loadInvoice(string $invoiceId): \BTCPayServer\Result\Invoice {
try {
$client = new Invoice(give_get_option('btcpay_url'), give_get_option('btcpay_api_key'));
$invoice = $client->getInvoice(give_get_option('btcpay_store_id'), $invoiceId);
return $invoice;
} catch (\Exception $e) {
throw new \Exception('Failed to load invoice from BTCPay: ' . $e->getMessage());
}
}
/**
* @inheritDoc
*/
public function refundDonation(Donation $donation): PaymentRefunded
{
throw new \Exception('Refunds are not supported for BTCPay Server payments.');
}
}