Skip to content

Commit

Permalink
Merge pull request #8 from ndeet/oc4-modal-checkout
Browse files Browse the repository at this point in the history
Adding modal checkout, fixing bugs and general refactor.
  • Loading branch information
ndeet authored Jan 25, 2023
2 parents 9b81bc6 + 3d5c6cc commit 4222968
Show file tree
Hide file tree
Showing 12 changed files with 274 additions and 80 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ Please let us always know your OpenCart, BTCPay extension, PHP versions. You can

## Development

OpenCart 4 development happens on the `master` branch and OpenCart 3 is maintained on the `3.x` branch but does not receive any new features.
OpenCart 4 development happens on the `master` branch and OpenCart 3 is maintained on the `3.x` branch but is mostly in maintenance mode.


1 change: 1 addition & 0 deletions admin/controller/payment/btcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public function index(): void {
'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',
Expand Down
2 changes: 2 additions & 0 deletions admin/language/en-gb/payment/btcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
$_['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';
Expand All @@ -33,6 +34,7 @@
$_['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.';

Expand Down
10 changes: 10 additions & 0 deletions admin/view/template/payment/btcpay.twig
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,16 @@
</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">
Expand Down
253 changes: 181 additions & 72 deletions catalog/controller/payment/btcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,110 +16,117 @@ public function index(): string
$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/btcpay/payment/btcpay|checkout',
'',
true
);

return $this->load->view('extension/btcpay/payment/btcpay', $data);
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(): void
{
$this->load->model('checkout/order');
$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/btcpay/payment/btcpay|success',
['token' => $token],
true
);

$checkoutOptions->setRedirectURL(htmlspecialchars_decode($redirectUrl));
if ($debug) {
$this->log->write( 'Setting redirect url to: ' . $redirectUrl );
}
$invoiceId = '';
$checkoutLink = '';

// 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.
// 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()
);

// 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');
/* 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
);
*/

// 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_btcpay_payment_btcpay->addOrder([
'order_id' => $order_info['order_id'],
'token' => $token,
'invoice_id' => $invoice->getData()['id'],
]);

$this->model_checkout_order->addHistory(
$order_info['order_id'],
$this->config->get('payment_btcpay_new_status_id'),
'BTCPay invoice id: ' . $invoice->getData()['id']
);

$this->response->redirect($invoice->getData()['checkoutLink']);
} else {
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."
);
$this->response->redirect(
$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(): void
Expand All @@ -136,11 +143,13 @@ public function success(): void

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']) {
if (isset($this->session->data['order_id'])) {
$order = $this->model_extension_btcpay_payment_btcpay->getOrder(
$order_id
$this->session->data['order_id']
);

// Check if token is present and valid.
Expand All @@ -149,7 +158,7 @@ public function success(): void
$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)
Expand All @@ -162,7 +171,7 @@ public function success(): void

} 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)
Expand Down Expand Up @@ -332,11 +341,111 @@ public function callback()
/**
* 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);
}

}
4 changes: 4 additions & 0 deletions catalog/language/en-gb/payment/btcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,7 @@

$_['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: ';
2 changes: 1 addition & 1 deletion catalog/model/payment/btcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function addOrder(array $data): bool
public function getOrder(int $order_id): array
{
$query = $this->db->query(
"SELECT * FROM `" . DB_PREFIX . "btcpay_order` WHERE `order_id` = '" . $order_id . "' LIMIT 1"
"SELECT * FROM `" . DB_PREFIX . "btcpay_order` WHERE `order_id` = '" . $order_id . "' ORDER BY btcpay_order_id DESC LIMIT 1"
);

return $query->row;
Expand Down
Loading

0 comments on commit 4222968

Please sign in to comment.