billwerk_subscriptions-1.x-dev/modules/billwerk_subscriptions_manage/js/billwerk_subscriptions_manage.js
modules/billwerk_subscriptions_manage/js/billwerk_subscriptions_manage.js
/**
* @file
* Billwerk behaviors.
*
* @see https://billwerk.readme.io/reference/subscriptionjsportal
*/
(function (Drupal, drupalSettings) {
Drupal.billwerk = {};
Drupal.billwerk.subscriptionjs = {};
Drupal.billwerk.handleError = (error) => {
// Use Drupal JavaScript Messages API: https://www.drupal.org/node/2930536
const messages = new Drupal.Message();
if (error && error.errorMessage && messages) {
messages.add(Drupal.t(error.errorMessage), {
type: 'error',
});
if (
drupalSettings.billwerk &&
drupalSettings.billwerk.paymentFormWrapperId
) {
// Until Billwerk allows us to validate the form in the iframe
// or validates it itself, we need to scroll to the error messages
// so that users notice the error.
// Also we add an "error" class on the wrapper, as we can't access
// the iframe contents due to CSP:
const paymentFormWrapper = document.getElementById(
drupalSettings.billwerk.paymentFormWrapperId,
);
if (paymentFormWrapper) {
paymentFormWrapper.classList.add('error');
}
}
// Scroll to the error messages (UX):
window.scroll({
top: 0,
left: 0,
behavior: 'smooth',
});
} else {
console.error(error);
alert(
`Error! Please contact us with the following error report: "${error.errorMessage}"`,
);
}
};
Drupal.billwerk.getBillwerkSubscriptionsManageFormEl = () => {
return document.querySelector('.billwerk-subscriptions-change-form');
};
Drupal.billwerk.validatePayerForm = () => {
const formEl = Drupal.billwerk.getBillwerkSubscriptionsManageFormEl();
const isValid = formEl.checkValidity();
if (!isValid) {
formEl.reportValidity();
}
return isValid;
};
Drupal.billwerk.getPayerFormData = () => {
const formData = Object.fromEntries(
new FormData(
Drupal.billwerk.getBillwerkSubscriptionsManageFormEl(),
).entries(),
);
// Remove Drupal form values:
delete formData.form_id;
delete formData.form_token;
delete formData.form_build_id;
// Map the values to the CustomerData Object Structure:
// @see https://billwerk.readme.io/reference/subscriptionjs-types#customerdata-object-structure
const customerData = {
// Only set the values that we select:
// EmailAddress: formData.EmailAddress ?? "",
CompanyName: formData.CompanyName ?? '',
FirstName: formData.FirstName ?? '',
LastName: formData.LastName ?? '',
// PhoneNumber: formData.PhoneNumber ?? "",
Address: {
// AddressLine1: formData.CompanyName ?? "",
Street: formData.Street ?? '',
HouseNumber: formData.HouseNumber ?? '',
PostalCode: formData.PostalCode ?? '',
City: formData.City ?? '',
Country: formData.Country ?? '',
},
VatId: formData.VatId ?? '',
// Locale: formData.Country ?? "",
// CustomFields: {
// },
};
return customerData;
};
Drupal.billwerk.getPayerData = (billwerkPortal, success, error) => {
billwerkPortal.contractDetails((contractData) => {
const billwerkCustomerData = contractData.Customer;
const payerFormData = Drupal.billwerk.getPayerFormData();
if (!billwerkCustomerData || !payerFormData) {
window.alert('Unable to determine required data. Please contact us!');
throw new Error(
'Unable to determine required data. Please contact us!',
);
}
const payerData = {
...billwerkCustomerData,
...payerFormData,
};
// Remove data that should never change:
delete payerData.Id;
delete payerData.ExternalCustomerId;
delete payerData.CreatedAt;
delete payerData.Locale;
success(payerData);
}, error);
};
Drupal.billwerk.handlePaymentSuccess = (data, providerReturnUrl) => {
// "data" typically has the following structure:
/*
{
ContractId: "6585ab89cb4a71df107a1606",
Currency: "EUR",
CustomerId: "6585ab89cb4a71df107a1605",
GrossTotal: 135.3,
OrderId: "65b1019f5bc95f7014658fad",
OrderStatus: "PaymentPending",
*/
// Redirect to providerReturnUrl (if not empty):
if (providerReturnUrl) {
// Do not allow to use the "back"-button:
window.location.replace(providerReturnUrl);
}
};
Drupal.billwerk.ensureSubscriptionJS = () => {
if (typeof SubscriptionJS !== 'object') {
// Ensure subscriptionJS is loaded correctly:
window.alert(
'Error. SubscriptionJS is not loaded correctly, please contact us!',
);
throw new Error(
'Error. SubscriptionJS is not loaded correctly, please contact us!',
);
}
};
Drupal.billwerk.initializeForm = (context, settings) => {
// Ensure SubscriptionJS:
Drupal.billwerk.ensureSubscriptionJS();
// Assign our backend provided variables:
const {
selfserviceToken,
selfservicePublicApiKey,
order,
userLocale,
providerReturnUrl,
currentContractPaymentProvider,
paymentFormWrapperId,
} = settings.billwerk;
// Initialize Billwerk helpers:
/* global SubscriptionJS */
const billwerkPortal = new SubscriptionJS.Portal(selfserviceToken);
// We need to determine if the user has a current payment
// provider selected in the account or not
// because billwerk doesn't provide a convenient way
// to reuse the payment provider again and prefill the
// payment selection form accordingly.
// So we have to use two different ways of confirming the order:
if (currentContractPaymentProvider !== '' && true) {
// This is the synchronous payment method without payment method selection.
// Sadly it doesn't provide an integrated way to update the customer
// address in the same step, so we had to implement this ourselves:
// Register the buy button:
document.addEventListener(
'click',
(event) => {
if (!event.target.matches('#buy')) return;
if (!Drupal.billwerk.validatePayerForm()) return;
event.preventDefault();
// Get the existing customer data from Billwerk:
Drupal.billwerk.getPayerData(
billwerkPortal,
(newCustomerData) => {
// First we need to update the new customer data:
// DANGER: This is not implemented very clever on the Billwerk
// side and overwrites ALL fields (PUT, not PATCH)!
// So we have to merge the form data with all existing customer
// data to not lose any.
// @see https://billwerk.readme.io/reference/subscriptionjsportal#customerchangecustomerdata-success-error
billwerkPortal.customerChange(
newCustomerData,
() => {
// Success!
// Execute the payment now:
billwerkPortal.upgradePaySync(
order.OrderId,
(upgradePaySyncResult) => {
Drupal.billwerk.handlePaymentSuccess(
upgradePaySyncResult,
providerReturnUrl,
);
},
Drupal.billwerk.handleError,
);
},
Drupal.billwerk.handleError,
);
},
Drupal.billwerk.handleError,
);
},
false,
);
} else {
// Initialize Billwerk payment form:
const paymentFormWrapperElement =
document.getElementById(paymentFormWrapperId);
// Clear the container and remove the default text:
paymentFormWrapperElement.innerHTML = '';
// @see https://billwerk.readme.io/reference/appearance-1
// @improve: We should inject this via an #attached, so it can be
// altered in Drupal!
const style = {
/* Some example values of available properties
{
backgroundColor: '#ffffff',
border: '1px solid #ccc',
borderBottomColor: '#ccc',
borderBottomWidth: '5px',
borderColor: '#ccc',
borderLeftColor: 'black',
borderLeftWidth: '5px',
borderRadius: '4px',
borderRightColor: '#ccc',
borderRightWidth: '5px',
borderTopColor: '#ccc',
borderTopWidth: '5px',
borderWidth: '2px',
boxShadow: 'inset 0 1px 1px rgba(0,0,0,.075)',
color: '#444444',
display: 'block'
fontSize: '16px',
fontFamily: '"Times New Roman", Times, serif',
fontStyle: 'normal',
fontWeight: '',
height: '34px',
lineHeight: 'normal',
margin: '0',
padding: '1px 2px'
}
*/
body: {
fontFamily: 'Poppins, Helvetica, Arial, sans-serif',
padding: 0,
margin: 0,
},
container: {
padding: 0,
margin: 0,
// width: "auto", // Unsupported!
},
input: {},
inputInvalid: {},
inputRequired: {},
label: {},
labelRequired: {},
sectionContent: {
margin: '15px',
padding: 0,
// width: "auto", // Unsupported!
},
select: {},
selectInvalid: {},
selectRequired: {},
};
const paymentForm = SubscriptionJS.createElement(
'paymentForm',
paymentFormWrapperElement,
{
// Public API key (required):
publicApiKey: selfservicePublicApiKey,
// Limit the payment methods (optional):
// paymentMethods: ["Debit:FakePSP", "CreditCard:FakePSP"],
locale: userLocale ?? 'en',
providerReturnUrl,
},
style,
Drupal.billwerk.handleError,
);
// Register the buy button:
document.addEventListener(
'click',
(event) => {
if (!event.target.matches('#buy')) return;
if (!Drupal.billwerk.validatePayerForm()) return;
event.preventDefault();
Drupal.billwerk.getPayerData(
billwerkPortal,
(newCustomerData) => {
// First we need to update the new customer data:
// DANGER: This is not implemented very clever on the Billwerk
// side and overwrites ALL fields (PUT, not PATCH)!
// So we have to merge the form data with all existing customer
// data to not lose any.
// @see https://billwerk.readme.io/reference/subscriptionjsportal#customerchangecustomerdata-success-error
billwerkPortal.customerChange(
newCustomerData,
() => {
// Inform the payment form about the new customer data:
// @see https://billwerk.readme.io/reference/subscriptionjscreateelement#paymentformpayerdatachangedcustomerdata
// This does NOT change the payer data at billwerk, we need to do that separately!
paymentForm.payerDataChanged(newCustomerData);
// Execute the order:
billwerkPortal.upgradePayInteractive(
null,
paymentForm,
order,
(data) => {
Drupal.billwerk.handlePaymentSuccess(
data,
providerReturnUrl,
);
},
Drupal.billwerk.handleError,
);
},
Drupal.billwerk.handleError,
);
},
Drupal.billwerk.handleError,
);
},
false,
);
}
};
Drupal.billwerk.finalize = (context, settings) => {
// Ensure SubscriptionJS:
Drupal.billwerk.ensureSubscriptionJS();
// If finalize() fails, do NOT show an error to the users,
// just log it. As this for example also happens when reloading
// the success page and the order has already been finalized.
SubscriptionJS.finalize((successData) => {}, console.error);
};
Drupal.behaviors.billwerkCheckoutInit = {
attach(context, settings) {
if (context === document) {
// Execute the initialization based on the given "billwerk_context":
switch (settings.billwerk_context) {
case 'form':
Drupal.billwerk.initializeForm(context, settings);
break;
case 'finalize':
Drupal.billwerk.finalize(context, settings);
break;
default:
// Nothing!
break;
}
}
},
};
})(Drupal, drupalSettings);
