-
Notifications
You must be signed in to change notification settings - Fork 5
Add checkout page #536
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Add checkout page #536
Changes from all commits
e43405b
2ee4955
d5dd929
3a0759d
d5c28a5
5227e5d
c2b4e9f
6361d19
67cb291
bcb6a34
6fa2cb5
5de1116
50f5028
40ce1c2
45fd752
7aa1c27
110d669
b870307
5900f5c
7b15f9d
c7f54e8
72be807
1496c0c
c9afe05
aab2c1e
e5ad075
2fd2ddf
f99d9b1
6509c41
c6cecbd
3d734fd
3f5316f
5fe8941
e279094
afebc8a
ce0dea0
05febd9
4662733
a2f77c4
5f84738
b1c2b5f
12d1c67
06249d6
f638047
076054f
45c07b1
723d169
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| } | ||
|
|
||
| /** | ||
| * 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) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| }; | ||
| } | ||
| 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. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| } | ||||||
| }; | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * 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
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||
|
|
||||||
| 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 || ''; | ||||||
| } | ||||||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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