Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
e43405b
Add empty page.
Vladyslav-Kuksiuk Apr 17, 2026
2ee4955
Add total section.
Vladyslav-Kuksiuk Apr 17, 2026
d5dd929
Fix layout.
Vladyslav-Kuksiuk Apr 17, 2026
3a0759d
Add checkout fields.
Vladyslav-Kuksiuk Apr 17, 2026
d5c28a5
Improve fields.
Vladyslav-Kuksiuk Apr 17, 2026
5227e5d
Move Checkout to nav hero.
Vladyslav-Kuksiuk Apr 17, 2026
c2b4e9f
Add product description.
Vladyslav-Kuksiuk Apr 20, 2026
6361d19
Improve naming.
Vladyslav-Kuksiuk Apr 20, 2026
67cb291
Add validation.
Vladyslav-Kuksiuk Apr 20, 2026
bcb6a34
Update API handling.
Vladyslav-Kuksiuk Apr 22, 2026
6fa2cb5
Improve checkout page.
Vladyslav-Kuksiuk Apr 22, 2026
5de1116
Add 'Thank you page'.
Vladyslav-Kuksiuk Apr 22, 2026
50f5028
Improve 'Thank you page' layout.
Vladyslav-Kuksiuk Apr 22, 2026
40ce1c2
Improve page-404.
Vladyslav-Kuksiuk Apr 22, 2026
45fd752
Extract submodels.
Vladyslav-Kuksiuk Apr 22, 2026
7aa1c27
Fix phone country changing.
Vladyslav-Kuksiuk Apr 22, 2026
110d669
Improve layout.
Vladyslav-Kuksiuk Apr 22, 2026
b870307
Improve readability.
Vladyslav-Kuksiuk Apr 22, 2026
5900f5c
Replace `jqXhr` by `fetch`.
Vladyslav-Kuksiuk Apr 22, 2026
7b15f9d
Shorten modal window.
Vladyslav-Kuksiuk Apr 22, 2026
c7f54e8
Improve documentation.
Vladyslav-Kuksiuk Apr 22, 2026
72be807
Add 'Product not found' view.
Vladyslav-Kuksiuk Apr 23, 2026
1496c0c
Add loading state.
Vladyslav-Kuksiuk Apr 23, 2026
c9afe05
Disable `Continue` button before charges calculated.
Vladyslav-Kuksiuk Apr 23, 2026
aab2c1e
Improve requests sending.
Vladyslav-Kuksiuk Apr 23, 2026
e5ad075
Remove checkout page from sitemap.
Vladyslav-Kuksiuk Apr 23, 2026
2fd2ddf
Improve error showing.
Vladyslav-Kuksiuk Apr 23, 2026
f99d9b1
Improve pages layout.
Vladyslav-Kuksiuk Apr 23, 2026
6509c41
Improve oops page view.
Vladyslav-Kuksiuk Apr 23, 2026
c6cecbd
Divide `chekout-pages.js` on logical blocks.
Vladyslav-Kuksiuk Apr 23, 2026
3d734fd
Remove `helpers.js`.
Vladyslav-Kuksiuk Apr 23, 2026
3f5316f
Improve docs.
Vladyslav-Kuksiuk Apr 23, 2026
5fe8941
Improve doc style.
Vladyslav-Kuksiuk Apr 23, 2026
e279094
Improve `charge-controller` readability.
Vladyslav-Kuksiuk Apr 23, 2026
afebc8a
Revert `nav-hero` changes.
Vladyslav-Kuksiuk Apr 23, 2026
ce0dea0
Improve `Ooops` page layout.
Vladyslav-Kuksiuk Apr 27, 2026
05febd9
Improve checkout page style.
Vladyslav-Kuksiuk Apr 27, 2026
4662733
Add copyrights.
Vladyslav-Kuksiuk Apr 27, 2026
a2f77c4
Add copyright for JS files.
Vladyslav-Kuksiuk Apr 27, 2026
5f84738
Improve checkout layout.
Vladyslav-Kuksiuk Apr 27, 2026
b1c2b5f
Fix navbar shadow.
Vladyslav-Kuksiuk Apr 27, 2026
12d1c67
Update font sizes.
Vladyslav-Kuksiuk Apr 27, 2026
06249d6
Revert navbar changes.
Vladyslav-Kuksiuk Apr 27, 2026
f638047
Improve readability.
Vladyslav-Kuksiuk Apr 27, 2026
076054f
Use root pathes.
Vladyslav-Kuksiuk Apr 27, 2026
45c07b1
Update hugo param names.
Vladyslav-Kuksiuk Apr 27, 2026
723d169
Extract dicts.
Vladyslav-Kuksiuk Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions site/assets/js/modules/forms/phone-number.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
* disclaimer.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

