Compare commits

...

14 Commits
3.x ... master

Author SHA1 Message Date
ndeet
9e87e5e124
Merge pull request #15 from ndeet/session-check
Some checks failed
Build release artifact. / Build release (release) Has been cancelled
Fix checkout in case session contains outdated order id; bump version.
2023-10-19 14:57:45 +02:00
ndeet
8d27778ced Fix checkout in case session contains outdated order id; bump version. 2023-10-19 14:41:25 +02:00
ndeet
7aa226468f
Merge pull request #13 from ndeet/fix-form
Some checks failed
Build release artifact. / Build release (release) Has been cancelled
Fix admin form
2023-09-25 11:29:31 +02:00
ndeet
f47aaccb43 Fix admin form; Bump version. 2023-09-25 11:28:18 +02:00
ndeet
9677c828a4
Merge pull request #11 from ndeet/fix-compatibility
Some checks failed
Build release artifact. / Build release (release) Has been cancelled
Fix compat to OC 4.0.2.0+ due to breaking change upstream.
2023-09-06 11:03:10 +02:00
ndeet
5d7a2d4d1b Fix compat to OC 4.0.2.0+ due to breaking change upstream. 2023-09-06 10:56:39 +02:00
ndeet
422296840d
Merge pull request #8 from ndeet/oc4-modal-checkout
Adding modal checkout, fixing bugs and general refactor.
2023-01-25 14:55:02 +01:00
Andreas Tasch
3d5c6ccfb2 Adding modal checkout, fixing bugs and general refactor. 2023-01-25 14:50:16 +01:00
Andreas Tasch
9b81bc6e2a Adding readme. 2022-10-14 11:55:34 +02:00
Andreas Tasch
0aeba289b9 Make sure the extension gets installed as in 'btcpay' directory. 2022-10-12 21:49:12 +02:00
Andreas Tasch
9633b63b1f Add install metadata. 2022-10-12 21:42:41 +02:00
Andreas Tasch
552a5de37a Fix artifact name. 2022-10-12 14:48:26 +02:00
Andreas Tasch
bdeb8cc931
Merge pull request #1 from ndeet/opencart4
Complete refactor for OpenCart 4 compatibility.
2022-10-12 14:42:47 +02:00
Andreas Tasch
b77bcd8740 Complete refactor for OpenCart 4 compatibility. 2022-10-12 14:40:44 +02:00
136 changed files with 1416 additions and 858 deletions

View File

@ -24,7 +24,7 @@ jobs:
with:
type: 'zip'
filename: 'btcpay.ocmod.zip'
exclusions: '*.git* *.github* composer.*'
exclusions: '*.git* *.github* composer.* README.md'
- name: Upload artifact to release page.
uses: ncipollo/release-action@v1
with:

19
README.md Normal file
View File

