diff --git a/package.json b/package.json
index 63329a19e..d3951c6f2 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"@iconscout/react-unicons": "^2.2.5",
"@internxt/css-config": "1.1.1",
"@internxt/lib": "=1.5.1",
- "@internxt/sdk": "=1.17.14",
+ "@internxt/sdk": "=1.17.19",
"@internxt/ui": "=0.1.21",
"@phosphor-icons/react": "^2.1.10",
"@popperjs/core": "^2.11.6",
diff --git a/src/app/i18n/locales/de.json b/src/app/i18n/locales/de.json
index 362b062dd..8d1ff847e 100644
--- a/src/app/i18n/locales/de.json
+++ b/src/app/i18n/locales/de.json
@@ -184,7 +184,6 @@
}
},
"checkout": {
- "title": "Sie sind fast da! Sicher zur Kasse gehen",
"pay": "Bezahlen",
"processing": "Verarbeitung läuft...",
"checkout": "Zur Kasse",
@@ -196,15 +195,14 @@
"signIn": {
"password": "Ihr Passwort"
},
- "title": {
- "signUp": "Ein Konto erstellen",
- "signIn": "Anmelden",
- "userIsSignedIn": "Benutzer angemeldet als"
- },
"emailMustNotBeEmpty": "Die E-Mail darf nicht leer sein",
"privacyGuarantee": "Datenschutzgarantie: Wir teilen Ihre Informationen nicht und werden Sie nur kontaktieren, wenn es notwendig ist, um unseren Service zu bieten."
},
"addressBillingTitle": "Rechnungsadresse",
+ "postalCode": {
+ "title": "Postleitzahl",
+ "placeholder": "Postleitzahl"
+ },
"crypto": "Crypto",
"addressBilling": {
"optional": {
@@ -213,10 +211,7 @@
"companyVatId": "USt-ID der Firma"
}
},
- "paymentTitle": "Wählen Sie eine Zahlungsmethode",
"productCard": {
- "title": "Bestellübersicht",
- "selectedPlan": "Ausgewählter Plan",
"renewalTitle": {
"month": "jährlich",
"year": "jährlich",
@@ -228,7 +223,6 @@
"lifetime": "Einmalige Zahlung"
},
"saving": "Sie sparen {{percent}}%",
- "planDetails": "Plandetails:",
"total": "Gesamt ",
"addCoupon": {
"buttonTitle": "Gutschein hinzufügen",
@@ -261,6 +255,8 @@
"invalidPlan": "Nicht unterstützter Plantyp. Bitte wählen Sie einen gültigen Plan.",
"fetchingCryptoCurrencies": "Die verfügbaren Kryptowährungsoptionen konnten nicht geladen werden",
"addressRequired": "Bitte vervollständigen Sie Ihre Rechnungsadresse, um fortzufahren",
+ "postalCodeRequired": "Bitte geben Sie Ihre Postleitzahl ein, um fortzufahren",
+ "countryRequired": "Wir konnten Ihr Land nicht ermitteln. Bitte versuchen Sie es später erneut.",
"businessPlan": "Der Unternehmensplan ist nicht verfügbar"
}
},
diff --git a/src/app/i18n/locales/en.json b/src/app/i18n/locales/en.json
index b06414a07..b4b00935c 100644
--- a/src/app/i18n/locales/en.json
+++ b/src/app/i18n/locales/en.json
@@ -205,7 +205,6 @@
}
},
"checkout": {
- "title": "You're almost there! Checkout securely",
"pay": "Pay",
"processing": "Processing...",
"checkout": "Checkout",
@@ -217,15 +216,14 @@
"signIn": {
"password": "Your password"
},
- "title": {
- "signUp": "Create an account",
- "signIn": "Login",
- "userIsSignedIn": "User signed in as"
- },
"emailMustNotBeEmpty": "Email must not be empty",
"privacyGuarantee": "Privacy guarantee: We do not share your information and will contact you only as needed to provide our service."
},
"addressBillingTitle": "Billing Address",
+ "postalCode": {
+ "title": "Postal code",
+ "placeholder": "Postal code"
+ },
"crypto": "Crypto",
"addressBilling": {
"optional": {
@@ -234,22 +232,18 @@
"companyVatId": "Company Vat ID"
}
},
- "paymentTitle": "Select a payment method",
"productCard": {
"renewalTitle": {
"month": "annual",
"year": "annual",
"lifetime": "lifetime"
},
- "title": "Order summary",
- "selectedPlan": "Selected plan",
"billed": {
"month": "Billed monthly",
"year": "Billed annually",
"lifetime": "One-time payment"
},
"saving": "You're saving {{percent}}%",
- "planDetails": "Plan details:",
"total": "Total ",
"addCoupon": {
"buttonTitle": "Add coupon",
@@ -282,6 +276,8 @@
"invalidPlan": "Unsupported plan type. Please select a valid plan.",
"fetchingCryptoCurrencies": "We couldn't load the available cryptocurrency options",
"addressRequired": "Please complete your billing address to continue",
+ "postalCodeRequired": "Please enter your postal code to continue",
+ "countryRequired": "We couldn't determine your billing country. Please try again later.",
"businessPlan": "Business plan is not available"
}
},
diff --git a/src/app/i18n/locales/es.json b/src/app/i18n/locales/es.json
index d3a3bb13f..239516953 100644
--- a/src/app/i18n/locales/es.json
+++ b/src/app/i18n/locales/es.json
@@ -184,7 +184,6 @@
}
},
"checkout": {
- "title": "¡Ya casi está! Finaliza tu compra de forma segura",
"pay": "Pagar",
"processing": "Procesando...",
"checkout": "Finalizar compra",
@@ -196,15 +195,14 @@
"signIn": {
"password": "Tu contraseña"
},
- "title": {
- "signUp": "Crea una cuenta",
- "signIn": "Inicia sesión",
- "userIsSignedIn": "Has iniciado sesión como"
- },
"emailMustNotBeEmpty": "El correo electrónico no debe estar vacío",
"privacyGuarantee": "Garantía de privacidad: No compartimos su información y solo contactaremos contigo cuando sea necesario para brindar nuestro servicio."
},
"addressBillingTitle": "Dirección de facturación",
+ "postalCode": {
+ "title": "Código postal",
+ "placeholder": "Código postal"
+ },
"crypto": "Crypto",
"addressBilling": {
"optional": {
@@ -213,10 +211,7 @@
"companyVatId": "VAT ID de la Empresa"
}
},
- "paymentTitle": "Seleccione un método de pago",
"productCard": {
- "title": "Resumen del pedido",
- "selectedPlan": "Plan seleccionado",
"renewalTitle": {
"month": "anual",
"year": "anual",
@@ -228,7 +223,6 @@
"lifetime": "Pago único"
},
"saving": "Estás ahorrando un {{percent}}%",
- "planDetails": "Detalles del plan:",
"total": "Total",
"addCoupon": {
"buttonTitle": "Añadir cupón",
@@ -261,6 +255,8 @@
"invalidPlan": "Tipo de plan no soportado. Por favor, selecciona un plan válido.",
"fetchingCryptoCurrencies": "No pudimos cargar las opciones de criptomonedas disponibles",
"addressRequired": "Por favor, completa tu dirección de facturación para continuar",
+ "postalCodeRequired": "Introduce tu código postal para continuar",
+ "countryRequired": "No hemos podido determinar tu país de facturación. Inténtalo de nuevo más tarde.",
"businessPlan": "El plan de empresa no está disponible"
}
},
diff --git a/src/app/i18n/locales/fr.json b/src/app/i18n/locales/fr.json
index ddfc7d3e1..b19eb59ca 100644
--- a/src/app/i18n/locales/fr.json
+++ b/src/app/i18n/locales/fr.json
@@ -184,7 +184,6 @@
}
},
"checkout": {
- "title": "Vous y êtes presque ! Finalisez votre achat en toute sécurité",
"pay": "Payer",
"processing": "Traitement en cours...",
"checkout": "Commander",
@@ -196,15 +195,14 @@
"signIn": {
"password": "Votre mot de passe"
},
- "title": {
- "signUp": "Créer un compte",
- "signIn": "Se connecter",
- "userIsSignedIn": "Utilisateur connecté en tant que"
- },
"emailMustNotBeEmpty": "L'email ne doit pas être vide",
"privacyGuarantee": "Garantie de confidentialité : Nous ne partageons pas vos informations et ne vous contacterons que si nécessaire pour fournir notre service."
},
"addressBillingTitle": "Facturation d'Adresse",
+ "postalCode": {
+ "title": "Code postal",
+ "placeholder": "Code postal"
+ },
"crypto": "Crypto",
"addressBilling": {
"optional": {
@@ -213,10 +211,7 @@
"companyVatId": "Numéro de TVA de l'entreprise"
}
},
- "paymentTitle": "Sélectionnez un mode de paiement",
"productCard": {
- "title": "Récapitulatif de la commande",
- "selectedPlan": "Plan sélectionné",
"renewalTitle": {
"month": "annuel",
"year": "annuel",
@@ -228,7 +223,6 @@
"lifetime": "Paiement unique"
},
"saving": "Vous économisez {{percent}}%",
- "planDetails": "Détails du plan :",
"total": "Total ",
"addCoupon": {
"buttonTitle": "Ajouter un coupon",
@@ -261,6 +255,8 @@
"invalidPlan": "Type de plan non pris en charge. Veuillez sélectionner un plan valide.",
"fetchingCryptoCurrencies": "Nous n'avons pas pu charger les options de crypto-monnaies disponibles",
"addressRequired": "Veuillez compléter votre adresse de facturation pour continuer",
+ "postalCodeRequired": "Veuillez saisir votre code postal pour continuer",
+ "countryRequired": "Nous n'avons pas pu déterminer votre pays. Veuillez réessayer plus tard.",
"businessPlan": "Le plan d’entreprise n’est pas disponible"
}
},
diff --git a/src/app/i18n/locales/it.json b/src/app/i18n/locales/it.json
index 2fce80b2f..00a0ab297 100644
--- a/src/app/i18n/locales/it.json
+++ b/src/app/i18n/locales/it.json
@@ -205,7 +205,6 @@
}
},
"checkout": {
- "title": "Ci sei quasi! Completa il pagamento in modo sicuro",
"pay": "Paga",
"processing": "Elaborazione in corso...",
"checkout": "Pagamento",
@@ -217,15 +216,14 @@
"signIn": {
"password": "La tua password"
},
- "title": {
- "signUp": "Crea un account",
- "signIn": "Accedi",
- "userIsSignedIn": "Utente connesso come"
- },
"emailMustNotBeEmpty": "L'email non deve essere vuota",
"privacyGuarantee": "Garanzia di privacy: Non condividiamo le tue informazioni e ti contatteremo solo se necessario per fornire il nostro servizio."
},
"addressBillingTitle": "Indirizzo di Fatturazione",
+ "postalCode": {
+ "title": "Codice postale",
+ "placeholder": "Codice postale"
+ },
"crypto": "Crypto",
"addressBilling": {
"optional": {
@@ -234,10 +232,7 @@
"companyVatId": "Partita IVA dell'azienda"
}
},
- "paymentTitle": "Seleziona un metodo di pagamento",
"productCard": {
- "title": "Riepilogo dell'ordine",
- "selectedPlan": "Piano selezionato",
"renewalTitle": {
"month": "annuale",
"year": "annuale",
@@ -249,7 +244,6 @@
"lifetime": "Pagamento unico"
},
"saving": "Stai risparmiando {{percent}}%",
- "planDetails": "Dettagli del piano:",
"total": "Totale ",
"addCoupon": {
"buttonTitle": "Aggiungi coupon",
@@ -282,6 +276,8 @@
"invalidPlan": "Tipo di piano non supportato. Seleziona un piano valido.",
"fetchingCryptoCurrencies": "Non siamo riusciti a caricare le opzioni di criptovalute disponibili",
"addressRequired": "Completa il tuo indirizzo di fatturazione per continuare",
+ "postalCodeRequired": "Inserisci il tuo codice postale per continuare",
+ "countryRequired": "Non siamo riusciti a determinare il tuo paese. Riprova più tardi.",
"businessPlan": "Il piano aziendale non è disponibile"
}
},
diff --git a/src/app/i18n/locales/ru.json b/src/app/i18n/locales/ru.json
index 77e5796d9..77e1281f3 100644
--- a/src/app/i18n/locales/ru.json
+++ b/src/app/i18n/locales/ru.json
@@ -184,7 +184,6 @@
}
},
"checkout": {
- "title": "Вы почти у цели! Безопасное оформление заказа",
"pay": "Оплатить",
"processing": "Обработка...",
"checkout": "Оформить заказ",
@@ -196,15 +195,14 @@
"signIn": {
"password": "Ваш пароль"
},
- "title": {
- "signUp": "Создать аккаунт",
- "signIn": "Войти",
- "userIsSignedIn": "Пользователь вошел как"
- },
"emailMustNotBeEmpty": "Электронная почта не должна быть пустой",
"privacyGuarantee": "Гарантия конфиденциальности: Мы не передаем вашу информацию и будем связываться с вами только при необходимости для предоставления наших услуг."
},
"addressBillingTitle": "Адрес для выставления счетов",
+ "postalCode": {
+ "title": "Почтовый индекс",
+ "placeholder": "Почтовый индекс"
+ },
"crypto": "Crypto",
"addressBilling": {
"optional": {
@@ -213,10 +211,7 @@
"companyVatId": "ИНН компании"
}
},
- "paymentTitle": "Выберите способ оплаты",
"productCard": {
- "title": "Сводка заказа",
- "selectedPlan": "Выбранный план",
"renewalTitle": {
"month": "ежегодный",
"year": "ежегодный",
@@ -228,7 +223,6 @@
"lifetime": "Разовый платеж"
},
"saving": "Вы экономите {{percent}}%",
- "planDetails": "Детали плана:",
"total": "Итого ",
"addCoupon": {
"buttonTitle": "Добавить купон",
@@ -261,6 +255,8 @@
"invalidPlan": "Неподдерживаемый тип плана. Пожалуйста, выберите действительный план.",
"fetchingCryptoCurrencies": "Не удалось загрузить доступные варианты криптовалют",
"addressRequired": "Пожалуйста, заполните адрес для выставления счета, чтобы продолжить",
+ "postalCodeRequired": "Пожалуйста, введите почтовый индекс, чтобы продолжить",
+ "countryRequired": "Не удалось определить вашу страну. Пожалуйста, повторите попытку позже.",
"businessPlan": "Бизнес-план недоступен"
}
},
diff --git a/src/app/i18n/locales/tw.json b/src/app/i18n/locales/tw.json
index 7a1bc1439..90acb346d 100644
--- a/src/app/i18n/locales/tw.json
+++ b/src/app/i18n/locales/tw.json
@@ -205,7 +205,6 @@
}
},
"checkout": {
- "title": "您快完成了!請安全結帳",
"pay": "支付",
"processing": "處理中...",
"checkout": "結帳",
@@ -217,15 +216,14 @@
"signIn": {
"password": "您的密碼"
},
- "title": {
- "signUp": "創建帳戶",
- "signIn": "登錄",
- "userIsSignedIn": "用戶已登錄為"
- },
"emailMustNotBeEmpty": "電子郵件不能為空",
"privacyGuarantee": "隱私保證:我們不會分享您的信息,並且只會在提供服務時與您聯繫。"
},
"addressBillingTitle": "帳單地址",
+ "postalCode": {
+ "title": "郵遞區號",
+ "placeholder": "郵遞區號"
+ },
"crypto": "Crypto",
"addressBilling": {
"optional": {
@@ -234,10 +232,7 @@
"companyVatId": "公司增值稅編號"
}
},
- "paymentTitle": "選擇支付方式",
"productCard": {
- "title": "訂單摘要",
- "selectedPlan": "選擇的計劃",
"renewalTitle": {
"month": "每年",
"year": "每年",
@@ -249,7 +244,6 @@
"lifetime": "一次性支付"
},
"saving": "您節省了 {{percent}}%",
- "planDetails": "計劃詳情:",
"total": "總計d",
"addCoupon": {
"buttonTitle": "添加優惠券",
@@ -282,6 +276,8 @@
"invalidPlan": "不支援的方案類型。請選擇有效的方案。",
"fetchingCryptoCurrencies": "無法載入可用的加密貨幣選項",
"addressRequired": "請填寫您的帳單地址以繼續",
+ "postalCodeRequired": "請輸入您的郵遞區號以繼續",
+ "countryRequired": "我們無法確定您的國家/地區,請稍後再試",
"businessPlan": "企業計劃不可用"
}
},
diff --git a/src/app/i18n/locales/zh.json b/src/app/i18n/locales/zh.json
index 913082028..44875fdf9 100644
--- a/src/app/i18n/locales/zh.json
+++ b/src/app/i18n/locales/zh.json
@@ -205,7 +205,6 @@
}
},
"checkout": {
- "title": "您快完成了!请安全结账",
"pay": "支付",
"processing": "处理中...",
"checkout": "结账",
@@ -217,15 +216,14 @@
"signIn": {
"password": "您的密码"
},
- "title": {
- "signUp": "创建账户",
- "signIn": "登录",
- "userIsSignedIn": "用户已登录为"
- },
"emailMustNotBeEmpty": "电子邮件不能为空",
"privacyGuarantee": "隐私保证:我们不会分享您的信息,并且只会在提供服务时与您联系。"
},
"addressBillingTitle": "账单地址",
+ "postalCode": {
+ "title": "邮政编码",
+ "placeholder": "邮政编码"
+ },
"crypto": "Crypto",
"addressBilling": {
"optional": {
@@ -234,10 +232,7 @@
"companyVatId": "公司增值税编号"
}
},
- "paymentTitle": "选择支付方式",
"productCard": {
- "title": "订单摘要",
- "selectedPlan": "选择的计划",
"renewalTitle": {
"month": "每年",
"year": "每年",
@@ -249,7 +244,6 @@
"lifetime": "一次性支付"
},
"saving": "您节省了 {{percent}}%",
- "planDetails": "计划详情:",
"total": "总计",
"addCoupon": {
"buttonTitle": "添加优惠券",
@@ -282,6 +276,8 @@
"invalidPlan": "不支持的套餐类型。请选择有效的套餐。",
"fetchingCryptoCurrencies": "无法加载可用的加密货币选项",
"addressRequired": "请填写您的账单地址以继续",
+ "postalCodeRequired": "请输入您的邮政编码以继续",
+ "countryRequired": "我们无法确定您的国家/地区,请稍后再试",
"businessPlan": "企业计划不可用"
}
},
diff --git a/src/views/Checkout/components/CheckoutProductCard.tsx b/src/views/Checkout/components/CheckoutProductCard.tsx
index 09d67147f..bef492dff 100644
--- a/src/views/Checkout/components/CheckoutProductCard.tsx
+++ b/src/views/Checkout/components/CheckoutProductCard.tsx
@@ -1,6 +1,6 @@
import { Transition } from '@headlessui/react';
import { CouponCodeData } from '@internxt/sdk/dist/drive/payments/types/types';
-import { Check, SealPercent, X } from '@phosphor-icons/react';
+import { SealPercent, X } from '@phosphor-icons/react';
import { useState } from 'react';
import { PriceWithTax } from '@internxt/sdk/dist/payments/types';
@@ -18,11 +18,11 @@ import { Currency } from '../types';
interface CheckoutProductCardProps {
selectedPlan: PriceWithTax;
showCouponCode: boolean;
- showHardcodedRenewal?: string;
onRemoveAppliedCouponCode: () => void;
onCouponInputChange: (promoCode?: string) => void;
couponCodeData?: CouponCodeData;
couponError?: string;
+ isPaymentProcessing?: boolean;
}
const Separator = () =>
;
@@ -31,12 +31,12 @@ export const CheckoutProductCard = ({
selectedPlan,
couponCodeData,
showCouponCode,
- showHardcodedRenewal,
couponError,
+ isPaymentProcessing,
onRemoveAppliedCouponCode,
onCouponInputChange,
}: CheckoutProductCardProps) => {
- const { translate, translateList } = useTranslationContext();
+ const { translate } = useTranslationContext();
const { checkoutTheme } = useThemeContext();
const [couponName, setCouponName] = useState('');
const [openCouponCodeDropdown, setOpenCouponCodeDropdown] = useState(false);
@@ -77,29 +77,10 @@ export const CheckoutProductCard = ({
? translate('preferences.account.plans.planFeaturesList.default.bytesTitle', { bytes })
: specificTransalatedKey;
- const specificFeatures = translateList(`preferences.account.plans.${planType}.${bytes}.features`, {
- returnObjects: true,
- });
- const featureKeys = Array.isArray(specificFeatures)
- ? specificFeatures
- : translateList('preferences.account.plans.planFeaturesList.default.features', { bytes, returnObjects: true });
-
- const featuresList = Array.isArray(
- translateList(`preferences.account.plans.${planType}.${bytes}.comingSoonFeatures`),
- );
- const comingSoonFeatureKeys = Array.isArray(featuresList) ? featuresList : [];
-
return (
-
-
{translate('checkout.productCard.title')}
-
- {checkoutTheme === 'dark' ? : }
-
-
-
{translate('checkout.productCard.selectedPlan')}
{productLabel + ' - ' + translate(`checkout.productCard.renewalTitle.${priceData.interval}`)}
@@ -137,30 +118,6 @@ export const CheckoutProductCard = ({
)}
-
-
{translate('checkout.productCard.planDetails')}
-
- {featureKeys.map((feature) => (
-
- ))}
-
- {comingSoonFeatureKeys.length > 0 &&
- comingSoonFeatureKeys?.map((feature) => (
-
-
-
{feature}
-
-
- {translate('preferences.account.plans.planFeaturesList.comingSoon')}
-
-
- ))}
-
-
-
{totalLabel}
@@ -177,6 +134,7 @@ export const CheckoutProductCard = ({
{couponCodeData.codeName}
{priceData.interval === 'month' && (
@@ -264,6 +226,9 @@ export const CheckoutProductCard = ({
})}
)}
+
+ {checkoutTheme === 'dark' ? : }
+
);
};
diff --git a/src/views/Checkout/components/CheckoutUserAuth.tsx b/src/views/Checkout/components/CheckoutUserAuth.tsx
index 18296b881..73ebceef4 100644
--- a/src/views/Checkout/components/CheckoutUserAuth.tsx
+++ b/src/views/Checkout/components/CheckoutUserAuth.tsx
@@ -52,9 +52,6 @@ export const CheckoutUserAuth = ({
return (
-
- 1. {translate(`checkout.authComponent.title.${authMethod}`)}
-
{AUTH_LABEL[authMethod]}
{
e.preventDefault();
const newAuthMethod = authMethod === 'signIn' ? 'signUp' : 'signIn';
diff --git a/src/views/Checkout/constants.ts b/src/views/Checkout/constants.ts
index 6824b143a..2b652281d 100644
--- a/src/views/Checkout/constants.ts
+++ b/src/views/Checkout/constants.ts
@@ -3,6 +3,8 @@ export const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
export const IS_CRYPTO_PAYMENT_ENABLED = true;
+export const POSTAL_CODE_REQUIRED_COUNTRIES = ['US', 'CA', 'IN'];
+
export const THEME_STYLES = {
dark: {
backgroundColor: 'rgb(17 17 17)',
diff --git a/src/views/Checkout/hooks/useBillingDetails.test.ts b/src/views/Checkout/hooks/useBillingDetails.test.ts
new file mode 100644
index 000000000..00ee66c3f
--- /dev/null
+++ b/src/views/Checkout/hooks/useBillingDetails.test.ts
@@ -0,0 +1,114 @@
+import { act, renderHook } from '@testing-library/react';
+import { describe, expect, test } from 'vitest';
+import { AddressProvider } from '../types/checkout.types';
+import { useBillingDetails } from './useBillingDetails';
+
+const mockAddress: AddressProvider = {
+ line1: '742 Evergreen Terrace',
+ line2: null,
+ city: 'Springfield',
+ state: 'OR',
+ postal_code: '97403',
+ country: 'US',
+};
+
+describe('Billing details checkout custom hook', () => {
+ test('When the user is located in a country that requires a postal code, then the postal code is marked as required', () => {
+ const { result } = renderHook(() => useBillingDetails({ userLocation: 'US' }));
+
+ expect(result.current.isPostalCodeRequired).toBe(true);
+ });
+
+ test('When the user is located in a country that does not require a postal code, then the postal code is not required', () => {
+ const { result } = renderHook(() => useBillingDetails({ userLocation: 'ES' }));
+
+ expect(result.current.isPostalCodeRequired).toBe(false);
+ });
+
+ test('When no address is provided, then the billing country and postal code fall back to the detected location and the typed postal code', () => {
+ const { result } = renderHook(() => useBillingDetails({ userLocation: 'ES' }));
+
+ act(() => {
+ result.current.onPostalCodeChanges('28001');
+ });
+
+ expect(result.current.billingCountry).toBe('ES');
+ expect(result.current.billingPostalCode).toBe('28001');
+ });
+
+ test('When an address is provided, then its country and postal code take precedence over the detected location', () => {
+ const { result } = renderHook(() => useBillingDetails({ userLocation: 'ES' }));
+
+ act(() => {
+ result.current.onUserAddressChanges(mockAddress);
+ });
+
+ expect(result.current.billingCountry).toBe('US');
+ expect(result.current.billingPostalCode).toBe('97403');
+ });
+
+ test('When the typed postal code is only whitespace and no address exists, then the billing postal code is undefined', () => {
+ const { result } = renderHook(() => useBillingDetails({ userLocation: 'ES' }));
+
+ act(() => {
+ result.current.onPostalCodeChanges(' ');
+ });
+
+ expect(result.current.billingPostalCode).toBeUndefined();
+ });
+
+ test('When the address or the customer name are missing, then the crypto address is considered incomplete', () => {
+ const { result } = renderHook(() => useBillingDetails({}));
+
+ expect(result.current.isCryptoAddressIncomplete).toBe(true);
+ });
+
+ test('When a full address and a customer name are provided, then the crypto address is considered complete', () => {
+ const { result } = renderHook(() => useBillingDetails({}));
+
+ act(() => {
+ result.current.onUserNameChanges('Homer Simpson');
+ result.current.onUserAddressChanges(mockAddress);
+ });
+
+ expect(result.current.isCryptoAddressIncomplete).toBe(false);
+ });
+
+ test('When a company name is provided, then it is used as the customer name', () => {
+ const { result } = renderHook(() => useBillingDetails({}));
+
+ const customerName = result.current.getCustomerName({ companyName: 'Internxt' });
+
+ expect(customerName).toBe('Internxt');
+ });
+
+ test('When no company name is provided, then the typed user name is used as the customer name', () => {
+ const { result } = renderHook(() => useBillingDetails({}));
+
+ act(() => {
+ result.current.onUserNameChanges('Homer Simpson');
+ });
+
+ const customerName = result.current.getCustomerName({});
+
+ expect(customerName).toBe('Homer Simpson');
+ });
+
+ test('When neither company name nor typed user name are provided, then the customer name falls back to the authenticated user full name', () => {
+ const { result } = renderHook(() => useBillingDetails({}));
+
+ const customerName = result.current.getCustomerName({
+ authenticatedUser: { name: 'Homer', lastname: 'Simpson' } as any,
+ });
+
+ expect(customerName).toBe('Homer Simpson');
+ });
+
+ test('When no name is available anywhere, then the customer name falls back to the provided email', () => {
+ const { result } = renderHook(() => useBillingDetails({}));
+
+ const customerName = result.current.getCustomerName({ email: 'homer@inxt.com' });
+
+ expect(customerName).toBe('homer@inxt.com');
+ });
+});
diff --git a/src/views/Checkout/hooks/useBillingDetails.ts b/src/views/Checkout/hooks/useBillingDetails.ts
new file mode 100644
index 000000000..036e643f7
--- /dev/null
+++ b/src/views/Checkout/hooks/useBillingDetails.ts
@@ -0,0 +1,49 @@
+import { UserSettings } from '@internxt/sdk/dist/shared/types/userSettings';
+import { useState } from 'react';
+import { POSTAL_CODE_REQUIRED_COUNTRIES } from '../constants';
+import { AddressProvider } from '../types/checkout.types';
+
+interface UseBillingDetailsProps {
+ user?: UserSettings;
+ userLocation?: string;
+}
+
+interface CustomerNameProps {
+ companyName?: string;
+ authenticatedUser?: UserSettings;
+ email?: string;
+}
+
+export const useBillingDetails = ({ user, userLocation }: UseBillingDetailsProps) => {
+ const [address, setAddress] = useState();
+ const [userName, setUserName] = useState(user?.name ?? '');
+ const [postalCode, setPostalCode] = useState('');
+
+ const isPostalCodeRequired = POSTAL_CODE_REQUIRED_COUNTRIES.includes(userLocation ?? '');
+
+ const billingCountry = address?.country ?? userLocation;
+ const billingPostalCode = address?.postal_code ?? (postalCode.trim() || undefined);
+
+ const isCryptoAddressIncomplete =
+ !userName.trim() || !address?.line1 || !address?.city || !address?.country || !address?.postal_code;
+
+ const getCustomerName = ({ companyName, authenticatedUser, email }: CustomerNameProps) => {
+ const authName = [authenticatedUser?.name, authenticatedUser?.lastname].filter(Boolean).join(' ').trim();
+ const fallbackCustomerName = userName.trim() || authName || authenticatedUser?.email || email;
+ return companyName ?? fallbackCustomerName;
+ };
+
+ return {
+ address,
+ userName,
+ postalCode,
+ isPostalCodeRequired,
+ isCryptoAddressIncomplete,
+ billingCountry,
+ billingPostalCode,
+ getCustomerName,
+ onUserAddressChanges: setAddress,
+ onUserNameChanges: setUserName,
+ onPostalCodeChanges: setPostalCode,
+ };
+};
diff --git a/src/views/Checkout/services/checkout.service.ts b/src/views/Checkout/services/checkout.service.ts
index 772c1e379..94a306d6f 100644
--- a/src/views/Checkout/services/checkout.service.ts
+++ b/src/views/Checkout/services/checkout.service.ts
@@ -24,6 +24,12 @@ const fetchPromotionCodeByName = async (priceId: string, promotionCodeName: stri
return paymentClient.fetchPromotionCodeByName(priceId, promotionCodeName);
};
+export type CreateCheckoutCustomerPayload = Pick<
+ CreateCustomerPayload,
+ 'country' | 'captchaToken' | 'companyVatId' | 'metadata' | 'lineAddress2'
+> &
+ Partial>;
+
const createCustomer = async ({
customerName,
city,
@@ -34,7 +40,7 @@ const createCustomer = async ({
captchaToken,
companyVatId,
metadata,
-}: CreateCustomerPayload): Promise<{
+}: CreateCheckoutCustomerPayload): Promise<{
customerId: string;
token: string;
}> => {
@@ -49,7 +55,7 @@ const createCustomer = async ({
captchaToken,
companyVatId,
metadata,
- });
+ } as CreateCustomerPayload);
};
const getPriceById = async ({
diff --git a/src/views/Checkout/types/checkout.types.ts b/src/views/Checkout/types/checkout.types.ts
index bd0af7f28..9cb383f6b 100644
--- a/src/views/Checkout/types/checkout.types.ts
+++ b/src/views/Checkout/types/checkout.types.ts
@@ -32,4 +32,5 @@ export interface CheckoutViewManager {
handleAuthMethodChange: (method: AuthMethodTypes) => void;
onCurrencyChange: (currency: string) => void;
onUserNameChanges: (userName: string) => void;
+ onPostalCodeChanges: (postalCode: string) => void;
}
diff --git a/src/views/Checkout/views/CheckoutView.tsx b/src/views/Checkout/views/CheckoutView.tsx
index 543d0c137..fc9e63e90 100644
--- a/src/views/Checkout/views/CheckoutView.tsx
+++ b/src/views/Checkout/views/CheckoutView.tsx
@@ -1,11 +1,12 @@
import { CouponCodeData } from '@internxt/sdk/dist/drive/payments/types/types';
-import { Button, Loader } from '@internxt/ui';
+import { Loader } from '@internxt/ui';
import { AddressElement, PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js';
import { StripePaymentElementOptions } from '@stripe/stripe-js';
import { IFormValues } from 'app/core/types';
import { useTranslationContext } from 'app/i18n/provider/TranslationProvider';
import { LegacyRef, useState } from 'react';
import { useForm } from 'react-hook-form';
+import TextInput from 'components/TextInput';
import { CheckoutProductCard } from '../components/CheckoutProductCard';
import { CheckoutUserAuth } from '../components/CheckoutUserAuth';
import { HeaderComponent } from '../components/Header';
@@ -43,6 +44,7 @@ interface CheckoutViewProps {
checkoutViewManager: CheckoutViewManager;
availableCryptoCurrencies?: CryptoCurrency[];
onCurrencyTypeChanges: (currency: PaymentType) => void;
+ isPostalCodeRequired?: boolean;
}
const AUTH_METHOD_VALUES = {
@@ -58,6 +60,7 @@ const CheckoutView = ({
checkoutViewManager,
availableCryptoCurrencies,
onCurrencyTypeChanges,
+ isPostalCodeRequired,
}: CheckoutViewProps) => {
const { translate } = useTranslationContext();
// Those custom hooks should be here.
@@ -65,6 +68,7 @@ const CheckoutView = ({
const stripeSDK = useStripe();
const elements = useElements();
const [isCryptoDropdownOpen, setIsCryptoDropdownOpen] = useState(false);
+ const [postalCode, setPostalCode] = useState('');
const { isPaying, couponCodeError, authError, authMethod, couponCodeData, currentSelectedPlan, selectedCurrency } =
checkoutViewVariables;
@@ -100,7 +104,7 @@ const CheckoutView = ({
);
}
- const isButtonDisabled = authMethod === AUTH_METHOD_VALUES.IS_SIGNED_IN ? isPaying : isPaying && isValid;
+ const isPaymentProcessing = authMethod === AUTH_METHOD_VALUES.IS_SIGNED_IN ? isPaying : isPaying && isValid;
function onAuthMethodToggled(authMethod: AuthMethodTypes) {
reset({
@@ -123,10 +127,7 @@ const CheckoutView = ({
-
- {translate('checkout.title')}
-
-
+
-
-
2. {translate('checkout.addressBillingTitle')}
-
-
-
{
- checkoutViewManager.onUserNameChanges(e.value.name);
- checkoutViewManager.onUserAddressChanges(e.value.address);
- }}
- options={{
- mode: 'billing',
- autocomplete: {
- mode: 'automatic',
- },
- }}
- />
-
-
-
3. {translate('checkout.paymentTitle')}
+
)}
+ {isPostalCodeRequired && !isCryptoDropdownOpen && (
+
+
{translate('checkout.postalCode.title')}
+
{
+ setPostalCode(e.target.value);
+ checkoutViewManager.onPostalCodeChanges(e.target.value);
+ }}
+ />
+
+ )}
+ {isCryptoDropdownOpen && (
+
+
{translate('checkout.addressBillingTitle')}
+
{
+ checkoutViewManager.onUserNameChanges(e.value.name);
+ checkoutViewManager.onUserAddressChanges(e.value.address);
+ }}
+ options={{
+ mode: 'billing',
+ autocomplete: {
+ mode: 'automatic',
+ },
+ }}
+ />
+
+ )}
-
- {isButtonDisabled ? translate('checkout.processing') : translate('checkout.pay')}
-
-
+
-
- {isButtonDisabled ? translate('checkout.processing') : translate('checkout.pay')}
-
diff --git a/src/views/Checkout/views/CheckoutViewWrapper.tsx b/src/views/Checkout/views/CheckoutViewWrapper.tsx
index 05dc4d3cf..1f289924f 100644
--- a/src/views/Checkout/views/CheckoutViewWrapper.tsx
+++ b/src/views/Checkout/views/CheckoutViewWrapper.tsx
@@ -22,7 +22,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/hooks';
import { planThunks } from 'app/store/slices/plan';
import { useThemeContext } from 'app/theme/ThemeProvider';
import { PaymentType } from 'views/Checkout/types';
-import { AddressProvider, CheckoutViewManager, UserInfoProps } from '../types/checkout.types';
+import { CheckoutViewManager, UserInfoProps } from '../types/checkout.types';
import CheckoutView from './CheckoutView';
import { useUserPayment } from 'views/Checkout/hooks/useUserPayment';
import { CRYPTO_PAYMENT_DIALOG_KEY, CryptoPaymentDialog } from 'views/Checkout/components/CryptoPaymentDialog';
@@ -42,6 +42,7 @@ import {
MILLISECONDS_PER_DAY,
STATUS_CODE_ERROR,
} from '../constants';
+import { useBillingDetails } from '../hooks/useBillingDetails';
import { usePromotionalCode } from '../hooks/usePromotionalCode';
import { useAuthCheckout } from '../hooks/useAuthCheckout';
import { checkoutReducer, initialStateForCheckout } from '../store';
@@ -85,8 +86,17 @@ const CheckoutViewWrapper = () => {
});
const dispatch = useAppDispatch();
- const [address, setAddress] = useState
();
- const [userName, setUserName] = useState(user?.name ?? '');
+ const {
+ address,
+ isPostalCodeRequired,
+ isCryptoAddressIncomplete,
+ billingCountry,
+ billingPostalCode,
+ getCustomerName,
+ onUserAddressChanges,
+ onUserNameChanges,
+ onPostalCodeChanges,
+ } = useBillingDetails({ user, userLocation: userLocationData?.location });
const isAuthenticated = useAppSelector((state) => state.user.isAuthenticated);
const { doRegister } = useSignUp('activate');
@@ -137,16 +147,22 @@ const CheckoutViewWrapper = () => {
return;
}
- if (userLocationData?.location !== address?.country && address?.postal_code) {
+ if (!billingCountry || !billingPostalCode) {
+ return;
+ }
+
+ const debounceTimer = setTimeout(() => {
fetchSelectedPlan({
priceId: selectedPlan.price.id,
currency: selectedPlan.price.currency,
promotionCode: promotionCode ?? undefined,
- postalCode: address.postal_code,
- country: address.country,
+ postalCode: billingPostalCode,
+ country: billingCountry,
});
- }
- }, [address?.country, address?.postal_code, selectedPlan?.price?.id, selectedPlan?.price?.currency]);
+ }, 500);
+
+ return () => clearTimeout(debounceTimer);
+ }, [billingCountry, billingPostalCode, selectedPlan?.price?.id, selectedPlan?.price?.currency]);
useEffect(() => {
if (isCheckoutReady && selectedPlan?.price) {
@@ -275,7 +291,6 @@ const CheckoutViewWrapper = () => {
const { email, password, companyName, companyVatId } = formData;
const isStripeNotLoaded = !stripeSDK || !elements;
- const customerName = companyName ?? userName;
const captchaToken = await generateCaptchaToken();
@@ -306,10 +321,20 @@ const CheckoutViewWrapper = () => {
return;
}
- if (!address?.line1 || !address?.city || !address.country) {
+ const isCryptoPurchase = currencyType === PaymentType['CRYPTO'];
+
+ if (isCryptoPurchase && isCryptoAddressIncomplete) {
throw new Error(translate('checkout.error.addressRequired'));
}
+ if (!billingCountry) {
+ throw new Error(translate('checkout.error.countryRequired'));
+ }
+
+ if (isPostalCodeRequired && !billingPostalCode) {
+ throw new Error(translate('checkout.error.postalCodeRequired'));
+ }
+
if (currencyType === PaymentType['FIAT']) {
const { error: elementsError } = await elements.submit();
@@ -328,12 +353,14 @@ const CheckoutViewWrapper = () => {
...(userUuid && { new_user_id: userUuid }),
}
: undefined;
+ const customerName = getCustomerName({ companyName, authenticatedUser, email });
+
const { customerId, token } = await checkoutService.createCustomer({
- customerName,
+ customerName: customerName || undefined,
lineAddress1: address?.line1,
lineAddress2: address?.line2 ?? undefined,
- country: address?.country,
- postalCode: address?.postal_code,
+ country: billingCountry,
+ postalCode: billingPostalCode,
city: address?.city,
companyVatId,
captchaToken: customerToken,
@@ -375,14 +402,6 @@ const CheckoutViewWrapper = () => {
}
};
- const onUserAddressChanges = (address: AddressProvider) => {
- setAddress(address);
- };
-
- const onUserNameChanges = (userName: string) => {
- setUserName(userName);
- };
-
const onCurrencyTypeChanges = (currency: PaymentType) => {
setCurrencyType(currency);
};
@@ -396,6 +415,7 @@ const CheckoutViewWrapper = () => {
handleAuthMethodChange: setAuthMethod,
onCurrencyChange: setSelectedCurrency,
onUserNameChanges,
+ onPostalCodeChanges,
};
return (
@@ -419,6 +439,7 @@ const CheckoutViewWrapper = () => {
checkoutViewManager={checkoutViewManager}
availableCryptoCurrencies={availableCryptoCurrencies}
onCurrencyTypeChanges={onCurrencyTypeChanges}
+ isPostalCodeRequired={isPostalCodeRequired}
/>
{canChangePlanDialogBeOpened ? (