diff --git a/modules/account_payment_stripe/CHANGELOG b/modules/account_payment_stripe/CHANGELOG
index e5423d8451..1c204a90f2 100644
--- a/modules/account_payment_stripe/CHANGELOG
+++ b/modules/account_payment_stripe/CHANGELOG
@@ -1,3 +1,4 @@
+* Use payment element for checkout
Version 7.4.0 - 2024-11-04
--------------------------
diff --git a/modules/account_payment_stripe/checkout.html b/modules/account_payment_stripe/checkout.html
index c20d420e67..2d65f59f8a 100644
--- a/modules/account_payment_stripe/checkout.html
+++ b/modules/account_payment_stripe/checkout.html
@@ -21,125 +21,64 @@
color: #fff;
display: block;
font-size:2em;
- margin: 0 auto;
+ margin: 10px auto;
padding: 0.5em 1em;
}
- .wrapper {
+ form {
width: 670px;
margin: 0 auto;
height: 100%;
}
- .StripeElement {
- background-color: white;
- border-radius: 4px;
- border: 1px solid transparent;
- box-shadow: 0 1px 3px 0 #e6ebf1;
- box-sizing: border-box;
- height: 40px;
- padding: 10px 12px;
- transition: box-shadow 150ms ease;
- }
- .StripeElement--focus {
- box-shadow: 0 1px 3px 0 #cfd7df;
- }
- .StripeElement--invalid {
- border-color: #fa755a;
- }
- .StripeElement--webkit-autofill {
- background-color: #fefde5 !important;
- }
- #card-errors {
+ #payment-errors {
padding: 4px 0;
color: #fa755a;
}
-
-
-
+
${record.party.rec_name}
-
+
-
-
diff --git a/modules/account_payment_stripe/message.xml b/modules/account_payment_stripe/message.xml
index 5d0583cbda..f7e6406791 100644
--- a/modules/account_payment_stripe/message.xml
+++ b/modules/account_payment_stripe/message.xml
@@ -7,7 +7,7 @@ this repository contains the full copyright notices and license terms. -->
Are you sure you want to modify the keys of the Stripe account?
- To process payment "%(payment)s" you must set a Stripe token or customer.
+ To process payment "%(payment)s" you must set a Stripe token, payment intent or customer.
To pay "%(payment)s", you cannot use a stripe journal "%(journal)s".
diff --git a/modules/account_payment_stripe/payment.py b/modules/account_payment_stripe/payment.py
index 340f39e1e4..758556a9fb 100644
--- a/modules/account_payment_stripe/payment.py
+++ b/modules/account_payment_stripe/payment.py
@@ -75,7 +75,9 @@ class Group(metaclass=PoolMeta):
pool = Pool()
Payment = pool.get('account.payment')
for payment in self.payments:
- if not payment.stripe_token and not payment.stripe_customer:
+ if (not payment.stripe_token
+ and not payment.stripe_payment_intent_id
+ and not payment.stripe_customer):
account = payment.journal.stripe_account
for customer in payment.party.stripe_customers:
if (customer.stripe_account == account
@@ -421,12 +423,14 @@ class Payment(StripeCustomerMethodMixin, CheckoutMixin, metaclass=PoolMeta):
'stripe_version': STRIPE_VERSION,
'amount': self.stripe_amount,
'currency': self.currency.code,
+ 'automatic_payment_methods': {
+ 'enabled': True,
+ },
'capture_method': 'automatic' if self.stripe_capture else 'manual',
'customer': (self.stripe_customer.stripe_customer_id
if self.stripe_customer else None),
'description': self.description,
'off_session': off_session,
- 'payment_method_types': ['card'],
'idempotency_key': idempotency_key,
}
if self.stripe_customer_payment_method:
@@ -685,7 +689,7 @@ class Payment(StripeCustomerMethodMixin, CheckoutMixin, metaclass=PoolMeta):
def stripe_update(self, charge):
assert (
(charge.id == self.stripe_charge_id)
- or (charge.payment_intent.id == self.stripe_payment_intent_id))
+ or (charge.payment_intent == self.stripe_payment_intent_id))
amount = charge.amount - charge.amount_refunded
if (self.state not in {'succeeded', 'failed'}
or self.stripe_amount != amount
@@ -1764,6 +1768,27 @@ class Customer(CheckoutMixin, DeactivableMixin, ModelSQL, ModelView):
name = '****' + payment_method.sepa_debit.last4
return name
+ def get_session(self):
+ if self.stripe_customer_id:
+ return stripe.CustomerSession.create(
+ api_key=self.stripe_account.secret_key,
+ customer=self.stripe_customer_id,
+ components={
+ 'payment_element': {
+ 'enabled': True,
+ 'features': self._session_payment_element_features,
+ },
+ })
+
+ @property
+ def _session_payment_element_features(self):
+ return {
+ 'payment_method_redisplay': 'enabled',
+ 'payment_method_save': 'enabled',
+ 'payment_method_save_usage': 'on_session',
+ 'payment_method_remove': 'enabled',
+ }
+
@property
def stripe_setup_intent(self):
if not self.stripe_setup_intent_id:
diff --git a/modules/account_payment_stripe/payment.xml b/modules/account_payment_stripe/payment.xml
index 95d7af04b2..c710b4e8c3 100644
--- a/modules/account_payment_stripe/payment.xml
+++ b/modules/account_payment_stripe/payment.xml
@@ -298,6 +298,7 @@ this repository contains the full copyright notices and license terms. -->
Stripe Checkout
+
account.payment.stripe.checkout
account_payment_stripe/checkout.html
html
diff --git a/modules/account_payment_stripe/routes.py b/modules/account_payment_stripe/routes.py
index bde7ab639e..3e396354d1 100644
--- a/modules/account_payment_stripe/routes.py
+++ b/modules/account_payment_stripe/routes.py
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
@app.route(
'//account_payment_stripe/checkout//',
- methods=['GET', 'POST'])
+ methods=['GET'])
@with_pool
@with_transaction(context={'_skip_warnings': True})
def checkout(request, pool, model, id):
@@ -33,22 +33,46 @@ def checkout(request, pool, model, id):
])
except ValueError:
abort(HTTPStatus.NOT_FOUND)
- if request.method == 'GET':
- Report = pool.get('account.payment.stripe.checkout', type='report')
- # TODO language
- data = {
- 'model': Model.__name__,
- }
- if model == Payment.__name__ and record.stripe_customer_payment_method:
- data['payment_method'] = record.stripe_customer_payment_method
- ext, content, _, _ = Report.execute([record.id], data)
- assert ext == 'html'
- return Response(content, HTTPStatus.OK, content_type='text/html')
- elif request.method == 'POST':
- record.stripe_intent_update()
- return Response(
- '', HTTPStatus.OK,
- content_type='text/html')
+ customer_session_client_secret = ''
+ if model == Payment.__name__ and record.stripe_customer:
+ if customer_session := record.stripe_customer.get_session():
+ customer_session_client_secret = customer_session.client_secret
+ Report = pool.get('account.payment.stripe.checkout', type='report')
+ # TODO language
+ data = {
+ 'model': Model.__name__,
+ 'customer_session_client_secret': customer_session_client_secret,
+ 'return_url': request.base_url + '/end'
+ }
+ ext, content, _, _ = Report.execute([record.id], data)
+ assert ext == 'html'
+ return Response(content, HTTPStatus.OK, content_type='text/html')
+
+
+@app.route(
+ '//account_payment_stripe/checkout///end',
+ methods=['GET'])
+@with_pool
+@with_transaction(readonly=False, context={'_skip_warnings': True})
+def checkout_end(request, pool, model, id):
+ Payment = pool.get('account.payment')
+ Customer = pool.get('account.payment.stripe.customer')
+ if model == Payment.__name__:
+ Model = Payment
+ elif model == Customer.__name__:
+ Model = Customer
+ else:
+ abort(HTTPStatus.FORBIDDEN)
+ try:
+ record, = Model.search([
+ ('stripe_checkout_id', '=', id),
+ ])
+ except ValueError:
+ abort(HTTPStatus.NOT_FOUND)
+ record.stripe_intent_update()
+ return Response(
+ '', HTTPStatus.OK,
+ content_type='text/html')
@app.route(