@@ -1,3 +1,4 @@
|
||||
* Use payment element for checkout
|
||||
|
||||
Version 7.4.0 - 2024-11-04
|
||||
--------------------------
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<py:for each="record in records[:1]">
|
||||
<py:with vars="intent = record.stripe_intent">
|
||||
<body py:with="intent = record.stripe_intent">
|
||||
<h1>${record.party.rec_name}</h1>
|
||||
<div class="wrapper">
|
||||
<form method="POST" id="form">
|
||||
<div>
|
||||
<py:if test="not data.get('payment_method')">
|
||||
<label for="card-element">
|
||||
Credit or debit card
|
||||
</label>
|
||||
<div id="card-element">
|
||||
</div>
|
||||
</py:if>
|
||||
<div id="card-errors" role="alert">
|
||||
</div>
|
||||
</div>
|
||||
<button id="card-button" data-secret="${intent.client_secret}" data-model="${data['model']}">
|
||||
<py:choose test="data['model']">
|
||||
<py:when test="'account.payment'">
|
||||
Pay ${record.currency.symbol}${record.amount}
|
||||
</py:when>
|
||||
<py:when test="'account.payment.stripe.customer'">
|
||||
Register card
|
||||
</py:when>
|
||||
</py:choose>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<form id="payment-form">
|
||||
<div id="payment-element">
|
||||
</div>
|
||||
<div id="payment-error">
|
||||
</div>
|
||||
<button id="payment-submit" data-model="${data['model']}">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
<script>
|
||||
var stripe = Stripe('${record.stripe_account.publishable_key}');
|
||||
var elements = stripe.elements();
|
||||
var style = {
|
||||
base: {
|
||||
color: '#32325d',
|
||||
fontSize: '16px',
|
||||
'::placeholder': {
|
||||
color: '#aab7c4'
|
||||
}
|
||||
},
|
||||
invalid: {
|
||||
color: '#fa755a',
|
||||
iconColor: '#fa755a'
|
||||
}
|
||||
};
|
||||
var card;
|
||||
if (document.getElementById('card-element')) {
|
||||
card = elements.create('card', {style: style});
|
||||
card.mount('#card-element');
|
||||
|
||||
card.addEventListener('change', function(event) {
|
||||
var displayError = document.getElementById('card-errors');
|
||||
if (event.error) {
|
||||
displayError.textContent = event.error.message;
|
||||
} else {
|
||||
displayError.textContent = '';
|
||||
}
|
||||
});
|
||||
} else {
|
||||
card = {payment_method: '${data.get('payment_method', '')}'}
|
||||
}
|
||||
|
||||
var cardButton = document.getElementById('card-button');
|
||||
var clientSecret = cardButton.dataset.secret;
|
||||
var model = cardButton.dataset.model;
|
||||
var form = document.getElementById('form');
|
||||
const model = '${data['model']}';
|
||||
const customer_session_client_secret = '${data['customer_session_client_secret']}';
|
||||
const stripe = Stripe('${record.stripe_account.publishable_key}');
|
||||
const elements = stripe.elements({
|
||||
'clientSecret': '${intent.client_secret}',
|
||||
'customerSessionClientSecret': customer_session_client_secret || null,
|
||||
});
|
||||
const paymentElement = elements.create('payment', {
|
||||
});
|
||||
paymentElement.mount('#payment-element');
|
||||
const form = document.getElementById('payment-form');
|
||||
form.addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
var prm;
|
||||
const options = {
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: '${data['return_url']}',
|
||||
},
|
||||
}
|
||||
let prm;
|
||||
if (model == 'account.payment.stripe.customer') {
|
||||
prm = stripe.handleCardSetup(clientSecret, card);
|
||||
prm = stripe.confirmSetup(options);
|
||||
} else {
|
||||
prm = stripe.handleCardPayment(clientSecret, card);
|
||||
prm = stripe.confirmPayment(options);
|
||||
}
|
||||
prm.then(function(result) {
|
||||
if (result.error) {
|
||||
var errorElement = document.getElementById('card-errors');
|
||||
errorElement.textContent = result.error.message;
|
||||
} else {
|
||||
form.submit();
|
||||
const messageContainer = document.getElementById('payment-error');
|
||||
messageContainer.textContent = result.error.message;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</py:with>
|
||||
</py:for>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -7,7 +7,7 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<field name="text">Are you sure you want to modify the keys of the Stripe account?</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_no_stripe_token">
|
||||
<field name="text">To process payment "%(payment)s" you must set a Stripe token or customer.</field>
|
||||
<field name="text">To process payment "%(payment)s" you must set a Stripe token, payment intent or customer.</field>
|
||||
</record>
|
||||
<record model="ir.message" id="msg_stripe_receivable">
|
||||
<field name="text">To pay "%(payment)s", you cannot use a stripe journal "%(journal)s".</field>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -298,6 +298,7 @@ this repository contains the full copyright notices and license terms. -->
|
||||
<record model="ir.action.report" id="report_checkout">
|
||||
<field name="name">Stripe Checkout</field>
|
||||
<field name="model" eval="None"/>
|
||||
<field name="single" eval="True"/>
|
||||
<field name="report_name">account.payment.stripe.checkout</field>
|
||||
<field name="report">account_payment_stripe/checkout.html</field>
|
||||
<field name="template_extension">html</field>
|
||||
|
||||
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
@app.route(
|
||||
'/<database_name>/account_payment_stripe/checkout/<model>/<id>',
|
||||
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(
|
||||
'<body onload="window.close()">', 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(
|
||||
'/<database_name>/account_payment_stripe/checkout/<model>/<id>/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(
|
||||
'<body onload="window.close()">', HTTPStatus.OK,
|
||||
content_type='text/html')
|
||||
|
||||
|
||||
@app.route(
|
||||
|
||||
Reference in New Issue
Block a user