[ADD] payment_stripe_sca: Stripe API changes following PSD2

With the PSD2 directive coming into effect on September 14th,
was had no choice but to switch our Stripe integration to use
newer APIs that support Strong Customer Authentication (SCA for short).

This meant switching from the old stripe.js implementation for
'redirection' flow to the Stripe Checkout API and from the
Charge API to the newer SetupIntent API for s2s flows.

This new modules introduce these changes in a non-intrusive way,
this means that installing this module is *not* required for
all customers (e.g. instances only accepting payment to and
from countries that are not part of the EU), that installing it
will support SCA flows and that uninstallation leaves you with
a working instance as well. Essentially a backport of 9f5f2fc
in a seperate module.

Task 2039100
This commit is contained in:
Damien Bouvy
2019-09-13 13:10:10 +00:00
committed by Nicolas Martinelli
parent a2c5410622
commit 302ed50cef
16 changed files with 921 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
+38
View File
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
# ____________________
# / \
# | DO NOT FORWARD |
# | PORT FURTHER |
# | THAN 12.4 |
# \____________________/
# ! !
# ! !
# L_ !
# / _)!
# / /__L
# _____/ (____)
# (____)
# _____ (____)
# \_(____)
# ! !
# ! !
# \__/
#
# Starting with 12.5, the payment_stripe module contains these changes OoB
{
'name': 'Stripe Payment Acquirer - Strong Customer Authentication Update',
'category': 'Hidden',
'summary': 'Payment Acquirer: Stripe Implementation for the EU PSD2',
'version': '1.0',
'description': """Stripe Payment Acquirer - Strong Customer Authentication Update""",
'depends': ['payment_stripe'],
'auto_install': True,
'data': [
'views/assets.xml',
'views/payment_templates.xml',
],
'images': ['static/description/icon.png'],
}
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
@@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import pprint
import werkzeug
from odoo.http import route, request
from odoo.addons.payment_stripe.controllers.main import StripeController
_logger = logging.getLogger(__name__)
class StripeControllerSCA(StripeController):
_success_url = '/payment/stripe/success'
_cancel_url = '/payment/stripe/cancel'
@route(['/payment/stripe/success', '/payment/stripe/cancel'], type='http', auth='public')
def stripe_success(self, **kwargs):
request.env['payment.transaction'].sudo().form_feedback(kwargs, 'stripe')
return werkzeug.utils.redirect('/payment/process')
@route(['/payment/stripe/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
def stripe_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
if not kwargs.get('partner_id'):
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
if not token:
res = {
'result': False,
}
return res
res = {
'result': True,
'id': token.id,
'short_name': token.short_name,
'3d_secure': False,
'verified': False,
}
if verify_validity != False:
token.validate()
res['verified'] = token.verified
return res
@route('/payment/stripe/s2s/create_setup_intent', type='json', auth='public', csrf=False)
def stripe_s2s_create_setup_intent(self, acquirer_id, **kwargs):
acquirer = request.env['payment.acquirer'].browse(int(acquirer_id))
res = acquirer._create_setup_intent(kwargs)
return res.get('client_secret')
@route('/payment/stripe/s2s/process_payment_intent', type='json')
def stripe_s2s_process_payment_intent(self, **post):
return request.env['payment.transaction'].sudo().form_feedback(post, 'stripe')
# These routes are deprecated, let's remove them for security's sake
def stripe_s2s_create_json(self, **post):
raise werkzeug.exceptions.NotFound()
def stripe_s2s_create(self, **post):
raise werkzeug.exceptions.NotFound()
def stripe_create_charge(self, **post):
raise werkzeug.exceptions.NotFound()
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment
+372
View File
@@ -0,0 +1,372 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import requests
import pprint
from werkzeug import urls
from odoo import api, models, fields, _
from odoo.exceptions import ValidationError
from odoo.addons.payment_stripe_sca.controllers.main import StripeControllerSCA as StripeController
from odoo.addons.payment_stripe.models.payment import INT_CURRENCIES
from odoo.tools.float_utils import float_round
_logger = logging.getLogger(__name__)
class PaymentAcquirerStripeSCA(models.Model):
_inherit = "payment.acquirer"
def stripe_form_generate_values(self, tx_values):
self.ensure_one()
base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
stripe_session_data = {
"payment_method_types[]": "card",
"line_items[][amount]": int(
tx_values["amount"]
if tx_values["currency"].name in INT_CURRENCIES
else float_round(tx_values["amount"] * 100, 2)
),
"line_items[][currency]": tx_values["currency"].name,
"line_items[][quantity]": 1,
"line_items[][name]": tx_values["reference"],
"client_reference_id": tx_values["reference"],
"success_url": urls.url_join(base_url, StripeController._success_url)
+ "?reference=%s" % tx_values["reference"],
"cancel_url": urls.url_join(base_url, StripeController._cancel_url)
+ "?reference=%s" % tx_values["reference"],
"customer_email": tx_values["partner_email"],
}
tx_values["session_id"] = self._create_stripe_session(stripe_session_data)
return tx_values
def _stripe_request(self, url, data=False, method="POST"):
self.ensure_one()
url = urls.url_join(self._get_stripe_api_url(), url)
headers = {
"AUTHORIZATION": "Bearer %s" % self.sudo().stripe_secret_key,
"Stripe-Version": "2019-05-16", # SetupIntent need a specific version
}
resp = requests.request(method, url, data=data, headers=headers)
try:
resp.raise_for_status()
except:
_logger.error(resp.text)
raise
return resp.json()
def _create_stripe_session(self, kwargs):
self.ensure_one()
resp = self._stripe_request("checkout/sessions", kwargs)
if resp.get("payment_intent") and kwargs.get("client_reference_id"):
tx = (
self.env["payment.transaction"]
.sudo()
.search([("reference", "=", kwargs["client_reference_id"])])
)
tx.stripe_payment_intent = resp["payment_intent"]
return resp["id"]
def _create_setup_intent(self, kwargs):
self.ensure_one()
params = {"usage": "off_session"}
_logger.info(
"_stripe_create_setup_intent: Sending values to stripe, values:\n%s",
pprint.pformat(params),
)
res = self._stripe_request("setup_intents", params)
_logger.info(
"_stripe_create_setup_intent: Values received:\n%s", pprint.pformat(res)
)
return res
@api.model
def _get_stripe_api_url(self):
return "https://api.stripe.com/v1/"
@api.model
def stripe_s2s_form_process(self, data):
last4 = data.get("card", {}).get("last4")
if not last4:
# PM was created with a setup intent, need to get last4 digits through
# yet another call -_-
acquirer_id = self.env["payment.acquirer"].browse(int(data["acquirer_id"]))
pm = data.get("payment_method")
res = acquirer_id._stripe_request(
"payment_methods/%s" % pm, data=False, method="GET"
)
last4 = res.get("card", {}).get("last4", "****")
payment_token = (
self.env["payment.token"]
.sudo()
.create(
{
"acquirer_id": int(data["acquirer_id"]),
"partner_id": int(data["partner_id"]),
"stripe_payment_method": data.get("payment_method"),
"name": "XXXXXXXXXXXX%s" % last4,
"acquirer_ref": data.get("customer"),
}
)
)
return payment_token
def stripe_s2s_form_validate(self, data):
return True
class PaymentTransactionStripeSCA(models.Model):
_inherit = "payment.transaction"
stripe_payment_intent = fields.Char(
string="Stripe Payment Intent ID", readonly=True
)
stripe_payment_intent_secret = fields.Char(string='Stripe Payment Intent Secret', readonly=True)
def _get_json_fields(self):
res = super()._get_json_fields()
res.append('stripe_payment_intent_secret')
return res
def _get_processing_info(self):
res = super()._get_processing_info()
if self.acquirer_id.provider == 'stripe':
stripe_info = {
'stripe_payment_intent': self.stripe_payment_intent,
'stripe_payment_intent_secret': self.stripe_payment_intent_secret,
'stripe_publishable_key': self.acquirer_id.stripe_publishable_key,
}
res.update(stripe_info)
return res
def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None):
raise NotImplementedError(
"This method can no longer be used with the payment_stripe_sca module."
)
def form_feedback(self, data, acquirer_name):
if data.get("reference") and acquirer_name == "stripe":
transaction = self.env["payment.transaction"].search(
[("reference", "=", data["reference"])]
)
url = "payment_intents/%s" % transaction.stripe_payment_intent
resp = transaction.acquirer_id._stripe_request(url)
if resp.get("charges") and resp.get("charges").get("total_count"):
resp = resp.get("charges").get("data")[0]
data.update(resp)
_logger.info(
"Stripe: entering form_feedback with post data %s"
% pprint.pformat(data)
)
# note: luckily, the base stripe module did not override this method, avoiding
# me from using a context key to avoid this call in the parent model
return super(PaymentTransactionStripeSCA, self).form_feedback(data, acquirer_name)
def _stripe_create_payment_intent(self, acquirer_ref=None, email=None):
if self.stripe_payment_intent:
_logger.info(
"_stripe_create_payment_intent: trying to create an intent when one already exists (tx #%s), refetching values for intent %s",
self.id, self.stripe_payment_intent
)
res = self.acquirer_id._stripe_request("payment_intents/%s" % self.stripe_payment_intent, method="GET")
_logger.info(
"_stripe_create_payment_intent: Values received:\n%s", pprint.pformat(res)
)
return res
charge_params = {
"amount": int(
self.amount
if self.currency_id.name in INT_CURRENCIES
else float_round(self.amount * 100, 2)
),
"currency": self.currency_id.name.lower(),
"confirm": True,
"off_session": True,
"payment_method": self.payment_token_id.stripe_payment_method,
"customer": self.payment_token_id.acquirer_ref,
"description": self.reference,
}
if not self.env.context.get('off_session'):
charge_params.update(setup_future_usage='off_session', off_session=False)
_logger.info(
"_stripe_create_payment_intent: Sending values to stripe, values:\n%s",
pprint.pformat(charge_params),
)
res = self.acquirer_id._stripe_request("payment_intents", charge_params)
if res.get("charges") and res.get("charges").get("total_count"):
res = res.get("charges").get("data")[0]
_logger.info(
"_stripe_create_payment_intent: Values received:\n%s", pprint.pformat(res)
)
return res
def stripe_s2s_do_transaction(self, **kwargs):
self.ensure_one()
result = self._stripe_create_payment_intent(
acquirer_ref=self.payment_token_id.acquirer_ref, email=self.partner_email
)
return self._stripe_s2s_validate_tree(result)
def _create_stripe_refund(self):
refund_params = {
"charge": self.acquirer_reference,
"amount": int(
float_round(self.amount * 100, 2)
), # by default, stripe refund the full amount (we don't really need to specify the value)
"metadata[reference]": self.reference,
}
_logger.info(
"_create_stripe_refund: Sending values to stripe URL, values:\n%s",
pprint.pformat(refund_params),
)
res = self.acquirer_id._stripe_request("refunds", refund_params)
_logger.info("_create_stripe_refund: Values received:\n%s", pprint.pformat(res))
return res
@api.model
def _stripe_form_get_tx_from_data(self, data):
""" Given a data dict coming from stripe, verify it and find the related
transaction record. """
reference = data.get("reference")
if not reference:
stripe_error = data.get("error", {}).get("message", "")
_logger.error(
"Stripe: invalid reply received from stripe API, looks like "
"the transaction failed. (error: %s)",
stripe_error or "n/a",
)
error_msg = _("We're sorry to report that the transaction has failed.")
if stripe_error:
error_msg += " " + (
_("Stripe gave us the following info about the problem: '%s'")
% stripe_error
)
error_msg += " " + _(
"Perhaps the problem can be solved by double-checking your "
"credit card details, or contacting your bank?"
)
raise ValidationError(error_msg)
tx = self.search([("reference", "=", reference)])
if not tx:
error_msg = _("Stripe: no order found for reference %s") % reference
_logger.error(error_msg)
raise ValidationError(error_msg)
elif len(tx) > 1:
error_msg = _("Stripe: %s orders found for reference %s") % (
len(tx),
reference,
)
_logger.error(error_msg)
raise ValidationError(error_msg)
return tx[0]
def _stripe_s2s_validate_tree(self, tree):
self.ensure_one()
if self.state not in ("draft", "pending"):
_logger.info(
"Stripe: trying to validate an already validated tx (ref %s)",
self.reference,
)
return True
status = tree.get("status")
tx_id = tree.get("id")
tx_secret = tree.get("client_secret")
vals = {"date": fields.datetime.now(), "acquirer_reference": tx_id, "stripe_payment_intent": tx_id, "stripe_payment_intent_secret": tx_secret}
if status == "succeeded":
self.write(vals)
self._set_transaction_done()
self.execute_callback()
if self.type == "form_save":
s2s_data = {
"customer": tree.get("customer"),
"payment_method": tree.get("payment_method"),
"card": tree.get("payment_method_details").get("card"),
"acquirer_id": self.acquirer_id.id,
"partner_id": self.partner_id.id,
}
token = self.acquirer_id.stripe_s2s_form_process(s2s_data)
self.payment_token_id = token.id
if self.payment_token_id:
self.payment_token_id.verified = True
return True
if status in ("processing", "requires_action"):
self.write(vals)
self._set_transaction_pending()
return True
else:
error = tree.get("failure_message")
_logger.warn(error)
vals.update({"state_message": error})
self.write(vals)
self._set_transaction_cancel()
return False
def _stripe_form_get_invalid_parameters(self, data):
invalid_parameters = []
if data.get("amount") != int(
self.amount
if self.currency_id.name in INT_CURRENCIES
else float_round(self.amount * 100, 2)
):
invalid_parameters.append(("Amount", data.get("amount"), self.amount * 100))
if data.get("currency").upper() != self.currency_id.name:
invalid_parameters.append(
("Currency", data.get("currency"), self.currency_id.name)
)
if (
data.get("payment_intent")
and data.get("payment_intent") != self.stripe_payment_intent
):
invalid_parameters.append(
(
"Payment Intent",
data.get("payment_intent"),
self.stripe_payment_intent,
)
)
return invalid_parameters
class PaymentTokenStripeSCA(models.Model):
_inherit = "payment.token"
stripe_payment_method = fields.Char("Payment Method ID")
@api.model
def stripe_create(self, values):
if values.get("stripe_payment_method") and not values.get("acquirer_ref"):
partner_id = self.env["res.partner"].browse(values.get("partner_id"))
payment_acquirer = self.env["payment.acquirer"].browse(
values.get("acquirer_id")
)
# create customer to stipe
customer_data = {"email": partner_id.email}
cust_resp = payment_acquirer._stripe_request("customers", customer_data)
# link customer with payment method
api_url_payment_method = (
"payment_methods/%s/attach" % values["stripe_payment_method"]
)
method_data = {"customer": cust_resp.get("id")}
payment_acquirer._stripe_request(api_url_payment_method, method_data)
return {"acquirer_ref": cust_resp["id"]}
return values
def _stripe_create_customer(self, token, description=None, acquirer_id=None):
raise NotImplementedError(
"This method can no longer be used with the payment_stripe_sca module."
)
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.5 KiB

@@ -0,0 +1,64 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h2 class="oe_slogan">Stripe - Strong Customer Authentication</h2>
<h3 class="oe_slogan">Add support for strong customer authentication as part of the EU PSD2</h3>
</div>
<div class="oe_span12">
<p class='oe_mt32'>
The PSD2 - the second edition of the Payment Services Directive - is coming into full effect in
the European Union (EU) and the European Economic Area (EEA) on September 14th, 2019. This
directive may indirectly impact your business by requiring stronger authentication mechanisms
for online payments.
</p>
<h2 class="oe_mt32">Am I impacted by the PSD2 directive?</h2>
<p>You are impacted by this change if all of these statements applies to your business:
<ul>
<li>You are legally based in the EU/EEA</li>
<li>You accept payments from customers based in the EU/EEA</li>
<li>You accept payments via cards (credit/debit)</li>
</ul>
</p>
<h2 class="oe_mt32">What should I do if I am impacted?</h2>
<p>
This directive does not bind you legally to do anything in particular regarding
your business processes - it applies to payment processors, acquirers and banks.
Legally, you don't have to do anything to comply with this directive - it does not
apply to you.
</p>
<p>
You might however be indirectly impacted because some online payment flows will
change for customers that pay online - they might need to authenticate themselves during
a transaction that previously didn't require it. This is why Odoo is adapting some of
its integration with payment acquirers to avoid disruptions in your business flows.
</p>
<p>
If you use Stripe and are impacted by the PSD2 directive, then simply installing this
module will avoid problems for your online customers during their payment flow. No extra
configuration is needed once the module is installed.
</p>
<h2 class="oe_mt32">What happens if I don't install this update?</h2>
<p>If you don't install this update, some of your customers may be unable to pay you.
There is no reason not to install it if you are impacted by the directive.</p>
<h2 class="oe_mt32">I'm not impacted by the PSD2, should I install this module anyway?</h2>
<p>While not required, installing this module remains a good idea nonetheless since
it will switch your instance to newer - and thus more future-proof - APIs of the Stripe
payment acquirer.
</p>
<h2 class="oe_mt32">What will changes for my online customers</h2>
<p>The interface displayed to your customers will change when they pay you online.</p>
<p>If your Stripe acquirer is configured with the payment flow 'Redirection to the acquirer website'
in Odoo, the small payment overlay will be replaced by a full-screen interface.
</p>
<img src="redirection_flow.png" class="img-fluid">
<p>If your Stripe acquirer is configured with the payment flow 'Payment from Odoo'
in Odoo, the credit card input form will look slightly different.
</p>
<img src="s2s_flow.png" class="img-fluid">
<p>The rest of the payment experience remains the same.</p>
</div>
</div>
</section>
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,188 @@
odoo.define('payment_stripe.payment_form', function (require) {
"use strict";
var ajax = require('web.ajax');
var core = require('web.core');
var rpc = require('web.rpc');
var Dialog = require('web.Dialog');
var PaymentForm = require('payment.payment_form');
var _t = core._t;
PaymentForm.include({
willStart: function () {
return this._super.apply(this, arguments).then(function () {
return ajax.loadJS("https://js.stripe.com/v3/");
})
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* called when clicking on pay now or add payment event to create token for credit card/debit card.
*
* @private
* @param {Event} ev
* @param {DOMElement} checkedRadio
* @param {Boolean} addPmEvent
*/
_createStripeToken: function (ev, $checkedRadio, addPmEvent) {
var self = this;
if (ev.type === 'submit') {
var button = $(ev.target).find('*[type="submit"]')[0]
} else {
var button = ev.target;
}
this.disableButton(button);
var acquirerID = this.getAcquirerIdFromRadio($checkedRadio);
var acquirerForm = this.$('#o_payment_add_token_acq_' + acquirerID);
var inputsForm = $('input', acquirerForm);
if (this.options.partnerId === undefined) {
console.warn('payment_form: unset partner_id when adding new token; things could go wrong');
}
var formData = self.getFormData(inputsForm);
var stripe = this.stripe;
var card = this.stripe_card_element;
if (card._invalid) {
return;
}
return rpc.query({
route: '/payment/stripe/s2s/create_setup_intent',
params: {'acquirer_id': formData.acquirer_id}
}).then(function(intent_secret){
// need to convert between ES6 promises and jQuery 2 deferred \o/
return $.Deferred(function(defer) {
stripe.handleCardSetup(intent_secret, card)
.then(function(result) {defer.resolve(result)})
});
}).then(function(result) {
if (result.error) {
return $.Deferred().reject({"message": {"data": { "message": result.error.message}}});
} else {
_.extend(formData, {"payment_method": result.setupIntent.payment_method});
return rpc.query({
route: formData.data_set,
params: formData,
})
}
}).then(function(result) {
if (addPmEvent) {
if (formData.return_url) {
window.location = formData.return_url;
} else {
window.location.reload();
}
} else {
$checkedRadio.val(result.id);
self.el.submit();
}
}).fail(function (error, event) {
// if the rpc fails, pretty obvious
self.enableButton(button);
self.displayError(
_t('Unable to save card'),
_t("We are not able to add your payment method at the moment. ") +
error.message.data.message
);
});
},
/**
* called when clicking a Stripe radio if configured for s2s flow; instanciates the card and bind it to the widget.
*
* @private
* @param {DOMElement} checkedRadio
*/
_bindStripeCard: function ($checkedRadio) {
var acquirerID = this.getAcquirerIdFromRadio($checkedRadio);
var acquirerForm = this.$('#o_payment_add_token_acq_' + acquirerID);
var inputsForm = $('input', acquirerForm);
var formData = this.getFormData(inputsForm);
var stripe = Stripe(formData.stripe_publishable_key);
var element = stripe.elements();
var card = element.create('card', {hidePostalCode: true});
card.mount('#card-element');
card.on('ready', function(ev) {
card.focus();
});
card.addEventListener('change', function (event) {
var displayError = document.getElementById('card-errors');
displayError.textContent = '';
if (event.error) {
displayError.textContent = event.error.message;
}
});
this.stripe = stripe;
this.stripe_card_element = card;
},
/**
* destroys the card element and any stripe instance linked to the widget.
*
* @private
*/
_unbindStripeCard: function () {
if (this.stripe_card_element) {
this.stripe_card_element.destroy();
}
this.stripe = undefined;
this.stripe_card_element = undefined;
},
/**
* @override
*/
updateNewPaymentDisplayStatus: function () {
var $checkedRadio = this.$('input[type="radio"]:checked');
if ($checkedRadio.length !== 1) {
return;
}
var provider = $checkedRadio.data('provider')
if (provider === 'stripe') {
// always re-init stripe (in case of multiple acquirers for stripe, make sure the stripe instance is using the right key)
this._unbindStripeCard();
if (this.isNewPaymentRadio($checkedRadio)) {
this._bindStripeCard($checkedRadio);
}
}
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @override
*/
payEvent: function (ev) {
ev.preventDefault();
var $checkedRadio = this.$('input[type="radio"]:checked');
// first we check that the user has selected a stripe as s2s payment method
if ($checkedRadio.length === 1 && $checkedRadio.data('provider') === 'stripe' && this.isNewPaymentRadio($checkedRadio)) {
return this._createStripeToken(ev, $checkedRadio);
} else {
return this._super.apply(this, arguments);
}
},
/**
* @override
*/
addPmEvent: function (ev) {
ev.stopPropagation();
ev.preventDefault();
var $checkedRadio = this.$('input[type="radio"]:checked');
// first we check that the user has selected a stripe as add payment method
if ($checkedRadio.length === 1 && this.isNewPaymentRadio($checkedRadio) && $checkedRadio.data('provider') === 'stripe') {
return this._createStripeToken(ev, $checkedRadio, true);
} else {
return this._super.apply(this, arguments);
}
},
});
});
@@ -0,0 +1,48 @@
odoo.define('payment_stripe_sca.processing', function (require) {
'use strict';
var ajax = require('web.ajax');
var rpc = require('web.rpc')
var PaymentProcessing = require('payment.processing');
return PaymentProcessing.include({
init: function () {
this._super.apply(this, arguments);
this._authInProgress = false;
},
willStart: function () {
return this._super.apply(this, arguments).then(function () {
return ajax.loadJS("https://js.stripe.com/v3/");
})
},
_stripeAuthenticate: function (tx) {
var stripe = Stripe(tx.stripe_publishable_key);
return $.Deferred(function(defer) {
stripe.handleCardPayment(tx.stripe_payment_intent_secret)
.then(function(result) {defer.resolve(result)})
}).then(function(result) {
if (result.error) {
return $.Deferred().reject({"message": {"data": { "message": result.error.message}}});
}
return rpc.query({
route: '/payment/stripe/s2s/process_payment_intent',
params: _.extend({}, result.paymentIntent, {reference: tx.reference}),
});
}).then(function(result) {
window.location = '/payment/process';
}).fail(function (error) {
this._authInProgress = false;
});
},
processPolledData: function(transactions) {
this._super.apply(this, arguments);
for (var itx=0; itx < transactions.length; itx++) {
var tx = transactions[itx];
if (tx.acquirer_provider === 'stripe' && tx.state === 'pending' && tx.stripe_payment_intent_secret && !this._authInProgress) {
this._authInProgress = true;
this._stripeAuthenticate(tx);
}
}
},
});
});
@@ -0,0 +1,82 @@
odoo.define('payment_stripe.stripe', function (require) {
"use strict";
var ajax = require('web.ajax');
var core = require('web.core');
var qweb = core.qweb;
var _t = core._t;
ajax.loadXML('/payment_stripe/static/src/xml/stripe_templates.xml', qweb);
if ($.blockUI) {
// our message needs to appear above the modal dialog
$.blockUI.defaults.baseZ = 2147483647; //same z-index as StripeCheckout
$.blockUI.defaults.css.border = '0';
$.blockUI.defaults.css["background-color"] = '';
$.blockUI.defaults.overlayCSS["opacity"] = '0.9';
}
require('web.dom_ready');
if (!$('.o_payment_form').length) {
return Promise.reject("DOM doesn't contain '.o_payment_form'");
}
var observer = new MutationObserver(function (mutations, observer) {
for (var i = 0; i < mutations.length; ++i) {
for (var j = 0; j < mutations[i].addedNodes.length; ++j) {
if (mutations[i].addedNodes[j].tagName.toLowerCase() === "form" && mutations[i].addedNodes[j].getAttribute('provider') === 'stripe') {
_redirectToStripeCheckout($(mutations[i].addedNodes[j]));
}
}
}
});
function displayError(message) {
var wizard = $(qweb.render('stripe.error', {'msg': message || _t('Payment error')}));
wizard.appendTo($('body')).modal({'keyboard': true});
if ($.blockUI) {
$.unblockUI();
}
$("#o_payment_form_pay").removeAttr('disabled');
}
function _redirectToStripeCheckout(providerForm) {
// Open Checkout with further options
if ($.blockUI) {
var msg = _t("Just one more second, we are redirecting you to Stripe...");
$.blockUI({
'message': '<h2 class="text-white"><img src="/web/static/src/img/spin.png" class="fa-pulse"/>' +
' <br />' + msg +
'</h2>'
});
}
var paymentForm = $('.o_payment_form');
if (!paymentForm.find('i').length) {
paymentForm.append('<i class="fa fa-spinner fa-spin"/>');
paymentForm.attr('disabled', 'disabled');
}
var _getStripeInputValue = function (name) {
return providerForm.find('input[name="' + name + '"]').val();
};
var stripe = Stripe(_getStripeInputValue('stripe_key'));
stripe.redirectToCheckout({
sessionId: _getStripeInputValue('session_id')
}).then(function (result) {
if (result.error) {
displayError(result.error.message);
}
});
}
$.getScript("https://js.stripe.com/v3/", function (data, textStatus, jqxhr) {
observer.observe(document.body, {childList: true});
_redirectToStripeCheckout($('form[provider="stripe"]'));
});
});
@@ -0,0 +1,8 @@
<odoo>
<template id="assets_frontend" inherit_id="web.assets_frontend" name="Payment Stripe SCA Assets">
<xpath expr="." position="inside">
<script type="text/javascript" src="/payment_stripe_sca/static/src/js/payment_form.js"></script>
<script type="text/javascript" src="/payment_stripe_sca/static/src/js/payment_processing.js"></script>
</xpath>
</template>
</odoo>
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<template id="stripe_form" inherit_id="payment_stripe.stripe_form">
<xpath expr="//script" position="before">
<input type='hidden' name='session_id' t-att-value='session_id'/>
</xpath>
<xpath expr="//script" position="replace">
<script type="text/javascript">
odoo.define(function (require) {
var ajax = require('web.ajax');
ajax.loadJS("/payment_stripe_sca/static/src/js/stripe_sca.js");
});
</script>
</xpath>
</template>
<template id="stripe_s2s_form" inherit_id="payment_stripe.stripe_s2s_form">
<xpath expr="//div" position="replace">
<input type="hidden" name="data_set" value="/payment/stripe/s2s/create_json_3ds"/>
<input type="hidden" name="acquirer_id" t-att-value="id"/>
<input type="hidden" name="stripe_publishable_key" t-att-value="acq.sudo().stripe_publishable_key"/>
<input type="hidden" name="currency_id" t-att-value="currency_id"/>
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div id="payment-form">
<div id="card-element" class="m-3"/>
<div id="card-errors" class="m-3 text-danger"/>
</div>
</xpath>
<xpath expr="//input[@name='data_set']" position="replace"/>
</template>
<template inherit_id="payment.payment_tokens_list" id="payment_tokens_list" name="Payment Tokens list">
<!-- add the provider on saved tokens -->
<xpath expr="//t[@t-foreach='pms']//input[@name='pm_id'][@type='radio'][@t-att-value='pm.id']" position="attributes">
<attribute name="t-att-data-provider">pm.acquirer_id.provider</attribute>
</xpath>
</template>
</data>
</odoo>