'use strict';

/**
* Checks whether the input contains only supported phone-number characters.
*
* Allowed: digits, parentheses, hyphens, and spaces.
*
* @param {string} value phone-number value to check
* @return {boolean} true when the value contains only allowed characters
*/
export function isValidPhoneNumberInput(value) {
return /^[0-9\s()-]+$/.test(value);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this? It looks were weird. Phone number it is only numbers.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is for case, when user copied number with those symbols, like (000) 000-0000

}

/**
* Removes characters that are not accepted by the phone-number field.
*
* Allowed: digits, parentheses, hyphens, and spaces.
*
* @param {string} value phone-number value to sanitize
* @return {string} sanitized phone-number value
*/
export function sanitizePhoneNumberInput(value) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this method used anywhere? Doesn't the above one help us to skip this one?

return String(value || '').replace(/[^0-9\s()-]/g, '');
}

/**
* Builds the phone-number payload with country code and number with digits only.
*
* @param {string} rawCountryCode phone country code
* @param {string} rawNumber local phone number
* @return {{countryCode: number, number: string}|null}
* normalized phone-number payload, or null when incomplete
*/
export function normalizePhoneNumber(rawCountryCode, rawNumber) {
const countryCode = String(rawCountryCode || '').replace(/\D/g, '');
const number = String(rawNumber || '').replace(/\D/g, '');

if (!countryCode || !number) {
return null;
}

const numericCountryCode = Number(countryCode);
if (!Number.isInteger(numericCountryCode) || numericCountryCode <= 0) {
return null;
}

return {
countryCode: numericCountryCode,
number
};
}
234 changes: 234 additions & 0 deletions site/assets/js/modules/paygate/purchases.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
/*
* Copyright 2026, TeamDev. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Redistribution and use in source and/or binary forms, with or without
* modification, must retain the above copyright notice and the following
* disclaimer.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

'use strict';

/**
* Paygate product data returned for a checkout order.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it right place for doc? Maybe we should place type doc in file where this type is used? Not sure.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a place where this type is defined, because it is a part of Purchases client API which is defined here.

*
* @typedef {Object} PaygateProduct
* @property {string} name product display name
* @property {string} description product description shown on checkout
* @property {number|string} netPrice product price before VAT
* @property {string} currency product currency code
*/

/**
* Paygate response for the `place-order` endpoint.
*
* @typedef {Object} PlaceOrderResponse
* @property {string} orderId created paygate order ID
* @property {PaygateProduct|null} product product data for the new order
*/

/**
* Paygate request payload for the `calculate-charges` endpoint.
*
* @typedef {Object} CalculateChargesRequest
* @property {string} orderId paygate order ID
* @property {string} buyerCountryCode iso billing country code
* @property {string} vatId buyer VAT ID
*/

/**
* Paygate response for the `calculate-charges` endpoint.
*
* @typedef {Object} CalculateChargesResponse
* @property {number|string} netPrice price before VAT
* @property {number|string} vatRate vat rate as a decimal fraction
* @property {number|string} vatAmount VAT amount for the order
* @property {number|string} total total price including VAT
* @property {string} currency order currency code
*/

/**
* Billing address submitted to Paygate.
*
* @typedef {Object} BillingAddress
* @property {string} countryCode iso billing country code
* @property {string} city billing city
* @property {string} street combined street address
* @property {string} postalCode billing postal code
*/

/**
* Company billing details submitted to Paygate.
*
* @typedef {Object} BillingCompany
* @property {string} name company legal or display name
* @property {string} vatId company VAT ID
*/

