Skip to content

Commit

Permalink
Merge pull request #7 from ndeet/oc3-modal-checkout
Browse files Browse the repository at this point in the history
OC3 modal checkout
  • Loading branch information
ndeet authored Jan 24, 2023
2 parents 0d6e551 + 0288b50 commit 1a54b0c
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 72 deletions.
1 change: 1 addition & 0 deletions upload/admin/controller/extension/payment/btcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public function index() {
'payment_btcpay_api_auth_token',
'payment_btcpay_btcpay_storeid',
'payment_btcpay_webhook',
'payment_btcpay_modal_mode',
'payment_btcpay_webhook_delete',
'payment_btcpay_new_status_id',
'payment_btcpay_paid_status_id',
Expand Down
2 changes: 2 additions & 0 deletions upload/admin/language/en-gb/extension/payment/btcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,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_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_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 upload/admin/view/template/extension/payment/btcpay.twig
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@

</div>

<div class="form-group">
<label class="col-sm-2 control-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-control" {% if payment_btcpay_modal_mode %} checked="checked" {% endif %} />
<div class="help-block">
{{ help_modal_mode }}
</div>
</div>
</div>

<div class="form-group">
<label class="col-sm-2 control-label" for="input-new-status">{{ entry_new_status }}</label>
<div class="col-sm-10">
Expand Down
228 changes: 157 additions & 71 deletions upload/catalog/controller/extension/payment/btcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,28 @@ public function index()
$this->load->language('extension/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',
'',
true
);

return $this->load->view('extension/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/payment/btcpay_modal', $data);
} else {
// Redirect.
return $this->load->view('extension/payment/btcpay', $data);
}
}

public function checkout()
Expand All @@ -32,94 +46,69 @@ public function checkout()
$this->load->model('extension/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 false;
}

$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 );
}

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

// 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 ($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_payment_btcpay->addOrder([
'order_id' => $order_info['order_id'],
'token' => $token,
'invoice_id' => $invoiceId,
]);
}
}

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_new_status_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()
Expand Down Expand Up @@ -149,7 +138,7 @@ public function success()
$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, request had no valid token.');
}
$this->response->redirect(
$this->url->link('common/home', '', true)
Expand All @@ -162,7 +151,7 @@ public function success()

} 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 @@ -323,11 +312,108 @@ 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');
$client = new Invoice($apiHost, $apiKey);

$debug = $this->config->get('payment_btcpay_debug_mode');

// 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 );
}

// 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');
$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_payment_btcpay->getOrder(
$order_info['order_id']
);

$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();
$isExpired = $existingInvoice->isExpired();
$sameTotal = $totalRounded === (float) $invoiceAmount->__toString();

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);
}
}
2 changes: 2 additions & 0 deletions upload/catalog/language/en-gb/extension/payment/btcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@

$_['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.';
2 changes: 1 addition & 1 deletion upload/catalog/model/extension/payment/btcpay.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ public function addOrder($data) {
}

public function getOrder($order_id) {
$query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "btcpay_order` WHERE `order_id` = '" . (int)$order_id . "' LIMIT 1");
$query = $this->db->query("SELECT * FROM `" . DB_PREFIX . "btcpay_order` WHERE `order_id` = '" . (int)$order_id . "' ORDER BY btcpay_order_id DESC LIMIT 1 ");

return $query->row;
}
Expand Down
Loading

0 comments on commit 1a54b0c

Please sign in to comment.