Skip to content

Commit

Permalink
feat(vue-demo-store): api error resolver (#463)
Browse files Browse the repository at this point in the history
* feat: add api errors resolver

* feat: add api errors resolver

* feat: add api errors resolver

* feat: add api errors resolver

* feat: add api errors resolver

---------

Co-authored-by: patzick <13100280+patzick@users.noreply.github.com>
  • Loading branch information
mdanilowicz and patzick committed Nov 27, 2023
1 parent 57e99f1 commit 543a8e1
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 14 deletions.
11 changes: 11 additions & 0 deletions .changeset/gentle-foxes-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"vue-demo-store": minor
---

Add api error resolver for:

- Login page
- Account registration
- Update product qty (cart)

Add `useApiErrorsResolver` composable
5 changes: 5 additions & 0 deletions .changeset/perfect-rockets-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopware-pwa/types": minor
---

Add Meta type for the global Error type
6 changes: 5 additions & 1 deletion packages/types/shopware-6-client/response/Error.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ type Error = {
code: string;
title: string;
detail: string;
meta: unknown;
meta?: {
parameters?: {
[key: string]: string;
};
};
trace?: ErrorTrace[];
source?: {
pointer: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ApiClientError, type ApiError } from "@shopware/api-client";
import { ApiClientError } from "@shopware/api-client";
import type { ApiError } from "@shopware/api-client";
const emits = defineEmits<{
(e: "success"): void;
Expand All @@ -26,6 +27,8 @@ const goToRegister = () => {
router.push(formatLink("/register"));
};
const { resolveApiErrors } = useApiErrorsResolver("account_login");
const invokeLogin = async (): Promise<void> => {
loginErrors.value = [];
try {
Expand All @@ -38,9 +41,7 @@ const invokeLogin = async (): Promise<void> => {
mergeWishlistProducts();
} catch (error) {
if (error instanceof ApiClientError) {
loginErrors.value = error.details.errors.map(
({ detail }: ApiError) => detail,
);
loginErrors.value = resolveApiErrors(error.details.errors as ApiError[]);
}
}
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { useVuelidate } from "@vuelidate/core";
import { customValidators } from "@/i18n/utils/i18n-validators";
import { ApiClientError, type ApiError } from "@shopware/api-client";
import { ApiClientError } from "@shopware/api-client";
const { required, minLength, email, requiredIf } = customValidators();
const props = defineProps<{
Expand Down Expand Up @@ -90,6 +90,7 @@ const rules = computed(() => ({
}));
const $v = useVuelidate(rules, state);
const { resolveApiErrors } = useApiErrorsResolver("account_login");
const invokeSubmit = async () => {
$v.value.$touch();
Expand All @@ -108,9 +109,8 @@ const invokeSubmit = async () => {
}
} catch (error) {
if (error instanceof ApiClientError) {
error.details.errors.forEach((m: ApiError) => {
pushError(m.detail || t("messages.error"));
});
const errors = resolveApiErrors(error.details.errors);
errors.forEach((error) => pushError(error));
}
} finally {
loading.value = false;
Expand Down
16 changes: 13 additions & 3 deletions templates/vue-demo-store/components/checkout/CheckoutCartItem.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { Schemas } from "#shopware";
import { getSmallestThumbnailUrl } from "@shopware-pwa/helpers-next";
import { ApiClientError } from "@shopware/api-client";
const props = withDefaults(
defineProps<{
Expand Down Expand Up @@ -32,14 +33,23 @@ const {
const quantity = ref();
syncRefs(itemQuantity, quantity);
const { resolveApiErrors } = useApiErrorsResolver("account_login");
const updateQuantity = async (quantityInput: number | undefined) => {
if (quantityInput === itemQuantity.value) return;
isLoading.value = true;
const response = await changeItemQuantity(Number(quantityInput));
// Refresh cart after qty update
await refreshCart(response);
try {
const response = await changeItemQuantity(Number(quantityInput));
// Refresh cart after qty update
await refreshCart(response);
} catch (error) {
if (error instanceof ApiClientError) {
const errors = resolveApiErrors(error.details.errors);
errors.forEach((error) => pushError(error));
}
}
// Make sure that qty is the same as it is in the response
quantity.value = itemQuantity.value;
Expand Down
64 changes: 64 additions & 0 deletions templates/vue-demo-store/composables/useApiErrorsResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import type { ApiError } from "@shopware/api-client";

type ContextErrors = {
[key: string]: {
[key: string]: string;
};
};

export type UseApiErrorsResolver = {
resolveApiErrors(errors: ApiError[]): string[];
};

/**
* List of the unsupported/broken backend errors
*/
const contextErrors: ContextErrors = {
account_login: {
"0": "login_no_matching_customer_internal",
},
};

export function useApiErrorsResolver(context?: string): UseApiErrorsResolver {
const { $i18n } = useNuxtApp();
const { t, te } = $i18n;

const resolveApiErrors = (errors: ApiError[]) => {
const errorsTable = errors.map(({ detail, code, meta }) => {
/**
* In some cases, parameters errors
* comes with additional special characters.
* We have to remove them
*
* Example:
* "meta": {
* "parameters": {
* "{{ email }}": "<value>>"
* }
* }
*/
if (meta?.parameters) {
const pureMeta: { [key: string]: string } = {};
for (const [key, value] of Object.entries(meta?.parameters)) {
pureMeta[key.replace(/[^a-zA-Z0-9 ]/g, "")] = value;
}
meta.parameters = pureMeta;
}

if (code && te(`errors.${code}`)) {
return t(`errors.${code}`, { ...meta?.parameters });
}
if (context && code && contextErrors[context][code]) {
return t(`errors.${contextErrors[context][code]}`);
}

return detail || "No details provided";
});

return errorsTable;
};

return {
resolveApiErrors,
};
}
22 changes: 21 additions & 1 deletion templates/vue-demo-store/i18n/de-DE/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@
"billing-address-invalid": "Die gewählte Rechnungsadresse ist nicht gültig oder unvollständig. Bitte prüfen Sie Ihre Angaben.",
"cart-merged-hint": "Der aktuelle Warenkorb enthält u.U. zusätzliche Produkte, die noch von einem früheren Besuch gespeichert waren.",
"product-not-found": "Das Produkt wurde nicht gefunden.",
"salutation-missing": "Es wurde keine Anrede konfiguriert, bitte wählen Sie beim Abschluss Ihrer Bestellung eine Anrede aus."
"salutation-missing": "Es wurde keine Anrede konfiguriert, bitte wählen Sie beim Abschluss Ihrer Bestellung eine Anrede aus.",
"login_no_matching_customer_internal": "Ungültiger Benutzername und/oder Passwort.",
"message-default": "Leider ist ein Fehler aufgetreten.",
"message-404": "Die angeforderte Seite konnte nicht gefunden werden.",
"addToCartError": "Beim Versuch, Artikel zum Warenkorb hinzuzufügen, ist ein Fehler aufgetreten.",
"productNotFound": "Produkt {number} nicht gefunden.",
"message-403-ajax": "Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.",
"message-403": "Ihre Sitzung ist abgelaufen. Bitte kehren Sie zur letzten Seite zurück und versuchen Sie es erneut.",
"rateLimitExceeded": "Zu viele Anfragen. Bitte warten Sie %seconds% Sekunden, bevor Sie es erneut versuchen.",
"CHECKOUT__CART_INVALID_LINE_ITEM_QUANTITY": "Die Menge muss eine positive ganze Zahl sein. Gegeben: {quantity}",
"VIOLATION::IS_BLANK_ERROR": "{field} sollte nicht leer sein.",
"VIOLATION::TOO_LOW_ERROR": "{field} sollte nicht leer sein.",
"VIOLATION::STRICT_CHECK_FAILED_ERROR": "{field} ist ungültig.",
"VIOLATION::CUSTOMER_PASSWORD_NOT_CORRECT": "Passwort inkorrekt.",
"VIOLATION::VAT_ID_FORMAT_NOT_CORRECT": "Die eingegebene USt-IdNr. hat nicht das richtige Format.",
"VIOLATION::ZIP_CODE_INVALID": "Die eingegebene Postleitzahl hat nicht das richtige Format.",
"FRAMEWORK__INVALID_UUID": "Die ausgewählte Zahlungsmethode existiert nicht.",
"CHECKOUT__UNKNOWN_PAYMENT_METHOD": "Die ausgewählte Zahlungsmethode existiert nicht.",
"VIOLATION::TOO_SHORT_ERROR": "{field} ist zu kurz.",
"CHECKOUT__ORDER_ORDER_ALREADY_PAID": "Die Bestellung mit der Bestellnummer {orderNumber} wurde bereits bezahlt und kann nicht mehr bearbeitet werden.",
"CHECKOUT__ORDER_ORDER_NOT_FOUND": "Diese Bestellung konnte nicht gefunden werden."
}
}
23 changes: 22 additions & 1 deletion templates/vue-demo-store/i18n/en-GB/errors.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,27 @@
"billing-address-invalid": "The selected billing address is not valid or incomplete. Please check your entries.",
"cart-merged-hint": "The current shopping cart might contain additional products that have been added and saved during a previous visit.",
"product-not-found": "The product could not be found.",
"salutation-missing": "A salutation is missing from your profile, please choose one during checkout."
"salutation-missing": "A salutation is missing from your profile, please choose one during checkout.",
"login_no_matching_customer_internal": "Invalid username and/or password.",
"message-default": "Unfortunately, something went wrong.",
"message-404": "The requested page cannot be found.",
"addToCartError": "An error occurred while trying to add items to the shopping cart.",
"productNotFound": "Product {number} not found.",
"message-403-ajax": "Your session has expired. Please reload the page and try again.",
"message-403": "Your session has expired. Please return to the last page and try again.",
"rateLimitExceeded": "Too many requests. Please wait %seconds% seconds before trying again.",
"CHECKOUT__CART_INVALID_LINE_ITEM_QUANTITY": "The quantity must be a positive integer. Given: {quantity}",
"VIOLATION::CUSTOMER_EMAIL_NOT_UNIQUE": "The email address {email} is already in use",
"VIOLATION::IS_BLANK_ERROR": "{field} should not be empty.",
"VIOLATION::TOO_LOW_ERROR": "{field} should not be empty.",
"VIOLATION::STRICT_CHECK_FAILED_ERROR": "{field} is invalid",
"VIOLATION::CUSTOMER_PASSWORD_NOT_CORRECT": "Password incorrect.",
"VIOLATION::VAT_ID_FORMAT_NOT_CORRECT": "The VAT Reg.No. you have entered does not have the correct format.",
"VIOLATION::ZIP_CODE_INVALID": "The postal code you have entered does not have the correct format.",
"FRAMEWORK__INVALID_UUID": "The selected payment method does not exist.",
"CHECKOUT__UNKNOWN_PAYMENT_METHOD": "The selected payment method does not exist.",
"VIOLATION::TOO_SHORT_ERROR": "{field} is too short.",
"CHECKOUT__ORDER_ORDER_ALREADY_PAID": "The order with the order number {orderNumber} was already paid and cannot be edited afterwards.",
"CHECKOUT__ORDER_ORDER_NOT_FOUND": "This order could not be found."
}
}

2 comments on commit 543a8e1

@vercel
Copy link

@vercel vercel bot commented on 543a8e1 Nov 27, 2023

Choose a reason for hiding this comment

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

@vercel
Copy link

@vercel vercel bot commented on 543a8e1 Nov 27, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

frontends-demo – ./templates/vue-demo-store

frontends-demo-git-main-shopware-frontends.vercel.app
frontends-demo-shopware-frontends.vercel.app
frontends-demo.vercel.app

Please sign in to comment.