@ -0,0 +1,19 @@
# BTCPay for OpenCart extension
This free extension can be used with [OpenCart](https://www.opencart.com) version 3 or 4. You can also find this extension on the OpenCart [marketplace](https://www.opencart.com/index.php?route=marketplace/extension/info&extension_id=44269).
## Installation
You can find a detailed installation guide on how to setup the extension on our [OpenCart documentation](https://docs.btcpayserver.org/OpenCart/).
## Feedback
If you have issues or feature requests feel free to open an [issue](https://github.com/btcpayserver/opencart/issues) or join us on our [chat](https://chat.btcpayserver.org) or [telegram channel](https://t.me/btcpayserver).
Please let us always know your OpenCart, BTCPay extension, PHP versions. You can find them on the extension configuration page.
## Development
OpenCart 4 development happens on the `master` branch and OpenCart 3 is maintained on the `3.x` branch but is mostly in maintenance mode.

View File

@ -0,0 +1,280 @@
<?php
namespace Opencart\Admin\Controller\Extension\Btcpay\Payment;
use BTCPayServer\Client\Store;
use BTCPayServer\Client\Webhook;
require_once DIR_EXTENSION . 'btcpay/system/library/btcpay/autoload.php';
require_once DIR_EXTENSION . 'btcpay/system/library/btcpay/version.php';
class Btcpay extends \Opencart\System\Engine\Controller {
private $error = [];
public function index(): void {
$this->load->language('extension/btcpay/payment/btcpay');
$this->document->setTitle($this->language->get('heading_title'));
$this->load->model('setting/setting');
$this->load->model('localisation/order_status');
$this->load->model('localisation/geo_zone');
$data['save'] = $this->url->link('extension/btcpay/payment/btcpay|save', 'user_token=' . $this->session->data['user_token']);
$data['back'] = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment');
$data['cancel'] = $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment', true);
$data['order_statuses'] = $this->model_localisation_order_status->getOrderStatuses();
$data['geo_zones'] = $this->model_localisation_geo_zone->getGeoZones();
if (isset($this->error['warning'])) {
$data['error_warning'] = $this->error['warning'];
} else {
$data['error_warning'] = '';
}
if (isset($this->session->data['success'])) {
$data['success'] = $this->session->data['success'];
unset($this->session->data['success']);
} else {
$data['success'] = '';
}
$data['breadcrumbs'] = [];
$data['breadcrumbs'][] = array(
'text' => $this->language->get('text_home'),
'href' => $this->url->link('common/dashboard', 'user_token=' . $this->session->data['user_token'], true)
);
$data['breadcrumbs'][] = array(
'text' => $this->language->get('text_extension'),
'href' => $this->url->link('marketplace/extension', 'user_token=' . $this->session->data['user_token'] . '&type=payment', true)
);
$data['breadcrumbs'][] = array(
'text' => $this->language->get('heading_title'),
'href' => $this->url->link('extension/btcpay/payment/btcpay', 'user_token=' . $this->session->data['user_token'], true)
);
$fields = [
'payment_btcpay_status',
'payment_btcpay_url',
'payment_btcpay_api_auth_token',
'payment_btcpay_btcpay_storeid',
'payment_btcpay_webhook',
'payment_btcpay_webhook_delete',
'payment_btcpay_modal_mode',
'payment_btcpay_new_status_id',
'payment_btcpay_paid_status_id',
'payment_btcpay_settled_status_id',
'payment_btcpay_settled_paidover_status_id',
'payment_btcpay_invalid_status_id',
'payment_btcpay_expired_status_id',
'payment_btcpay_expired_partialpayment_status_id',
'payment_btcpay_expired_paidlate_status_id',
'payment_btcpay_refunded_status_id',
'payment_btcpay_total',
'payment_btcpay_geo_zone_id',
'payment_btcpay_debug_mode',
];
// Process our fields to be sure they are displayed.
foreach ($fields as $field) {
if (isset($this->request->post[$field])) {
$data[$field] = $this->request->post[$field];
} else {
$data[$field] = $this->config->get($field);
}
}
$data['payment_btcpay_sort_order'] = isset($this->request->post['payment_btcpay_sort_order']) ?
$this->request->post['payment_btcpay_sort_order'] : $this->config->get('payment_btcpay_sort_order');
$data['header'] = $this->load->controller('common/header');
$data['column_left'] = $this->load->controller('common/column_left');
$data['footer'] = $this->load->controller('common/footer');
$this->response->setOutput($this->load->view('extension/btcpay/payment/btcpay', $data));
}
protected function validate($messages): array {
$this->load->language('extension/btcpay/payment/btcpay');
if (!$this->user->hasPermission('modify', 'extension/btcpay/payment/btcpay')) {
$messages['error'] = $this->language->get('error_permission');
}
if (!class_exists('BTCPayServer\Client\Health')) {
$messages['error'] = $this->language->get('error_composer');
}
if (!isset($messages['error'])) {
$host = $this->request->post['payment_btcpay_url'];
$apiKey = $this->request->post['payment_btcpay_api_auth_token'];
$storeId = $this->request->post['payment_btcpay_btcpay_storeid'];
try {
$client = new Store($host, $apiKey);
$store = $client->getStore($storeId);
if (empty($store->getId())) {
$messages['error'] = $this->language->get('error_store_not_found');
}
} catch (\Throwable $e) {
$messages['error'] = $this->language->get('error_connect_to_btcpay');
$this->log->write($e->getMessage());
}
}
return $messages;
}
public function save(): void {
$this->load->language('extension/btcpay/payment/btcpay');
$this->load->model('setting/setting');
$json = [];
$redirect = false;
$json = $this->validate($json);
if (empty($json['error'])) {
$host = $this->request->post['payment_btcpay_url'];
$apiKey = $this->request->post['payment_btcpay_api_auth_token'];
$storeId = $this->request->post['payment_btcpay_btcpay_storeid'];
// On saving we create a webhook if there is none yet.
if ($this->webhookExists() === false) {
if ($whData = $this->webhookSetup($host, $apiKey, $storeId)) {
$this->request->post['payment_btcpay_webhook'] = $whData;
$json['success'] = $this->language->get('notice_success_create_webhook');
$redirect = true;
} else {
$json['error'] = $this->language->get('error_creating_webhook');
}
} else {
// Check if the user wants to delete an existing webhook.
if (isset($this->request->post['payment_btcpay_webhook_delete']) &&
$this->request->post['payment_btcpay_webhook_delete'] === '1'
) {
// Try to delete the webhook on the provided host.
$this->webhookDelete($host, $apiKey, $storeId);
unset($this->request->post['payment_btcpay_webhook']);
unset($this->request->post['payment_btcpay_webhook_delete']);
$json['success'] = $this->language->get('notice_success_delete_webhook');
$redirect = true;
} else {
// Need to convert existing webhook values back to array for storage.
if (isset($this->request->post['payment_btcpay_webhook'])) {
$whString = $this->request->post['payment_btcpay_webhook'];
$whString = str_replace(['ID: ', 'SECRET: ', 'URL: '], '', $whString);
$whArr = explode(' | ', $whString);
if (count($whArr) === 3) {
$whData = [
'id' => $whArr[0],
'secret' => $whArr[1],
'url' => $whArr[2]
];
$this->request->post['payment_btcpay_webhook'] = $whData;
}
}
}
}
}
if (empty($json['error'])) {
if (!empty($json['success'])) {
$json['success'] = $this->language->get('notice_success') . ' ' . $json['success'];
} else {
$json['success'] = $this->language->get('notice_success');
}
$this->model_setting_setting->editSetting('payment_btcpay', $this->request->post);
}
if ($redirect) {
$this->session->data['success'] = $json['success'];
unset($json['success']);
$json['redirect'] = $this->url->link('extension/btcpay/payment/btcpay', 'user_token=' . $this->session->data['user_token'], true);
}
$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($json));
}
public function install(): void {
$this->load->model('extension/btcpay/payment/btcpay');
$this->model_extension_btcpay_payment_btcpay->install();
}
public function uninstall(): void {
$this->load->model('extension/btcpay/payment/btcpay');
$this->model_extension_btcpay_payment_btcpay->uninstall();
}
private function webhookExists(): bool {
// Check if the config is any value set at all.
$data = $this->config->get('payment_btcpay_webhook');
if (empty($data) || !is_array($data)) {
return false;
}
// todo: load webhook form BTCPay to check if the callback url domain is the same
return true;
}
private function webhookSetup($host, $apiKey, $storeId): ?array {
$whEvents = [
'InvoiceReceivedPayment',
'InvoicePaymentSettled',
'InvoiceProcessing',
'InvoiceExpired',
'InvoiceSettled',
'InvoiceInvalid'
];
try {
$whClient = new Webhook( $host, $apiKey );
$webhook = $whClient->createWebhook(
$storeId,
$this->webhookCallbackUrl(),
$whEvents,
null
);
// Prepare data for settings storage.
$whData = [
'id' => $webhook->getData()['id'],
'secret' => $webhook->getData()['secret'],
'url' => $webhook->getData()['url']
];
return $whData;
} catch (\Throwable $e) {
$this->log->write($e->getMessage());
}
return NULL;
}
private function webhookDelete($host, $apiKey, $storeId): void {
$data = $this->config->get('payment_btcpay_webhook');
$client = new \BTCPayServer\Client\Webhook($host, $apiKey);
try {
$client->deleteWebhook($storeId, $data['id']);
} catch (\Throwable $e) {
$this->log->write('Error deleting webhook: ' . $e->getMessage());
}
}
private function webhookCallbackUrl(): string {
$url = $this->url->link('extension/btcpay/payment/btcpay|callback', '', true);
// As we are in admin controller context we need to strip out the admin
// path to receive the correct frontend callback url.
$adminPathParts = explode('/', DIR_APPLICATION);
end($adminPathParts); // Last array item is empty.
$adminPath = prev($adminPathParts);
if (!empty($adminPath)) {
$url = str_replace($adminPath . '/', '', $url);
}
return $url;
}
}

View File

@ -1,12 +1,11 @@
<?php
require_once DIR_SYSTEM . 'library/btcpay/version.php';
require_once DIR_EXTENSION . '/btcpay/system/library/btcpay/version.php';
$_['heading_title'] = 'BTCPay Server';
$_['text_edit'] = 'Edit BTCPay Settings';
$_['text_version_info'] = 'Debug info: OpenCart ' . VERSION . ' with BTCPay Extension ' . BTCPAY_OPENCART_EXTENSION_VERSION . ' on PHP ' . phpversion();
$_['text_support_info'] = 'For setup instructions follow our <a href="https://docs.btcpayserver.org/OpenCart" target="_blank" rel="noopener">setup guide</a>. If you run into any problems feel free to <a href="https://github.com/btcpayserver/opencart" target="_blank" rel="noopener">open an issue on Github</a> or come to our <a href="https://chat.btcpayserver.org/" target="_blank" rel="noopener">Mattermost chat</a>.';
$_['text_extension'] = 'Extensions';
$_['entry_status'] = 'Payment Method Enabled';
@ -16,6 +15,7 @@ $_['entry_btcpay_storeid'] = 'BTCPay Store ID';
$_['entry_webhook'] = 'Webhook Data';
$_['entry_webhook_secret'] = 'Webhook Secret';
$_['entry_webhook_delete'] = 'Delete Webhook';
$_['entry_modal_mode'] = 'Modal/iFrame mode';
$_['entry_total'] = 'Total';
$_['entry_geo_zone'] = 'Geo Zone';
$_['entry_sort_order'] = 'Sort Order';
@ -34,15 +34,19 @@ $_['entry_debug_mode'] = 'Debug mode';
$_['help_btcpay_url'] = 'The public URL of your BTCPay Server instance. e.g. https://demo.mainnet.btcpayserver.org. You need to have a BTCPay Server instance running, see "Requirements" for several options of deployment on our <a href="https://docs.btcpayserver.org/OpenCart" target="_blank" rel="noopener">setup guide</a>.';
$_['help_webhook'] = 'The webhook will get created automatically after you entered BTCPay Server URL, API Key and Store ID. If you see this field filled with data (after you saved the form) all went well.';
$_['help_webhook_delete'] = 'This is useful if you switch hosts or have problems with webhooks. When checked this will delete the webhook on OpenCart (and BTCPay Server if possible). Make sure to delete the webhook on BTCPay Server Store settings too if not done automatically. <strong>ATTENTION:</strong> You need to edit and <strong>save</strong> this settings page again so a new webhook gets created on BTCPay Server.';
$_['help_modal_mode'] = 'If enabled the invoice will be shown in a modal/overlay (iFrame). Default behaviour is that the user will get redirected to BTCPay Server invoice page.';
$_['help_total'] = 'The checkout total the order must reach before this payment method becomes active.';
$_['help_debug_mode'] = 'If enabled debug output will be saved to the error logs found in System -> Maintenance -> Error logs. Should be disabled after debugging.';
$_['notice_success'] = 'BTCPay Server Payment details have been successfully updated.';
$_['notice_success_webhook_renew'] = 'BTCPay Server Payment details have been successfully updated and successfully deleted webhook data.';
$_['notice_success'] = 'BTCPay Server Payment details have been updated.';
$_['notice_success_delete_webhook'] = 'Successfully deleted webhook. Please save again to create a new one, make sure that on BTCPay Server it does not exist twice.';
$_['notice_success_create_webhook'] = 'Successfully created a webhook on the BTCPay instance.';
$_['error_permission'] = 'Warning: You do not have permission to modify BTCPay Server!';
$_['error_composer'] = 'Unable to load btcpayserver-greenfield-php. Please download a compiled vendor folder or run composer.';
$_['error_store_not_found'] = 'Successfully connected to BTCPay Server but no store with that ID found. Make sure you entered the correct store ID on that corresponding BTCPay Server URL.';
$_['error_connect_to_btcpay'] = 'Error connecting to BTCPay Server instance. Make sure you provided the correct URL, API key.';
$_['error_creating_webhook'] = 'Error creating webhook. Make sure you have a correct store and api key combination with the required permissions. Check OpenCart error logs.';
$_['text_btcpay'] = '<a href="https://btcpayserver.org/" target="_blank" rel="noopener"><img src="view/image/payment/btcpay.png" alt="BTCPay Server" title="BTCPay Server" style="border: 1px solid #EEEEEE;" /></a>';
$_['text_btcpay'] = '<a href="https://btcpayserver.org/" target="_blank" rel="noopener"><img src="/extension/btcpay/admin/view/image/payment/btcpay.png" alt="BTCPay Server" title="BTCPay Server" style="border: 1px solid #EEEEEE;" /></a>';

View File

@ -1,7 +1,7 @@
<?php
class ModelExtensionPaymentBTCPay extends Model {
public function install() {
namespace Opencart\Admin\Model\Extension\Btcpay\Payment;
class Btcpay extends \Opencart\System\Engine\Model {
public function install(): void {
$this->db->query("
CREATE TABLE IF NOT EXISTS `" . DB_PREFIX . "btcpay_order` (
`btcpay_order_id` INT(11) NOT NULL AUTO_INCREMENT,
@ -30,7 +30,7 @@ class ModelExtensionPaymentBTCPay extends Model {
$this->model_setting_setting->editSetting('payment_btcpay', $defaults);
}
public function uninstall() {
public function uninstall(): void {
$this->db->query("DROP TABLE IF EXISTS `" . DB_PREFIX . "btcpay_order`;");
}
}

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,301 @@
{{ header }}{{ column_left }}
<div id="content">
<div class="page-header">
<div class="container-fluid">
<div class="float-end">
<button type="submit" form="form_payment" data-bs-toggle="tooltip" title="{{ button_save }}" class="btn btn-primary"><i class="fas fa-save"></i></button>
<a href="{{ back }}" data-bs-toggle="tooltip" title="{{ button_back }}" class="btn btn-light"><i class="fas fa-reply"></i></a>
</div>
<h1>{{ heading_title }}</h1>
<ul class="breadcrumb">
{% for breadcrumb in breadcrumbs %}
<li class="breadcrumb-item"><a href="{{ breadcrumb['href'] }}">{{ breadcrumb['text'] }}</a></li>
{% endfor %}
</ul>
</div>
</div>
<div class="container-fluid">
{% if error %}
<div class="alert alert-danger alert-dismissible"><i class="fas fa-exclamation-circle"></i> {{ error_warning }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endif %}
{% if success %}
<div class="alert alert-success">
{{ success }}
</div>
{% endif %}
<div class="alert alert-info" style="overflow: hidden;">
<div class="row">
<div class="col-sm-12">{{ text_version_info }}</div>
</div>
<div class="row">
<div class="col-sm-12">{{ text_support_info }}</div>
</div>
</div>
<div class="card">
<div class="card-header"><i class="fas fa-pencil-alt"></i> {{ text_edit }}</div>
<div class="card-body">
<form action="{{ save }}" method="post" id="form_payment" data-oc-toggle="ajax">
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-status">{{ entry_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_status" id="input-status" class="form-select">
{% if payment_btcpay_status %}
<option value="1" selected="selected">{{ text_enabled }}</option>
<option value="0">{{ text_disabled }}</option>
{% else %}
<option value="1">{{ text_enabled }}</option>
<option value="0" selected="selected">{{ text_disabled }}</option>
{% endif %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-btcpay-url">{{ entry_btcpay_url }}</label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_url" value="{{ payment_btcpay_url }}" placeholder="{{ entry_btcpay_url }}" id="input-btcpay-url" class="form-control" />
<div class="help-block mt-1">
{{ help_btcpay_url }}
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-api-auth-token">{{ entry_api_auth_token }}</label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_api_auth_token" value="{{ payment_btcpay_api_auth_token }}" placeholder="{{ entry_api_auth_token }}" id="input-api-auth-token" class="form-control" />
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-btcpay-storeid">{{ entry_btcpay_storeid }}</label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_btcpay_storeid" value="{{ payment_btcpay_btcpay_storeid }}" placeholder="{{ entry_btcpay_btcpay_storeid }}" id="input-btcpay-storeid" class="form-control" />
</div>
</div>
<div class="row mb-3">
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-webhook">{{ entry_webhook }}</label>
<div class="col-sm-10">
{% if payment_btcpay_webhook.id %}
{% set whData = 'ID: ' ~ payment_btcpay_webhook.id ~ ' | SECRET: ' ~ payment_btcpay_webhook.secret ~ ' | URL: ' ~ payment_btcpay_webhook.url %}
{% else %}
{% set whData = '-- webhook not configured yet --' %}
{% endif %}
<input readonly="readonly" type="text" name="payment_btcpay_webhook" value="{{ whData }}" placeholder="{{ entry_webhook }}" id="input-webhook" class="form-control" />
<div class="help-block">
{{ help_webhook }}
</div>
</div>
</div>
<div class="row">
<label class="col-sm-2 col-form-label" for="input-webhook-delete">{{ entry_webhook_delete }}</label>
<div class="col-sm-10">
<input type="checkbox" name="payment_btcpay_webhook_delete" value="1" id="input-webhook-delete" class="form-check mt-2" {% if payment_btcpay_webhook_delete %} checked="checked" {% endif %} {% if not payment_btcpay_webhook.id %}disabled="disabled"{% endif %}/>
<div class="help-block mt-1">
{{ help_webhook_delete }}
</div>
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-modal-mode">{{ entry_modal_mode }}</label>
<div class="col-sm-10">
<input type="checkbox" name="payment_btcpay_modal_mode" value="1" id="input-modal-mode" class="form-check mt-2" {% if payment_btcpay_modal_mode %} checked="checked" {% endif %} />
<div class="help-block mt-1">
{{ help_modal_mode }}
</div>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-new-status">{{ entry_new_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_new_status_id" id="input-new-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_new_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-paid-status">{{ entry_paid_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_paid_status_id" id="input-paid-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_paid_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-settled-status">{{ entry_settled_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_settled_status_id" id="input-settled-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_settled_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-settled-paidover-status">{{ entry_settled_paidover_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_settled_paidover_status_id" id="input-settled-paidover-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_settled_paidover_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-invalid-status">{{ entry_invalid_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_invalid_status_id" id="input-invalid-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_invalid_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-expired-status">{{ entry_expired_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_expired_status_id" id="input-expired-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_expired_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-expired-partialpayment-status">{{ entry_expired_partialpayment_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_expired_partialpayment_status_id" id="input-expired-partialpayment-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_expired_partialpayment_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-expired-paidlate-status">{{ entry_expired_paidlate_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_expired_paidlate_status_id" id="input-expired-paidlate-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_expired_partialpayment_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-refunded-status">{{ entry_refunded_status }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_refunded_status_id" id="input-refunded-status" class="form-select">
{% for order_status in order_statuses %}
{% if order_status.order_status_id == payment_btcpay_refunded_status_id %}
<option value="{{ order_status.order_status_id }}" selected="selected">{{ order_status.name }}</option>
{% else %}
<option value="{{ order_status.order_status_id }}">{{ order_status.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-total"><span data-toggle="tooltip" title="{{ help_total }}">{{ entry_total }}</span></label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_total" value="{{ payment_btcpay_total }}" placeholder="{{ entry_total }}" id="input-total" class="form-control" />
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-geo-zone">{{ entry_geo_zone }}</label>
<div class="col-sm-10">
<select name="payment_btcpay_geo_zone_id" id="input-geo-zone" class="form-select">
<option value="0">{{ text_all_zones }}</option>
{% for geo_zone in geo_zones %}
{% if geo_zone.geo_zone_id == payment_btcpay_geo_zone_id %}
<option value="{{ geo_zone.geo_zone_id }}" selected="selected">{{ geo_zone.name }}</option>
{% else %}
<option value="{{ geo_zone.geo_zone_id }}">{{ geo_zone.name }}</option>
{% endif %}
{% endfor %}
</select>
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-sort-order">{{ entry_sort_order }}</label>
<div class="col-sm-10">
<input type="text" name="payment_btcpay_sort_order" value="{{ payment_btcpay_sort_order }}" placeholder="{{ entry_sort_order }}" id="input-sort-order" class="form-control" />
</div>
</div>
<div class="row mb-3">
<label class="col-sm-2 col-form-label" for="input-debug-mode">{{ entry_debug_mode }}</label>
<div class="col-sm-10">
<input type="checkbox" name="payment_btcpay_debug_mode" value="1" id="input-debug-mode" class="form-check" {% if payment_btcpay_debug_mode %} checked="checked" {% endif %} />
<div class="help-block mt-1">
{{ help_debug_mode }}
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{{ footer }}

View File

@ -1,118 +1,131 @@
<?php
namespace Opencart\Catalog\Controller\Extension\Btcpay\Payment;
use BTCPayServer\Client\Invoice;
use BTCPayServer\Client\InvoiceCheckoutOptions;
use BTCPayServer\Client\Webhook;
use BTCPayServer\Util\PreciseNumber;
require DIR_SYSTEM . 'library/btcpay/autoload.php';
require DIR_SYSTEM . 'library/btcpay/version.php';
require_once DIR_EXTENSION . 'btcpay/system/library/btcpay/autoload.php';
require_once DIR_EXTENSION . 'btcpay/system/library/btcpay/version.php';
class ControllerExtensionPaymentBTCPay extends Controller
class Btcpay extends \Opencart\System\Engine\Controller
{
public function index()
public function index(): string
{
$this->load->language('extension/payment/btcpay');
$this->load->language('extension/btcpay/payment/btcpay');
$this->load->model('checkout/order');
$useModal = $this->config->get('payment_btcpay_modal_mode');
$data['button_confirm'] = $this->language->get('button_confirm');
$data['action'] = $this->url->link(
'extension/payment/btcpay/checkout',
'extension/btcpay/payment/btcpay|checkout',
'',
true
);
return $this->load->view('extension/payment/btcpay', $data);
if (isset($this->session->data['error_warning'])) {
$data['error_warning'] = $this->session->data['error_warning'];
unset($this->session->data['error_warning']);
} else {
$data['error_warning'] = '';
}
if ($useModal) {
$host = $this->config->get('payment_btcpay_url');
$data['btcpay_host'] = $host;
$data['modal_url'] = $host . '/modal/btcpay.js';
$data['success_link'] = $this->url->link('checkout/success', '', true);
$data['invoice_expired_text'] = $this->language->get('invoice_expired_text');
return $this->load->view('extension/btcpay/payment/btcpay_modal', $data);
} else {
// Redirect.
return $this->load->view('extension/btcpay/payment/btcpay', $data);
}
}
public function checkout()
public function checkout(): void
{
$this->load->model('checkout/order');
$this->load->model('extension/payment/btcpay');
$this->load->model('extension/btcpay/payment/btcpay');
$this->load->language('extension/btcpay/payment/btcpay');
$debug = $this->config->get('payment_btcpay_debug_mode');
$useModal = $this->config->get('payment_btcpay_modal_mode');
if ($debug) {
$this->log->write('Entering checkout() of BTCPay catalog controller.');
$this->log->write('Session data:');
$this->log->write(print_r($this->session->data, true));
}
$metadata = [];
$token = md5(uniqid(rand(), true));
if (!isset($this->session->data['order_id'])) {
$this->log->write('No session data order_id present, aborting.');
return;
}
$order_info = $this->model_checkout_order->getOrder(
$this->session->data['order_id']
);
// Set included tax amount.
//// $metadata['taxIncluded'] = $order->get_cart_tax();
// POS metadata.
////todo: $metadata['posData'] = $this->preparePosMetadata( $order );
// Checkout options.
$checkoutOptions = new InvoiceCheckoutOptions();
$redirectUrl = $this->url->link(
'extension/payment/btcpay/success',
['token' => $token],
true
);
$checkoutOptions->setRedirectURL(htmlspecialchars_decode($redirectUrl));
if ($debug) {
$this->log->write( 'Setting redirect url to: ' . $redirectUrl );
if (empty($order_info)) {
if ($debug) {
$this->log->write('Could not load order passed by session, order id: ' . $this->session->data['order_id']);
}
$this->session->data['error_warning'] = $this->language->get('session_checkout_order_error');
$this->response->redirect(
$this->url->link('checkout/checkout', '', true)
);
}
// Calculate total and format it properly.
$total = number_format(
$order_info['total'] * $this->currency->getvalue(
$order_info['currency_code']
),
8,
'.',
''
);
$amount = PreciseNumber::parseString(
$total
); // unlike method signature suggests, it returns string.
$invoiceId = '';
$checkoutLink = '';
// API credentials.
$apiKey = $this->config->get('payment_btcpay_api_auth_token');
$host = $this->config->get('payment_btcpay_url');
$storeId = $this->config->get('payment_btcpay_btcpay_storeid');
// Create the invoice on BTCPay Server.
$client = new Invoice($host, $apiKey);
try {
$invoice = $client->createInvoice(
$storeId,
$order_info['currency_code'],
$amount,
$order_info['order_id'],
null, // this is null here as we handle it in the metadata.
$metadata,
$checkoutOptions
);
} catch (\Throwable $e) {
$this->log->write($e->getMessage());
}
if ($invoice->getData()['id']) {
$this->model_extension_payment_btcpay->addOrder([
'order_id' => $order_info['order_id'],
'token' => $token,
'invoice_id' => $invoice->getData(
)['id'],
]);
$this->model_checkout_order->addOrderHistory(
$order_info['order_id'],
$this->config->get('payment_btcpay_order_status_id')
);
$this->response->redirect($invoice->getData()['checkoutLink']);
// First, check if we have an existing and not expired wallet and do not create a new one.
if ($existingInvoice = $this->orderHasExistingInvoice($order_info)) {
$invoiceId = $existingInvoice->getId();
$checkoutLink = $existingInvoice->getCheckoutLink();
if ($debug) {
$this->log->write('Found existing and not yet expired invoice: ' . $invoiceId);
}
} else {
// Create the invoice on BTCPay Server.
$token = md5(uniqid(rand(), true));
if ($newInvoice = $this->createInvoice($order_info, $token)) {
$invoiceId = $newInvoice->getId();
$checkoutLink = $newInvoice->getCheckoutLink();
// Add invoiceId to the btcpay order table.
$this->model_extension_btcpay_payment_btcpay->addOrder([
'order_id' => $order_info['order_id'],
'token' => $token,
'invoice_id' => $invoiceId,
]);
$this->model_checkout_order->addHistory(
$order_info['order_id'],
$this->config->get('payment_btcpay_new_status_id'),
'BTCPay invoice id: ' . $newInvoice->getId()
);
/* TODO: wip have BTCPay Server invoice link in customer comments, needs option.
// Add user facing comment with a link to BTCPay Server invoice:
$this->model_checkout_order->addHistory(
$order_info['order_id'],
$this->config->get('payment_btcpay_new_status_id'),
$this->language->get(
'order_payment_link'
) . '<a href="' . $newInvoice->getCheckoutLink() . '" target="_blank">' . $newInvoice->getCheckoutLink() . '</a>',
true
);
*/
}
}
if (empty($invoiceId)) {
$this->log->write(
"Order #" . $order_info['order_id'] . " is not valid or something went wrong. Please check BTCPay Server API request logs."
);
@ -120,27 +133,40 @@ class ControllerExtensionPaymentBTCPay extends Controller
$this->url->link('checkout/checkout', '', true)
);
}
// Handle invoice in modal or redirect to BTCPay Server.
if ($useModal) {
// Return JSON data for Javascript to process.
$data['invoiceId'] = $invoiceId;
$this->response->addHeader('Content-Type: application/json');
$this->response->setOutput(json_encode($data));
} else {
// Redirect to BTCPay Server.
$this->response->redirect($checkoutLink);
}
}
public function cancel()
public function cancel(): void
{
$this->response->redirect($this->url->link('checkout/cart', ''));
}
public function success()
public function success(): void
{
$this->load->model('checkout/order');
$this->load->model('extension/payment/btcpay');
$this->load->model('extension/btcpay/payment/btcpay');
$debug = $this->config->get('payment_btcpay_debug_mode');
if ($debug) {
$this->log->write('Entering success callback / redirect page.');
$this->log->write('SESSION data: ');
$this->log->write(print_r($this->session->data, true));
}
if ($order_id = $this->session->data['order_id']) {
$order = $this->model_extension_payment_btcpay->getOrder(
$order_id
if (isset($this->session->data['order_id'])) {
$order = $this->model_extension_btcpay_payment_btcpay->getOrder(
$this->session->data['order_id']
);
// Check if token is present and valid.
@ -149,7 +175,7 @@ class ControllerExtensionPaymentBTCPay extends Controller
$this->request->get['token']
) !== 0) {
if ($debug) {
$this->log->write('Redirect to success page had no valid token.');
$this->log->write('Redirect to home page, had no valid token.');
}
$this->response->redirect(
$this->url->link('common/home', '', true)
@ -162,7 +188,7 @@ class ControllerExtensionPaymentBTCPay extends Controller
} else {
if ($debug) {
$this->log->write('Redirect to success page valid order id or session expired.');
$this->log->write('Redirect to home page, no valid order id or session expired.');
}
$this->response->redirect(
$this->url->link('common/home', '', true)
@ -173,7 +199,9 @@ class ControllerExtensionPaymentBTCPay extends Controller
public function callback()
{
$this->load->model('checkout/order');
$this->load->model('extension/payment/btcpay');
$this->load->model('extension/btcpay/payment/btcpay');
$json = [];
$debug = $this->config->get('payment_btcpay_debug_mode');
@ -202,10 +230,16 @@ class ControllerExtensionPaymentBTCPay extends Controller
die($whMessage);
}
$btcpay_order = $this->model_extension_payment_btcpay->getOrderByInvoiceId(
$btcpay_order = $this->model_extension_btcpay_payment_btcpay->getOrderByInvoiceId(
$data->invoiceId
);
if (empty($btcpay_order)) {
$this->log->write('Order not found. Aborting.');
http_response_code(200);
die();
}
$order_info = $this->model_checkout_order->getOrder(
$btcpay_order['order_id']
);
@ -214,7 +248,7 @@ class ControllerExtensionPaymentBTCPay extends Controller
$this->log->write('Order:');
$this->log->write($btcpay_order);
$this->log->write('Webhook payload: ');
$this->log->write($data);
$this->log->write(print_r($data, true));
}
if (!empty($order_info) && !empty($btcpay_order)) {
@ -236,7 +270,7 @@ class ControllerExtensionPaymentBTCPay extends Controller
if ($debug) {
$this->log->write('Invoice data: ');
$this->log->write($invoice);
$this->log->write(print_r($invoice, true));
}
$invStatus = $invoice->getStatus();
@ -244,7 +278,7 @@ class ControllerExtensionPaymentBTCPay extends Controller
if ($invoice) {
$order_status = NULL;
$order_message = 'Event: ' . $data->type;
$order_message = 'Event: ' . $data->type . ': ';
$notify = false;
switch ($data->type) {
case "InvoiceReceivedPayment":
@ -300,7 +334,7 @@ class ControllerExtensionPaymentBTCPay extends Controller
}
if (!is_null($order_status)) {
$this->model_checkout_order->addOrderHistory(
$this->model_checkout_order->addHistory(
$btcpay_order['order_id'],
$this->config->get($order_status),
'Payment status update: ' . $order_message,
@ -318,16 +352,117 @@ class ControllerExtensionPaymentBTCPay extends Controller
}
$this->response->addHeader('HTTP/1.1 200 OK');
$this->response->setOutput(json_encode($json));
}
/**
* Check webhook signature to be a valid request.
*/
public function validWebhookRequest(string $signature, string $requestData): bool {
protected function validWebhookRequest(string $signature, string $requestData): bool {
if ($whData = $this->config->get('payment_btcpay_webhook')) {
return Webhook::isIncomingWebhookRequestValid($requestData, $signature, $whData['secret']);
}
return false;
}
protected function createInvoice(array $order_info, string $token): ?\BTCPayServer\Result\Invoice {
// API credentials.
$apiKey = $this->config->get('payment_btcpay_api_auth_token');
$apiHost = $this->config->get('payment_btcpay_url');
$apiStoreId = $this->config->get('payment_btcpay_btcpay_storeid');
$debug = $this->config->get('payment_btcpay_debug_mode');
$client = new Invoice($apiHost, $apiKey);
// Checkout options.
$checkoutOptions = new InvoiceCheckoutOptions();
$redirectUrl = $this->url->link(
'extension/btcpay/payment/btcpay|success',
['token' => $token],
true
);
$checkoutOptions->setRedirectURL(htmlspecialchars_decode($redirectUrl));
if ($debug) {
$this->log->write( 'Setting redirect url to: ' . $redirectUrl );
}
// Metadata.
$metadata = [];
$amount = $this->prepareOrderTotal($order_info['total'], $order_info['currency_code']);
// Create the invoice on BTCPay Server.
try {
$invoice = $client->createInvoice(
$apiStoreId,
$order_info['currency_code'],
$amount,
$order_info['order_id'],
null, // this is null here as we handle it in the metadata.
$metadata,
$checkoutOptions
);
return $invoice;
} catch (\Throwable $e) {
$this->log->write($e->getMessage());
}
return null;
}
/**
* Check if the order already has an invoice id and it is still not expired.
*/
protected function orderHasExistingInvoice(array $order_info): ? \BTCPayServer\Result\Invoice {
// API credentials.
$apiKey = $this->config->get('payment_btcpay_api_auth_token');
$apiHost = $this->config->get('payment_btcpay_url');
$apiStoreId = $this->config->get('payment_btcpay_btcpay_storeid');
$debug = $this->config->get('payment_btcpay_debug_mode');
$client = new Invoice($apiHost, $apiKey);
// Calculate order total.
$total = $this->prepareOrderTotal($order_info['total'], $order_info['currency_code']);
// Round to 2 decimals to avoid mismatch.
$totalRounded = round((float) $total->__toString(), 2);
$btcpay_order = $this->model_extension_btcpay_payment_btcpay->getOrder(
$order_info['order_id']
);
if ($debug) {
$this->log->write(__FUNCTION__);
$this->log->write(print_r($btcpay_order, true));
}
if (!empty($btcpay_order['invoice_id'])) {
$existingInvoice = $client->getInvoice($apiStoreId, $btcpay_order['invoice_id']);
$invoiceAmount = $existingInvoice->getAmount();
if ($existingInvoice->isExpired() === false &&
$totalRounded === (float) $invoiceAmount->__toString()
) {
return $existingInvoice;
}
}
return null;
}
protected function prepareOrderTotal($total, $currencyCode): \BTCPayserver\Util\PreciseNumber {
// Calculate total and format it properly.
$total = number_format(
$total * $this->currency->getvalue(
$currencyCode
),
8,
'.',
''
);
return PreciseNumber::parseString($total);
}
}

View File

@ -0,0 +1,9 @@
<?php
$_['text_title'] = 'Bitcoin via BTCPay Server';
$_['button_confirm'] = 'Pay with Bitcoin';
$_['invoice_expired_text'] = 'The invoice expired. Please try again or choose a different payment method.';
$_['invoice_closed_text'] = 'Payment aborted. Please try again or choose a different payment method.';
$_['invoice_failed_text'] = 'Payment aborted. Error processing the request. Please contact store owner if the problem persists.';
$_['order_payment_link'] = 'You can check your payment status here: ';
$_['session_checkout_order_error'] = 'Your cart order id could not be found, please try to log out and in again.';

View File

@ -0,0 +1,80 @@
<?php
namespace Opencart\Catalog\Model\Extension\Btcpay\Payment;
class Btcpay extends \Opencart\System\Engine\Model
{
public function addOrder(array $data): bool
{
return $this->db->query(
"INSERT INTO `" . DB_PREFIX . "btcpay_order` SET `order_id` = '" . (int)$data['order_id'] . "', `token` = '" . $this->db->escape(
$data['token']
) . "', `invoice_id` = '" . $this->db->escape(
$data['invoice_id']
) . "'"
);
}
public function getOrder(int $order_id): array
{
$query = $this->db->query(
"SELECT * FROM `" . DB_PREFIX . "btcpay_order` WHERE `order_id` = '" . $order_id . "' ORDER BY btcpay_order_id DESC LIMIT 1"
);
return $query->row;
}
public function getOrderByInvoiceId(string $invoice_id): array
{
$query = $this->db->query(
"SELECT * FROM `" . DB_PREFIX . "btcpay_order` WHERE `invoice_id` = '" . $invoice_id . "' LIMIT 1"
);
return $query->row;
}
public function getMethods(array $address = []): array
{
$this->load->language('extension/btcpay/payment/btcpay');
$qStr = "SELECT * FROM `" . DB_PREFIX . "zone_to_geo_zone` WHERE `geo_zone_id` = '" . (int)$this->config->get(
'payment_btcpay_geo_zone_id'
) . "'";
if (isset($address['country_id'])) {
$qStr .= " AND `country_id` = '" . (int)$address['country_id'] ."'";
}
if (isset($address['zone_id'])) {
$qStr .= " AND (`zone_id` = '" . (int)$address['zone_id'] . "' OR `zone_id` = '0')";
}
$query = $this->db->query($qStr);
if (!$this->config->get('payment_btcpay_geo_zone_id')) {
$status = true;
} elseif ($query->num_rows) {
$status = true;
} else {
$status = false;
}
$method_data = [];
if ($status) {
$option_data['btcpay'] = [
'code' => 'btcpay.btcpay',
'name' => $this->language->get('text_title')
];
$method_data = [
'code' => 'btcpay',
'name' => $this->language->get('text_title'),
'option' => $option_data,
'sort_order' => $this->config->get('payment_btcpay_sort_order')
];
}
return $method_data;
}
}

View File

@ -1,6 +1,9 @@
{% if error_warning %}
<div class="alert alert-danger">{{ error_warning }}</div>
{% endif %}
<form action="{{ action }}" method="post">
<div class="buttons">
<div class="pull-right">
<div class="text-end">
<input type="submit" value="{{ button_confirm }}" class="btn btn-primary" />
</div>
</div>

View File

@ -0,0 +1,57 @@
<script src="{{ modal_url }}"></script>
<div class="buttons">
<div class="text-end">
<button class="btn btn-primary btcpay-modal">{{ button_confirm }}</button>
</div>
</div>
<script>
$('button.btcpay-modal').click(function() {
$.post("{{ action }}", function (data) {
console.log(JSON.stringify(data));
if (data.invoiceId !== undefined) {
window.btcpay.setApiUrlPrefix('{{ btcpay_host }}');
window.btcpay.showInvoice(data.invoiceId);
} else {
showError('{{ invoice_failed_text }}');
}
let invoice_paid = false;
window.btcpay.onModalReceiveMessage(function (event) {
if (isObject(event.data)) {
console.log('invoiceId: ' + event.data.invoiceId);
console.log('status: ' + event.data.status);
if (event.data.status) {
switch (event.data.status) {
case 'complete':
case 'paid':
invoice_paid = true;
window.location='{{ success_link }}';
break;
case 'expired':
window.btcpay.hideFrame();
showError('{{ invoice_expired_text }}');
break;
}
}
} else { // handle event.data "loaded" "closed"
if (event.data === 'close') {
if (invoice_paid === true) {
window.location='{{ success_link }}';
}
showError('{{ invoice_closed_text }}');
}
}
});
const isObject = obj => {
return Object.prototype.toString.call(obj) === '[object Object]'
}
}).fail(function() {
showError('{{ invoice_failed_text }}');
});
const showError = err => {
const errFail = '<div class="alert alert-danger alert-dismissible"><i class="fa-solid fa-circle-exclamation"></i> ' + err + '<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>';
$('#alert').prepend(errFail);
}
});
</script>

View File

@ -1,6 +1,6 @@
{
"name": "btcpayserver/opencart",
"description": "BTCPay Server plugin for OpenCart 3",
"description": "BTCPay Server plugin for OpenCart 4",
"type": "opencart-extension",
"require": {
"btcpayserver/btcpayserver-greenfield-php": "^1.3"
@ -14,6 +14,6 @@
],
"minimum-stability": "stable",
"config": {
"vendor-dir": "upload/system/library/btcpay"
"vendor-dir": "system/library/btcpay"
}
}

14
composer.lock generated
View File

@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0411678fc60196f00a3748362d083e80",
"content-hash": "bdf8d088a05991246e9aa2f8ec35d506",
"packages": [
{
"name": "btcpayserver/btcpayserver-greenfield-php",
"version": "v1.3.3",
"version": "v1.3.6",
"source": {
"type": "git",
"url": "https://github.com/btcpayserver/btcpayserver-greenfield-php.git",
"reference": "aff6ab92151431c2faa63c72805aa60736b0deea"
"reference": "cc9ab93a8ecda8a8158b717f11ff37dfa5ab8962"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/btcpayserver/btcpayserver-greenfield-php/zipball/aff6ab92151431c2faa63c72805aa60736b0deea",
"reference": "aff6ab92151431c2faa63c72805aa60736b0deea",
"url": "https://api.github.com/repos/btcpayserver/btcpayserver-greenfield-php/zipball/cc9ab93a8ecda8a8158b717f11ff37dfa5ab8962",
"reference": "cc9ab93a8ecda8a8158b717f11ff37dfa5ab8962",
"shasum": ""
},
"require": {
@ -55,9 +55,9 @@
"description": "BTCPay Server Greenfield API PHP client library.",
"support": {
"issues": "https://github.com/btcpayserver/btcpayserver-greenfield-php/issues",
"source": "https://github.com/btcpayserver/btcpayserver-greenfield-php/tree/v1.3.3"
"source": "https://github.com/btcpayserver/btcpayserver-greenfield-php/tree/v1.3.6"
},
"time": "2022-05-21T15:11:12+00:00"
"time": "2022-09-12T21:30:25+00:00"
}
],
"packages-dev": [],

7
install.json Normal file
View File

@ -0,0 +1,7 @@
{
"name": "BTCPay Server payment gateway",
"version": "4.2.2",
"license": "MIT",
"author": "BTCPay Server",
"link": "https://btcpayserver.org"
}

View File

@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit0630a220d429326d17ccb4047a5ba4f3::getLoader();

View File

@ -40,6 +40,7 @@ class PullPayments
$paymentCurrency = 'BTC';
$paymentPeriod = null;
$boltExpiration = 1;
$autoApproveClaims = false;
$startsAt = null;
$expiresAt = null;
$paymentMethods = ['BTC'];
@ -54,6 +55,7 @@ class PullPayments
$paymentCurrency,
$paymentPeriod,
$boltExpiration,
$autoApproveClaims,
$startsAt,
$expiresAt,
$paymentMethods
@ -109,13 +111,29 @@ class PullPayments
}
}
public function approvePayout()
{
$payoutId ='';
try {
$client = new PullPayment($this->host, $this->apiKey);
var_dump($client->approvePayout(
$this->storeId,
$payoutId,
0,
null
));
} catch (\Throwable $e) {
echo "Error: " . $e->getMessage();
}
}
public function getPullPayment()
{
$pullPaymentId = '';
try {
$client = new PullPayment($this->host, $this->apiKey);
var_dump($client->markPayoutAsPaid(
var_dump($client->getPullPayment(
$this->storeId,
$pullPaymentId
));
@ -131,7 +149,7 @@ class PullPayments
try {
$client = new PullPayment($this->host, $this->apiKey);
var_dump($client->markPayoutAsPaid(
var_dump($client->getPayouts(
$pullPaymentId,
$includeCancelled
));
@ -149,7 +167,7 @@ class PullPayments
try {
$client = new PullPayment($this->host, $this->apiKey);
var_dump($client->markPayoutAsPaid(
var_dump($client->createPayout(
$pullPaymentId,
$destination,
$amount,
@ -167,7 +185,7 @@ class PullPayments
try {
$client = new PullPayment($this->host, $this->apiKey);
var_dump($client->markPayoutAsPaid(
var_dump($client->getPayout(
$pullPaymentId,
$payoutId
));
@ -183,6 +201,7 @@ $pp = new PullPayments();
//$pp->archivePullPayment();
//$pp->cancelPayout();
//$pp->markPayoutAsPaid();
//$pp->approvePayout();
//$pp->getPullPayment();
//$pp->getPayouts();
//$pp->createPayout();

View File

@ -0,0 +1,175 @@
<?php
declare(strict_types=1);
// Include autoload file.
require __DIR__ . '/../vendor/autoload.php';
// Import Invoice client class.
use BTCPayServer\Client\Invoice;
use BTCPayServer\Client\Webhook;
class WebhookExample
{
public $apiKey;
public $host;
public $storeId;
public $secret;
public $webhookId;
public function __construct()
{
// Fill in with your BTCPay Server data.
$this->apiKey = '';
$this->host = ''; // e.g. https://your.btcpay-server.tld
$this->storeId = '';
$this->secret = ''; // webhook secret as shown in the webhook UI / returned by createWebhook()
$this->webhookId = ''; // only needed for the updateWebhook() example.
}
public function processWebhook()
{
$myfile = fopen("BTCPay.log", 'ab');
$raw_post_data = file_get_contents('php://input');
$date = date('m/d/Y h:i:s a');
if (false === $raw_post_data) {
fwrite(
$myfile,
$date . " : Error. Could not read from the php://input stream or invalid BTCPayServer payload received.\n"
);
fclose($myfile);
throw new \RuntimeException(
'Could not read from the php://input stream or invalid BTCPayServer payload received.'
);
}
$payload = json_decode($raw_post_data, false, 512, JSON_THROW_ON_ERROR);
if (empty($payload)) {
fwrite(
$myfile,
$date . " : Error. Could not decode the JSON payload from BTCPay.\n"
);
fclose($myfile);
throw new \RuntimeException('Could not decode the JSON payload from BTCPay.');
}
// verify hmac256
$headers = getallheaders();
$sig = $headers['Btcpay-Sig'];
$webhookClient = new Webhook($this->host, $this->apiKey);
if ($webhookClient->isIncomingWebhookRequestValid($raw_post_data, $sig, $this->secret)) {
fwrite(
$myfile,
$date . " : Error. Invalid Signature detected! \n was: " . $sig . " should be: " . hash_hmac(
'sha256',
$raw_post_data,
$this->secret
) . "\n"
);
fclose($myfile);
throw new \RuntimeException(
'Invalid BTCPayServer payment notification message received - signature did not match.'
);
}
if (true === empty($payload->invoiceId)) {
fwrite(
$myfile,
$date . " : Error. Invalid BTCPayServer payment notification message received - did not receive invoice ID.\n"
);
fclose($myfile);
throw new \RuntimeException(
'Invalid BTCPayServer payment notification message received - did not receive invoice ID.'
);
}
// Load an existing invoice with the provided invoiceId.
// Most of the time this is not needed as you can listen to specific webhook events
// See: https://docs.btcpayserver.org/API/Greenfield/v1/#tag/Webhooks/paths/InvoiceCreated/post
try {
$client = new Invoice($this->host, $this->apiKey);
$invoice = $client->getInvoice($this->storeId, $payload->invoiceId);
} catch (\Throwable $e) {
fwrite($myfile, "Error: " . $e->getMessage());
throw $e;
}
// optional: check whether your webhook is of the desired type
if ($payload->type !== "InvoiceSettled") {
throw new \RuntimeException(
'Invalid payload message type. Only InvoiceSettled is supported, check the configuration of the webhook.'
);
}
$invoicePrice = $invoice->getData()['amount'];
$buyerEmail = $invoice->getData()['metadata']['buyerEmail'];
fwrite(
$myfile,
$date . " : Payload received for BtcPay invoice " . $payload->invoiceId . " Type: " . $payload->type . " Price: " . $invoicePrice . " E-Mail: " . $buyerEmail . "\n"
);
fwrite($myfile, "Raw payload: " . $raw_post_data . "\n");
// your own processing code goes here!
echo 'OK';
}
public function createWebhook()
{
$url = 'https://createdurl.test.example.com/webhook';
$specificEvents = [
'InvoiceExpired',
'InvoiceSettled',
'InvoiceInvalid'
];
try {
$client = new \BTCPayServer\Client\Webhook($this->host, $this->apiKey);
var_dump($client->createWebhook($this->storeId, $url, $specificEvents, null));
} catch (\Throwable $e) {
echo "Error: " . $e->getMessage();
}
}
public function updateWebhook()
{
$url = 'https://updatedurl.test.example.com/webhook';
$specificEvents = [
'InvoiceReceivedPayment',
'InvoicePaymentSettled',
'InvoiceProcessing',
'InvoiceExpired',
'InvoiceSettled',
'InvoiceInvalid'
];
try {
$client = new \BTCPayServer\Client\Webhook($this->host, $this->apiKey);
var_dump($client->updateWebhook($this->storeId, $url, $this->webhookId, $specificEvents));
} catch (\Throwable $e) {
echo "Error: " . $e->getMessage();
}
}
public function getWebhook()
{
try {
$client = new \BTCPayServer\Client\Webhook($this->host, $this->apiKey);
var_dump($client->getWebhook($this->storeId, $this->webhookId));
} catch (\Throwable $e) {
echo "Error: " . $e->getMessage();
}
}
}
$wh = new WebhookExample();
//$wh->processWebhook();
//$wh->createWebhook();
//$wh->getWebhook();
//$wh->updateWebhook();

View File

@ -43,6 +43,7 @@ class PullPayment extends AbstractClient
string $currency,
?int $period,
?int $BOLT11Expiration,
?bool $autoApproveClaims = false,
?int $startsAt,
?int $expiresAt,
array $paymentMethods
@ -60,6 +61,7 @@ class PullPayment extends AbstractClient
'currency' => $currency,
'period' => $period,
'BOLT11Expiration' => $BOLT11Expiration,
'autoApproveClaims' => $autoApproveClaims,
'startsAt' => $startsAt,
'expiresAt' => $expiresAt,
'paymentMethods' => $paymentMethods
@ -156,7 +158,7 @@ class PullPayment extends AbstractClient
): bool {
$url = $this->getApiUrl() . 'stores/' .
urlencode($storeId) . '/' . 'payouts/' .
urlencode($payoutId);
urlencode($payoutId) . '/mark-paid';
$headers = $this->getRequestHeaders();
$method = 'POST';

View File

@ -118,11 +118,17 @@ class Webhook extends AbstractClient
}
}
public function createWebhook(string $storeId, string $url, ?array $specificEvents, ?string $secret): \BTCPayServer\Result\WebhookCreated
{
public function createWebhook(
string $storeId,
string $url,
?array $specificEvents,
?string $secret,
bool $enabled = true,
bool $automaticRedelivery = true
): \BTCPayServer\Result\WebhookCreated {
$data = [
'enabled' => true,
'automaticRedelivery' => true,
'enabled' => $enabled,
'automaticRedelivery' => $automaticRedelivery,
'url' => $url
];
@ -158,6 +164,62 @@ class Webhook extends AbstractClient
}
}
/**
* Updates an existing webhook.
*
* Important: due to a bug in BTCPay Server versions <= 1.6.3.0 you need
* to pass the $secret explicitly as it would overwrite your existing secret
* otherwise. On newer versions BTCPay Server >= 1.6.4.0, if you do NOT set
* a secret it won't change it and everything will continue to work.
*
* @see https://github.com/btcpayserver/btcpayserver/issues/4010
*
* @return \BTCPayServer\Result\Webhook
* @throws \JsonException
*/
public function updateWebhook(
string $storeId,
string $url,
string $webhookId,
?array $specificEvents,
bool $enabled = true,
bool $automaticRedelivery = true,
?string $secret = null
): \BTCPayServer\Result\Webhook {
$data = [
'enabled' => $enabled,
'automaticRedelivery' => $automaticRedelivery,
'url' => $url,
'secret' => $secret
];
// Specific events or all.
if ($specificEvents === null) {
$data['authorizedEvents'] = [
'everything' => true
];
} elseif (count($specificEvents) === 0) {
throw new \InvalidArgumentException('Argument $specificEvents should be NULL or contains at least 1 item.');
} else {
$data['authorizedEvents'] = [
'everything' => false,
'specificEvents' => $specificEvents
];
}
$url = $this->getApiUrl() . 'stores/' . urlencode($storeId) . '/webhooks/' . urlencode($webhookId);
$headers = $this->getRequestHeaders();
$method = 'PUT';
$response = $this->getHttpClient()->request($method, $url, $headers, json_encode($data, JSON_THROW_ON_ERROR));
if ($response->getStatus() === 200) {
$data = json_decode($response->getBody(), true, 512, JSON_THROW_ON_ERROR);
return new \BTCPayServer\Result\Webhook($data);
} else {
throw $this->getExceptionByStatusCode($method, $url, $response);
}
}
/**
* Check if the request your received from a webhook is authentic and can be trusted.
* @param string $requestBody Most likely you will use `$requestBody = file_get_contents('php://input');`

View File

@ -19,24 +19,24 @@ abstract class AbstractResult implements \ArrayAccess
return $this->data;
}
public function offsetExists($offset)
public function offsetExists($offset): bool
{
$data = $this->getData();
return array_key_exists($offset, $data);
}
public function offsetGet($offset)
public function offsetGet($offset): mixed
{
$data = $this->getData();
return $data[$offset] ?? null;
}
public function offsetSet($offset, $value)
public function offsetSet($offset, $value): void
{
throw new \RuntimeException('You should not change the data in a result.');
}
public function offsetUnset($offset)
public function offsetUnset($offset): void
{
throw new \RuntimeException('You should not change the data in a result.');
}

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace BTCPayServer\Result;
use BTCPayServer\Util\PreciseNumber;
class Invoice extends AbstractResult
{
public const STATUS_NEW = 'New';
@ -24,6 +26,51 @@ class Invoice extends AbstractResult
public const ADDITIONAL_STATUS_PAID_LATE = 'PaidLate';
public function getId(): string
{
return $this->getData()['id'];
}
public function getAmount(): PreciseNumber
{
return PreciseNumber::parseString($this->getData()['amount']);
}
public function getCurrency(): string
{
return $this->getData()['currency'];
}
public function getType(): string
{
return $this->getData()['type'];
}
public function getCheckoutLink(): string
{
return $this->getData()['checkoutLink'];
}
public function getCreatedTime(): int
{
return $this->getData()['createdTime'];
}
public function getExpirationTime(): int
{
return $this->getData()['expirationTime'];
}
public function getMonitoringTime(): int
{
return $this->getData()['monitoringTime'];
}
public function isArchived(): bool
{
return $this->getData()['archived'];
}
public function isPaid(): bool
{
$data = $this->getData();

Some files were not shown because too many files have changed in this diff Show More