/**
* Billing information submitted to Paygate before redirecting to payment.
*
* @typedef {Object} BillingInfo
* @property {string} name full buyer name or company fallback name
* @property {string} email buyer email address
* @property {BillingAddress} address billing address details
* @property {BillingCompany} company company billing details
* @property {string} [phoneNumber] optional normalized phone number including
* country code
*/

/**
* Paygate request payload for the `submit-billing-info` endpoint.
*
* @typedef {Object} SubmitBillingInfoRequest
* @property {string} orderId paygate order ID
* @property {BillingInfo} billingInfo billing details for the order
*/

/**
* Paygate response for the billing-info submission step.
*
* @typedef {Object} SubmitBillingInfoResponse
* @property {string} paymentLink preferred payment redirect URL
* @property {string} redirectUrl alternative payment redirect URL
* @property {string} url alternative response URL field
* @property {string} link alternative response link field
*/

/**
* Error object thrown when a Paygate request fails.
*
* @typedef {Object} PurchaseApiError
* @property {number} status http response status code
* @property {string} statusText http response status text
* @property {*|null} body parsed response body, if any
* @property {string} message human-readable error message
*/

/**
* Paygate purchase endpoint methods used by checkout.
*
* @typedef {Object} PaygatePurchaseClient
* @property {function(string): Promise<PlaceOrderResponse>} placeOrder
* creates a checkout order for the given product ID
* @property {function(CalculateChargesRequest): Promise<CalculateChargesResponse>} calculateCharges
* calculates VAT and totals for the current order
* @property {function(SubmitBillingInfoRequest): Promise<SubmitBillingInfoResponse>} submitBillingInfo
* sends billing details and returns payment redirect data
*/

/**
* Creates a client for Paygate purchase endpoints.
*
* @param {string} serverUrl base URL of the Paygate API server
* @return {PaygatePurchaseClient} paygate purchase endpoint methods
*/
export function createPurchaseClient(serverUrl) {
const baseUrl = normalizeServerUrl(serverUrl);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of this call? Can't the server URL be normalized manually since it's a config value? If for some reason Hugo adds some noise to the string, can we look for the way to remove those noise from the Hugo side?


return {
placeOrder(productId) {
return postJson(`${baseUrl}/purchases/place-order`, {productId});
},
calculateCharges(payload) {
return postJson(`${baseUrl}/purchases/calculate-charges`, payload);
},
submitBillingInfo(payload) {
return postJson(`${baseUrl}/purchases/submit-billing-info`, payload);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return postJson(`${baseUrl}/purchases/submit-billing-info`, payload);
return postJson(`${baseUrl}/purchases/provide-billing-info`, payload);

}
};
}

/**
* Removes trailing slashes from the configured Paygate server URL.
*
* @param {string} url raw configured Paygate server URL
* @return {string} server URL without trailing slashes
*/
function normalizeServerUrl(url) {
return String(url || '').replace(/\/+$/, '');
}

/**
* Sends a JSON POST request and returns the parsed response body.
*
* @param {string} url endpoint URL
* @param {Object} payload json request body
* @return {Promise<*>} parsed response body when the request succeeds
*
* @throws {PurchaseApiError} if response status is not OK
*/
async function postJson(url, payload) {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const body = await readResponseBody(response);

if (!response.ok) {
throw ({
status: response.status,
statusText: response.statusText,
body,
message: errorMessage(body)
});
}

return body;
}

/**
* Parses a fetch response body as JSON when possible, otherwise as text.
*
* @param {Response} response fetch response to parse
* @return {Promise<*>} parsed JSON, text, or null when there is no readable body
*/
async function readResponseBody(response) {
const contentType = response.headers.get('content-type') || '';

if (response.status === 204) {
return null;
}
Comment on lines +209 to +211
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This an below response - to - null conversion should be documented in the doc now it's unclear how we handle response.
Maybe the method above should be updated somehow as well.


try {
return contentType.includes('application/json')
? await response.json()
: await response.text();
} catch (ignored) {
return null;
}
}

/**
* Extracts a human-readable error message from a parsed response body.
*
* @param {*} body parsed response body
* @return {string} message text when present, otherwise an empty string
*/
function errorMessage(body) {
if (!body) {
return '';
}

return typeof body === 'string' ? body : body.message || '';
}
Loading
Loading