summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDamien Bouvy <dbo@odoo.com>2019-07-11 13:08:33 +0000
committerDamien Bouvy <dbo@odoo.com>2019-07-18 06:55:59 +0000
commitad5472b8c5afcfae3cb34b3daa15c3c642ce1d27 (patch)
treec69849fd5005581180c24d39aeab06fbd7fedd50
parent55548931fdd3bb9c5f5937ec74851899b8ea4070 (diff)
[IMP] payment_stripe: flow changes
The previous commit introduced some changes for SCA/PSD2 but some things were missing; most notably, registering payment cards in a s2s flow used the old Charge API that is not SCA-ready. This commit uses the SetupIntent API instead, allowing to register the card prior to payment. The global flow is described in the Stripe documentation in more details: https://stripe.com/docs/payments/cards/saving-cards Unfortunately, given the complete flow of payments, it is not possible to use the PaymentIntents API since this would require having the reference of the transaction before it gets created (the TX is created after the form submission, but the token must already exist at that point). Since this is meant to be backported, this changes are kept minimal in this feature branch. The PaymentIntents API would have been better since registering a card during a payment makes it more likely that future off-session transactions will not require an authentication step. We'll have to live with that for now; it's difficult to know exactly how this will affect customers before PSD2 goes into effect on september 14. Another branch might completely change the flow soon-ish to a more asynchroneous model, although it may miss the v13 freeze date -_-
-rw-r--r--addons/payment/static/src/js/payment_form.js27
-rw-r--r--addons/payment/views/payment_templates.xml3
-rw-r--r--addons/payment_stripe/controllers/main.py6
-rw-r--r--addons/payment_stripe/models/payment.py62
-rw-r--r--addons/payment_stripe/static/src/js/payment_form.js177
-rw-r--r--addons/payment_stripe/static/src/xml/stripe_templates.xml7
-rw-r--r--addons/payment_stripe/views/payment_stripe_templates.xml5
7 files changed, 186 insertions, 101 deletions
diff --git a/addons/payment/static/src/js/payment_form.js b/addons/payment/static/src/js/payment_form.js
index 07432e741b1..884aee83640 100644
--- a/addons/payment/static/src/js/payment_form.js
+++ b/addons/payment/static/src/js/payment_form.js
@@ -10,7 +10,7 @@ var _t = core._t;
publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
selector: '.o_payment_form',
events: {
- 'submit': 'async payEvent',
+ 'submit': 'async onSubmit',
'click #o_payment_form_pay': 'async payEvent',
'click #o_payment_form_add_pm': 'addPmEvent',
'click button[name="delete_pm"]': 'deletePmEvent',
@@ -329,7 +329,11 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
ev.preventDefault();
var checked_radio = this.$('input[type="radio"]:checked');
var self = this;
- var button = ev.target;
+ if (ev.type === 'submit') {
+ var button = $(ev.target).find('*[type="submit"]')[0]
+ } else {
+ var button = ev.target;
+ }
// we check if the user has selected a 'add a new payment' option
if (checked_radio.length === 1 && this.isNewPaymentRadio(checked_radio[0])) {
@@ -433,6 +437,25 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
}
},
/**
+ * Called when submitting the form (e.g. through the Return key).
+ * We need to check whether we are paying or adding a new pm and dispatch
+ * to the correct method.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ onSubmit: function(ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ var button = $(ev.target).find('*[type="submit"]')[0]
+ if (button.id === 'o_payment_form_pay') {
+ return this.payEvent(ev);
+ } else if (button.id === 'o_payment_form_add_pm') {
+ return this.addPmEvent(ev);
+ }
+ return;
+ },
+ /**
* Called when clicking on a button to delete a payment method.
*
* @private
diff --git a/addons/payment/views/payment_templates.xml b/addons/payment/views/payment_templates.xml
index 2de3d8a1faf..8d2c1916d0d 100644
--- a/addons/payment/views/payment_templates.xml
+++ b/addons/payment/views/payment_templates.xml
@@ -29,7 +29,8 @@
t-att-data-access-token="access_token or ''"
t-att-data-partner-id="partner_id"
t-att-data-callback-method="callback_method or ''"
- t-att-data-order-id="order_id or ''">
+ t-att-data-order-id="order_id or ''"
+ t-att-data-mode="mode">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<input type="hidden" t-if="prepare_tx_url" name="prepare_tx_url" t-att-value="prepare_tx_url"/>
<input type="hidden" t-if="order_id" name="order_id" t-att-value="order_id"/>
diff --git a/addons/payment_stripe/controllers/main.py b/addons/payment_stripe/controllers/main.py
index be915f49080..d506621e5a3 100644
--- a/addons/payment_stripe/controllers/main.py
+++ b/addons/payment_stripe/controllers/main.py
@@ -42,3 +42,9 @@ class StripeController(http.Controller):
res['verified'] = token.verified
return res
+
+ @http.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')
diff --git a/addons/payment_stripe/models/payment.py b/addons/payment_stripe/models/payment.py
index efde9123b8c..9272f194cbf 100644
--- a/addons/payment_stripe/models/payment.py
+++ b/addons/payment_stripe/models/payment.py
@@ -51,12 +51,19 @@ class PaymentAcquirerStripe(models.Model):
return tx_values
- def _stripe_request(self, url, data=False):
+ 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}
- resp = requests.post(url, data, headers=headers)
- resp.raise_for_status()
+ 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):
@@ -67,17 +74,38 @@ class PaymentAcquirerStripe(models.Model):
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('id'),
- 'name': 'XXXXXXXXXXXX%s' % data.get('card', {}).get('last4'),
+ 'stripe_payment_method': data.get('payment_method'),
+ 'name': 'XXXXXXXXXXXX%s' % last4,
'acquirer_ref': data.get('customer')
})
return payment_token
@@ -113,29 +141,27 @@ class PaymentTransactionStripe(models.Model):
_logger.info('Stripe: entering form_feedback with post data %s' % pprint.pformat(data))
return super(PaymentTransactionStripe, self).form_feedback(data, acquirer_name)
- def _create_stripe_charge(self, acquirer_ref=None, email=None):
-
+ def _stripe_create_payment_intent(self, acquirer_ref=None, email=None):
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,
- 'payment_method_types[]': 'card',
- 'customer': acquirer_ref,
- 'off_session': 'recurring',
+ 'currency': self.currency_id.name.lower(),
+ 'setup_future_usage': 'off_session',
'confirm': True,
- 'payment_method': self.payment_token_id.stripe_payment_method
+ 'payment_method': self.payment_token_id.stripe_payment_method,
+ 'customer': self.payment_token_id.acquirer_ref,
}
- _logger.info('_create_stripe_charge: Sending values to stripe, values:\n%s', pprint.pformat(charge_params))
+ _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('_create_stripe_charge: Values received:\n%s', pprint.pformat(res))
+ _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._create_stripe_charge(acquirer_ref=self.payment_token_id.acquirer_ref, email=self.partner_email)
+ 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):
@@ -204,7 +230,7 @@ class PaymentTransactionStripe(models.Model):
if self.type == 'form_save':
s2s_data = {
'customer': tree.get('customer'),
- 'id': tree.get('payment_method'),
+ '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
@@ -214,7 +240,7 @@ class PaymentTransactionStripe(models.Model):
if self.payment_token_id:
self.payment_token_id.verified = True
return True
- if status == 'processing':
+ if status in ('processing', 'requires_action'):
self.write(vals)
self._set_transaction_pending()
return True
diff --git a/addons/payment_stripe/static/src/js/payment_form.js b/addons/payment_stripe/static/src/js/payment_form.js
index 43227941d56..c3d77bf4b7a 100644
--- a/addons/payment_stripe/static/src/js/payment_form.js
+++ b/addons/payment_stripe/static/src/js/payment_form.js
@@ -13,6 +13,12 @@ ajax.loadXML('/payment_stripe/static/src/xml/stripe_templates.xml', qweb);
PaymentForm.include({
+ willStart: function () {
+ return this._super.apply(this, arguments).then(function () {
+ return ajax.loadJS("https://js.stripe.com/v3/");
+ })
+ },
+
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -27,6 +33,12 @@ PaymentForm.include({
*/
_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);
@@ -34,70 +46,87 @@ PaymentForm.include({
console.warn('payment_form: unset partner_id when adding new token; things could go wrong');
}
- ajax.loadJS("https://js.stripe.com/v3/").then(function () {
- var formData = self.getFormData(inputsForm);
- var stripe = Stripe(formData.stripe_publishable_key);
- var element = stripe.elements();
- var card = element.create('card', {hidePostalCode: true});
- var dialog = new Dialog(self, {
- title: _t('Card Details'),
- size: 'medium',
- technical: false,
- buttons: [{text: _t('Submit'), classes: 'btn-primary', click: function (ev) {
- stripe.createPaymentMethod('card', card).then(function (result) {
- if (result.error) {
- // Inform the user if there was an error.
- var errorElement = document.getElementById('card-errors');
- errorElement.textContent = result.error.message;
- } else {
- ev.currentTarget.disabled = true;
- _.extend(formData, result.paymentMethod);
- // Send the token to server.
- self._rpc({
- route: formData.data_set,
- params: formData
- }).then (function (data) {
- if (addPmEvent) {
- if (formData.return_url) {
- window.location = formData.return_url;
- } else {
- window.location.reload();
- }
- } else {
- $checkedRadio.val(data.id);
- self.$el.submit();
- }
- }).guardedCatch(function (error) {
- // if the rpc fails, pretty obvious
- self.enableButton(ev.target);
-
- self.displayError(
- _t('Server Error'),
- _t("We are not able to add your payment method at the moment.") +
- error.message.data.message
- );
- });
- }
- });
- }}],
- $content: $(qweb.render('stripe.payment.element')),
- });
- dialog.opened().then(function () {
- self.disableButton(ev.target);
- card.mount('#card-element');
- card.addEventListener('change', function (event) {
- var displayError = document.getElementById('card-errors');
- displayError.textContent = '';
- if (event.error) {
- displayError.textContent = event.error.message;
- }
- });
- });
- dialog.on('closed', self, function () {
- self.enableButton(ev.target);
- });
- dialog.open();
+ var formData = self.getFormData(inputsForm);
+ var stripe = this.stripe;
+ var card = this.stripe_card_element;
+ if (card._invalid) {
+ return;
+ }
+ return this._rpc({
+ route: '/payment/stripe/s2s/create_setup_intent',
+ params: {'acquirer_id': formData.acquirer_id}
+ }).then(function(intent_secret){
+ return stripe.handleCardSetup(intent_secret, card);
+ }).then(function(result) {
+ if (result.error) {
+ return Promise.reject({"message": {"data": { "message": result.error.message}}});
+ } else {
+ _.extend(formData, {"payment_method": result.setupIntent.payment_method});
+ return self._rpc({
+ 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();
+ }
+ }).guardedCatch(function (error) {
+ // 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
@@ -108,13 +137,15 @@ PaymentForm.include({
if ($checkedRadio.length !== 1) {
return;
}
-
- // hide add token form for stripe
- if ($checkedRadio.data('provider') === 'stripe' && this.isNewPaymentRadio($checkedRadio)) {
- this.$('[id*="o_payment_add_token_acq_"]').addClass('d-none');
- } else {
- this._super.apply(this, arguments);
+ 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);
},
//--------------------------------------------------------------------------
@@ -130,9 +161,9 @@ PaymentForm.include({
// first we check that the user has selected a stripe as s2s payment method
if ($checkedRadio.length === 1 && this.isNewPaymentRadio($checkedRadio) && $checkedRadio.data('provider') === 'stripe') {
- this._createStripeToken(ev, $checkedRadio);
+ return this._createStripeToken(ev, $checkedRadio);
} else {
- this._super.apply(this, arguments);
+ return this._super.apply(this, arguments);
}
},
/**
@@ -145,9 +176,9 @@ PaymentForm.include({
// 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') {
- this._createStripeToken(ev, $checkedRadio, true);
+ return this._createStripeToken(ev, $checkedRadio, true);
} else {
- this._super.apply(this, arguments);
+ return this._super.apply(this, arguments);
}
},
});
diff --git a/addons/payment_stripe/static/src/xml/stripe_templates.xml b/addons/payment_stripe/static/src/xml/stripe_templates.xml
index e0ab901a825..8272aea8532 100644
--- a/addons/payment_stripe/static/src/xml/stripe_templates.xml
+++ b/addons/payment_stripe/static/src/xml/stripe_templates.xml
@@ -18,11 +18,4 @@
</div>
</div>
</t>
-
- <div t-name="stripe.payment.element">
- <div id="payment-form">
- <div id="card-errors" class="m-3 text-danger text-center"/>
- <div id="card-element" class="m-3"/>
- </div>
- </div>
</templates>
diff --git a/addons/payment_stripe/views/payment_stripe_templates.xml b/addons/payment_stripe/views/payment_stripe_templates.xml
index 56c89289fdf..a980edbc33c 100644
--- a/addons/payment_stripe/views/payment_stripe_templates.xml
+++ b/addons/payment_stripe/views/payment_stripe_templates.xml
@@ -17,9 +17,14 @@
<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>
</template>
<template id="assets_frontend" inherit_id="web.assets_frontend">