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
131 changed files with 815 additions and 653 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';
@ -34,17 +33,20 @@ $_['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_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_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,29 +1,37 @@
<?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
);
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;
@ -31,17 +39,18 @@ class ControllerExtensionPaymentBTCPay extends Controller
$data['success_link'] = $this->url->link('checkout/success', '', true);
$data['invoice_expired_text'] = $this->language->get('invoice_expired_text');
return $this->load->view('extension/payment/btcpay_modal', $data);
return $this->load->view('extension/btcpay/payment/btcpay_modal', $data);
} else {
// Redirect.
return $this->load->view('extension/payment/btcpay', $data);
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');
@ -54,13 +63,23 @@ class ControllerExtensionPaymentBTCPay extends Controller
if (!isset($this->session->data['order_id'])) {
$this->log->write('No session data order_id present, aborting.');
return false;
return;
}
$order_info = $this->model_checkout_order->getOrder(
$this->session->data['order_id']
);
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)
);
}
$invoiceId = '';
$checkoutLink = '';
@ -68,7 +87,6 @@ class ControllerExtensionPaymentBTCPay extends Controller
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);
}
@ -80,11 +98,30 @@ class ControllerExtensionPaymentBTCPay extends Controller
$checkoutLink = $newInvoice->getCheckoutLink();
// Add invoiceId to the btcpay order table.
$this->model_extension_payment_btcpay->addOrder([
'order_id' => $order_info['order_id'],
'token' => $token,
'invoice_id' => $invoiceId,
]);
$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
);
*/
}
}
@ -109,25 +146,27 @@ class ControllerExtensionPaymentBTCPay extends Controller
}
}
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.
@ -136,7 +175,7 @@ class ControllerExtensionPaymentBTCPay extends Controller
$this->request->get['token']
) !== 0) {
if ($debug) {
$this->log->write('Redirect to home page, request had no valid token.');
$this->log->write('Redirect to home page, had no valid token.');
}
$this->response->redirect(
$this->url->link('common/home', '', true)
@ -160,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');
@ -189,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']
);
@ -201,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)) {
@ -223,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();
@ -231,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":
@ -287,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,
@ -305,6 +352,7 @@ class ControllerExtensionPaymentBTCPay extends Controller
}
$this->response->addHeader('HTTP/1.1 200 OK');
$this->response->setOutput(json_encode($json));
}
/**
@ -322,14 +370,14 @@ class ControllerExtensionPaymentBTCPay extends Controller
$apiKey = $this->config->get('payment_btcpay_api_auth_token');
$apiHost = $this->config->get('payment_btcpay_url');
$apiStoreId = $this->config->get('payment_btcpay_btcpay_storeid');
$client = new Invoice($apiHost, $apiKey);
$debug = $this->config->get('payment_btcpay_debug_mode');
$client = new Invoice($apiHost, $apiKey);
// Checkout options.
$checkoutOptions = new InvoiceCheckoutOptions();
$redirectUrl = $this->url->link(
'extension/payment/btcpay/success',
'extension/btcpay/payment/btcpay|success',
['token' => $token],
true
);
@ -381,7 +429,7 @@ class ControllerExtensionPaymentBTCPay extends Controller
// Round to 2 decimals to avoid mismatch.
$totalRounded = round((float) $total->__toString(), 2);
$btcpay_order = $this->model_extension_payment_btcpay->getOrder(
$btcpay_order = $this->model_extension_btcpay_payment_btcpay->getOrder(
$order_info['order_id']
);
@ -416,4 +464,5 @@ class ControllerExtensionPaymentBTCPay extends Controller
);
return PreciseNumber::parseString($total);
}
}

View File

@ -5,3 +5,5 @@ $_['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

@ -1,7 +1,7 @@
<script src="{{ modal_url }}"></script>
<div class="buttons">
<div class="pull-right">
<div class="text-end">
<button class="btn btn-primary btcpay-modal">{{ button_confirm }}</button>
</div>
</div>
@ -13,10 +13,10 @@
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);
@ -43,19 +43,15 @@
}
}
});
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">' + err + '<button type="button" class="close" data-dismiss="alert">×</button></div>';
$(this).closest('.panel-body').first().prepend(errFail);
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"
}
}

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

@ -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.');
}

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