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(