feat(checkout): show a German message when a checkout step fails

checkoutErrorMessage (utils/checkoutError.ts) maps the status and body of a
failed request, the shop's answer or a raw CMS (Strapi) error, to one fixed
message for the customer, addressed with "du": an invalid e-mail address
(when data.email is among the rejected fields), other invalid input, an order
already paid, a product no longer available, an order not ready for checkout
(naming the missing steps), a payment that does not fit the order or an order
changed during the payment ("Bitte starte die Zahlung neu"), PayPal not
reachable ("versuche es in ein paar Minuten noch einmal"), a payment PayPal
may have taken that the CMS could not confirm or record ("Bitte bezahle nicht
noch einmal"), and a general fallback. It never shows the server's text.

Steps 1 and 2 show the message above their submit button, in the style of
step 3's payment error, instead of logging the error only. Step 3 shows it
for a failed PayPal order creation or capture. The e-mail input of step 1 is
type="email" (Input.vue takes a type).

Refs libretech/mp#71
This commit is contained in:
2026-10-09 02:32:32 +02:00
parent bfcbbfac39
commit 8931afc670
6 changed files with 309 additions and 3 deletions
+4 -1
View File
@@ -5,6 +5,7 @@
<span :class="spanClasses" class="absolute left-2 top-0 transition-all select-none">{{ label }}</span> <span :class="spanClasses" class="absolute left-2 top-0 transition-all select-none">{{ label }}</span>
<input <input
:value="modelValue" :value="modelValue"
:type="type"
@input="update(($event.target as HTMLInputElement).value)" @input="update(($event.target as HTMLInputElement).value)"
:required="required" :required="required"
class="outline-none w-full h-full rounded-sm pt-4 p-2 border-transparent" class="outline-none w-full h-full rounded-sm pt-4 p-2 border-transparent"
@@ -40,9 +41,11 @@ const props = withDefaults(
labelClass?: string; labelClass?: string;
inputClass?: string; inputClass?: string;
autocomplete?: Autocomplete; autocomplete?: Autocomplete;
type?: "text" | "email";
}>(), }>(),
{ {
autocomplete: "off" autocomplete: "off",
type: "text"
} }
); );
+9 -1
View File
@@ -29,7 +29,7 @@
<div v-if="uuid"> <div v-if="uuid">
<form @submit.prevent="submit" class="flex flex-col gap-12"> <form @submit.prevent="submit" class="flex flex-col gap-12">
<Input label="E-Mail-Adresse" v-model="emailAddress" :required="true" autocomplete="email" /> <Input label="E-Mail-Adresse" v-model="emailAddress" type="email" :required="true" autocomplete="email" />
<label class="flex gap-4 text-sm cursor-pointer"> <label class="flex gap-4 text-sm cursor-pointer">
<input v-model="acceptedTermsAndConditions" required type="checkbox" class="w-4 cursor-pointer" /> <input v-model="acceptedTermsAndConditions" required type="checkbox" class="w-4 cursor-pointer" />
@@ -44,6 +44,10 @@
<hr /> <hr />
<div v-if="errorMessage" class="p-4 lg:p-8 text-center mx-auto rounded-md bg-rose-100" role="alert" data-e2e="checkout-error">
<span class="text-rose-800">{{ errorMessage }}</span>
</div>
<Button type="submit" classes="w-full" :is-pending="formSubmitIsPending">Weiter zur Lieferadresse</Button> <Button type="submit" classes="w-full" :is-pending="formSubmitIsPending">Weiter zur Lieferadresse</Button>
</form> </form>
</div> </div>
@@ -60,6 +64,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { trackEvent } from "~/utils/trackEvent"; import { trackEvent } from "~/utils/trackEvent";
import { checkoutErrorMessageFor } from "~/utils/checkoutError";
const COOKIE_CONSENT_KEY = "shop:cookie-consent"; const COOKIE_CONSENT_KEY = "shop:cookie-consent";
@@ -68,6 +73,7 @@ const cart = useCart();
const emailAddress = ref(""); const emailAddress = ref("");
const acceptedTermsAndConditions = ref(false); const acceptedTermsAndConditions = ref(false);
const formSubmitIsPending = ref(false); const formSubmitIsPending = ref(false);
const errorMessage = ref("");
const uuid = computed(() => cart.uuid.value); const uuid = computed(() => cart.uuid.value);
@@ -120,6 +126,7 @@ async function submit() {
try { try {
formSubmitIsPending.value = true; formSubmitIsPending.value = true;
errorMessage.value = "";
// Use cart.update() to sync local state after API call // Use cart.update() to sync local state after API call
await cart.update({ await cart.update({
@@ -138,6 +145,7 @@ async function submit() {
navigateTo("/checkout/2"); navigateTo("/checkout/2");
} catch (error) { } catch (error) {
console.error("Error submitting email address", error); console.error("Error submitting email address", error);
errorMessage.value = checkoutErrorMessageFor(error);
} finally { } finally {
formSubmitIsPending.value = false; formSubmitIsPending.value = false;
} }
+8
View File
@@ -97,6 +97,10 @@
<hr class="my-12" /> <hr class="my-12" />
<div v-if="errorMessage" class="p-4 lg:p-8 text-center mx-auto rounded-md bg-rose-100" role="alert" data-e2e="checkout-error">
<span class="text-rose-800">{{ errorMessage }}</span>
</div>
<div class="mt-12"> <div class="mt-12">
<Button classes="w-full" type="submit" :is-pending="formSubmitIsPending">Weiter zur Zahlung</Button> <Button classes="w-full" type="submit" :is-pending="formSubmitIsPending">Weiter zur Zahlung</Button>
</div> </div>
@@ -110,6 +114,7 @@
import type { Address, StructuredAddress } from "~/types"; import type { Address, StructuredAddress } from "~/types";
import { trackEvent } from "~/utils/trackEvent"; import { trackEvent } from "~/utils/trackEvent";
import { numberFormatter } from "~/utils/numberFormatter"; import { numberFormatter } from "~/utils/numberFormatter";
import { checkoutErrorMessageFor } from "~/utils/checkoutError";
const cart = useCart(); const cart = useCart();
const shopApi = useShopApi(); const shopApi = useShopApi();
@@ -134,6 +139,7 @@ const displayState = computed(() => {
}); });
const showOptionalDeliveryAddress = ref(false); const showOptionalDeliveryAddress = ref(false);
const formSubmitIsPending = ref(false); const formSubmitIsPending = ref(false);
const errorMessage = ref("");
const deliveryMethods = ref<any[]>([]); const deliveryMethods = ref<any[]>([]);
const selectedDeliveryMethod = ref<number | null>(null); const selectedDeliveryMethod = ref<number | null>(null);
@@ -243,6 +249,7 @@ async function submit() {
try { try {
formSubmitIsPending.value = true; formSubmitIsPending.value = true;
errorMessage.value = "";
// Use cart.update() to sync local state after API call // Use cart.update() to sync local state after API call
await cart.update({ await cart.update({
@@ -262,6 +269,7 @@ async function submit() {
navigateTo("/checkout/3"); navigateTo("/checkout/3");
} catch (error) { } catch (error) {
console.error("Error submitting address form:", error); console.error("Error submitting address form:", error);
errorMessage.value = checkoutErrorMessageFor(error);
} finally { } finally {
formSubmitIsPending.value = false; formSubmitIsPending.value = false;
} }
+5 -1
View File
@@ -64,7 +64,7 @@
<div v-else> <div v-else>
<div v-if="!hasPaymentError" id="paypal-button-container" class="payment sticky top-4" ref="paymentContainer"></div> <div v-if="!hasPaymentError" id="paypal-button-container" class="payment sticky top-4" ref="paymentContainer"></div>
<div v-else class="p-4 lg:p-8 text-center mx-auto rounded-md bg-rose-100" data-e2e="payment-error"> <div v-else class="p-4 lg:p-8 text-center mx-auto rounded-md bg-rose-100" data-e2e="payment-error">
<span class="text-rose-800">Es ist ein Fehler aufgetreten. Bitte versuche es erneut.</span> <span class="text-rose-800">{{ paymentErrorMessage || "Es ist ein Fehler aufgetreten. Bitte versuche es erneut." }}</span>
</div> </div>
<!-- Trust signals --> <!-- Trust signals -->
@@ -190,6 +190,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { numberFormatter } from "~/utils/numberFormatter"; import { numberFormatter } from "~/utils/numberFormatter";
import { trackEvent } from "~/utils/trackEvent"; import { trackEvent } from "~/utils/trackEvent";
import { checkoutErrorMessageFor } from "~/utils/checkoutError";
import type { Order } from "~/types"; import type { Order } from "~/types";
const config = useRuntimeConfig(); const config = useRuntimeConfig();
@@ -197,6 +198,7 @@ const shopApi = useShopApi();
const CART_UUID_KEY = "shop:cart"; const CART_UUID_KEY = "shop:cart";
const hasPaymentError = ref(false); const hasPaymentError = ref(false);
const paymentErrorMessage = ref("");
const hasOrderError = ref(false); const hasOrderError = ref(false);
const hasAuthorisedPayment = ref(false); const hasAuthorisedPayment = ref(false);
const orderData = ref<Order>({} as Order); const orderData = ref<Order>({} as Order);
@@ -298,6 +300,7 @@ async function initializePayPalButtons() {
return response.id; return response.id;
} catch (error) { } catch (error) {
console.error("Error creating PayPal order:", error); console.error("Error creating PayPal order:", error);
paymentErrorMessage.value = checkoutErrorMessageFor(error);
hasPaymentError.value = true; hasPaymentError.value = true;
trackEvent("checkout-payment-error", { trackEvent("checkout-payment-error", {
stage: "create-order", stage: "create-order",
@@ -324,6 +327,7 @@ async function initializePayPalButtons() {
} }
} catch (error) { } catch (error) {
console.error("Error capturing payment:", error); console.error("Error capturing payment:", error);
paymentErrorMessage.value = checkoutErrorMessageFor(error);
hasPaymentError.value = true; hasPaymentError.value = true;
trackEvent("checkout-payment-error", { trackEvent("checkout-payment-error", {
stage: "capture-exception", stage: "capture-exception",
+185
View File
@@ -0,0 +1,185 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { CHECKOUT_ERROR_MESSAGES as M, checkoutErrorMessage, checkoutErrorMessageFor } from "../../utils/checkoutError.ts";
// The shop's answer for a CMS error (server/utils/cmsError.ts), as Nitro writes it for a handled error.
const shopError = (statusCode: number, data: { message: string; errors?: string[]; missing?: string[] }) => ({
error: true,
url: "/api/orders/11111111-2222-4333-8444-555555555555",
statusCode,
statusMessage: data.message,
message: data.message,
data
});
// The CMS's own answer, Strapi's error body.
const strapiError = (status: number, message: string, details: Record<string, unknown> = {}) => ({
data: null,
error: { status, name: "Error", message, details }
});
// Built like ofetch's FetchError in the browser: getters for the answer's status and parsed body.
const fetchError = (status: number | undefined, body: unknown): Error => {
const error = new Error(`[PUT] "/api/orders/11111111-2222-4333-8444-555555555555": ${status ?? "<no response>"}`);
error.name = "FetchError";
for (const [key, value] of [
["data", body],
["status", status],
["statusCode", status]
] as const) {
Object.defineProperty(error, key, { get: () => value });
}
return error;
};
const ALL_MESSAGES = new Set<string>(Object.values(M));
test("asks for a valid e-mail address when the CMS rejects data.email", () => {
const body = shopError(400, { message: "Invalid order update", errors: ["data.email: must be an email address of at most 254 characters"] });
assert.equal(checkoutErrorMessage(400, body), "Bitte gib eine gültige E-Mail-Adresse ein.");
});
test("asks to check the input when the CMS rejects another field", () => {
const body = shopError(400, {
message: "Invalid order update",
errors: ["data.invoiceAddressStructured.postalCode: must be a string of at most 200 characters, or null"]
});
assert.equal(checkoutErrorMessage(400, body), M.invalidInput);
assert.equal(
checkoutErrorMessage(400, shopError(400, { message: "Invalid order update", errors: ["data.delivery: no delivery method has this id"] })),
M.invalidInput
);
});
test("asks to check the input when the shop rejects a field before the CMS", () => {
const body = shopError(400, { message: "Invalid order update", errors: ["data.paymentAuthorised: not a field the customer may set"] });
assert.equal(checkoutErrorMessage(400, body), M.invalidInput);
});
test("mentions the e-mail address when it is among several rejected fields", () => {
const body = shopError(400, {
message: "Invalid order update",
errors: [
"data.acceptedTermsAndConditionsAt: must be sent only to accept the terms, as true or a timestamp",
"data.email: must be an email address"
]
});
assert.equal(checkoutErrorMessage(400, body), M.invalidEmail);
});
test("says that a paid order can no longer be changed", () => {
assert.equal(checkoutErrorMessage(409, shopError(409, { message: "Order can no longer be changed" })), M.alreadyPaid);
assert.equal(checkoutErrorMessage(409, shopError(409, { message: "Order is already paid" })), M.alreadyPaid);
assert.equal(M.alreadyPaid, "Diese Bestellung ist bereits bezahlt und kann nicht mehr geändert werden.");
});
test("says that a product is no longer available", () => {
assert.equal(checkoutErrorMessage(409, shopError(409, { message: "A product in the cart is no longer available" })), M.productUnavailable);
assert.equal(M.productUnavailable, "Ein Produkt in deinem Warenkorb ist nicht mehr verfügbar. Bitte prüfe deinen Warenkorb.");
});
test("names the fields an order that is not ready for checkout misses", () => {
const body = (missing: string[]) => shopError(400, { message: "Order is not ready for checkout", missing });
assert.equal(checkoutErrorMessage(400, body(["email"])), "Deine Bestellung ist noch nicht vollständig. Bitte ergänze: E-Mail-Adresse.");
assert.equal(
checkoutErrorMessage(400, body(["email", "invoiceAddress", "delivery"])),
"Deine Bestellung ist noch nicht vollständig. Bitte ergänze: E-Mail-Adresse, Rechnungsadresse, Versandart."
);
});
test("says that an order is not ready without naming a field the shop does not know", () => {
assert.equal(checkoutErrorMessage(400, shopError(400, { message: "Order is not ready for checkout" })), M.notReady);
assert.equal(checkoutErrorMessage(400, shopError(400, { message: "Order is not ready for checkout", missing: ["<b>phone</b>"] })), M.notReady);
assert.equal(
checkoutErrorMessage(400, shopError(400, { message: "Order is not ready for checkout", missing: ["phone", "delivery"] })),
"Deine Bestellung ist noch nicht vollständig. Bitte ergänze: Versandart."
);
});
test("asks to restart the payment when it does not fit the order or the order changed", () => {
for (const message of ["Payment does not belong to this order", "Payment does not match this order", "Order changed during the payment"]) {
assert.equal(checkoutErrorMessage(409, shopError(409, { message })), M.restartPayment, message);
}
assert.match(M.restartPayment, /Bitte starte die Zahlung neu\.$/);
});
test("asks to try again in a few minutes when PayPal is not reachable", () => {
for (const message of ["Could not create the PayPal order", "PayPal could not capture the payment"]) {
assert.equal(checkoutErrorMessage(502, shopError(502, { message })), M.paypalUnavailable, message);
}
assert.equal(checkoutErrorMessage(502, "<html>502 Bad Gateway</html>"), M.paypalUnavailable);
assert.match(M.paypalUnavailable, /versuche es in ein paar Minuten noch einmal\.$/);
});
test("asks not to pay again when PayPal may have taken the money but the order is not marked paid", () => {
assert.equal(checkoutErrorMessage(502, shopError(502, { message: "Payment could not be confirmed" })), M.paymentUnconfirmed);
assert.equal(checkoutErrorMessage(500, shopError(500, { message: "Payment could not be recorded" })), M.paymentUnconfirmed);
});
test("falls back to a general message for anything else", () => {
assert.equal(checkoutErrorMessage(404, shopError(404, { message: "Order not found" })), M.generic);
assert.equal(checkoutErrorMessage(400, shopError(400, { message: "Missing order UUID" })), M.generic);
assert.equal(checkoutErrorMessage(400, shopError(400, { message: "Product not found in cart" })), M.generic);
assert.equal(checkoutErrorMessage(409, shopError(409, { message: "Something else" })), M.generic);
assert.equal(checkoutErrorMessage(500, { error: true, statusCode: 500, statusMessage: "Server Error", message: "Server Error" }), M.generic);
assert.equal(checkoutErrorMessage(503, shopError(503, { message: "The CMS did not answer" })), M.generic);
assert.equal(checkoutErrorMessage(undefined, undefined), M.generic);
assert.equal(checkoutErrorMessage(409, null), M.generic);
assert.equal(M.generic, "Es ist ein Fehler aufgetreten. Bitte versuche es erneut.");
});
test("reads the CMS's own error body as well", () => {
assert.equal(
checkoutErrorMessage(400, strapiError(400, "Invalid order update", { errors: ["data.email: must be an email address"] })),
M.invalidEmail
);
assert.equal(
checkoutErrorMessage(400, strapiError(400, "Order is not ready for checkout", { missing: ["delivery"] })),
"Deine Bestellung ist noch nicht vollständig. Bitte ergänze: Versandart."
);
assert.equal(checkoutErrorMessage(409, strapiError(409, "Order can no longer be changed")), M.alreadyPaid);
assert.equal(checkoutErrorMessage(409, strapiError(409, "Order changed during the payment")), M.restartPayment);
});
test("never shows the server's text or personal data", () => {
const personal = "erika@example.org, Musterstraße 1, 70190 Stuttgart";
const bodies = [
shopError(400, { message: personal, errors: [`data.email: ${personal}`], missing: [personal] }),
shopError(409, { message: personal }),
shopError(502, { message: personal }),
strapiError(400, personal, { errors: [personal] }),
{ message: personal },
personal
];
for (const status of [undefined, 400, 404, 409, 500, 502, 503]) {
for (const body of bodies) {
const message = checkoutErrorMessage(status, body);
assert.ok(ALL_MESSAGES.has(message) || message.startsWith("Deine Bestellung ist noch nicht vollständig."), message);
assert.ok(!message.includes("erika") && !message.includes("Muster"), message);
}
}
});
test("reads the status and the body of the error $fetch throws", () => {
const body = shopError(409, { message: "Order can no longer be changed" });
assert.equal(checkoutErrorMessageFor(fetchError(409, body)), M.alreadyPaid);
assert.equal(checkoutErrorMessageFor(fetchError(502, shopError(502, { message: "Could not create the PayPal order" }))), M.paypalUnavailable);
assert.equal(
checkoutErrorMessageFor({ statusCode: 400, data: shopError(400, { message: "Invalid order update", errors: ["data.email: x"] }) }),
M.invalidEmail
);
});
test("falls back to the general message for an error without an answer", () => {
assert.equal(checkoutErrorMessageFor(fetchError(undefined, undefined)), M.generic);
assert.equal(checkoutErrorMessageFor(new TypeError("Failed to fetch")), M.generic);
assert.equal(checkoutErrorMessageFor(undefined), M.generic);
assert.equal(checkoutErrorMessageFor("error"), M.generic);
});
+98
View File
@@ -0,0 +1,98 @@
// The message a customer sees when a checkout request fails: German, addressed with "du" like the rest of the shop, chosen from the
// constants below, so it never shows the server's text or personal data. The shop's order routes answer a CMS error with its status
// and { …, data: { message, errors?, missing? } } (server/utils/cmsError.ts); a raw CMS (Strapi) error body,
// { data: null, error: { status, name, message, details } }, is read as well.
// Pure: no Nuxt or Vue imports, tested in tests/unit/checkoutError.test.ts.
// The CMS's messages (libreshop/cms src/api/order/controllers/order.ts and src/checkout/), matched but never shown.
const INVALID_ORDER_UPDATE = "Invalid order update";
const ORDER_LOCKED = "Order can no longer be changed";
const ALREADY_PAID = "Order is already paid";
const CHECKOUT_NOT_READY = "Order is not ready for checkout";
const PRODUCT_UNAVAILABLE = "A product in the cart is no longer available";
const PAYMENT_NOT_THIS_ORDER = "Payment does not belong to this order";
const PAYMENT_NOT_MATCHING = "Payment does not match this order";
const ORDER_CHANGED = "Order changed during the payment";
// PayPal answered the capture, or captured the payment, but the CMS could not confirm or record it: the money may be taken.
const PAYMENT_NOT_CONFIRMED = "Payment could not be confirmed";
const PAYMENT_NOT_RECORDED = "Payment could not be recorded";
export const CHECKOUT_ERROR_MESSAGES = {
invalidEmail: "Bitte gib eine gültige E-Mail-Adresse ein.",
invalidInput: "Deine Angaben konnten nicht gespeichert werden. Bitte prüfe sie und versuche es noch einmal.",
alreadyPaid: "Diese Bestellung ist bereits bezahlt und kann nicht mehr geändert werden.",
productUnavailable: "Ein Produkt in deinem Warenkorb ist nicht mehr verfügbar. Bitte prüfe deinen Warenkorb.",
notReady: "Deine Bestellung ist noch nicht vollständig. Bitte prüfe deine Angaben in den vorherigen Schritten.",
restartPayment: "Die Zahlung passt nicht zu deiner Bestellung, oder deine Bestellung wurde während der Zahlung geändert. Bitte starte die Zahlung neu.",
paypalUnavailable: "PayPal ist gerade nicht erreichbar. Bitte versuche es in ein paar Minuten noch einmal.",
paymentUnconfirmed: "Deine Zahlung konnte nicht bestätigt werden. Bitte bezahle nicht noch einmal, sondern melde dich bei uns.",
generic: "Es ist ein Fehler aufgetreten. Bitte versuche es erneut."
} as const;
// The fields a checkout can miss (the CMS's missingCheckoutFields), as the customer knows them from the checkout's steps.
const MISSING_FIELD_LABELS = new Map<string, string>([
["email", "E-Mail-Adresse"],
["invoiceAddress", "Rechnungsadresse"],
["delivery", "Versandart"]
]);
type CheckoutErrorFields = { message: string | undefined; errors: string[]; missing: string[] };
const isObject = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
const text = (value: unknown): string | undefined => (typeof value === "string" ? value : undefined);
const strings = (value: unknown): string[] => (Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : []);
const readCheckoutError = (body: unknown): CheckoutErrorFields => {
if (!isObject(body)) return { message: undefined, errors: [], missing: [] };
// The shop's answer: { error: true, url, statusCode, statusMessage, message, data: { message, errors?, missing? } }.
if (isObject(body.data)) {
return { message: text(body.data.message) ?? text(body.message), errors: strings(body.data.errors), missing: strings(body.data.missing) };
}
// The CMS's answer: { data: null, error: { status, name, message, details: { errors?, missing? } } }.
if (isObject(body.error)) {
const details: Record<string, unknown> = isObject(body.error.details) ? body.error.details : {};
return { message: text(body.error.message), errors: strings(details.errors), missing: strings(details.missing) };
}
return { message: text(body.message), errors: [], missing: [] };
};
// The CMS names a rejected field first, as in "data.email: must be an email address of at most 254 characters".
const isEmailError = (error: string): boolean => error === "data.email" || error.startsWith("data.email:");
const notReadyMessage = (missing: string[]): string => {
const labels = [...new Set(missing.map((field) => MISSING_FIELD_LABELS.get(field)).filter((label) => label !== undefined))];
return labels.length > 0 ? `Deine Bestellung ist noch nicht vollständig. Bitte ergänze: ${labels.join(", ")}.` : CHECKOUT_ERROR_MESSAGES.notReady;
};
/** The customer's message for a failed checkout request, from the status and the parsed body of its answer. */
export const checkoutErrorMessage = (status: number | undefined, body: unknown): string => {
const { message, errors, missing } = readCheckoutError(body);
if (status === 400) {
if (message === INVALID_ORDER_UPDATE || errors.length > 0) {
return errors.some(isEmailError) ? CHECKOUT_ERROR_MESSAGES.invalidEmail : CHECKOUT_ERROR_MESSAGES.invalidInput;
}
if (message === CHECKOUT_NOT_READY || missing.length > 0) return notReadyMessage(missing);
return CHECKOUT_ERROR_MESSAGES.generic;
}
if (status === 409) {
if (message === ORDER_LOCKED || message === ALREADY_PAID) return CHECKOUT_ERROR_MESSAGES.alreadyPaid;
if (message === PRODUCT_UNAVAILABLE) return CHECKOUT_ERROR_MESSAGES.productUnavailable;
if (message === PAYMENT_NOT_THIS_ORDER || message === PAYMENT_NOT_MATCHING || message === ORDER_CHANGED) {
return CHECKOUT_ERROR_MESSAGES.restartPayment;
}
return CHECKOUT_ERROR_MESSAGES.generic;
}
if (message === PAYMENT_NOT_CONFIRMED || message === PAYMENT_NOT_RECORDED) return CHECKOUT_ERROR_MESSAGES.paymentUnconfirmed;
if (status === 502) return CHECKOUT_ERROR_MESSAGES.paypalUnavailable;
return CHECKOUT_ERROR_MESSAGES.generic;
};
/** The customer's message for an error thrown by $fetch: ofetch's FetchError carries the answer's status and parsed body. */
export const checkoutErrorMessageFor = (error: unknown): string => {
if (!isObject(error)) return CHECKOUT_ERROR_MESSAGES.generic;
const status = typeof error.status === "number" ? error.status : typeof error.statusCode === "number" ? error.statusCode : undefined;
return checkoutErrorMessage(status, error.data);
};