19.0 vanilla
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,105 @@
|
|||
import { registry } from '@web/core/registry';
|
||||
import { Interaction } from '@web/public/interaction';
|
||||
|
||||
export class ExpressCheckout extends Interaction {
|
||||
static selector = 'form[name="o_payment_express_checkout_form"]';
|
||||
|
||||
setup() {
|
||||
this.paymentContext = {};
|
||||
Object.assign(this.paymentContext, this.el.dataset);
|
||||
this.paymentContext.shippingInfoRequired = !!this.paymentContext.shippingInfoRequired;
|
||||
}
|
||||
|
||||
async willStart() {
|
||||
const expressCheckoutForm = this._getExpressCheckoutForm();
|
||||
if (expressCheckoutForm) {
|
||||
await this._prepareExpressCheckoutForm(expressCheckoutForm.dataset);
|
||||
}
|
||||
}
|
||||
|
||||
start() {
|
||||
// Monitor updates of the amount on eCommerce's cart pages.
|
||||
this.env.bus.addEventListener('cart_amount_changed', (ev) =>
|
||||
this._updateAmount(...ev.detail)
|
||||
);
|
||||
// Monitor when the page is restored from the bfcache.
|
||||
this.addListener(window, 'pageshow', this._onNavigationBack);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the page when the page is restored from the bfcache.
|
||||
*
|
||||
* @param {PageTransitionEvent} event - The pageshow event.
|
||||
* @private
|
||||
*/
|
||||
_onNavigationBack(event) {
|
||||
if (event.persisted) {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the express checkout form, if found.
|
||||
*
|
||||
* @private
|
||||
* @return {Element|null} - The express checkout form.
|
||||
*/
|
||||
_getExpressCheckoutForm() {
|
||||
return document.querySelector(
|
||||
'form[name="o_payment_express_checkout_form"] div[name="o_express_checkout_container"]'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the provider-specific express checkout form based on the provided data.
|
||||
*
|
||||
* For a provider to manage an express checkout form, it must override this method.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} providerData - The provider-specific data.
|
||||
* @return {void}
|
||||
*/
|
||||
async _prepareExpressCheckoutForm(providerData) {}
|
||||
|
||||
/**
|
||||
* Prepare the params for the RPC to the transaction route.
|
||||
*
|
||||
* @private
|
||||
* @param {number} providerId - The id of the provider handling the transaction.
|
||||
* @returns {object} - The transaction route params.
|
||||
*/
|
||||
_prepareTransactionRouteParams(providerId) {
|
||||
return {
|
||||
'provider_id': parseInt(providerId),
|
||||
'payment_method_id': parseInt(this.paymentContext['paymentMethodUnknownId']),
|
||||
'token_id': null,
|
||||
'flow': 'direct',
|
||||
'tokenization_requested': false,
|
||||
'landing_route': this.paymentContext['landingRoute'],
|
||||
'access_token': this.paymentContext['accessToken'],
|
||||
'csrf_token': odoo.csrf_token,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the amount of the express checkout form.
|
||||
*
|
||||
* For a provider to manage an express form, it must override this method.
|
||||
*
|
||||
* @private
|
||||
* @param {number} newAmount - The new amount.
|
||||
* @param {number} newMinorAmount - The new minor amount.
|
||||
* @return {void}
|
||||
*/
|
||||
_updateAmount(newAmount, newMinorAmount) {
|
||||
this.paymentContext.amount = parseFloat(newAmount);
|
||||
this.paymentContext.minorAmount = parseInt(newMinorAmount);
|
||||
this._getExpressCheckoutForm()?.classList?.toggle(
|
||||
'd-none', this.paymentContext.amount === 0
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
registry
|
||||
.category('public.interactions')
|
||||
.add('payment.express_checkout', ExpressCheckout);
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { registry } from '@web/core/registry';
|
||||
import { Interaction } from '@web/public/interaction';
|
||||
|
||||
export class PaymentButton extends Interaction {
|
||||
static selector = '[name="o_payment_submit_button"]';
|
||||
|
||||
setup() {
|
||||
this.paymentButton = this.el;
|
||||
this.iconClass = this.paymentButton.dataset.iconClass;
|
||||
this._enable();
|
||||
this.env.bus.addEventListener('enablePaymentButton', this._enable.bind(this));
|
||||
this.env.bus.addEventListener('disablePaymentButton', this._disable.bind(this));
|
||||
this.env.bus.addEventListener('hidePaymentButton', this._hide.bind(this));
|
||||
this.env.bus.addEventListener('showPaymentButton', this._show.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the payment button can be enabled and do it if so.
|
||||
*
|
||||
* @private
|
||||
* @return {void}
|
||||
*/
|
||||
_enable() {
|
||||
if (this._canSubmit()) {
|
||||
this._setEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the payment form can be submitted, i.e. whether exactly one payment option is
|
||||
* selected.
|
||||
*
|
||||
* For a module to add a condition on the submission of the form, it must override this method
|
||||
* and return whether both this method's condition and the override method's condition are met.
|
||||
*
|
||||
* @private
|
||||
* @return {boolean} Whether the form can be submitted.
|
||||
*/
|
||||
_canSubmit() {
|
||||
const paymentForm = document.querySelector('#o_payment_form');
|
||||
if (!paymentForm) { // Payment form is not present.
|
||||
return true; // Ignore the check.
|
||||
}
|
||||
return document.querySelectorAll('input[name="o_payment_radio"]:checked').length === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the payment button.
|
||||
*
|
||||
* @private
|
||||
* @return {void}
|
||||
*/
|
||||
_setEnabled() {
|
||||
this.paymentButton.disabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the payment button.
|
||||
*
|
||||
* @private
|
||||
* @return {void}
|
||||
*/
|
||||
_disable() {
|
||||
this.paymentButton.disabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the payment button.
|
||||
*
|
||||
* @private
|
||||
* @return {void}
|
||||
*/
|
||||
_hide() {
|
||||
this.paymentButton.classList.add('d-none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the payment button.
|
||||
*
|
||||
* @private
|
||||
* @return {void}
|
||||
*/
|
||||
_show() {
|
||||
this.paymentButton.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
registry.category('public.interactions').add('payment.payment_button', PaymentButton);
|
||||
|
|
@ -0,0 +1,655 @@
|
|||
import { browser } from '@web/core/browser/browser';
|
||||
import { ConfirmationDialog } from '@web/core/confirmation_dialog/confirmation_dialog';
|
||||
import { _t } from '@web/core/l10n/translation';
|
||||
import { rpc, RPCError } from '@web/core/network/rpc';
|
||||
import { registry } from '@web/core/registry';
|
||||
import { renderToMarkup } from '@web/core/utils/render';
|
||||
import { Interaction } from '@web/public/interaction';
|
||||
|
||||
export class PaymentForm extends Interaction {
|
||||
static selector = '#o_payment_form';
|
||||
dynamicContent = {
|
||||
'[name="o_payment_radio"]': { 't-on-change': this.selectPaymentOption },
|
||||
'[name="o_payment_delete_token"]': { 't-on-click': this.fetchTokenData },
|
||||
'[name="o_payment_expand_button"]': { 't-on-click': this.hideExpandButton },
|
||||
'[name="o_payment_submit_button"]': { 't-on-click': this.submitForm },
|
||||
};
|
||||
|
||||
// #=== INTERACTION LIFECYCLE ===#
|
||||
|
||||
setup() {
|
||||
// Load the payment context from the payment form dataset.
|
||||
this.paymentContext = {};
|
||||
Object.assign(this.paymentContext, this.el.dataset);
|
||||
|
||||
this.defaultSubmitButtonLabel = document.querySelector(
|
||||
'button[name="o_payment_submit_button"]'
|
||||
)?.textContent;
|
||||
|
||||
// Enable tooltips.
|
||||
this.el.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
|
||||
const tooltip = window.Tooltip.getOrCreateInstance(el);
|
||||
this.registerCleanup(() => tooltip.dispose());
|
||||
});
|
||||
}
|
||||
|
||||
async willStart() {
|
||||
// Expand the payment form of the selected payment option if there is only one.
|
||||
const checkedRadio = document.querySelector('input[name="o_payment_radio"]:checked');
|
||||
if (checkedRadio) {
|
||||
await this.waitFor(this._expandInlineForm(checkedRadio));
|
||||
this._enableButton(false);
|
||||
} else {
|
||||
this._setPaymentFlow(); // Initialize the payment flow to let providers overwrite it.
|
||||
}
|
||||
}
|
||||
|
||||
// #=== EVENT HANDLERS ===#
|
||||
|
||||
/**
|
||||
* Open the inline form of the selected payment option, if any.
|
||||
*
|
||||
* @param {Event} ev
|
||||
* @return {void}
|
||||
*/
|
||||
async selectPaymentOption(ev) {
|
||||
// Show the inputs in case they have been hidden.
|
||||
this._showInputs();
|
||||
|
||||
// Disable the submit button while preparing the inline form.
|
||||
this._disableButton();
|
||||
|
||||
// Unfold and prepare the inline form of the selected payment option.
|
||||
const checkedRadio = ev.target;
|
||||
await this.waitFor(this._expandInlineForm(checkedRadio));
|
||||
|
||||
// Re-enable the submit button after the inline form has been prepared.
|
||||
this._enableButton(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch data relative to the documents linked to the token and delegate them to the token
|
||||
* deletion confirmation dialog.
|
||||
*
|
||||
* @param {Event} ev
|
||||
* @return {void}
|
||||
*/
|
||||
async fetchTokenData(ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
const linkedRadio = document.getElementById(ev.currentTarget.dataset['linkedRadio']);
|
||||
const tokenId = this._getPaymentOptionId(linkedRadio);
|
||||
try {
|
||||
const linkedRecordsInfo = await this.waitFor(this.services.orm.call(
|
||||
'payment.token', 'get_linked_records_info', [tokenId]
|
||||
));
|
||||
this._challengeTokenDeletion(tokenId, linkedRecordsInfo);
|
||||
} catch (error) {
|
||||
if (error instanceof RPCError) {
|
||||
this._displayErrorDialog(_t("Cannot delete payment method"), error.data.message);
|
||||
} else {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the button to expand the payment methods section once it has been clicked.
|
||||
*
|
||||
* @param {Event} ev
|
||||
* @return {void}
|
||||
*/
|
||||
hideExpandButton(ev) {
|
||||
ev.target.classList.add('d-none');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the payment context with the selected payment option and initiate its payment flow.
|
||||
*
|
||||
* @param {Event} ev
|
||||
* @return {void}
|
||||
*/
|
||||
async submitForm(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
const checkedRadio = this.el.querySelector('input[name="o_payment_radio"]:checked');
|
||||
|
||||
// Block the entire UI to prevent fiddling with other interactions.
|
||||
this._disableButton(true);
|
||||
|
||||
// Initiate the payment flow of the selected payment option.
|
||||
const flow = this.paymentContext.flow = this._getPaymentFlow(checkedRadio);
|
||||
const paymentOptionId = this.paymentContext.paymentOptionId = this._getPaymentOptionId(
|
||||
checkedRadio
|
||||
);
|
||||
if (flow === 'token' && this.paymentContext['assignTokenRoute']) { // Assign token flow.
|
||||
await this._assignToken(paymentOptionId);
|
||||
} else { // Both tokens and payment methods must process a payment operation.
|
||||
const providerCode = this.paymentContext.providerCode = this._getProviderCode(
|
||||
checkedRadio
|
||||
);
|
||||
const pmCode = this.paymentContext.paymentMethodCode = this._getPaymentMethodCode(
|
||||
checkedRadio
|
||||
);
|
||||
this.paymentContext.providerId = this._getProviderId(checkedRadio);
|
||||
if (this._getPaymentOptionType(checkedRadio) === 'token') {
|
||||
this.paymentContext.tokenId = paymentOptionId;
|
||||
} else { // 'payment_method'
|
||||
this.paymentContext.paymentMethodId = paymentOptionId;
|
||||
}
|
||||
const inlineForm = this._getInlineForm(checkedRadio);
|
||||
this.paymentContext.tokenizationRequested = inlineForm?.querySelector(
|
||||
'[name="o_payment_tokenize_checkbox"]'
|
||||
)?.checked ?? this.paymentContext['mode'] === 'validation';
|
||||
await this._initiatePaymentFlow(providerCode, paymentOptionId, pmCode, flow);
|
||||
}
|
||||
}
|
||||
|
||||
// #=== DOM MANIPULATION ===#
|
||||
|
||||
/**
|
||||
* Check if the submit button can be enabled and do it if so.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} unblockUI - Whether the UI should also be unblocked.
|
||||
* @return {void}
|
||||
*/
|
||||
_enableButton(unblockUI = true) {
|
||||
this.env.bus.trigger('enablePaymentButton');
|
||||
if (unblockUI) {
|
||||
this.env.bus.trigger('ui', 'unblock');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the submit button.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} blockUI - Whether the UI should also be blocked.
|
||||
* @return {void}
|
||||
*/
|
||||
_disableButton(blockUI = false) {
|
||||
this.env.bus.trigger('disablePaymentButton');
|
||||
if (blockUI) {
|
||||
this.env.bus.trigger('ui', 'block');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the tokenization checkbox, its label, and the submit button.
|
||||
*
|
||||
* @private
|
||||
* @return {void}
|
||||
*/
|
||||
_showInputs() {
|
||||
// Show the tokenization checkbox and its label.
|
||||
const tokenizeContainer = this.el.querySelector('[name="o_payment_tokenize_container"]');
|
||||
tokenizeContainer?.classList.remove('d-none');
|
||||
|
||||
// Show the submit button.
|
||||
this.env.bus.trigger('showPaymentButton');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the tokenization checkbox, its label, and the submit button.
|
||||
*
|
||||
* The inputs should typically be hidden when the customer has to perform additional actions in
|
||||
* the inline form. All inputs are automatically shown again when the customer selects another
|
||||
* payment option.
|
||||
*
|
||||
* @private
|
||||
* @return {void}
|
||||
*/
|
||||
_hideInputs() {
|
||||
// Hide the tokenization checkbox and its label.
|
||||
const tokenizeContainer = this.el.querySelector('[name="o_payment_tokenize_container"]');
|
||||
tokenizeContainer?.classList.add('d-none');
|
||||
|
||||
// Hide the submit button.
|
||||
this.env.bus.trigger('hidePaymentButton');
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the inline form of the selected payment option and collapse the others.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLInputElement} radio - The radio button linked to the payment option.
|
||||
* @return {void}
|
||||
*/
|
||||
async _expandInlineForm(radio) {
|
||||
this._collapseInlineForms(); // Collapse previously opened inline forms.
|
||||
this._setPaymentFlow(); // Reset the payment flow to let providers overwrite it.
|
||||
|
||||
// Prepare the inline form of the selected payment option.
|
||||
const providerId = this._getProviderId(radio);
|
||||
const providerCode = this._getProviderCode(radio);
|
||||
const paymentOptionId = this._getPaymentOptionId(radio);
|
||||
const paymentMethodCode = this._getPaymentMethodCode(radio);
|
||||
const flow = this._getPaymentFlow(radio);
|
||||
await this.waitFor(this._prepareInlineForm(
|
||||
providerId, providerCode, paymentOptionId, paymentMethodCode, flow
|
||||
));
|
||||
|
||||
// Adapt the payment button's label based on the selected payment method.
|
||||
this._adaptSubmitButtonLabel(paymentMethodCode);
|
||||
|
||||
// Display the prepared inline form if it contains visible elements.
|
||||
const isVisible = element => {
|
||||
if (
|
||||
element.getAttribute('name') !== 'o_payment_inline_form' // Skip the container.
|
||||
&& element.classList.contains('d-none')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (element.children.length === 0) {
|
||||
return true; // The element is visible if it has no children.
|
||||
}
|
||||
return Array.from(element.children).some(child => isVisible(child));
|
||||
};
|
||||
const inlineForm = this._getInlineForm(radio);
|
||||
if (inlineForm && isVisible(inlineForm)) {
|
||||
inlineForm.classList.remove('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse all inline forms of the current interaction.
|
||||
*
|
||||
* @private
|
||||
* @return {void}
|
||||
*/
|
||||
_collapseInlineForms() {
|
||||
this.el.querySelectorAll('[name="o_payment_inline_form"]').forEach(inlineForm => {
|
||||
inlineForm.classList.add('d-none');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the provider-specific inline form of the selected payment option.
|
||||
*
|
||||
* For a provider to manage an inline form, it must override this method and render the content
|
||||
* of the form.
|
||||
*
|
||||
* @private
|
||||
* @param {number} providerId - The id of the selected payment option's provider.
|
||||
* @param {string} providerCode - The code of the selected payment option's provider.
|
||||
* @param {number} paymentOptionId - The id of the selected payment option.
|
||||
* @param {string} paymentMethodCode - The code of the selected payment method, if any.
|
||||
* @param {string} flow - The online payment flow of the selected payment option.
|
||||
* @return {void}
|
||||
*/
|
||||
async _prepareInlineForm(providerId, providerCode, paymentOptionId, paymentMethodCode, flow) {}
|
||||
|
||||
/**
|
||||
* Update the payment button's label for "pay later" payment methods.
|
||||
*
|
||||
* @private
|
||||
* @param {string} paymentMethodCode - The code of the selected payment method, if any.
|
||||
* @return {void}
|
||||
*/
|
||||
_adaptSubmitButtonLabel(paymentMethodCode) {
|
||||
const buttonLabel = this._isPayLaterPaymentMethod(paymentMethodCode)
|
||||
? _t("Confirm")
|
||||
: this.defaultSubmitButtonLabel;
|
||||
for (const btn of document.querySelectorAll('button[name="o_payment_submit_button"]')) {
|
||||
if (btn.textContent !== buttonLabel) {
|
||||
btn.textContent = buttonLabel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given payment method expects immediate payment.
|
||||
*
|
||||
* Override this method to change the submit button label from the default label to "Confirm".
|
||||
*
|
||||
* @private
|
||||
* @param {string} paymentMethodCode - The code of the selected payment method, if any.
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isPayLaterPaymentMethod(paymentMethodCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display an error dialog.
|
||||
*
|
||||
* @private
|
||||
* @param {string} title - The title of the dialog.
|
||||
* @param {string} errorMessage - The error message.
|
||||
* @return {void}
|
||||
*/
|
||||
_displayErrorDialog(title, errorMessage = '') {
|
||||
this.services.dialog.add(ConfirmationDialog, { title: title, body: errorMessage || "" });
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the token deletion confirmation dialog.
|
||||
*
|
||||
* @private
|
||||
* @param {number} tokenId - The id of the token whose deletion was requested.
|
||||
* @param {object} linkedRecordsInfo - The data relative to the documents linked to the token.
|
||||
* @return {void}
|
||||
*/
|
||||
_challengeTokenDeletion(tokenId, linkedRecordsInfo) {
|
||||
const body = renderToMarkup('payment.deleteTokenDialog', { linkedRecordsInfo });
|
||||
this.services.dialog.add(ConfirmationDialog, {
|
||||
title: _t("Warning!"),
|
||||
body,
|
||||
confirmLabel: _t("Confirm Deletion"),
|
||||
confirm: () => this._archiveToken(tokenId),
|
||||
cancel: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
// #=== PAYMENT FLOW ===#
|
||||
|
||||
/**
|
||||
* Set the payment flow for the selected payment option.
|
||||
*
|
||||
* For a provider to manage direct payments, it must call this method and set the payment flow
|
||||
* when its payment option is selected.
|
||||
*
|
||||
* @private
|
||||
* @param {string} flow - The flow for the selected payment option. Either 'redirect', 'direct',
|
||||
* or 'token'
|
||||
* @return {void}
|
||||
*/
|
||||
_setPaymentFlow(flow = 'redirect') {
|
||||
if (['redirect', 'direct', 'token'].includes(flow)) {
|
||||
this.paymentContext.flow = flow;
|
||||
} else {
|
||||
console.warn(`The value ${flow} is not a supported flow. Falling back to redirect.`);
|
||||
this.paymentContext.flow = 'redirect';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign the selected token to a document through the `assignTokenRoute`.
|
||||
*
|
||||
* @private
|
||||
* @param {number} tokenId - The id of the token to assign.
|
||||
* @return {void}
|
||||
*/
|
||||
async _assignToken(tokenId) {
|
||||
try {
|
||||
await this.waitFor(rpc(this.paymentContext['assignTokenRoute'], {
|
||||
'token_id': tokenId,
|
||||
'access_token': this.paymentContext['accessToken'],
|
||||
}));
|
||||
window.location = this.paymentContext['landingRoute'];
|
||||
} catch (error) {
|
||||
if (error instanceof RPCError) {
|
||||
this._displayErrorDialog(_t("Cannot save payment method"), error.data.message);
|
||||
this._enableButton(); // The button has been disabled before initiating the flow.
|
||||
} else {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an RPC to initiate the payment flow by creating a new transaction.
|
||||
*
|
||||
* For a provider to do pre-processing work (e.g., perform checks on the form inputs), or to
|
||||
* process the payment flow in its own terms (e.g., re-schedule the RPC to the transaction
|
||||
* route), it must override this method.
|
||||
*
|
||||
* To alter the flow-specific processing, it is advised to override `_processRedirectFlow`,
|
||||
* `_processDirectFlow`, or `_processTokenFlow` instead.
|
||||
*
|
||||
* @private
|
||||
* @param {string} providerCode - The code of the selected payment option's provider.
|
||||
* @param {number} paymentOptionId - The id of the selected payment option.
|
||||
* @param {string} paymentMethodCode - The code of the selected payment method, if any.
|
||||
* @param {string} flow - The payment flow of the selected payment option.
|
||||
* @return {void}
|
||||
*/
|
||||
async _initiatePaymentFlow(providerCode, paymentOptionId, paymentMethodCode, flow) {
|
||||
try {
|
||||
// Create a transaction and retrieve its processing values.
|
||||
const processingValues = await this.waitFor(rpc(
|
||||
this.paymentContext['transactionRoute'], this._prepareTransactionRouteParams()
|
||||
));
|
||||
if (processingValues.state === 'error') {
|
||||
this._displayErrorDialog(
|
||||
_t("Payment processing failed"), processingValues.state_message
|
||||
);
|
||||
this._enableButton(); // The button has been disabled before initiating the flow.
|
||||
return;
|
||||
}
|
||||
if (flow === 'redirect') {
|
||||
this._processRedirectFlow(
|
||||
providerCode, paymentOptionId, paymentMethodCode, processingValues
|
||||
);
|
||||
} else if (flow === 'direct') {
|
||||
this._processDirectFlow(
|
||||
providerCode, paymentOptionId, paymentMethodCode, processingValues
|
||||
);
|
||||
} else if (flow === 'token') {
|
||||
this._processTokenFlow(
|
||||
providerCode, paymentOptionId, paymentMethodCode, processingValues
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof RPCError) {
|
||||
this._displayErrorDialog(_t("Payment processing failed"), error.data.message);
|
||||
this._enableButton(); // The button has been disabled before initiating the flow.
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the params for the RPC to the transaction route.
|
||||
*
|
||||
* @private
|
||||
* @return {object} The transaction route params.
|
||||
*/
|
||||
_prepareTransactionRouteParams() {
|
||||
const transactionRouteParams = {
|
||||
'provider_id': this.paymentContext.providerId,
|
||||
'payment_method_id': this.paymentContext.paymentMethodId ?? null,
|
||||
'token_id': this.paymentContext.tokenId ?? null,
|
||||
'amount': this.paymentContext['amount'] !== undefined
|
||||
? parseFloat(this.paymentContext['amount']) : null,
|
||||
'flow': this.paymentContext['flow'],
|
||||
'tokenization_requested': this.paymentContext['tokenizationRequested'],
|
||||
'landing_route': this.paymentContext['landingRoute'],
|
||||
'is_validation': this.paymentContext['mode'] === 'validation',
|
||||
'access_token': this.paymentContext['accessToken'],
|
||||
'csrf_token': odoo.csrf_token,
|
||||
};
|
||||
// Generic payment flows (i.e., that are not attached to a document) require extra params.
|
||||
if (this.paymentContext['transactionRoute'] === '/payment/transaction') {
|
||||
Object.assign(transactionRouteParams, {
|
||||
'currency_id': this.paymentContext['currencyId']
|
||||
? parseInt(this.paymentContext['currencyId']) : null,
|
||||
'partner_id': parseInt(this.paymentContext['partnerId']),
|
||||
'reference_prefix': this.paymentContext['referencePrefix']?.toString(),
|
||||
});
|
||||
}
|
||||
return transactionRouteParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect the customer by submitting the redirect form included in the processing values.
|
||||
*
|
||||
* @private
|
||||
* @param {string} providerCode - The code of the selected payment option's provider.
|
||||
* @param {number} paymentOptionId - The id of the selected payment option.
|
||||
* @param {string} paymentMethodCode - The code of the selected payment method, if any.
|
||||
* @param {object} processingValues - The processing values of the transaction.
|
||||
* @return {void}
|
||||
*/
|
||||
_processRedirectFlow(providerCode, paymentOptionId, paymentMethodCode, processingValues) {
|
||||
// Create and configure the form element with the content rendered by the server.
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = processingValues['redirect_form_html'];
|
||||
const redirectForm = div.querySelector('form');
|
||||
redirectForm.setAttribute('id', 'o_payment_redirect_form');
|
||||
redirectForm.setAttribute('target', '_top'); // Ensures redirections when in an iframe.
|
||||
|
||||
// Submit the form.
|
||||
document.body.appendChild(redirectForm);
|
||||
redirectForm.submit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the provider-specific implementation of the direct payment flow.
|
||||
*
|
||||
* @private
|
||||
* @param {string} providerCode - The code of the selected payment option's provider.
|
||||
* @param {number} paymentOptionId - The id of the selected payment option.
|
||||
* @param {string} paymentMethodCode - The code of the selected payment method, if any.
|
||||
* @param {object} processingValues - The processing values of the transaction.
|
||||
* @return {void}
|
||||
*/
|
||||
_processDirectFlow(providerCode, paymentOptionId, paymentMethodCode, processingValues) {}
|
||||
|
||||
/**
|
||||
* Redirect the customer to the status route.
|
||||
*
|
||||
* @private
|
||||
* @param {string} providerCode - The code of the selected payment option's provider.
|
||||
* @param {number} paymentOptionId - The id of the selected payment option.
|
||||
* @param {string} paymentMethodCode - The code of the selected payment method, if any.
|
||||
* @param {object} processingValues - The processing values of the transaction.
|
||||
* @return {void}
|
||||
*/
|
||||
_processTokenFlow(providerCode, paymentOptionId, paymentMethodCode, processingValues) {
|
||||
// The flow is already completed as payments by tokens are immediately processed.
|
||||
window.location = '/payment/status';
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive the provided token.
|
||||
*
|
||||
* @private
|
||||
* @param {number} tokenId - The id of the token whose deletion was requested.
|
||||
* @return {void}
|
||||
*/
|
||||
async _archiveToken(tokenId) {
|
||||
try {
|
||||
await this.waitFor(rpc('/payment/archive_token', {
|
||||
'token_id': tokenId,
|
||||
}));
|
||||
browser.location.reload();
|
||||
} catch (error) {
|
||||
if (error instanceof RPCError) {
|
||||
this._displayErrorDialog(
|
||||
_t("Cannot delete payment method"), error.data.message
|
||||
);
|
||||
} else {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #=== GETTERS ===#
|
||||
|
||||
/**
|
||||
* Determine and return the inline form of the selected payment option.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLInputElement} radio - The radio button linked to the payment option.
|
||||
* @return {Element | null} The inline form of the selected payment option, if any.
|
||||
*/
|
||||
_getInlineForm(radio) {
|
||||
const inlineFormContainer = radio.closest('[name="o_payment_option"]');
|
||||
return inlineFormContainer?.querySelector('[name="o_payment_inline_form"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine and return the payment flow of the selected payment option.
|
||||
*
|
||||
* As some providers implement both direct payments and the payment with redirection flow, we
|
||||
* cannot infer it from the radio button only. The radio button indicates only whether the
|
||||
* payment option is a token. If not, the payment context is looked up to determine whether the
|
||||
* flow is 'direct' or 'redirect'.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLInputElement} radio - The radio button linked to the payment option.
|
||||
* @return {string} The flow of the selected payment option: 'redirect', 'direct' or 'token'.
|
||||
*/
|
||||
_getPaymentFlow(radio) {
|
||||
// The flow is read from the payment context too in case it was forced in a custom implem.
|
||||
if (this._getPaymentOptionType(radio) === 'token' || this.paymentContext.flow === 'token') {
|
||||
return 'token';
|
||||
} else if (this.paymentContext.flow === 'redirect') {
|
||||
return 'redirect';
|
||||
} else {
|
||||
return 'direct';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine and return the code of the selected payment method.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLElement} radio - The radio button linked to the payment method.
|
||||
* @return {string} The code of the selected payment method.
|
||||
*/
|
||||
_getPaymentMethodCode(radio) {
|
||||
return radio.dataset['paymentMethodCode'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine and return the id of the selected payment option.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLElement} radio - The radio button linked to the payment option.
|
||||
* @return {number} The id of the selected payment option.
|
||||
*/
|
||||
_getPaymentOptionId(radio) {
|
||||
return Number(radio.dataset['paymentOptionId']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine and return the type of the selected payment option.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLElement} radio - The radio button linked to the payment option.
|
||||
* @return {string} The type of the selected payment option: 'token' or 'payment_method'.
|
||||
*/
|
||||
_getPaymentOptionType(radio) {
|
||||
return radio.dataset['paymentOptionType'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine and return the id of the provider of the selected payment option.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLElement} radio - The radio button linked to the payment option.
|
||||
* @return {number} The id of the provider of the selected payment option.
|
||||
*/
|
||||
_getProviderId(radio) {
|
||||
return Number(radio.dataset['providerId']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine and return the code of the provider of the selected payment option.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLElement} radio - The radio button linked to the payment option.
|
||||
* @return {string} The code of the provider of the selected payment option.
|
||||
*/
|
||||
_getProviderCode(radio) {
|
||||
return radio.dataset['providerCode'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine and return the state of the provider of the selected payment option.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLElement} radio - The radio button linked to the payment option.
|
||||
* @return {string} The state of the provider of the selected payment option.
|
||||
*/
|
||||
_getProviderState(radio) {
|
||||
return radio.dataset['providerState'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
registry.category('public.interactions').add('payment.payment_form', PaymentForm);
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { ConnectionLostError, rpc, RPCError } from '@web/core/network/rpc';
|
||||
import { registry } from '@web/core/registry';
|
||||
import { Interaction } from '@web/public/interaction';
|
||||
|
||||
export class PaymentPostProcessing extends Interaction {
|
||||
static selector = 'div[name="o_payment_status"]';
|
||||
|
||||
setup() {
|
||||
this.timeout = 0;
|
||||
this.pollCount = 0;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.poll();
|
||||
}
|
||||
|
||||
poll() {
|
||||
this.updateTimeout();
|
||||
this.waitForTimeout(async () => {
|
||||
try {
|
||||
// Fetch the post-processing values from the server.
|
||||
const postProcessingValues = await this.waitFor(
|
||||
rpc('/payment/status/poll', { csrf_token: odoo.csrf_token })
|
||||
);
|
||||
|
||||
// Redirect the user to the landing route if the transaction reached a final state.
|
||||
const { provider_code, state, landing_route } = postProcessingValues;
|
||||
if (PaymentPostProcessing.getFinalStates(provider_code).has(state)) {
|
||||
window.location = landing_route;
|
||||
} else {
|
||||
this.poll();
|
||||
}
|
||||
} catch (error) {
|
||||
const isRetryError = error instanceof RPCError && error.data.message === 'retry';
|
||||
const isConnectionLostError = error instanceof ConnectionLostError;
|
||||
if (isRetryError || isConnectionLostError) {
|
||||
this.poll();
|
||||
}
|
||||
if (!isRetryError) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}, this.timeout);
|
||||
}
|
||||
|
||||
static getFinalStates(providerCode) {
|
||||
return new Set(['authorized', 'done', 'cancel', 'error']);
|
||||
}
|
||||
|
||||
updateTimeout() {
|
||||
if (this.pollCount >= 1 && this.pollCount < 10) {
|
||||
this.timeout = 3000;
|
||||
}
|
||||
if (this.pollCount >= 10 && this.pollCount < 20) {
|
||||
this.timeout = 10000;
|
||||
} else if (this.pollCount >= 20) {
|
||||
this.timeout = 30000;
|
||||
}
|
||||
this.pollCount++;
|
||||
}
|
||||
}
|
||||
|
||||
registry
|
||||
.category('public.interactions')
|
||||
.add('payment.payment_post_processing', PaymentPostProcessing);
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
odoo.define('payment.checkout_form', require => {
|
||||
'use strict';
|
||||
|
||||
const publicWidget = require('web.public.widget');
|
||||
|
||||
const paymentFormMixin = require('payment.payment_form_mixin');
|
||||
|
||||
publicWidget.registry.PaymentCheckoutForm = publicWidget.Widget.extend(paymentFormMixin, {
|
||||
selector: 'form[name="o_payment_checkout"]',
|
||||
events: Object.assign({}, publicWidget.Widget.prototype.events, {
|
||||
'click div[name="o_payment_option_card"]': '_onClickPaymentOption',
|
||||
'click a[name="o_payment_icon_more"]': '_onClickMorePaymentIcons',
|
||||
'click a[name="o_payment_icon_less"]': '_onClickLessPaymentIcons',
|
||||
'click button[name="o_payment_submit_button"]': '_onClickPay',
|
||||
'submit': '_onSubmit',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function () {
|
||||
const preventDoubleClick = handlerMethod => {
|
||||
return _.debounce(handlerMethod, 500, true);
|
||||
};
|
||||
this._super(...arguments);
|
||||
// Prevent double-clicks and browser glitches on all inputs
|
||||
this._onClickLessPaymentIcons = preventDoubleClick(this._onClickLessPaymentIcons);
|
||||
this._onClickMorePaymentIcons = preventDoubleClick(this._onClickMorePaymentIcons);
|
||||
this._onClickPay = preventDoubleClick(this._onClickPay);
|
||||
this._onClickPaymentOption = preventDoubleClick(this._onClickPaymentOption);
|
||||
this._onSubmit = preventDoubleClick(this._onSubmit);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handle a direct payment, a payment with redirection, or a payment by token.
|
||||
*
|
||||
* Called when clicking on the 'Pay' button or when submitting the form.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @return {undefined}
|
||||
*/
|
||||
_onClickPay: async function (ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
// Check that the user has selected a payment option
|
||||
const $checkedRadios = this.$('input[name="o_payment_radio"]:checked');
|
||||
if (!this._ensureRadioIsChecked($checkedRadios)) {
|
||||
return;
|
||||
}
|
||||
const checkedRadio = $checkedRadios[0];
|
||||
|
||||
// Extract contextual values from the radio button
|
||||
const provider = this._getProviderFromRadio(checkedRadio);
|
||||
const paymentOptionId = this._getPaymentOptionIdFromRadio(checkedRadio);
|
||||
const flow = this._getPaymentFlowFromRadio(checkedRadio);
|
||||
|
||||
// Update the tx context with the value of the "Save my payment details" checkbox
|
||||
if (flow !== 'token') {
|
||||
const $tokenizeCheckbox = this.$(
|
||||
`#o_payment_provider_inline_form_${paymentOptionId}` // Only match provider radios
|
||||
).find('input[name="o_payment_save_as_token"]');
|
||||
this.txContext.tokenizationRequested = $tokenizeCheckbox.length === 1
|
||||
&& $tokenizeCheckbox[0].checked;
|
||||
} else {
|
||||
this.txContext.tokenizationRequested = false;
|
||||
}
|
||||
|
||||
// Make the payment
|
||||
this._hideError(); // Don't keep the error displayed if the user is going through 3DS2
|
||||
this._disableButton(true); // Disable until it is needed again
|
||||
$('body').block({
|
||||
message: false,
|
||||
overlayCSS: {backgroundColor: "#000", opacity: 0, zIndex: 1050},
|
||||
});
|
||||
this._processPayment(provider, paymentOptionId, flow);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delegate the handling of the payment request to `_onClickPay`.
|
||||
*
|
||||
* Called when submitting the form (e.g. through the Return key).
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @return {undefined}
|
||||
*/
|
||||
_onSubmit: function (ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
this._onClickPay(ev);
|
||||
},
|
||||
|
||||
});
|
||||
return publicWidget.registry.PaymentCheckoutForm;
|
||||
});
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
/** @odoo-module */
|
||||
|
||||
import core from 'web.core';
|
||||
import publicWidget from 'web.public.widget';
|
||||
|
||||
publicWidget.registry.PaymentExpressCheckoutForm = publicWidget.Widget.extend({
|
||||
selector: 'form[name="o_payment_express_checkout_form"]',
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: async function () {
|
||||
await this._super(...arguments);
|
||||
this.txContext = {};
|
||||
Object.assign(this.txContext, this.$el.data());
|
||||
this.txContext.shippingInfoRequired = !!this.txContext.shippingInfoRequired;
|
||||
const expressCheckoutForms = this._getExpressCheckoutForms();
|
||||
for (const expressCheckoutForm of expressCheckoutForms) {
|
||||
await this._prepareExpressCheckoutForm(expressCheckoutForm.dataset);
|
||||
}
|
||||
// Monitor updates of the amount on eCommerce's cart pages.
|
||||
core.bus.on('cart_amount_changed', this, this._updateAmount.bind(this));
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Return all express checkout forms found on the page.
|
||||
*
|
||||
* @private
|
||||
* @return {NodeList} - All express checkout forms found on the page.
|
||||
*/
|
||||
_getExpressCheckoutForms() {
|
||||
return document.querySelectorAll(
|
||||
'form[name="o_payment_express_checkout_form"] div[name="o_express_checkout_container"]'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the provider-specific express checkout form based on the provided data.
|
||||
*
|
||||
* For a provider to manage an express checkout form, it must override this method.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} providerData - The provider-specific data.
|
||||
* @return {Promise}
|
||||
*/
|
||||
async _prepareExpressCheckoutForm(providerData) {
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the params to send to the transaction route.
|
||||
*
|
||||
* For a provider to overwrite generic params or to add provider-specific ones, it must override
|
||||
* this method and return the extended transaction route params.
|
||||
*
|
||||
* @private
|
||||
* @param {number} providerId - The id of the provider handling the transaction.
|
||||
* @returns {object} - The transaction route params
|
||||
*/
|
||||
_prepareTransactionRouteParams(providerId) {
|
||||
return {
|
||||
'payment_option_id': parseInt(providerId),
|
||||
'reference_prefix': this.txContext.referencePrefix &&
|
||||
this.txContent.referencePrefix.toString(),
|
||||
'currency_id': this.txContext.currencyId &&
|
||||
parseInt(this.txContext.currencyId),
|
||||
'partner_id': parseInt(this.txContext.partnerId),
|
||||
'flow': 'direct',
|
||||
'tokenization_requested': false,
|
||||
'landing_route': this.txContext.landingRoute,
|
||||
'access_token': this.txContext.accessToken,
|
||||
'csrf_token': core.csrf_token,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the amount of the express checkout form.
|
||||
*
|
||||
* For a provider to manage an express form, it must override this method.
|
||||
*
|
||||
* @private
|
||||
* @param {number} newAmount - The new amount.
|
||||
* @param {number} newMinorAmount - The new minor amount.
|
||||
* @return {undefined}
|
||||
*/
|
||||
_updateAmount(newAmount, newMinorAmount) {
|
||||
this.txContext.amount = parseFloat(newAmount);
|
||||
this.txContext.minorAmount = parseInt(newMinorAmount);
|
||||
this._getExpressCheckoutForms().forEach(form => {
|
||||
if (newAmount == 0) {
|
||||
form.classList.add('d-none')}
|
||||
else {
|
||||
form.classList.remove('d-none')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
export const paymentExpressCheckoutForm = publicWidget.registry.PaymentExpressCheckoutForm;
|
||||
|
|
@ -1,253 +0,0 @@
|
|||
odoo.define('payment.manage_form', require => {
|
||||
'use strict';
|
||||
|
||||
const core = require('web.core');
|
||||
const publicWidget = require('web.public.widget');
|
||||
const Dialog = require('web.Dialog');
|
||||
|
||||
const paymentFormMixin = require('payment.payment_form_mixin');
|
||||
|
||||
const _t = core._t;
|
||||
|
||||
publicWidget.registry.PaymentManageForm = publicWidget.Widget.extend(paymentFormMixin, {
|
||||
selector: 'form[name="o_payment_manage"]',
|
||||
events: Object.assign({}, publicWidget.Widget.prototype.events, {
|
||||
'click div[name="o_payment_option_card"]': '_onClickPaymentOption',
|
||||
'click a[name="o_payment_icon_more"]': '_onClickMorePaymentIcons',
|
||||
'click a[name="o_payment_icon_less"]': '_onClickLessPaymentIcons',
|
||||
'click button[name="o_payment_submit_button"]': '_onClickSaveToken',
|
||||
'click button[name="o_payment_delete_token"]': '_onClickDeleteToken',
|
||||
'submit': '_onSubmit',
|
||||
}),
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
init: function () {
|
||||
const preventDoubleClick = handlerMethod => {
|
||||
return _.debounce(handlerMethod, 500, true);
|
||||
};
|
||||
this._super(...arguments);
|
||||
// Prevent double-clicks and browser glitches on all inputs
|
||||
this._onClickDeleteToken = preventDoubleClick(this._onClickDeleteToken);
|
||||
this._onClickLessPaymentIcons = preventDoubleClick(this._onClickLessPaymentIcons);
|
||||
this._onClickMorePaymentIcons = preventDoubleClick(this._onClickMorePaymentIcons);
|
||||
this._onClickPaymentOption = preventDoubleClick(this._onClickPaymentOption);
|
||||
this._onClickSaveToken = preventDoubleClick(this._onClickSaveToken);
|
||||
this._onSubmit = preventDoubleClick(this._onSubmit);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Assign the token to a record.
|
||||
*
|
||||
* @private
|
||||
* @param {number} tokenId - The id of the token to assign
|
||||
* @return {undefined}
|
||||
*/
|
||||
_assignToken: function (tokenId) {
|
||||
// Call the assign route to assign the token to a record
|
||||
this._rpc({
|
||||
route: this.txContext.assignTokenRoute,
|
||||
params: {
|
||||
'access_token': this.txContext.accessToken,
|
||||
'token_id': tokenId,
|
||||
}
|
||||
}).then(() => {
|
||||
window.location = this.txContext.landingRoute;
|
||||
}).guardedCatch(error => {
|
||||
error.event.preventDefault();
|
||||
this._displayError(
|
||||
_t("Server Error"),
|
||||
_t("We are not able to save your payment method."),
|
||||
error.message.data.message
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Build the confirmation dialog based on the linked records' information.
|
||||
*
|
||||
* @private
|
||||
* @param {Array} linkedRecordsInfo - The list of information about linked records.
|
||||
* @param confirmCallback - The callback method called when the user clicks on the
|
||||
* confirmation button.
|
||||
* @return {object}
|
||||
*/
|
||||
_buildConfirmationDialog: function (linkedRecordsInfo, confirmCallback) {
|
||||
const $dialogContentMessage = $(
|
||||
'<span>', {text: _t("Are you sure you want to delete this payment method?")}
|
||||
);
|
||||
if (linkedRecordsInfo.length > 0) { // List the documents linked to the token.
|
||||
$dialogContentMessage.append($('<br>'));
|
||||
$dialogContentMessage.append($(
|
||||
'<span>', {text: _t("It is currently linked to the following documents:")}
|
||||
));
|
||||
const $documentInfoList = $('<ul>');
|
||||
linkedRecordsInfo.forEach(documentInfo => {
|
||||
$documentInfoList.append($('<li>').append($(
|
||||
'<a>', {
|
||||
href: documentInfo.url,
|
||||
target: '_blank',
|
||||
title: documentInfo.description,
|
||||
text: documentInfo.name
|
||||
}
|
||||
)));
|
||||
});
|
||||
$dialogContentMessage.append($documentInfoList);
|
||||
}
|
||||
return new Dialog(this, {
|
||||
title: _t("Warning!"),
|
||||
size: 'medium',
|
||||
$content: $('<div>').append($dialogContentMessage),
|
||||
buttons: [
|
||||
{
|
||||
text: _t("Confirm Deletion"), classes: 'btn-primary', close: true,
|
||||
click: confirmCallback,
|
||||
},
|
||||
{
|
||||
text: _t("Cancel"), close: true
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Search for documents linked to the token and ask the user for confirmation.
|
||||
*
|
||||
* If any such document is found, a confirmation dialog is shown.
|
||||
*
|
||||
* @private
|
||||
* @param {number} tokenId - The id of the token to delete
|
||||
* @return {undefined}
|
||||
*/
|
||||
_deleteToken: function (tokenId) {
|
||||
const execute = () => {
|
||||
this._rpc({
|
||||
route: '/payment/archive_token',
|
||||
params: {
|
||||
'token_id': tokenId,
|
||||
},
|
||||
}).then(() => {
|
||||
const $tokenCard = this.$(
|
||||
`input[name="o_payment_radio"][data-payment-option-id="${tokenId}"]` +
|
||||
`[data-payment-option-type="token"]`
|
||||
).closest('div[name="o_payment_option_card"]');
|
||||
$tokenCard.siblings(`#o_payment_token_inline_form_${tokenId}`).remove();
|
||||
$tokenCard.remove();
|
||||
this._disableButton(false);
|
||||
}).guardedCatch(error => {
|
||||
error.event.preventDefault();
|
||||
this._displayError(
|
||||
_t("Server Error"),
|
||||
_t("We are not able to delete your payment method."),
|
||||
error.message.data.message
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Fetch documents linked to the token
|
||||
this._rpc({
|
||||
model: 'payment.token',
|
||||
method: 'get_linked_records_info',
|
||||
args: [tokenId],
|
||||
}).then(linkedRecordsInfo => {
|
||||
this._buildConfirmationDialog(linkedRecordsInfo, execute).open();
|
||||
}).guardedCatch(error => {
|
||||
this._displayError(
|
||||
_t("Server Error"),
|
||||
_t("We are not able to delete your payment method."),
|
||||
error.message.data.message
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Find the radio button linked to the click 'Delete' button and trigger the token deletion.
|
||||
*
|
||||
* Let `_onClickPaymentOption` select the radio button and display the inline form.
|
||||
*
|
||||
* Called when clicking on the 'Delete' button of a token.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @return {undefined}
|
||||
*/
|
||||
_onClickDeleteToken: function (ev) {
|
||||
ev.preventDefault();
|
||||
|
||||
// Extract contextual values from the delete button
|
||||
const linkedRadio = $(ev.currentTarget).siblings().find('input[name="o_payment_radio"]')[0];
|
||||
const tokenId = this._getPaymentOptionIdFromRadio(linkedRadio);
|
||||
|
||||
// Delete the token
|
||||
this._deleteToken(tokenId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle the creation of a new token or the assignation of a token to a record.
|
||||
*
|
||||
* Called when clicking on the 'Save Payment Method' button of when submitting the form.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @return {undefined}
|
||||
*/
|
||||
_onClickSaveToken: async function (ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
// Check that the user has selected a payment option
|
||||
const $checkedRadios = this.$('input[name="o_payment_radio"]:checked');
|
||||
if (!this._ensureRadioIsChecked($checkedRadios)) {
|
||||
return;
|
||||
}
|
||||
const checkedRadio = $checkedRadios[0];
|
||||
|
||||
// Extract contextual values from the radio button
|
||||
const provider = this._getProviderFromRadio(checkedRadio);
|
||||
const paymentOptionId = this._getPaymentOptionIdFromRadio(checkedRadio);
|
||||
const flow = this._getPaymentFlowFromRadio(checkedRadio);
|
||||
|
||||
// Save the payment method
|
||||
this._hideError(); // Don't keep the error displayed if the user is going through 3DS2
|
||||
this._disableButton(true); // Disable until it is needed again
|
||||
$('body').block({
|
||||
message: false,
|
||||
overlayCSS: {backgroundColor: "#000", opacity: 0, zIndex: 1050},
|
||||
});
|
||||
if (flow !== 'token') { // Creation of a new token
|
||||
this.txContext.tokenizationRequested = true;
|
||||
this.txContext.isValidation = true;
|
||||
this._processPayment(provider, paymentOptionId, flow);
|
||||
} else if (this.txContext.allowTokenSelection) { // Assignation of a token to a record
|
||||
this._assignToken(paymentOptionId);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delegate the handling of the token to `_onClickSaveToken`.
|
||||
*
|
||||
* Called when submitting the form (e.g. through the Return key).
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @return {undefined}
|
||||
*/
|
||||
_onSubmit: function (ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
|
||||
this._onClickSaveToken(ev);
|
||||
},
|
||||
|
||||
});
|
||||
return publicWidget.registry.PaymentManageForm;
|
||||
});
|
||||
|
|
@ -1,533 +0,0 @@
|
|||
odoo.define('payment.payment_form_mixin', require => {
|
||||
'use strict';
|
||||
|
||||
const core = require('web.core');
|
||||
const Dialog = require('web.Dialog');
|
||||
|
||||
const _t = core._t;
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
start: async function () {
|
||||
this.txContext = {}; // Synchronously initialize txContext before any await.
|
||||
Object.assign(this.txContext, this.$el.data());
|
||||
await this._super(...arguments);
|
||||
window.addEventListener('pageshow', function (event) {
|
||||
if (event.persisted) {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
this.$('[data-bs-toggle="tooltip"]').tooltip();
|
||||
const $checkedRadios = this.$('input[name="o_payment_radio"]:checked');
|
||||
if ($checkedRadios.length === 1) {
|
||||
const checkedRadio = $checkedRadios[0];
|
||||
this._displayInlineForm(checkedRadio);
|
||||
this._enableButton();
|
||||
} else {
|
||||
this._setPaymentFlow(); // Initialize the payment flow to let providers overwrite it
|
||||
}
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Disable the submit button.
|
||||
*
|
||||
* The icons are updated to either show that an action is processing or that the button is
|
||||
* not ready, depending on the value of `showLoadingAnimation`.
|
||||
*
|
||||
* @private
|
||||
* @param {boolean} showLoadingAnimation - Whether a spinning loader should be shown
|
||||
* @return {undefined}
|
||||
*/
|
||||
_disableButton: function (showLoadingAnimation = true) {
|
||||
const $submitButton = this.$('button[name="o_payment_submit_button"]');
|
||||
const iconClass = $submitButton.data('icon-class');
|
||||
$submitButton.attr('disabled', true);
|
||||
if (showLoadingAnimation) {
|
||||
$submitButton.find('i').removeClass(iconClass);
|
||||
$submitButton.prepend(
|
||||
'<span class="o_loader"><i class="fa fa-refresh fa-spin"></i> </span>'
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Display an error in the payment form.
|
||||
*
|
||||
* If no payment option is selected, the error is displayed in a dialog. If exactly one
|
||||
* payment option is selected, the error is displayed in the inline form of that payment
|
||||
* option and the view is focused on the error.
|
||||
*
|
||||
* @private
|
||||
* @param {string} title - The title of the error
|
||||
* @param {string} description - The description of the error
|
||||
* @param {string} error - The raw error message
|
||||
* @return {(Dialog|undefined)} A dialog showing the error if no payment option is selected,
|
||||
* undefined otherwise.
|
||||
*/
|
||||
_displayError: function (title, description = '', error = '') {
|
||||
const $checkedRadios = this.$('input[name="o_payment_radio"]:checked');
|
||||
if ($checkedRadios.length !== 1) { // Cannot find selected payment option, show dialog
|
||||
return new Dialog(null, {
|
||||
title: _.str.sprintf(_t("Error: %s"), title),
|
||||
size: 'medium',
|
||||
$content: `<p>${_.str.escapeHTML(description) || ''}</p>`,
|
||||
buttons: [{text: _t("Ok"), close: true}]
|
||||
}).open();
|
||||
} else { // Show error in inline form
|
||||
this._hideError(); // Remove any previous error
|
||||
|
||||
// Build the html for the error
|
||||
let errorHtml = `<div class="alert alert-danger mb4" name="o_payment_error">
|
||||
<b>${_.str.escapeHTML(title)}</b>`;
|
||||
if (description !== '') {
|
||||
errorHtml += `</br>${_.str.escapeHTML(description)}`;
|
||||
}
|
||||
if (error !== '') {
|
||||
errorHtml += `</br>${_.str.escapeHTML(error)}`;
|
||||
}
|
||||
errorHtml += '</div>';
|
||||
|
||||
// Append error to inline form and center the page on the error
|
||||
const checkedRadio = $checkedRadios[0];
|
||||
const paymentOptionId = this._getPaymentOptionIdFromRadio(checkedRadio);
|
||||
const formType = $(checkedRadio).data('payment-option-type');
|
||||
const $inlineForm = this.$(`#o_payment_${formType}_inline_form_${paymentOptionId}`);
|
||||
$inlineForm.removeClass('d-none'); // Show the inline form even if it was empty
|
||||
$inlineForm.append(errorHtml).find('div[name="o_payment_error"]')[0]
|
||||
.scrollIntoView({behavior: 'smooth', block: 'center'});
|
||||
}
|
||||
this._enableButton(); // Enable button back after it was disabled before processing
|
||||
$('body').unblock(); // The page is blocked at this point, unblock it
|
||||
},
|
||||
|
||||
/**
|
||||
* Display the inline form of the selected payment option and hide others.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLInputElement} radio - The radio button linked to the payment option
|
||||
* @return {undefined}
|
||||
*/
|
||||
_displayInlineForm: function (radio) {
|
||||
this._hideInlineForms(); // Collapse previously opened inline forms
|
||||
this._hideError(); // The error is only relevant until it is hidden with its inline form
|
||||
this._setPaymentFlow(); // Reset the payment flow to let providers overwrite it
|
||||
|
||||
// Extract contextual values from the radio button
|
||||
const provider = this._getProviderFromRadio(radio);
|
||||
const paymentOptionId = this._getPaymentOptionIdFromRadio(radio);
|
||||
const flow = this._getPaymentFlowFromRadio(radio);
|
||||
|
||||
// Prepare the inline form of the selected payment option and display it if not empty
|
||||
this._prepareInlineForm(provider, paymentOptionId, flow);
|
||||
const formType = $(radio).data('payment-option-type');
|
||||
const $inlineForm = this.$(`#o_payment_${formType}_inline_form_${paymentOptionId}`);
|
||||
if ($inlineForm.children().length > 0) {
|
||||
$inlineForm.removeClass('d-none');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the submit button can be enabled and do it if so.
|
||||
*
|
||||
* The icons are updated to show that the button is ready.
|
||||
*
|
||||
* @private
|
||||
* @return {boolean} Whether the button was enabled.
|
||||
*/
|
||||
_enableButton: function () {
|
||||
if (this._isButtonReady()) {
|
||||
const $submitButton = this.$('button[name="o_payment_submit_button"]');
|
||||
const iconClass = $submitButton.data('icon-class');
|
||||
$submitButton.attr('disabled', false);
|
||||
$submitButton.find('i').addClass(iconClass);
|
||||
$submitButton.find('span.o_loader').remove();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify that exactly one radio button is checked and display an error otherwise.
|
||||
*
|
||||
* @private
|
||||
* @param {jQuery} $checkedRadios - The currently check radio buttons
|
||||
*
|
||||
* @return {boolean} Whether exactly one radio button among the provided radios is checked
|
||||
*/
|
||||
_ensureRadioIsChecked: function ($checkedRadios) {
|
||||
if ($checkedRadios.length === 0) {
|
||||
this._displayError(
|
||||
_t("No payment option selected"),
|
||||
_t("Please select a payment option.")
|
||||
);
|
||||
return false;
|
||||
} else if ($checkedRadios.length > 1) {
|
||||
this._displayError(
|
||||
_t("Multiple payment options selected"),
|
||||
_t("Please select only one payment option.")
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine and return the online payment flow of the selected payment option.
|
||||
*
|
||||
* As some providers implement both the direct payment and the payment with redirection, the
|
||||
* flow cannot be inferred from the radio button only. The radio button only indicates
|
||||
* whether the payment option is a token. If not, the transaction context is looked up to
|
||||
* determine whether the flow is 'direct' or 'redirect'.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLInputElement} radio - The radio button linked to the payment option
|
||||
* @return {string} The flow of the selected payment option. redirect, direct or token.
|
||||
*/
|
||||
_getPaymentFlowFromRadio: function (radio) {
|
||||
if (
|
||||
$(radio).data('payment-option-type') === 'token'
|
||||
|| this.txContext.flow === 'token'
|
||||
) {
|
||||
return 'token';
|
||||
} else if (this.txContext.flow === 'redirect') {
|
||||
return 'redirect';
|
||||
} else {
|
||||
return 'direct';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine and return the id of the selected payment option.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLInputElement} radio - The radio button linked to the payment option
|
||||
* @return {number} The provider id or the token id or of the payment option linked to the
|
||||
* radio button.
|
||||
*/
|
||||
_getPaymentOptionIdFromRadio: radio => $(radio).data('payment-option-id'),
|
||||
|
||||
/**
|
||||
* Determine and return the provider of the selected payment option.
|
||||
*
|
||||
* @private
|
||||
* @param {HTMLInputElement} radio - The radio button linked to the payment option
|
||||
* @return {number} The provider of the payment option linked to the radio button.
|
||||
*/
|
||||
_getProviderFromRadio: radio => $(radio).data('provider'),
|
||||
|
||||
/**
|
||||
* Remove the error in the provider form.
|
||||
*
|
||||
* @private
|
||||
* @return {jQuery} The removed error
|
||||
*/
|
||||
_hideError: () => this.$('div[name="o_payment_error"]').remove(),
|
||||
|
||||
/**
|
||||
* Collapse all inline forms.
|
||||
*
|
||||
* @private
|
||||
* @return {undefined}.
|
||||
*/
|
||||
_hideInlineForms: () => this.$('[name="o_payment_inline_form"]').addClass('d-none'),
|
||||
|
||||
/**
|
||||
* Hide the "Save my payment details" label and checkbox, and the submit button.
|
||||
*
|
||||
* The inputs should typically be hidden when the customer has to perform additional actions
|
||||
* in the inline form. All inputs are automatically shown again when the customer clicks on
|
||||
* another inline form.
|
||||
*
|
||||
* @private
|
||||
* @return {undefined}
|
||||
*/
|
||||
_hideInputs: function () {
|
||||
const $submitButton = this.$('button[name="o_payment_submit_button"]');
|
||||
const $tokenizeCheckboxes = this.$('input[name="o_payment_save_as_token"]');
|
||||
$submitButton.addClass('d-none');
|
||||
$tokenizeCheckboxes.closest('label').addClass('d-none');
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify that the submit button is ready to be enabled.
|
||||
*
|
||||
* For a module to support a custom behavior for the submit button, it must override this
|
||||
* method and only return true if the result of this method is true and if nothing prevents
|
||||
* enabling the submit button for that custom behavior.
|
||||
*
|
||||
* @private
|
||||
*
|
||||
* @return {boolean} Whether the submit button can be enabled
|
||||
*/
|
||||
_isButtonReady: function () {
|
||||
const $checkedRadios = this.$('input[name="o_payment_radio"]:checked');
|
||||
if ($checkedRadios.length === 1) {
|
||||
const checkedRadio = $checkedRadios[0];
|
||||
const flow = this._getPaymentFlowFromRadio(checkedRadio);
|
||||
return flow !== 'token' || this.txContext.allowTokenSelection;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the params to send to the transaction route.
|
||||
*
|
||||
* For a provider to overwrite generic params or to add provider-specific ones, it must
|
||||
* override this method and return the extended transaction route params.
|
||||
*
|
||||
* @private
|
||||
* @param {string} code - The code of the selected payment option provider
|
||||
* @param {number} paymentOptionId - The id of the selected payment option
|
||||
* @param {string} flow - The online payment flow of the selected payment option
|
||||
* @return {object} The transaction route params
|
||||
*/
|
||||
_prepareTransactionRouteParams: function (code, paymentOptionId, flow) {
|
||||
return {
|
||||
'payment_option_id': paymentOptionId,
|
||||
'reference_prefix': this.txContext.referencePrefix !== undefined
|
||||
? this.txContext.referencePrefix.toString() : null,
|
||||
'amount': this.txContext.amount !== undefined
|
||||
? parseFloat(this.txContext.amount) : null,
|
||||
'currency_id': this.txContext.currencyId
|
||||
? parseInt(this.txContext.currencyId) : null,
|
||||
'partner_id': parseInt(this.txContext.partnerId),
|
||||
'flow': flow,
|
||||
'tokenization_requested': this.txContext.tokenizationRequested,
|
||||
'landing_route': this.txContext.landingRoute,
|
||||
'is_validation': this.txContext.isValidation,
|
||||
'access_token': this.txContext.accessToken
|
||||
? this.txContext.accessToken : undefined,
|
||||
'csrf_token': core.csrf_token,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Prepare the provider-specific inline form of the selected payment option.
|
||||
*
|
||||
* For a provider to manage an inline form, it must override this method. When the override
|
||||
* is called, it must lookup the parameters to decide whether it is necessary to prepare its
|
||||
* inline form. Otherwise, the call must be sent back to the parent method.
|
||||
*
|
||||
* @private
|
||||
* @param {string} code - The code of the selected payment option's provider
|
||||
* @param {number} paymentOptionId - The id of the selected payment option
|
||||
* @param {string} flow - The online payment flow of the selected payment option
|
||||
* @return {Promise}
|
||||
*/
|
||||
_prepareInlineForm: (code, paymentOptionId, flow) => Promise.resolve(),
|
||||
|
||||
/**
|
||||
* Process the payment.
|
||||
*
|
||||
* For a provider to do pre-processing work on the transaction processing flow, or to
|
||||
* define its entire own flow that requires re-scheduling the RPC to the transaction route,
|
||||
* it must override this method.
|
||||
* If only post-processing work is needed, an override of `_processRedirectPayment`,
|
||||
* `_processDirectPayment` or `_processTokenPayment` might be more appropriate.
|
||||
*
|
||||
* @private
|
||||
* @param {string} code - The code of the payment option's provider
|
||||
* @param {number} paymentOptionId - The id of the payment option handling the transaction
|
||||
* @param {string} flow - The online payment flow of the transaction
|
||||
* @return {Promise}
|
||||
*/
|
||||
_processPayment: function (code, paymentOptionId, flow) {
|
||||
// Call the transaction route to create a tx and retrieve the processing values
|
||||
return this._rpc({
|
||||
route: this.txContext.transactionRoute,
|
||||
params: this._prepareTransactionRouteParams(code, paymentOptionId, flow),
|
||||
}).then(processingValues => {
|
||||
if (flow === 'redirect') {
|
||||
return this._processRedirectPayment(
|
||||
code, paymentOptionId, processingValues
|
||||
);
|
||||
} else if (flow === 'direct') {
|
||||
return this._processDirectPayment(code, paymentOptionId, processingValues);
|
||||
} else if (flow === 'token') {
|
||||
return this._processTokenPayment(code, paymentOptionId, processingValues);
|
||||
}
|
||||
}).guardedCatch(error => {
|
||||
error.event.preventDefault();
|
||||
this._displayError(
|
||||
_t("Server Error"),
|
||||
_t("We are not able to process your payment."),
|
||||
error.message.data.message
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Execute the provider-specific implementation of the direct payment flow.
|
||||
*
|
||||
* For a provider to redefine the processing of the direct payment flow, it must override
|
||||
* this method.
|
||||
*
|
||||
* @private
|
||||
* @param {string} code - The code of the provider
|
||||
* @param {number} providerId - The id of the provider handling the transaction
|
||||
* @param {object} processingValues - The processing values of the transaction
|
||||
* @return {Promise}
|
||||
*/
|
||||
_processDirectPayment: (code, providerId, processingValues) => Promise.resolve(),
|
||||
|
||||
/**
|
||||
* Redirect the customer by submitting the redirect form included in the processing values.
|
||||
*
|
||||
* For a provider to redefine the processing of the payment with redirection flow, it must
|
||||
* override this method.
|
||||
*
|
||||
* @private
|
||||
* @param {string} code - The code of the provider
|
||||
* @param {number} providerId - The id of the provider handling the transaction
|
||||
* @param {object} processingValues - The processing values of the transaction
|
||||
* @return {undefined}
|
||||
*/
|
||||
_processRedirectPayment: (code, providerId, processingValues) => {
|
||||
// Append the redirect form to the body
|
||||
const $redirectForm = $(processingValues.redirect_form_html).attr(
|
||||
'id', 'o_payment_redirect_form'
|
||||
);
|
||||
// Ensures external redirections when in an iframe.
|
||||
$redirectForm[0].setAttribute('target', '_top');
|
||||
$(document.getElementsByTagName('body')[0]).append($redirectForm);
|
||||
|
||||
// Submit the form
|
||||
$redirectForm.submit();
|
||||
},
|
||||
|
||||
/**
|
||||
* Redirect the customer to the status route.
|
||||
*
|
||||
* For a provider to redefine the processing of the payment by token flow, it must override
|
||||
* this method.
|
||||
*
|
||||
* @private
|
||||
* @param {string} provider_code - The code of the token's provider
|
||||
* @param {number} tokenId - The id of the token handling the transaction
|
||||
* @param {object} processingValues - The processing values of the transaction
|
||||
* @return {undefined}
|
||||
*/
|
||||
_processTokenPayment: (provider_code, tokenId, processingValues) => {
|
||||
// The flow is already completed as payments by tokens are immediately processed
|
||||
window.location = '/payment/status';
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the online payment flow for the selected payment option.
|
||||
*
|
||||
* For a provider to manage direct payments, it must call this method from within its
|
||||
* override of `_prepareInlineForm` to declare its payment flow for the selected payment
|
||||
* option.
|
||||
*
|
||||
* @private
|
||||
* @param {string} flow - The flow for the selected payment option. Either 'redirect',
|
||||
* 'direct' or 'token'
|
||||
* @return {undefined}
|
||||
*/
|
||||
_setPaymentFlow: function (flow = 'redirect') {
|
||||
if (flow !== 'redirect' && flow !== 'direct' && flow !== 'token') {
|
||||
console.warn(
|
||||
`payment_form_mixin: method '_setPaymentFlow' was called with invalid flow:
|
||||
${flow}. Falling back to 'redirect'.`
|
||||
);
|
||||
this.txContext.flow = 'redirect';
|
||||
} else {
|
||||
this.txContext.flow = flow;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Show the "Save my payment details" label and checkbox, and the submit button.
|
||||
*
|
||||
* @private
|
||||
* @return {undefined}.
|
||||
*/
|
||||
_showInputs: function () {
|
||||
const $submitButton = this.$('button[name="o_payment_submit_button"]');
|
||||
const $tokenizeCheckboxes = this.$('input[name="o_payment_save_as_token"]');
|
||||
$submitButton.removeClass('d-none');
|
||||
$tokenizeCheckboxes.closest('label').removeClass('d-none');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Hide all extra payment icons of the provider linked to the clicked button.
|
||||
*
|
||||
* Called when clicking on the "show less" button.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @return {undefined}
|
||||
*/
|
||||
_onClickLessPaymentIcons: ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
// Hide the extra payment icons, and the "show less" button
|
||||
const $itemList = $(ev.currentTarget).parents('ul');
|
||||
const maxIconNumber = $itemList.data('max-icons');
|
||||
$itemList.children('li').slice(maxIconNumber).addClass('d-none');
|
||||
// Show the "show more" button
|
||||
$itemList.find('a[name="o_payment_icon_more"]').parents('li').removeClass('d-none');
|
||||
},
|
||||
|
||||
/**
|
||||
* Display all the payment icons of the provider linked to the clicked button.
|
||||
*
|
||||
* Called when clicking on the "show more" button.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @return {undefined}
|
||||
*/
|
||||
_onClickMorePaymentIcons: ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
// Display all the payment icons, and the "show less" button
|
||||
$(ev.currentTarget).parents('ul').children('li').removeClass('d-none');
|
||||
// Hide the "show more" button
|
||||
$(ev.currentTarget).parents('li').addClass('d-none');
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark the clicked card radio button as checked and open the inline form, if any.
|
||||
*
|
||||
* Called when clicking on the card of a payment option.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
* @return {undefined}
|
||||
*/
|
||||
_onClickPaymentOption: function (ev) {
|
||||
// Uncheck all radio buttons
|
||||
this.$('input[name="o_payment_radio"]').prop('checked', false);
|
||||
// Check radio button linked to selected payment option
|
||||
const checkedRadio = $(ev.currentTarget).find('input[name="o_payment_radio"]')[0];
|
||||
$(checkedRadio).prop('checked', true);
|
||||
|
||||
// Show the inputs in case they had been hidden
|
||||
this._showInputs();
|
||||
|
||||
// Disable the submit button while building the content
|
||||
this._disableButton(false);
|
||||
|
||||
// Unfold and prepare the inline form of selected payment option
|
||||
this._displayInlineForm(checkedRadio);
|
||||
|
||||
// Re-enable the submit button
|
||||
this._enableButton();
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import {
|
||||
copyClipboardButtonField,
|
||||
CopyClipboardButtonField,
|
||||
} from "@web/views/fields/copy_clipboard/copy_clipboard_field";
|
||||
|
||||
import { CopyButton } from "@web/core/copy_button/copy_button";
|
||||
|
||||
class PaymentWizardCopyButton extends CopyButton {
|
||||
async onClick() {
|
||||
await this.env.model.mutex.getUnlockedDef();
|
||||
return super.onClick();
|
||||
}
|
||||
}
|
||||
|
||||
class PaymentWizardCopyClipboardButtonField extends CopyClipboardButtonField {
|
||||
static components = { CopyButton: PaymentWizardCopyButton };
|
||||
}
|
||||
|
||||
const paymentWizardCopyClipboardButtonField = {
|
||||
...copyClipboardButtonField,
|
||||
component: PaymentWizardCopyClipboardButtonField,
|
||||
};
|
||||
|
||||
registry
|
||||
.category("fields")
|
||||
.add("PaymentWizardCopyClipboardButtonField", paymentWizardCopyClipboardButtonField);
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
odoo.define('payment.post_processing', function (require) {
|
||||
'use strict';
|
||||
|
||||
var publicWidget = require('web.public.widget');
|
||||
var core = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
var _t = core._t;
|
||||
|
||||
$.blockUI.defaults.css.border = '0';
|
||||
$.blockUI.defaults.css["background-color"] = '';
|
||||
$.blockUI.defaults.overlayCSS["opacity"] = '0.9';
|
||||
|
||||
publicWidget.registry.PaymentPostProcessing = publicWidget.Widget.extend({
|
||||
selector: 'div[name="o_payment_status"]',
|
||||
|
||||
_pollCount: 0,
|
||||
|
||||
start: function() {
|
||||
this.displayLoading();
|
||||
this.poll();
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
/* Methods */
|
||||
startPolling: function () {
|
||||
var timeout = 3000;
|
||||
//
|
||||
if(this._pollCount >= 10 && this._pollCount < 20) {
|
||||
timeout = 10000;
|
||||
}
|
||||
else if(this._pollCount >= 20) {
|
||||
timeout = 30000;
|
||||
}
|
||||
//
|
||||
setTimeout(this.poll.bind(this), timeout);
|
||||
this._pollCount ++;
|
||||
},
|
||||
poll: function () {
|
||||
var self = this;
|
||||
this._rpc({
|
||||
route: '/payment/status/poll',
|
||||
params: {
|
||||
'csrf_token': core.csrf_token,
|
||||
}
|
||||
}).then(function(data) {
|
||||
if(data.success === true) {
|
||||
self.processPolledData(data.display_values_list);
|
||||
}
|
||||
else {
|
||||
switch(data.error) {
|
||||
case "tx_process_retry":
|
||||
break;
|
||||
case "no_tx_found":
|
||||
self.displayContent("payment.no_tx_found", {});
|
||||
break;
|
||||
default: // if an exception is raised
|
||||
self.displayContent("payment.exception", {exception_msg: data.error});
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.startPolling();
|
||||
|
||||
}).guardedCatch(function() {
|
||||
self.displayContent("payment.rpc_error", {});
|
||||
self.startPolling();
|
||||
});
|
||||
},
|
||||
processPolledData: function (display_values_list) {
|
||||
var render_values = {
|
||||
'tx_draft': [],
|
||||
'tx_pending': [],
|
||||
'tx_authorized': [],
|
||||
'tx_done': [],
|
||||
'tx_cancel': [],
|
||||
'tx_error': [],
|
||||
};
|
||||
|
||||
// group the transaction according to their state
|
||||
display_values_list.forEach(function (display_values) {
|
||||
var key = 'tx_' + display_values.state;
|
||||
if(key in render_values) {
|
||||
if (display_values["display_message"]) {
|
||||
display_values.display_message = Markup(display_values.display_message)
|
||||
}
|
||||
render_values[key].push(display_values);
|
||||
}
|
||||
});
|
||||
|
||||
function countTxInState(states) {
|
||||
var nbTx = 0;
|
||||
for (var prop in render_values) {
|
||||
if (states.indexOf(prop) > -1 && render_values.hasOwnProperty(prop)) {
|
||||
nbTx += render_values[prop].length;
|
||||
}
|
||||
}
|
||||
return nbTx;
|
||||
}
|
||||
|
||||
/*
|
||||
* When the server sends the list of monitored transactions, it tries to post-process
|
||||
* all the successful ones. If it succeeds or if the post-process has already been made,
|
||||
* the transaction is removed from the list of monitored transactions and won't be
|
||||
* included in the next response. We assume that successful and post-process
|
||||
* transactions should always prevail on others, regardless of their number or state.
|
||||
*/
|
||||
if (render_values['tx_done'].length === 1 &&
|
||||
render_values['tx_done'][0].is_post_processed) {
|
||||
window.location = render_values['tx_done'][0].landing_route;
|
||||
return;
|
||||
}
|
||||
// If there are multiple transactions monitored, display them all to the customer. If
|
||||
// there is only one transaction monitored, redirect directly the customer to the
|
||||
// landing route.
|
||||
if(countTxInState(['tx_done', 'tx_error', 'tx_pending', 'tx_authorized']) === 1) {
|
||||
// We don't want to redirect customers to the landing page when they have a pending
|
||||
// transaction. The successful transactions are dealt with before.
|
||||
var tx = render_values['tx_authorized'][0] || render_values['tx_error'][0];
|
||||
if (tx) {
|
||||
window.location = tx.landing_route;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.displayContent("payment.display_tx_list", render_values);
|
||||
},
|
||||
displayContent: function (xmlid, render_values) {
|
||||
var html = core.qweb.render(xmlid, render_values);
|
||||
$.unblockUI();
|
||||
this.$el.find('div[name="o_payment_status_content"]').html(html);
|
||||
},
|
||||
displayLoading: function () {
|
||||
var msg = _t("We are processing your payment, please wait ...");
|
||||
$.blockUI({
|
||||
'message': '<h2 class="text-white"><img src="/web/static/img/spin.png" class="fa-pulse"/>' +
|
||||
' <br />' + msg +
|
||||
'</h2>'
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return publicWidget.registry.PaymentPostProcessing;
|
||||
});
|
||||
|
|
@ -1,62 +1,20 @@
|
|||
.o_payment_form {
|
||||
label > input[type="radio"], input[type="checkbox"]{
|
||||
vertical-align: middle;
|
||||
margin-right: 5px;
|
||||
.o_payment_form .o_outline {
|
||||
|
||||
&:hover {
|
||||
border-color: $primary;
|
||||
}
|
||||
|
||||
.payment_option_name {
|
||||
font-size: 14px;
|
||||
font-weight: normal !important;
|
||||
font-family: Helvetica Neue, sans-serif;
|
||||
line-height: 1.3em;
|
||||
color: #4d4d4d;
|
||||
&:not(:first-child):hover {
|
||||
// Since list-group items, except the first child, have no top border, this emulates the top
|
||||
// border for the hovered state.
|
||||
box-shadow: 0 (-$border-width) 0 $primary;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: normal;
|
||||
margin-top: 5px;
|
||||
.o_payment_option_label:before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
content: '';
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card {
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid #dddddd;
|
||||
}
|
||||
padding: 1.14em !important;
|
||||
&.o_payment_option_card:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 0.5rem;
|
||||
label {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.card-footer:last-child {
|
||||
border-bottom-right-radius: 10px !important;
|
||||
border-bottom-left-radius: 10px !important;
|
||||
}
|
||||
|
||||
.payment_icon_list {
|
||||
position: relative;
|
||||
li {
|
||||
padding-left: 5px !important;
|
||||
padding-right: 0px !important;
|
||||
}
|
||||
|
||||
.more_option {
|
||||
@include o-position-absolute($right: 10px);
|
||||
font-size:10px;
|
||||
}
|
||||
|
||||
margin-top: 0px !important;
|
||||
margin-bottom: -5px !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
div#o_payment_status_alert > p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
input#cc_number {
|
||||
background-repeat: no-repeat;
|
||||
background-position: center right calc(2.7em);
|
||||
}
|
||||
|
||||
div.card_placeholder {
|
||||
background-image: url("/payment/static/src/img/placeholder.png");
|
||||
background-repeat: no-repeat;
|
||||
width: 32px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 20px;
|
||||
-webkit-transition: 0.4s cubic-bezier(0.455,0.03,0.515,0.955);
|
||||
transition: 0.4s cubic-bezier(0.455,0.03,0.515,0.955);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* if s2s form not in bootstrap_formatting */
|
||||
div.o_card_brand_detail {
|
||||
position: relative;
|
||||
|
||||
div.card_placeholder {
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
div.amex {
|
||||
background-image: url("/payment/static/src/img/amex.png");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
div.diners {
|
||||
background-image: url("/payment/static/src/img/diners.png");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
div.discover {
|
||||
background-image: url("/payment/static/src/img/discover.png");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
div.jcb {
|
||||
background-image: url("/payment/static/src/img/jcb.png");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
div.mastercard {
|
||||
background-image: url("/payment/static/src/img/mastercard.png");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
div.visa {
|
||||
background-image: url("/payment/static/src/img/visa.png");
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
ul.checkout img.rounded {
|
||||
max-width: 100px;
|
||||
max-height: 40px;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
div[name="o_payment_status_alert"] div > p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.o_payment_summary_separator {
|
||||
@include media-breakpoint-up(md) {
|
||||
border-left: $border-width solid $border-color;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="payment.deleteTokenDialog">
|
||||
<div>
|
||||
<p>Are you sure you want to delete this payment method?</p>
|
||||
<t t-if="linkedRecordsInfo.length > 0">
|
||||
<p>It is currently linked to the following documents:</p>
|
||||
<ul>
|
||||
<li t-foreach="linkedRecordsInfo" t-as="documentInfo" t-key="documentInfoIndex">
|
||||
<a t-att-title="documentInfo.description"
|
||||
t-att-href="documentInfo.url"
|
||||
t-esc="documentInfo.name"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="payment" xml:space="preserve">
|
||||
<!-- The templates here as rendered by 'post_processing.js', you can also take
|
||||
a look at payment_portal_templates.xml (xmlid: payment_status) for more infos-->
|
||||
<t t-name="payment.display_tx_list">
|
||||
<div>
|
||||
<!-- Error transactions -->
|
||||
<div t-if="tx_error.length > 0">
|
||||
<h1>Failed operations</h1>
|
||||
<ul class="list-group">
|
||||
<t t-foreach="tx_error" t-as="tx">
|
||||
<a t-att-href="tx['landing_route']" class="list-group-item">
|
||||
<h4 class="list-group-item-heading mb5">
|
||||
<t t-esc="tx['reference']"/>
|
||||
<span class="badge text-bg-light float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
|
||||
</h4>
|
||||
<small class="list-group-item-text">
|
||||
An error occurred during the processing of this payment.<br/>
|
||||
<strong>Reason:</strong> <t t-esc="tx['state_message']"/>
|
||||
</small>
|
||||
</a>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Pending/Authorized/Confirmed transactions -->
|
||||
<div t-if="tx_done.length > 0 || tx_authorized.length > 0 || tx_pending.length > 0">
|
||||
<h1>Operations in progress</h1>
|
||||
<div class="list-group">
|
||||
<!-- Done transactions -->
|
||||
<t t-foreach="tx_done" t-as="tx">
|
||||
<a t-att-href="tx['landing_route']" class="list-group-item">
|
||||
<h4 class="list-group-item-heading mb5">
|
||||
<t t-esc="tx['reference']"/>
|
||||
<span class="badge text-bg-light float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
|
||||
</h4>
|
||||
<small class="list-group-item-text">
|
||||
<t t-if="!tx['is_post_processed']">
|
||||
<t t-if="tx['operation'] != 'validation'">
|
||||
Your payment is being processed, please wait... <i class="fa fa-cog fa-spin"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
Saving your payment method, please wait... <i class="fa fa-cog fa-spin"/>
|
||||
</t>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-if="tx['operation'] != 'validation'">
|
||||
Your payment has been processed.<br/>
|
||||
Click here to be redirected to the confirmation page.
|
||||
</t>
|
||||
<t t-else="">
|
||||
Your payment method has been saved.<br/>
|
||||
Click here to be redirected to the confirmation page.
|
||||
</t>
|
||||
</t>
|
||||
</small>
|
||||
</a>
|
||||
</t>
|
||||
<!-- Pending transactions -->
|
||||
<t t-foreach="tx_pending" t-as="tx">
|
||||
<a t-att-href="tx['landing_route']" class="list-group-item">
|
||||
<h4 class="list-group-item-heading mb5">
|
||||
<t t-esc="tx['reference']"/>
|
||||
<span class="badge text-bg-light float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
|
||||
</h4>
|
||||
<small class="list-group-item-text">
|
||||
<t t-if="tx['display_message']">
|
||||
<!-- display_message is the content of the HTML field associated
|
||||
with the current transaction state, set on the provider. -->
|
||||
<t t-out="tx['display_message']"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
Your payment is in pending state.<br/>
|
||||
You will be notified when the payment is fully confirmed.<br/>
|
||||
Click here to be redirected to the confirmation page.
|
||||
</t>
|
||||
</small>
|
||||
</a>
|
||||
</t>
|
||||
<!-- Authorized transactions -->
|
||||
<t t-foreach="tx_authorized" t-as="tx">
|
||||
<a t-att-href="tx['landing_route']" class="list-group-item">
|
||||
<h4 class="list-group-item-heading mb5">
|
||||
<t t-esc="tx['reference']"/>
|
||||
<span class="badge text-bg-light float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
|
||||
</h4>
|
||||
<small class="list-group-item-text">
|
||||
<t t-if="tx['display_message']">
|
||||
<!-- display_message is the content of the HTML field associated
|
||||
with the current transaction state, set on the provider. -->
|
||||
<t t-out="tx['display_message']"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
Your payment has been received but need to be confirmed manually.<br/>
|
||||
You will be notified when the payment is confirmed.
|
||||
</t>
|
||||
</small>
|
||||
</a>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Draft transactions -->
|
||||
<div t-if="tx_draft.length > 0">
|
||||
<h1>Waiting for operations to process</h1>
|
||||
<ul class="list-group">
|
||||
<t t-foreach="tx_draft" t-as="tx">
|
||||
<a t-att-href="tx['landing_route']" class="list-group-item">
|
||||
<h4 class="list-group-item-heading mb5">
|
||||
<t t-esc="tx['reference']"/>
|
||||
<span class="badge text-bg-light float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
|
||||
</h4>
|
||||
<small class="list-group-item-text">
|
||||
<t t-if="tx['display_message']">
|
||||
<!-- display_message is the content of the HTML field associated
|
||||
with the current transaction state, set on the provider. -->
|
||||
<t t-out="tx['display_message']"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
We are waiting for the payment provider to confirm the payment.
|
||||
</t>
|
||||
</small>
|
||||
</a>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Cancel transactions -->
|
||||
<div t-if="tx_cancel.length > 0">
|
||||
<h1>Canceled operations</h1>
|
||||
<ul class="list-group">
|
||||
<t t-foreach="tx_cancel" t-as="tx">
|
||||
<a t-att-href="tx['landing_route']" class="list-group-item">
|
||||
<h4 class="list-group-item-heading mb5">
|
||||
<t t-esc="tx['reference']"/>
|
||||
<span class="badge text-bg-light float-end"><t t-esc="tx['amount']"/> <t t-esc="tx['currency_code']"/></span>
|
||||
</h4>
|
||||
<small class="list-group-item-text">
|
||||
This payment has been canceled.<br/>
|
||||
No payment has been processed.<br/>
|
||||
<t t-if="tx['state_message']">
|
||||
<strong>Reason:</strong> <t t-esc="tx['state_message']"/>
|
||||
</t>
|
||||
</small>
|
||||
</a>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="payment.no_tx_found">
|
||||
<div class="text-center">
|
||||
<p>We are not able to find your payment, but don't worry.</p>
|
||||
<p>You should receive an email confirming your payment in a few minutes.</p>
|
||||
<p>If the payment hasn't been confirmed you can contact us.</p>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="payment.rpc_error">
|
||||
<div class="text-center">
|
||||
<p><strong>Server error:</strong> Unable to contact the Odoo server.</p>
|
||||
<p>Please wait ... <i class="fa fa-refresh fa-spin"></i></p>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="payment.exception">
|
||||
<div class="text-center">
|
||||
<h2>Internal server error</h2>
|
||||
<pre><t t-esc="exception_msg"/></pre>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||