diff --git a/package.json b/package.json index db22afe..7993871 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,9 @@ "graphql-request": "^7.1.0", "lucide-react": "^0.407.0", "modern-errors": "^7.0.1", + "modern-errors-serialize": "^6.1.0", "next": "14.2.5", + "next-safe-action": "7.9.3", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.52.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 07940cd..1c07db6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -55,9 +55,15 @@ importers: modern-errors: specifier: ^7.0.1 version: 7.0.1 + modern-errors-serialize: + specifier: ^6.1.0 + version: 6.1.0(modern-errors@7.0.1) next: specifier: 14.2.5 version: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-safe-action: + specifier: 7.9.3 + version: 7.9.3(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.23.8) react: specifier: ^18 version: 18.3.1 @@ -1735,6 +1741,13 @@ packages: } engines: { node: ">=18.18.0" } + error-serializer@8.0.0: + resolution: + { + integrity: sha512-mh1xG0257SA4P0XfT8RHPyygomaFvVekLpJWvxvNdOnBWIaJLs9otIMCd75MNUbm3aEOCOZ1VkMxxQlmloq7NA==, + } + engines: { node: ">=18.18.0" } + es-abstract@1.23.3: resolution: { @@ -2937,6 +2950,15 @@ packages: } engines: { node: ">=16 || 14 >=14.17" } + modern-errors-serialize@6.1.0: + resolution: + { + integrity: sha512-Us+REHRbeOM9np4GzDiBtNd1JBHwg8BZrt2kIjAivCtl32635FlszrAwynYx8bGcyRDGM5jVeZJeY4HOMwgBzA==, + } + engines: { node: ">=18.18.0" } + peerDependencies: + modern-errors: ^7.0.1 + modern-errors@7.0.1: resolution: { @@ -2982,6 +3004,30 @@ packages: integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, } + next-safe-action@7.9.3: + resolution: + { + integrity: sha512-2GH7/iRiM5R/y6sIQZsNHGeRr/iKQJsg8ejP63WhTS7fXS9KzxVbEKrWwLNNhL33V9cn0448cPSI/aiSK/PUbA==, + } + engines: { node: ">=18.17" } + peerDependencies: + "@sinclair/typebox": ">= 0.33.3" + next: ">= 14.0.0" + react: ">= 18.2.0" + react-dom: ">= 18.2.0" + valibot: ">= 0.36.0" + yup: ">= 1.0.0" + zod: ">= 3.0.0" + peerDependenciesMeta: + "@sinclair/typebox": + optional: true + valibot: + optional: true + yup: + optional: true + zod: + optional: true + next@14.2.5: resolution: { @@ -3681,6 +3727,13 @@ packages: } engines: { node: ">=0.4" } + safe-json-value@3.0.0: + resolution: + { + integrity: sha512-d9NN/9QDNTfhHr3K1gGdDCn7K0OBvBHrNDwA4PqGPkn6nUmQL7GzMPBC+nmkY7G450B26wkfu7lZTVH7CJ+Jgw==, + } + engines: { node: ">=18.18.0" } + safe-regex-test@1.0.3: resolution: { @@ -5327,6 +5380,14 @@ snapshots: dependencies: error-class-utils: 4.0.0 + error-serializer@8.0.0: + dependencies: + is-error-instance: 3.0.0 + is-plain-obj: 4.1.0 + normalize-exception: 3.0.0 + safe-json-value: 3.0.0 + set-error-class: 3.0.0 + es-abstract@1.23.3: dependencies: array-buffer-byte-length: 1.0.1 @@ -6190,6 +6251,12 @@ snapshots: minipass@7.1.2: {} + modern-errors-serialize@6.1.0(modern-errors@7.0.1): + dependencies: + error-serializer: 8.0.0 + is-plain-obj: 4.1.0 + modern-errors: 7.0.1 + modern-errors@7.0.1: dependencies: error-class-utils: 4.0.0 @@ -6218,6 +6285,14 @@ snapshots: natural-compare@1.4.0: {} + next-safe-action@7.9.3(next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(zod@3.23.8): + dependencies: + next: 14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + zod: 3.23.8 + next@14.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: "@next/env": 14.2.5 @@ -6593,6 +6668,11 @@ snapshots: has-symbols: 1.0.3 isarray: 2.0.5 + safe-json-value@3.0.0: + dependencies: + is-plain-obj: 4.1.0 + normalize-exception: 3.0.0 + safe-regex-test@1.0.3: dependencies: call-bind: 1.0.7 diff --git a/src/app/env/[envUrl]/checkout/[checkoutId]/page.tsx b/src/app/env/[envUrl]/checkout/[checkoutId]/page.tsx index 8124f7a..131b31e 100644 --- a/src/app/env/[envUrl]/checkout/[checkoutId]/page.tsx +++ b/src/app/env/[envUrl]/checkout/[checkoutId]/page.tsx @@ -19,33 +19,35 @@ export default async function CheckoutDetailsPage(props: { checkoutId, }); - if (checkoutDetailsResponse.type === "error") { + if (checkoutDetailsResponse?.serverError) { // Sends the error to the error boundary - throw new CheckoutDetailsPageError(checkoutDetailsResponse.message); + throw new CheckoutDetailsPageError( + checkoutDetailsResponse?.serverError.message, + ); } const hasDeliveryMethodsToSelect = - checkoutDetailsResponse.value.checkout?.shippingMethods?.length ?? 0; + checkoutDetailsResponse?.data?.checkout?.shippingMethods?.length ?? 0; return (
{hasDeliveryMethodsToSelect ? ( {checkout?.id ? ( ) : ( diff --git a/src/app/env/[envUrl]/checkout/[checkoutId]/payment-gateway/page.tsx b/src/app/env/[envUrl]/checkout/[checkoutId]/payment-gateway/page.tsx index 5690eb3..0cd3a84 100644 --- a/src/app/env/[envUrl]/checkout/[checkoutId]/payment-gateway/page.tsx +++ b/src/app/env/[envUrl]/checkout/[checkoutId]/payment-gateway/page.tsx @@ -18,15 +18,17 @@ export default async function PaymentGatewaysPage(props: { checkoutId, }); - if (paymentGatewaysResponse.type === "error") { + if (paymentGatewaysResponse?.serverError) { // Sends the error to the error boundary - throw new PaymentGatewaysError(paymentGatewaysResponse.message); + throw new PaymentGatewaysError( + paymentGatewaysResponse?.serverError.message, + ); } return (
); diff --git a/src/env.ts b/src/env.ts index d2f3ae4..9d9e608 100644 --- a/src/env.ts +++ b/src/env.ts @@ -2,12 +2,11 @@ import { createEnv } from "@t3-oss/env-nextjs"; import { vercel } from "@t3-oss/env-nextjs/presets"; import { z } from "zod"; +import { envUrlSchema } from "./lib/env-url"; + export const env = createEnv({ client: { - NEXT_PUBLIC_INITIAL_ENV_URL: z - .string() - .url() - .refine((v) => v.endsWith("/graphql/"), "Must end with /graphql/"), + NEXT_PUBLIC_INITIAL_ENV_URL: envUrlSchema, NEXT_PUBLIC_INITIAL_CHANNEL_SLUG: z.string(), NEXT_PUBLIC_INITIAL_CHECKOUT_COUNTRY_CODE: z .enum(["PL", "SE", "US"]) diff --git a/src/graphql-cache.d.ts b/src/graphql-cache.d.ts index 16fa1a1..01e4c69 100644 --- a/src/graphql-cache.d.ts +++ b/src/graphql-cache.d.ts @@ -18,10 +18,10 @@ declare module 'gql.tada' { TadaDocumentNode<{ checkout: { billingAddress: { [$tada.fragmentRefs]: { BillingAddress: "Address"; }; } | null; shippingAddress: { [$tada.fragmentRefs]: { ShippingAddress: "Address"; }; } | null; shippingMethods: { [$tada.fragmentRefs]: { ShippingMethod: "ShippingMethod"; }; }[]; deliveryMethod: { __typename?: "ShippingMethod" | undefined; [$tada.fragmentRefs]: { DeliveryMethod: "ShippingMethod"; }; } | { __typename?: "Warehouse" | undefined; [$tada.fragmentRefs]: { CollectionPoint: "Warehouse"; }; } | null; } | null; }, { checkoutId: string; }, void>; "\n mutation updateBillingAddress($checkoutId: ID!, $input: AddressInput!) {\n checkoutBillingAddressUpdate(\n checkoutId: $checkoutId\n billingAddress: $input\n ) {\n errors {\n field\n message\n }\n }\n }\n": TadaDocumentNode<{ checkoutBillingAddressUpdate: { errors: { field: string | null; message: string | null; }[]; } | null; }, { input: { metadata?: { value: string; key: string; }[] | null | undefined; phone?: string | null | undefined; countryArea?: string | null | undefined; country?: "ID" | "AF" | "AX" | "AL" | "DZ" | "AS" | "AD" | "AO" | "AI" | "AQ" | "AG" | "AR" | "AM" | "AW" | "AU" | "AT" | "AZ" | "BS" | "BH" | "BD" | "BB" | "BY" | "BE" | "BZ" | "BJ" | "BM" | "BT" | "BO" | "BQ" | "BA" | "BW" | "BV" | "BR" | "IO" | "BN" | "BG" | "BF" | "BI" | "CV" | "KH" | "CM" | "CA" | "KY" | "CF" | "TD" | "CL" | "CN" | "CX" | "CC" | "CO" | "KM" | "CG" | "CD" | "CK" | "CR" | "CI" | "HR" | "CU" | "CW" | "CY" | "CZ" | "DK" | "DJ" | "DM" | "DO" | "EC" | "EG" | "SV" | "GQ" | "ER" | "EE" | "SZ" | "ET" | "EU" | "FK" | "FO" | "FJ" | "FI" | "FR" | "GF" | "PF" | "TF" | "GA" | "GM" | "GE" | "DE" | "GH" | "GI" | "GR" | "GL" | "GD" | "GP" | "GU" | "GT" | "GG" | "GN" | "GW" | "GY" | "HT" | "HM" | "VA" | "HN" | "HK" | "HU" | "IS" | "IN" | "IR" | "IQ" | "IE" | "IM" | "IL" | "IT" | "JM" | "JP" | "JE" | "JO" | "KZ" | "KE" | "KI" | "KW" | "KG" | "LA" | "LV" | "LB" | "LS" | "LR" | "LY" | "LI" | "LT" | "LU" | "MO" | "MG" | "MW" | "MY" | "MV" | "ML" | "MT" | "MH" | "MQ" | "MR" | "MU" | "YT" | "MX" | "FM" | "MD" | "MC" | "MN" | "ME" | "MS" | "MA" | "MZ" | "MM" | "NA" | "NR" | "NP" | "NL" | "NC" | "NZ" | "NI" | "NE" | "NG" | "NU" | "NF" | "KP" | "MK" | "MP" | "NO" | "OM" | "PK" | "PW" | "PS" | "PA" | "PG" | "PY" | "PE" | "PH" | "PN" | "PL" | "PT" | "PR" | "QA" | "RE" | "RO" | "RU" | "RW" | "BL" | "SH" | "KN" | "LC" | "MF" | "PM" | "VC" | "WS" | "SM" | "ST" | "SA" | "SN" | "RS" | "SC" | "SL" | "SG" | "SX" | "SK" | "SI" | "SB" | "SO" | "ZA" | "GS" | "KR" | "SS" | "ES" | "LK" | "SD" | "SR" | "SJ" | "SE" | "CH" | "SY" | "TW" | "TJ" | "TZ" | "TH" | "TL" | "TG" | "TK" | "TO" | "TT" | "TN" | "TR" | "TM" | "TC" | "TV" | "UG" | "UA" | "AE" | "GB" | "UM" | "US" | "UY" | "UZ" | "VU" | "VE" | "VN" | "VG" | "VI" | "WF" | "EH" | "YE" | "ZM" | "ZW" | null | undefined; postalCode?: string | null | undefined; cityArea?: string | null | undefined; city?: string | null | undefined; streetAddress2?: string | null | undefined; streetAddress1?: string | null | undefined; companyName?: string | null | undefined; lastName?: string | null | undefined; firstName?: string | null | undefined; }; checkoutId: string; }, void>; - "\n mutation checkoutDeliveryMethodUpdate($checkoutId: ID!, $input: ID!) {\n checkoutDeliveryMethodUpdate(id: $checkoutId, deliveryMethodId: $input) {\n errors {\n field\n message\n }\n }\n }\n": - TadaDocumentNode<{ checkoutDeliveryMethodUpdate: { errors: { field: string | null; message: string | null; }[]; } | null; }, { input: string; checkoutId: string; }, void>; "\n mutation updateShippingAddress($checkoutId: ID!, $input: AddressInput!) {\n checkoutShippingAddressUpdate(\n checkoutId: $checkoutId\n shippingAddress: $input\n ) {\n errors {\n field\n message\n }\n }\n }\n": TadaDocumentNode<{ checkoutShippingAddressUpdate: { errors: { field: string | null; message: string | null; }[]; } | null; }, { input: { metadata?: { value: string; key: string; }[] | null | undefined; phone?: string | null | undefined; countryArea?: string | null | undefined; country?: "PL" | "SE" | "US" | "ID" | "AF" | "AX" | "AL" | "DZ" | "AS" | "AD" | "AO" | "AI" | "AQ" | "AG" | "AR" | "AM" | "AW" | "AU" | "AT" | "AZ" | "BS" | "BH" | "BD" | "BB" | "BY" | "BE" | "BZ" | "BJ" | "BM" | "BT" | "BO" | "BQ" | "BA" | "BW" | "BV" | "BR" | "IO" | "BN" | "BG" | "BF" | "BI" | "CV" | "KH" | "CM" | "CA" | "KY" | "CF" | "TD" | "CL" | "CN" | "CX" | "CC" | "CO" | "KM" | "CG" | "CD" | "CK" | "CR" | "CI" | "HR" | "CU" | "CW" | "CY" | "CZ" | "DK" | "DJ" | "DM" | "DO" | "EC" | "EG" | "SV" | "GQ" | "ER" | "EE" | "SZ" | "ET" | "EU" | "FK" | "FO" | "FJ" | "FI" | "FR" | "GF" | "PF" | "TF" | "GA" | "GM" | "GE" | "DE" | "GH" | "GI" | "GR" | "GL" | "GD" | "GP" | "GU" | "GT" | "GG" | "GN" | "GW" | "GY" | "HT" | "HM" | "VA" | "HN" | "HK" | "HU" | "IS" | "IN" | "IR" | "IQ" | "IE" | "IM" | "IL" | "IT" | "JM" | "JP" | "JE" | "JO" | "KZ" | "KE" | "KI" | "KW" | "KG" | "LA" | "LV" | "LB" | "LS" | "LR" | "LY" | "LI" | "LT" | "LU" | "MO" | "MG" | "MW" | "MY" | "MV" | "ML" | "MT" | "MH" | "MQ" | "MR" | "MU" | "YT" | "MX" | "FM" | "MD" | "MC" | "MN" | "ME" | "MS" | "MA" | "MZ" | "MM" | "NA" | "NR" | "NP" | "NL" | "NC" | "NZ" | "NI" | "NE" | "NG" | "NU" | "NF" | "KP" | "MK" | "MP" | "NO" | "OM" | "PK" | "PW" | "PS" | "PA" | "PG" | "PY" | "PE" | "PH" | "PN" | "PT" | "PR" | "QA" | "RE" | "RO" | "RU" | "RW" | "BL" | "SH" | "KN" | "LC" | "MF" | "PM" | "VC" | "WS" | "SM" | "ST" | "SA" | "SN" | "RS" | "SC" | "SL" | "SG" | "SX" | "SK" | "SI" | "SB" | "SO" | "ZA" | "GS" | "KR" | "SS" | "ES" | "LK" | "SD" | "SR" | "SJ" | "CH" | "SY" | "TW" | "TJ" | "TZ" | "TH" | "TL" | "TG" | "TK" | "TO" | "TT" | "TN" | "TR" | "TM" | "TC" | "TV" | "UG" | "UA" | "AE" | "GB" | "UM" | "UY" | "UZ" | "VU" | "VE" | "VN" | "VG" | "VI" | "WF" | "EH" | "YE" | "ZM" | "ZW" | null | undefined; postalCode?: string | null | undefined; cityArea?: string | null | undefined; city?: string | null | undefined; streetAddress2?: string | null | undefined; streetAddress1?: string | null | undefined; companyName?: string | null | undefined; lastName?: string | null | undefined; firstName?: string | null | undefined; }; checkoutId: string; }, void>; + "\n mutation checkoutDeliveryMethodUpdate($checkoutId: ID!, $input: ID!) {\n checkoutDeliveryMethodUpdate(id: $checkoutId, deliveryMethodId: $input) {\n errors {\n field\n message\n }\n }\n }\n": + TadaDocumentNode<{ checkoutDeliveryMethodUpdate: { errors: { field: string | null; message: string | null; }[]; } | null; }, { input: string; checkoutId: string; }, void>; "\n fragment TotalPrice on TaxedMoney {\n gross {\n amount\n currency\n }\n }\n": TadaDocumentNode<{ gross: { amount: number; currency: string; }; }, {}, { fragment: "TotalPrice"; on: "TaxedMoney"; masked: true; }>; "\n query GetCheckoutTotalPrice($checkoutId: ID!) {\n checkout(id: $checkoutId) {\n totalPrice {\n ...TotalPrice\n }\n }\n }\n ": diff --git a/src/lib/env-url.ts b/src/lib/env-url.ts new file mode 100644 index 0000000..70f75dc --- /dev/null +++ b/src/lib/env-url.ts @@ -0,0 +1,6 @@ +import { z } from "zod"; + +export const envUrlSchema = z + .string() + .url() + .endsWith("/graphql/", "Must end with /graphql/"); diff --git a/src/lib/errors.ts b/src/lib/errors.ts index 8063038..0c8115d 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -1,3 +1,8 @@ import ModernError from "modern-errors"; +import modernErrorsSerialize from "modern-errors-serialize"; -export const BaseError = ModernError.subclass("BaseError"); +export const BaseError = ModernError.subclass("BaseError", { + plugins: [modernErrorsSerialize], +}); + +export const UnknownError = BaseError.subclass("UnknownError"); diff --git a/src/lib/logger.ts b/src/lib/logger.ts index 52dfb2a..e49536b 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -57,7 +57,7 @@ logger.attachTransport((log) => { }; // eslint-disable-next-line no-console - console.log(`${name} ${message}`, JSON.stringify(attributes, null, 2)); + console.log(`${name}: ${message}`, JSON.stringify(attributes, null, 2)); }); export const createLogger = (name: string, params?: Record) => diff --git a/src/lib/safe-action.ts b/src/lib/safe-action.ts new file mode 100644 index 0000000..c271e12 --- /dev/null +++ b/src/lib/safe-action.ts @@ -0,0 +1,36 @@ +import { + createSafeActionClient, + DEFAULT_SERVER_ERROR_MESSAGE, +} from "next-safe-action"; +import { z } from "zod"; + +import { BaseError } from "./errors"; +import { createLogger } from "./logger"; + +const logger = createLogger("serverAction"); + +export const actionClient = createSafeActionClient({ + defineMetadataSchema() { + return z.object({ + actionName: z.string(), + }); + }, + handleServerError(error, utils) { + logger.error(`Error during ${utils.metadata.actionName} action handling:`, { + error: error, + actionName: utils.metadata.actionName, + }); + + if (error instanceof BaseError) { + return { + message: error.message, + name: error.name, + }; + } + + return { + message: DEFAULT_SERVER_ERROR_MESSAGE, + name: "Adyen testclient error", + }; + }, +}); diff --git a/src/modules/checkout-details/actions/get-checkout-details.ts b/src/modules/checkout-details/actions/get-checkout-details.ts index ea46c2f..d158f0f 100644 --- a/src/modules/checkout-details/actions/get-checkout-details.ts +++ b/src/modules/checkout-details/actions/get-checkout-details.ts @@ -1,9 +1,12 @@ "use server"; -import { graphql, ResultOf } from "gql.tada"; +import { graphql } from "gql.tada"; import request from "graphql-request"; +import { z } from "zod"; -import { createLogger } from "@/lib/logger"; +import { envUrlSchema } from "@/lib/env-url"; +import { BaseError, UnknownError } from "@/lib/errors"; +import { actionClient } from "@/lib/safe-action"; import { BillingAddressFragment, @@ -13,8 +16,6 @@ import { ShippingMethodFragment, } from "../fragments"; -const logger = createLogger("getCheckoutDetails"); - const GetCheckoutQuery = graphql( ` query getCheckout($checkoutId: ID!) { @@ -44,29 +45,20 @@ const GetCheckoutQuery = graphql( ], ); -export const getCheckoutDetails = async (props: { - envUrl: string; - checkoutId: string; -}): Promise< - | { type: "error"; name: string; message: string } - | { type: "success"; value: ResultOf } -> => { - const { envUrl, checkoutId } = props; - try { +export const getCheckoutDetails = actionClient + .schema( + z.object({ + checkoutId: z.string(), + envUrl: envUrlSchema, + }), + ) + .metadata({ actionName: "getCheckoutDetails" }) + .action(async ({ parsedInput: { envUrl, checkoutId } }) => { const response = await request(envUrl, GetCheckoutQuery, { checkoutId, + }).catch((error) => { + throw BaseError.normalize(error, UnknownError); }); - return { - type: "success", - value: response, - }; - } catch (error) { - logger.error("Failed to get checkout details", { error }); - return { - type: "error", - name: "GetCheckoutError", - message: "Failed to get checkout details", - }; - } -}; + return response; + }); diff --git a/src/modules/checkout-details/actions/index.ts b/src/modules/checkout-details/actions/index.ts index ad8a494..1020c69 100644 --- a/src/modules/checkout-details/actions/index.ts +++ b/src/modules/checkout-details/actions/index.ts @@ -1,3 +1,4 @@ export * from "./get-checkout-details"; export * from "./redirect-to-payment-gateway-select"; export * from "./update-billing-address"; +export * from "./update-shipping-address"; diff --git a/src/modules/checkout-details/actions/update-billing-address.ts b/src/modules/checkout-details/actions/update-billing-address.ts index c025a63..388efb1 100644 --- a/src/modules/checkout-details/actions/update-billing-address.ts +++ b/src/modules/checkout-details/actions/update-billing-address.ts @@ -4,12 +4,12 @@ import request from "graphql-request"; import { revalidatePath } from "next/cache"; import { z } from "zod"; -import { createLogger } from "@/lib/logger"; +import { envUrlSchema } from "@/lib/env-url"; +import { BaseError, UnknownError } from "@/lib/errors"; +import { actionClient } from "@/lib/safe-action"; import { createPath } from "@/lib/utils"; -import { BillingAddressSchemaType } from "../components/billing"; - -const logger = createLogger("updateBillingAddress"); +import { BillingAddressSchema } from "../schemas"; const UpdateBillingAddressSchema = z.object({ checkoutBillingAddressUpdate: z.object({ @@ -36,60 +36,51 @@ const UpdateBillingAddressMutation = graphql(` } `); -export const updateBillingAddress = async (props: { - envUrl: string; - checkoutId: string; - billingAddress: BillingAddressSchemaType; -}): Promise< - | { type: "error"; name: string; message: string } - | { - type: "success"; - value: z.infer; - } -> => { - const { envUrl, checkoutId, billingAddress } = props; +const UpdateBillingAddressParsingResponseError = BaseError.subclass( + "UpdateBillingAddressParsingResponseError", +); +const UpdateBillingAddressMutationError = BaseError.subclass( + "UpdateBillingAddressMutationError", +); - try { +export const updateBillingAddress = actionClient + .schema( + z.object({ + envUrl: envUrlSchema, + checkoutId: z.string(), + billingAddress: BillingAddressSchema, + }), + ) + .metadata({ actionName: "updateBillingAddress" }) + .action(async ({ parsedInput: { envUrl, checkoutId, billingAddress } }) => { const response = await request(envUrl, UpdateBillingAddressMutation, { checkoutId, input: billingAddress, + }).catch((error) => { + throw BaseError.normalize(error, UnknownError); }); + const parsedResponse = UpdateBillingAddressSchema.safeParse(response); if (parsedResponse.error) { - logger.error("Failed to parse updateBillingAddress response", { - error: parsedResponse.error, - }); - return { - type: "error", - name: "ParsingUpdateBillingAddressError", - message: parsedResponse.error.message, - }; + throw UpdateBillingAddressParsingResponseError.normalize( + parsedResponse.error, + ); } if (parsedResponse.data.checkoutBillingAddressUpdate.errors.length > 0) { - logger.error("Failed to update billing address of checkout", { - errors: parsedResponse.data.checkoutBillingAddressUpdate.errors, - }); - return { - type: "error", - name: "UpdateBillingAddressError", - message: - "Failed to update billing address - errors in updateBillingAddress mutation", - }; + throw new UpdateBillingAddressMutationError( + "Failed to create checkout - errors in createCheckout mutation.", + { + errors: parsedResponse.data.checkoutBillingAddressUpdate.errors.map( + (e) => UpdateBillingAddressMutationError.normalize(e), + ), + }, + ); } - revalidatePath( createPath("env", encodeURIComponent(envUrl), "checkout", checkoutId), ); - return { type: "success", value: parsedResponse.data }; - } catch (error) { - logger.error("Failed to update billing address", { error }); - return { - type: "error", - name: "UpdateBillingAddressError", - message: "Failed to update billing address", - }; - } -}; + return parsedResponse.data; + }); diff --git a/src/modules/checkout-details/actions/update-delivery-method.tsx b/src/modules/checkout-details/actions/update-delivery-method.tsx index bba8652..5e70a69 100644 --- a/src/modules/checkout-details/actions/update-delivery-method.tsx +++ b/src/modules/checkout-details/actions/update-delivery-method.tsx @@ -3,14 +3,11 @@ import { graphql } from "gql.tada"; import request from "graphql-request"; import { z } from "zod"; -import { BaseError } from "@/lib/errors"; -import { createLogger } from "@/lib/logger"; +import { envUrlSchema } from "@/lib/env-url"; +import { BaseError, UnknownError } from "@/lib/errors"; +import { actionClient } from "@/lib/safe-action"; -const logger = createLogger("updateDeliveryMethod"); - -const UpdateDeliveryMethodError = BaseError.subclass( - "UpdateDeliveryMethodError", -); +import { redirectToPaymentGatewaySelect } from "./redirect-to-payment-gateway-select"; const UpdateDeliveryMethodSchema = z.object({ checkoutDeliveryMethodUpdate: z.object({ @@ -34,58 +31,57 @@ const UpdateDeliveryMethodMutation = graphql(` } `); -export const updateDeliveryMethod = async (props: { - envUrl: string; - checkoutId: string; - deliveryMethod: string; -}): Promise< - | { type: "error"; name: string; message: string } - | { - type: "success"; - value: z.infer; - } -> => { - const { envUrl, checkoutId, deliveryMethod } = props; - - try { - const response = await request(envUrl, UpdateDeliveryMethodMutation, { - checkoutId, - input: deliveryMethod, - }); - - const parsedResponse = UpdateDeliveryMethodSchema.safeParse(response); +const UpdateDeliveryMethodParsingResponseError = BaseError.subclass( + "UpdateDeliveryMethodParsingResponseError", +); +const UpdateDeliveryMethodMutationError = BaseError.subclass( + "UpdateDeliveryMethodMutationError", +); - if (parsedResponse.error) { - logger.error("Failed to parse checkoutDeliveryMethodUpdate response", { - error: parsedResponse.error, +export const updateDeliveryMethod = actionClient + .schema( + z.object({ + envUrl: envUrlSchema, + checkoutId: z.string(), + deliveryMethod: z.string(), + }), + ) + .metadata({ actionName: "updateDeliveryMethod" }) + .action( + async ({ parsedInput: { envUrl, checkoutId, deliveryMethod } }) => { + const response = await request(envUrl, UpdateDeliveryMethodMutation, { + checkoutId, + input: deliveryMethod, + }).catch((error) => { + throw BaseError.normalize(error, UnknownError); }); - return { - type: "error", - name: "ParsingUpdateDeliveryMethodError", - message: parsedResponse.error.message, - }; - } - if (parsedResponse.data.checkoutDeliveryMethodUpdate.errors.length > 0) { - logger.error("Failed to update delivery method of checkout", { - errors: parsedResponse.data.checkoutDeliveryMethodUpdate.errors, - }); + const parsedResponse = UpdateDeliveryMethodSchema.safeParse(response); - return { - type: "error", - name: "UpdateDeliveryMethodError", - message: - "Failed to update delivery method - errors in updateBillingAddress mutation", - }; - } + if (parsedResponse.error) { + throw UpdateDeliveryMethodParsingResponseError.normalize( + parsedResponse.error, + ); + } - return { type: "success", value: parsedResponse.data }; - } catch (error) { - logger.error("Failed to update delivery method", { error }); - return { - type: "error", - name: "UpdateDeliveryMethodError", - message: "Failed to update delivery method", - }; - } -}; + if (parsedResponse.data.checkoutDeliveryMethodUpdate.errors.length > 0) { + throw new UpdateDeliveryMethodMutationError( + "Failed to create checkout - errors in createCheckout mutation.", + { + errors: parsedResponse.data.checkoutDeliveryMethodUpdate.errors.map( + (e) => UpdateDeliveryMethodMutationError.normalize(e), + ), + }, + ); + } + + return parsedResponse.data; + }, + { + onSuccess: async ({ parsedInput }) => { + await redirectToPaymentGatewaySelect({ + checkoutId: parsedInput.checkoutId, + }); + }, + }, + ); diff --git a/src/modules/checkout-details/actions/update-shipping-address.ts b/src/modules/checkout-details/actions/update-shipping-address.ts index 0606d8e..97834f7 100644 --- a/src/modules/checkout-details/actions/update-shipping-address.ts +++ b/src/modules/checkout-details/actions/update-shipping-address.ts @@ -4,12 +4,12 @@ import request from "graphql-request"; import { revalidatePath } from "next/cache"; import { z } from "zod"; -import { createLogger } from "@/lib/logger"; +import { envUrlSchema } from "@/lib/env-url"; +import { BaseError, UnknownError } from "@/lib/errors"; +import { actionClient } from "@/lib/safe-action"; import { createPath } from "@/lib/utils"; -import { ShippingAddressSchemaType } from "../components/shipping"; - -const logger = createLogger("updateShippingAddress"); +import { ShippingAddressSchema } from "../schemas"; const UpdateShippingAddressSchema = z.object({ checkoutShippingAddressUpdate: z.object({ @@ -36,65 +36,52 @@ const UpdateShippingAddressMutation = graphql(` } `); -export const updateShippingAddress = async (props: { - envUrl: string; - checkoutId: string; - shippingAddress: ShippingAddressSchemaType; -}): Promise< - | { type: "error"; name: string; message: string } - | { - type: "success"; - value: z.infer; - } -> => { - const { envUrl, checkoutId, shippingAddress } = props; +const UpdateShippingAddressParsingResponseError = BaseError.subclass( + "UpdateShippingAddressParsingResponseError", +); +const UpdateShippingAddressMutationError = BaseError.subclass( + "UpdateShippingAddressMutationError", +); - try { +export const updateShippingAddress = actionClient + .schema( + z.object({ + envUrl: envUrlSchema, + checkoutId: z.string(), + shippingAddress: ShippingAddressSchema, + }), + ) + .metadata({ actionName: "updateShippingAddress" }) + .action(async ({ parsedInput: { envUrl, checkoutId, shippingAddress } }) => { const response = await request(envUrl, UpdateShippingAddressMutation, { checkoutId, input: shippingAddress, + }).catch((error) => { + throw BaseError.normalize(error, UnknownError); }); const parsedResponse = UpdateShippingAddressSchema.safeParse(response); if (parsedResponse.error) { - logger.error("Failed to parse updateShippingAddress response", { - error: parsedResponse.error, - }); - return { - type: "error", - name: "ParsingUpdateShippingAddressError", - message: parsedResponse.error.message, - }; + throw UpdateShippingAddressParsingResponseError.normalize( + parsedResponse.error, + ); } if (parsedResponse.data.checkoutShippingAddressUpdate.errors.length > 0) { - logger.error("Failed to update shipping address of checkout", { - errors: parsedResponse.data.checkoutShippingAddressUpdate.errors, - }); - - return { - type: "error", - name: "UpdateShippingAddressError", - message: - "Failed to update billing address - errors in updateShippingAddress mutation", - }; + throw new UpdateShippingAddressMutationError( + "Failed to create checkout - errors in createCheckout mutation.", + { + errors: parsedResponse.data.checkoutShippingAddressUpdate.errors.map( + (e) => UpdateShippingAddressMutationError.normalize(e), + ), + }, + ); } revalidatePath( createPath("env", encodeURIComponent(envUrl), "checkout", checkoutId), ); - return { - type: "success", - value: parsedResponse.data, - }; - } catch (error) { - logger.error("Failed to update shipping address", { error }); - return { - type: "error", - name: "UpdateShippingAddressError", - message: "Failed to update shipping address", - }; - } -}; + return parsedResponse.data; + }); diff --git a/src/modules/checkout-details/components/billing.tsx b/src/modules/checkout-details/components/billing.tsx index 8402eab..008910b 100644 --- a/src/modules/checkout-details/components/billing.tsx +++ b/src/modules/checkout-details/components/billing.tsx @@ -19,18 +19,9 @@ import { toast } from "@/components/ui/use-toast"; import { updateBillingAddress } from "../actions"; import { getDefaultAddressByCountryCode } from "../address"; -import { convertStringToCountryCode, countryCodes } from "../countries"; +import { convertStringToCountryCode } from "../countries"; import { BillingAddressFragment } from "../fragments"; - -const BillingAddressSchema = z.object({ - firstName: z.string(), - lastName: z.string(), - streetAddress1: z.string(), - city: z.string(), - countryArea: z.string().optional(), - country: z.enum(countryCodes), - postalCode: z.string(), -}); +import { BillingAddressSchema } from "../schemas"; export type BillingAddressSchemaType = z.infer; @@ -66,15 +57,22 @@ export const Billing = (props: { billingAddress: data, }); - if (response.type === "error") { + if (response?.validationErrors) { + toast({ + title: "Validation error", + variant: "destructive", + }); + } + + if (response?.serverError) { toast({ - title: response.name, + title: response.serverError.name, variant: "destructive", - description: response.message, + description: response.serverError.message, }); } - if (response.type === "success") { + if (response?.data) { toast({ title: "Successfully updated billing address", }); diff --git a/src/modules/checkout-details/components/delivery-method.tsx b/src/modules/checkout-details/components/delivery-method.tsx index 1c045ea..d695d08 100644 --- a/src/modules/checkout-details/components/delivery-method.tsx +++ b/src/modules/checkout-details/components/delivery-method.tsx @@ -22,7 +22,6 @@ import { } from "@/components/ui/select"; import { toast } from "@/components/ui/use-toast"; -import { redirectToPaymentGatewaySelect } from "../actions"; import { updateDeliveryMethod } from "../actions/update-delivery-method"; import { CollectionPointFragment, @@ -89,21 +88,18 @@ export const DeliveryMethod = (props: { deliveryMethod: data.deliveryMethodId, }); - if (response.type === "error") { + if (response?.serverError) { toast({ - title: response.name, + title: response.serverError.name, variant: "destructive", - description: response.message, + description: response.serverError.message, }); } - if (response.type === "success") { + if (response?.data) { toast({ title: "Successfully updated delivery method", }); - await redirectToPaymentGatewaySelect({ - checkoutId, - }); } } diff --git a/src/modules/checkout-details/components/shipping.tsx b/src/modules/checkout-details/components/shipping.tsx index 7701b14..bbd7499 100644 --- a/src/modules/checkout-details/components/shipping.tsx +++ b/src/modules/checkout-details/components/shipping.tsx @@ -17,20 +17,11 @@ import { import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/use-toast"; -import { updateShippingAddress } from "../actions/update-shipping-address"; +import { updateShippingAddress } from "../actions"; import { getDefaultAddressByCountryCode } from "../address"; -import { convertStringToCountryCode, countryCodes } from "../countries"; +import { convertStringToCountryCode } from "../countries"; import { ShippingAddressFragment } from "../fragments"; - -const ShippingAddressSchema = z.object({ - firstName: z.string().min(1), - lastName: z.string().min(1), - streetAddress1: z.string().min(1), - city: z.string().min(1), - countryArea: z.string().optional(), - country: z.enum(countryCodes), - postalCode: z.string().min(1), -}); +import { ShippingAddressSchema } from "../schemas"; export type ShippingAddressSchemaType = z.infer; @@ -66,15 +57,15 @@ export const Shipping = (props: { shippingAddress: data, }); - if (response.type === "error") { + if (response?.serverError) { toast({ - title: response.name, + title: response.serverError.name, variant: "destructive", - description: response.message, + description: response.serverError.message, }); } - if (response.type === "success") { + if (response?.data) { toast({ title: "Successfully updated shipping address", }); diff --git a/src/modules/checkout-details/schemas/billing-address.ts b/src/modules/checkout-details/schemas/billing-address.ts new file mode 100644 index 0000000..a192f91 --- /dev/null +++ b/src/modules/checkout-details/schemas/billing-address.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +import { countryCodes } from "../countries"; + +export const BillingAddressSchema = z.object({ + firstName: z.string(), + lastName: z.string(), + streetAddress1: z.string(), + city: z.string(), + countryArea: z.string().optional(), + country: z.enum(countryCodes), + postalCode: z.string(), +}); diff --git a/src/modules/checkout-details/schemas/index.ts b/src/modules/checkout-details/schemas/index.ts new file mode 100644 index 0000000..48d6afc --- /dev/null +++ b/src/modules/checkout-details/schemas/index.ts @@ -0,0 +1,2 @@ +export * from "./billing-address"; +export * from "./shipping-address"; diff --git a/src/modules/checkout-details/schemas/shipping-address.ts b/src/modules/checkout-details/schemas/shipping-address.ts new file mode 100644 index 0000000..8a0f787 --- /dev/null +++ b/src/modules/checkout-details/schemas/shipping-address.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +import { countryCodes } from "../countries"; + +export const ShippingAddressSchema = z.object({ + firstName: z.string().min(1), + lastName: z.string().min(1), + streetAddress1: z.string().min(1), + city: z.string().min(1), + countryArea: z.string().optional(), + country: z.enum(countryCodes), + postalCode: z.string().min(1), +}); diff --git a/src/modules/dropin/actions/get-checkout-total-price.ts b/src/modules/dropin/actions/get-checkout-total-price.ts index b2dfa2b..99975c0 100644 --- a/src/modules/dropin/actions/get-checkout-total-price.ts +++ b/src/modules/dropin/actions/get-checkout-total-price.ts @@ -1,13 +1,14 @@ "use server"; -import { graphql, ResultOf } from "gql.tada"; +import { graphql } from "gql.tada"; import request from "graphql-request"; +import { z } from "zod"; -import { createLogger } from "@/lib/logger"; +import { envUrlSchema } from "@/lib/env-url"; +import { BaseError, UnknownError } from "@/lib/errors"; +import { actionClient } from "@/lib/safe-action"; import { TotalPriceFragment } from "../fragments"; -const logger = createLogger("getCheckoutTotalPrice"); - const GetCheckoutTotalPriceQuery = graphql( ` query GetCheckoutTotalPrice($checkoutId: ID!) { @@ -21,27 +22,20 @@ const GetCheckoutTotalPriceQuery = graphql( [TotalPriceFragment], ); -export const getCheckoutTotalPrice = async (props: { - envUrl: string; - checkoutId: string; -}): Promise< - | { type: "error"; name: string; message: string } - | { type: "success"; value: ResultOf } -> => { - const { envUrl, checkoutId } = props; - - try { +export const getCheckoutTotalPrice = actionClient + .schema( + z.object({ + checkoutId: z.string(), + envUrl: envUrlSchema, + }), + ) + .metadata({ actionName: "getCheckoutTotalPrice" }) + .action(async ({ parsedInput: { envUrl, checkoutId } }) => { const response = await request(envUrl, GetCheckoutTotalPriceQuery, { checkoutId, + }).catch((error) => { + throw BaseError.normalize(error, UnknownError); }); - return { type: "success", value: response }; - } catch (error) { - logger.error("Failed to get checkout total price", { error }); - return { - type: "error", - name: "GetCheckoutTotalPriceError", - message: "Failed to get checkout total price", - }; - } -}; + return response; + }); diff --git a/src/modules/dropin/actions/initalize-payment-gateway.ts b/src/modules/dropin/actions/initalize-payment-gateway.ts index b6646ca..49547b4 100644 --- a/src/modules/dropin/actions/initalize-payment-gateway.ts +++ b/src/modules/dropin/actions/initalize-payment-gateway.ts @@ -1,15 +1,13 @@ "use server"; import { graphql } from "gql.tada"; import request from "graphql-request"; +import { z } from "zod"; -import { createLogger } from "@/lib/logger"; +import { envUrlSchema } from "@/lib/env-url"; +import { BaseError, UnknownError } from "@/lib/errors"; +import { actionClient } from "@/lib/safe-action"; -import { - InitalizePaymentGatewaySchema, - InitalizePaymentGatewaySchemaType, -} from "../schemas"; - -const logger = createLogger("initalizePaymentGateway"); +import { InitalizePaymentGatewaySchema } from "../schemas"; const InitalizePaymentGatewayMutation = graphql(` mutation initalizePaymentGateway( @@ -41,91 +39,92 @@ const InitalizePaymentGatewayMutation = graphql(` } `); -type InitalizePaymentGatewayDataInput = - | { - action: "checkBalance"; - paymentMethod: { - type: "giftcard"; - brand: string; - encryptedCardNumber: string; - encryptedSecurityCode: string; - }; - } - | { action: "createOrder" } - | { - action: "cancelOrder"; - pspReference: string; - orderData: string; - }; +const InitalizePaymentGatewayParsingResponseError = BaseError.subclass( + "InitalizePaymentGatewayParsingResponseError", +); +const InitalizePaymentGatewayMutationError = BaseError.subclass( + "InitalizePaymentGatewayMutationError", +); -export const initalizePaymentGateway = async (props: { - envUrl: string; - checkoutId: string; - paymentGatewayId: string; - amount?: number; - data?: InitalizePaymentGatewayDataInput; -}): Promise< - | { type: "error"; name: string; message: string } - | { type: "success"; value: InitalizePaymentGatewaySchemaType } -> => { - const { envUrl, checkoutId, paymentGatewayId, amount, data } = props; - try { - const response = await request(envUrl, InitalizePaymentGatewayMutation, { - checkoutId, - paymentGatewayId, - amount, - data, - }); +export const initalizePaymentGateway = actionClient + .schema( + z.object({ + checkoutId: z.string(), + envUrl: envUrlSchema, + paymentGatewayId: z.string(), + amount: z.number().optional(), + data: z + .discriminatedUnion("action", [ + z.object({ + action: z.literal("checkBalance"), + paymentMethod: z.object({ + type: z.literal("giftcard"), + brand: z.string(), + encryptedCardNumber: z.string(), + encryptedSecurityCode: z.string(), + }), + }), + z.object({ + action: z.literal("createOrder"), + }), + z.object({ + action: z.literal("cancelOrder"), + pspReference: z.string(), + orderData: z.string(), + }), + ]) + .optional(), + }), + ) + .metadata({ + actionName: "initalizePaymentGateway", + }) + .action( + async ({ + parsedInput: { envUrl, checkoutId, paymentGatewayId, amount, data }, + }) => { + const response = await request(envUrl, InitalizePaymentGatewayMutation, { + checkoutId, + paymentGatewayId, + amount, + data, + }).catch((error) => { + throw BaseError.normalize(error, UnknownError); + }); - const parsedResponse = - InitalizePaymentGatewaySchema.passthrough().safeParse(response); + const parsedResponse = + InitalizePaymentGatewaySchema.passthrough().safeParse(response); - if (parsedResponse.error) { - logger.error("Failed to parse initalize payment gateway response", { - error: parsedResponse.error, - }); - return { - type: "error", - name: "ParsingInitalizePaymentGatewayResponseError", - message: parsedResponse.error.message, - }; - } + if (parsedResponse.error) { + throw InitalizePaymentGatewayParsingResponseError.normalize( + parsedResponse.error, + ); + } - if (parsedResponse.data.paymentGatewayInitialize.errors.length > 0) { - logger.error("Failed to initalize payment gateway - errors in mutation", { - errors: parsedResponse.data.paymentGatewayInitialize.errors, - }); - return { - type: "error", - name: "InitalizePaymentGatewayError", - message: + if (parsedResponse.data.paymentGatewayInitialize.errors.length > 0) { + throw new InitalizePaymentGatewayMutationError( "Failed to initalize payment gateway - errors in initalizePaymentGateway mutation", - }; - } + { + errors: parsedResponse.data.paymentGatewayInitialize.errors.map( + (e) => InitalizePaymentGatewayMutationError.normalize(e), + ), + }, + ); + } - if ( - parsedResponse.data.paymentGatewayInitialize.gatewayConfigs.some( - (config) => config.errors.length > 0, - ) - ) { - logger.error("Failed to initalize payment gateway - errors in configs", { - errors: parsedResponse.data.paymentGatewayInitialize.gatewayConfigs, - }); - return { - type: "error", - name: "InitalizePaymentGatewayError", - message: + if ( + parsedResponse.data.paymentGatewayInitialize.gatewayConfigs.some( + (config) => config.errors.length > 0, + ) + ) { + throw new InitalizePaymentGatewayMutationError( "Failed to initalize payment gateway - errors in initalizePaymentGateway configurations", - }; - } + { + errors: parsedResponse.data.paymentGatewayInitialize.gatewayConfigs, + }, + ); + } - return { type: "success", value: parsedResponse.data }; - } catch (error) { - logger.error("Failed to initalize payment gateway", { error }); - return { - type: "error", - name: "InitalizePaymentGatewayError", - message: "Failed to initalize payment gateway", - }; - } -}; + return parsedResponse.data; + }, + ); diff --git a/src/modules/dropin/actions/initalize-transaction.ts b/src/modules/dropin/actions/initalize-transaction.ts index b4e923b..260dbae 100644 --- a/src/modules/dropin/actions/initalize-transaction.ts +++ b/src/modules/dropin/actions/initalize-transaction.ts @@ -4,7 +4,10 @@ import { graphql } from "gql.tada"; import request from "graphql-request"; import { z } from "zod"; +import { envUrlSchema } from "@/lib/env-url"; +import { BaseError, UnknownError } from "@/lib/errors"; import { createLogger } from "@/lib/logger"; +import { actionClient } from "@/lib/safe-action"; import { InitalizeTransactionSchema } from "../schemas"; @@ -37,63 +40,64 @@ const initalizeTransactionMutation = graphql(` } `); -export const initalizeTransaction = async (props: { - envUrl: string; - checkoutId: string; - paymentGatewayId: string; - data: unknown; - amount: number; - idempotencyKey: string; -}): Promise< - | { type: "error"; name: string; message: string } - | { type: "success"; value: z.infer } -> => { - const { envUrl, checkoutId, paymentGatewayId, data, amount, idempotencyKey } = - props; +const InitalizeTransactionParsingResponseError = BaseError.subclass( + "InitalizeTransactionParsingResponseError", +); +const InitalizeTransactionMutationError = BaseError.subclass( + "InitalizeTransactionMutationError", +); - try { - const response = await request(envUrl, initalizeTransactionMutation, { - checkoutId, - data, - amount, - idempotencyKey, - paymentGatewayId, - }); - - const parsedResponse = InitalizeTransactionSchema.safeParse(response); - - if (parsedResponse.error) { - logger.error("Failed to parse initalize transaction response", { - error: parsedResponse.error, +export const initalizeTransaction = actionClient + .schema( + z.object({ + envUrl: envUrlSchema, + checkoutId: z.string(), + paymentGatewayId: z.string(), + data: z.unknown(), + amount: z.number(), + idempotencyKey: z.string(), + }), + ) + .metadata({ actionName: "initalizeTransaction" }) + .action( + async ({ + parsedInput: { + envUrl, + checkoutId, + paymentGatewayId, + data, + amount, + idempotencyKey, + }, + }) => { + const response = await request(envUrl, initalizeTransactionMutation, { + checkoutId, + data, + amount, + idempotencyKey, + paymentGatewayId, + }).catch((error) => { + throw BaseError.normalize(error, UnknownError); }); - return { - type: "error", - name: "ParsingInitalizeTransactionResponseError", - message: parsedResponse.error.message, - }; - } + const parsedResponse = InitalizeTransactionSchema.safeParse(response); - if (parsedResponse.data.transactionInitialize.errors.length > 0) { - logger.error("Failed to initalize transaction - errors in mutation", { - errors: parsedResponse.data.transactionInitialize.errors, - }); - return { - type: "error", - name: "InitalizeTransactionError", - message: "Failed to initalize transaction - errors in mutation", - }; - } + if (parsedResponse.error) { + throw InitalizeTransactionParsingResponseError.normalize( + parsedResponse.error, + ); + } - return { - type: "success", - value: parsedResponse.data, - }; - } catch (error) { - logger.error("Failed to initalize transaction", { error }); - return { - type: "error", - name: "InitalizeTransactionError", - message: "Failed to initalize transaction", - }; - } -}; + if (parsedResponse.data.transactionInitialize.errors.length > 0) { + throw new InitalizeTransactionMutationError( + "Failed to create checkout - errors in initalizeTransaction mutation.", + { + errors: parsedResponse.data.transactionInitialize.errors.map((e) => + InitalizeTransactionMutationError.normalize(e), + ), + }, + ); + } + + return parsedResponse.data; + }, + ); diff --git a/src/modules/dropin/actions/process-transaction.ts b/src/modules/dropin/actions/process-transaction.ts index d2d7643..478e3b3 100644 --- a/src/modules/dropin/actions/process-transaction.ts +++ b/src/modules/dropin/actions/process-transaction.ts @@ -4,7 +4,10 @@ import { graphql } from "gql.tada"; import request from "graphql-request"; import { z } from "zod"; +import { envUrlSchema } from "@/lib/env-url"; +import { BaseError, UnknownError } from "@/lib/errors"; import { createLogger } from "@/lib/logger"; +import { actionClient } from "@/lib/safe-action"; import { TransactionProcessSchema } from "../schemas/transaction-process"; @@ -36,55 +39,48 @@ const processTransactionMutation = graphql(` } `); -export const processTransaction = async (props: { - envUrl: string; - transactionId: string; - data: unknown; -}): Promise< - | { type: "error"; name: string; message: string } - | { type: "success"; value: z.infer } -> => { - const { envUrl, transactionId, data } = props; - try { +const ProcessTransactionParsingResponseError = BaseError.subclass( + "ProcessTransactionParsingResponseError", +); +const ProcessTransactionMutationError = BaseError.subclass( + "ProcessTransactionMutationError", +); + +export const processTransaction = actionClient + .schema( + z.object({ + envUrl: envUrlSchema, + transactionId: z.string(), + data: z.unknown(), + }), + ) + .metadata({ actionName: "processTransaction" }) + .action(async ({ parsedInput: { envUrl, transactionId, data } }) => { const response = await request(envUrl, processTransactionMutation, { transactionId, data, + }).catch((error) => { + throw BaseError.normalize(error, UnknownError); }); const parsedResponse = TransactionProcessSchema.safeParse(response); if (parsedResponse.error) { - logger.error("Failed to parse process transaction response", { - error: parsedResponse.error, - }); - return { - type: "error", - name: "ParsingProcessTransactionResponseError", - message: parsedResponse.error.message, - }; + throw ProcessTransactionParsingResponseError.normalize( + parsedResponse.error, + ); } if (parsedResponse.data.transactionProcess.errors.length > 0) { - logger.error("Failed to process transaction - errors in mutation", { - errors: parsedResponse.data.transactionProcess.errors, - }); - return { - type: "error", - name: "ProcessTransactionError", - message: "Failed to process transaction - errors in mutation", - }; + throw new ProcessTransactionMutationError( + "Failed to process transaction - errors in processTransaction mutation", + { + errors: parsedResponse.data.transactionProcess.errors.map((e) => + ProcessTransactionMutationError.normalize(e), + ), + }, + ); } - return { - type: "success", - value: parsedResponse.data, - }; - } catch (error) { - logger.error("Failed to process transaction", { error }); - return { - type: "error", - name: "ProcessTransactionError", - message: "Failed to process transaction", - }; - } -}; + return parsedResponse.data; + }); diff --git a/src/modules/dropin/adyen/dropin-config.tsx b/src/modules/dropin/adyen/dropin-config.tsx index e8857d4..0939f1a 100644 --- a/src/modules/dropin/adyen/dropin-config.tsx +++ b/src/modules/dropin/adyen/dropin-config.tsx @@ -77,19 +77,29 @@ export const getAdyenDropinConfig = (props: { data: state.data, }); - if (transactionProcessResponse.type === "error") { + if (transactionProcessResponse?.serverError) { dropin?.setStatus("error"); toast({ - title: transactionProcessResponse.name, + title: transactionProcessResponse.serverError.name, + variant: "destructive", + description: transactionProcessResponse.serverError.message, + }); + return; + } + + if (!transactionProcessResponse?.data) { + dropin?.setStatus("error"); + toast({ + title: "Error while processing transaction", + description: "No data back from Adyen app", variant: "destructive", - description: transactionProcessResponse.message, }); return; } const adyenPaymentDetailResponse = AdyenPaymentDetailResponse.createFromTransactionProcess( - transactionProcessResponse.value, + transactionProcessResponse.data, ); if (adyenPaymentDetailResponse.isRefused()) { @@ -123,11 +133,22 @@ export const getAdyenDropinConfig = (props: { idempotencyKey: window.crypto.randomUUID(), }); - if (transactionInitializeResponse.type === "error") { + if (transactionInitializeResponse?.serverError) { + toast({ + title: transactionInitializeResponse?.serverError.name, + variant: "destructive", + description: transactionInitializeResponse?.serverError.message, + }); + dropin.setStatus("error"); + + return; + } + + if (!transactionInitializeResponse?.data) { toast({ - title: transactionInitializeResponse.name, + title: "Error while initializing transaction", + description: "No data back from Adyen app", variant: "destructive", - description: transactionInitializeResponse.message, }); dropin.setStatus("error"); @@ -136,7 +157,7 @@ export const getAdyenDropinConfig = (props: { const adyenPaymentResponse = AdyenPaymentResponse.createFromTransactionInitalize( - transactionInitializeResponse.value, + transactionInitializeResponse?.data, ); if (adyenPaymentResponse.isRedirectOrAdditionalActionFlow()) { @@ -217,10 +238,21 @@ export const getAdyenDropinConfig = (props: { }, ); - if (initalizePaymentGatewayDataResponse.type === "error") { + if (initalizePaymentGatewayDataResponse?.serverError) { + toast({ + title: initalizePaymentGatewayDataResponse.serverError.name, + description: initalizePaymentGatewayDataResponse.serverError.message, + variant: "destructive", + }); + + return; + } + + if (!initalizePaymentGatewayDataResponse?.data) { toast({ - title: initalizePaymentGatewayDataResponse.name, - description: initalizePaymentGatewayDataResponse.message, + title: "Error while checking balance", + description: + "Balance couldn't be checked - no data back from Adyen app", variant: "destructive", }); @@ -229,7 +261,7 @@ export const getAdyenDropinConfig = (props: { const adyenBallanceCheckResponse = AdyenGiftCardBalanceResponse.createFromInitializePaymentGateway( - initalizePaymentGatewayDataResponse.value, + initalizePaymentGatewayDataResponse?.data, ); void resolve(adyenBallanceCheckResponse.getResponse()); @@ -245,10 +277,21 @@ export const getAdyenDropinConfig = (props: { }, ); - if (initalizePaymentGatewayDataResponse.type === "error") { + if (initalizePaymentGatewayDataResponse?.serverError) { + toast({ + title: initalizePaymentGatewayDataResponse.serverError.name, + description: initalizePaymentGatewayDataResponse.serverError.message, + variant: "destructive", + }); + + return; + } + + if (!initalizePaymentGatewayDataResponse?.data) { toast({ - title: initalizePaymentGatewayDataResponse.name, - description: initalizePaymentGatewayDataResponse.message, + title: "Error while creating order", + description: + "Order couldn't be created - no data back from Adyen app", variant: "destructive", }); @@ -257,7 +300,7 @@ export const getAdyenDropinConfig = (props: { const adyenOrderCreateResponse = AdyenOrderCreateResponse.createFromInitializePaymentGateway( - initalizePaymentGatewayDataResponse.value, + initalizePaymentGatewayDataResponse?.data, ); void resolve(adyenOrderCreateResponse.getResponse()); @@ -278,19 +321,29 @@ export const getAdyenDropinConfig = (props: { }, ); - if (initalizePaymentGatewayDataResponse.type === "error") { + if (initalizePaymentGatewayDataResponse?.serverError) { toast({ - title: initalizePaymentGatewayDataResponse.name, - description: initalizePaymentGatewayDataResponse.message, + title: initalizePaymentGatewayDataResponse.serverError.name, + description: initalizePaymentGatewayDataResponse.serverError.message, variant: "destructive", }); return; } + if (!initalizePaymentGatewayDataResponse?.data) { + toast({ + title: "Error while cancelling order", + description: + "Order couldn't be cancelled - no data back from Adyen app", + variant: "destructive", + }); + return; + } + const adyenOrderCancelledResponse = AdyenOrderCancelledResponse.createFromInitializePaymentGateway( - initalizePaymentGatewayDataResponse.value, + initalizePaymentGatewayDataResponse?.data, ); if (adyenOrderCancelledResponse.isOrderNotCancelled()) { diff --git a/src/modules/dropin/schemas/payment-method-response.ts b/src/modules/dropin/schemas/payment-method-response.ts index f8e9beb..a65cce1 100644 --- a/src/modules/dropin/schemas/payment-method-response.ts +++ b/src/modules/dropin/schemas/payment-method-response.ts @@ -11,6 +11,10 @@ export const PaymentMethodsResponseSchema = z.object({ intent: z.enum(["sale", "capture", "authorize", "order", "tokenize"]), }), }), + z.object({ + type: z.literal("afterpaytouch"), + name: z.string(), + }), z.object({ type: z.literal("scheme"), name: z.string(), diff --git a/src/modules/environment/actions/create-checkout.ts b/src/modules/environment/actions/create-checkout.ts index da8aadf..5a6bcf1 100644 --- a/src/modules/environment/actions/create-checkout.ts +++ b/src/modules/environment/actions/create-checkout.ts @@ -4,15 +4,19 @@ import { graphql } from "gql.tada"; import request from "graphql-request"; import { z } from "zod"; -import { createLogger } from "@/lib/logger"; +import { envUrlSchema } from "@/lib/env-url"; +import { BaseError, UnknownError } from "@/lib/errors"; +import { actionClient } from "@/lib/safe-action"; -const logger = createLogger("createCheckout"); +import { redirectToCheckoutDetails } from "./redirect-to-checkout-details"; const CreateCheckoutSchema = z.object({ checkoutCreate: z.object({ - checkout: z.object({ - id: z.string(), - }), + checkout: z + .object({ + id: z.string(), + }) + .nullable(), errors: z.array( z.object({ field: z.string(), @@ -36,63 +40,66 @@ const CreateCheckoutMutation = graphql(` } `); -export const createCheckout = async (props: { - envUrl: string; - channelSlug: string; - variantId: string; -}): Promise< - | { type: "error"; name: string; message: string } - | { type: "success"; value: z.infer } -> => { - const { envUrl, channelSlug, variantId } = props; - try { - const response = await request(envUrl, CreateCheckoutMutation, { - input: { - channel: channelSlug, - email: "adyen-testclient@saleor.io", - lines: [ - { - variantId, - quantity: 1, - }, - ], - }, - }); - - const parsedResponse = CreateCheckoutSchema.safeParse(response); +const CreateCheckoutParsingResponseError = BaseError.subclass( + "CreateCheckoutParsingResponseError", +); +const CreateCheckoutMutationError = BaseError.subclass( + "CreateCheckoutMutationError", +); - if (parsedResponse.error) { - logger.error("Failed to parse checkoutCreate response", { - error: parsedResponse.error, +export const createCheckout = actionClient + .schema( + z.object({ + envUrl: envUrlSchema, + channelSlug: z.string(), + variantId: z.string(), + }), + ) + .metadata({ actionName: "createCheckout" }) + .action( + async ({ parsedInput: { channelSlug, envUrl, variantId } }) => { + const response = await request(envUrl, CreateCheckoutMutation, { + input: { + channel: channelSlug, + email: "adyen-testclient@saleor.io", + lines: [ + { + variantId, + quantity: 1, + }, + ], + }, + }).catch((error) => { + throw BaseError.normalize(error, UnknownError); }); - return { - type: "error", - name: "ParsingCheckoutResponseError", - message: parsedResponse.error.errors - .map((error) => error.message) - .join(", "), - }; - } - if (parsedResponse.data.checkoutCreate.errors.length > 0) { - logger.error("Failed to create checkout", { - errors: parsedResponse.data.checkoutCreate.errors, - }); - return { - type: "error", - name: "CreateCheckoutError", - message: - "Failed to create checkout - errors in createCheckout mutation", - }; - } + const parsedResponse = CreateCheckoutSchema.safeParse(response); - return { type: "success", value: parsedResponse.data }; - } catch (error) { - logger.error("Failed to create checkout", { error }); - return { - type: "error", - name: "CreateCheckoutError", - message: "Failed to create checkout", - }; - } -}; + if (parsedResponse.error) { + throw CreateCheckoutParsingResponseError.normalize( + parsedResponse.error, + ); + } + + if (parsedResponse.data.checkoutCreate.errors.length > 0) { + throw new CreateCheckoutMutationError( + "Failed to create checkout - errors in createCheckout mutation.", + { + errors: parsedResponse.data.checkoutCreate.errors.map((e) => + CreateCheckoutMutationError.normalize(e), + ), + }, + ); + } + + return parsedResponse.data; + }, + { + onSuccess: async ({ data, parsedInput }) => { + await redirectToCheckoutDetails({ + envUrl: parsedInput.envUrl, + checkoutId: data?.checkoutCreate.checkout?.id, + }); + }, + }, + ); diff --git a/src/modules/environment/actions/fetch-product.ts b/src/modules/environment/actions/fetch-product.ts index 8efe94c..43c8c71 100644 --- a/src/modules/environment/actions/fetch-product.ts +++ b/src/modules/environment/actions/fetch-product.ts @@ -1,14 +1,15 @@ "use server"; -import { graphql, readFragment, ResultOf } from "gql.tada"; +import { graphql, readFragment } from "gql.tada"; import request from "graphql-request"; +import { z } from "zod"; -import { createLogger } from "@/lib/logger"; +import { envUrlSchema } from "@/lib/env-url"; +import { BaseError, UnknownError } from "@/lib/errors"; +import { actionClient } from "@/lib/safe-action"; import { ProductFragment } from "../fragments"; -const logger = createLogger("fetchProduct"); - const FetchProductQuery = graphql( ` query FetchProduct($channelSlug: String!) { @@ -33,18 +34,24 @@ const FetchProductQuery = graphql( [ProductFragment], ); -export const fetchProduct = async (props: { - channelSlug: string; - envUrl: string; -}): Promise< - | { type: "error"; name: string; message: string } - | { type: "success"; value: ResultOf } -> => { - const { envUrl, channelSlug } = props; +const NoProductsFoundError = BaseError.subclass("NoProductsFoundError"); +const DefaultVariantNotAvailableError = BaseError.subclass( + "DefaultVariantNotAvailableError", +); - try { +export const fetchProduct = actionClient + .schema( + z.object({ + envUrl: envUrlSchema, + channelSlug: z.string(), + }), + ) + .metadata({ actionName: "fetchProduct" }) + .action(async ({ parsedInput: { channelSlug, envUrl } }) => { const response = await request(envUrl, FetchProductQuery, { channelSlug, + }).catch((error) => { + throw BaseError.normalize(error, UnknownError); }); const products = response.products?.edges.map((edge) => @@ -52,28 +59,14 @@ export const fetchProduct = async (props: { ); if (products?.length === 0) { - return { - type: "error", - name: "FetchProductError", - message: "No products found for selected channel.", - }; + throw new NoProductsFoundError("No products found for selected channel."); } if (products?.some((product) => product.defaultVariant?.pricing === null)) { - return { - type: "error", - name: "FetchProductError", - message: "Default variant not available for selected channel.", - }; + throw new DefaultVariantNotAvailableError( + "Default variant not available for selected channel.", + ); } - return { type: "success", value: response }; - } catch (error) { - logger.error("Failed to fetch products", { error }); - return { - type: "error", - name: "FetchProductError", - message: "Failed to fetch products", - }; - } -}; + return response; + }); diff --git a/src/modules/environment/components/cart.tsx b/src/modules/environment/components/cart.tsx index 441815b..8db83cb 100644 --- a/src/modules/environment/components/cart.tsx +++ b/src/modules/environment/components/cart.tsx @@ -7,7 +7,7 @@ import { useTransition } from "react"; import { FormButton } from "@/components/form-button"; import { toast } from "@/components/ui/use-toast"; -import { createCheckout, redirectToCheckoutDetails } from "../actions"; +import { createCheckout } from "../actions"; import { ProductFragment } from "../fragments"; export const Cart = (props: { @@ -30,23 +30,18 @@ export const Cart = (props: { variantId: products[0].defaultVariant?.id ?? "", }); - if (response.type === "error") { + if (response?.serverError) { toast({ - title: response.name, + title: response.serverError.name, variant: "destructive", - description: response.message, + description: response.serverError.message, }); } - if (response.type === "success") { + if (response?.data) { toast({ title: "Successfully created checkout", }); - - await redirectToCheckoutDetails({ - envUrl, - checkoutId: response.value.checkoutCreate.checkout.id, - }); } }); }; diff --git a/src/modules/environment/components/environment.tsx b/src/modules/environment/components/environment.tsx index 18f915e..c26525f 100644 --- a/src/modules/environment/components/environment.tsx +++ b/src/modules/environment/components/environment.tsx @@ -18,13 +18,14 @@ import { import { Input } from "@/components/ui/input"; import { toast } from "@/components/ui/use-toast"; import { env } from "@/env"; +import { envUrlSchema } from "@/lib/env-url"; import { fetchProduct } from "../actions"; import { ProductFragment } from "../fragments"; import { Cart } from "./cart"; const EnvironmentConfigSchema = z.object({ - url: z.string().url().min(1), + url: envUrlSchema, channelSlug: z.string().min(1), }); @@ -49,18 +50,18 @@ export const Environment = () => { envUrl: data.url, }); - if (response.type === "success") { - setProducts(response.value.products?.edges.map((e) => e.node) ?? []); + if (response?.serverError) { toast({ - title: "Successfully fetched products", + title: response.serverError.name, + variant: "destructive", + description: response.serverError.message, }); } - if (response.type === "error") { + if (response?.data) { + setProducts(response?.data?.products?.edges.map((e) => e.node) ?? []); toast({ - title: response.name, - variant: "destructive", - description: response.message, + title: "Successfully fetched products", }); } }; diff --git a/src/modules/payment-gateway/actions/get-payment-gateways.ts b/src/modules/payment-gateway/actions/get-payment-gateways.ts index 8d8f3d6..f7db15e 100644 --- a/src/modules/payment-gateway/actions/get-payment-gateways.ts +++ b/src/modules/payment-gateway/actions/get-payment-gateways.ts @@ -1,13 +1,14 @@ "use server"; -import { graphql, ResultOf } from "gql.tada"; +import { graphql } from "gql.tada"; import request from "graphql-request"; +import { z } from "zod"; -import { createLogger } from "@/lib/logger"; +import { envUrlSchema } from "@/lib/env-url"; +import { BaseError, UnknownError } from "@/lib/errors"; +import { actionClient } from "@/lib/safe-action"; import { PaymentGatewayFragment, TotalPriceFragment } from "../fragments"; -const logger = createLogger("getPaymentGateways"); - const GetPaymentGatewaysQuery = graphql( ` query GetPaymentGateways($checkoutId: ID!) { @@ -24,27 +25,20 @@ const GetPaymentGatewaysQuery = graphql( [TotalPriceFragment, PaymentGatewayFragment], ); -export const getPaymentGateways = async (props: { - envUrl: string; - checkoutId: string; -}): Promise< - | { type: "error"; name: string; message: string } - | { type: "success"; value: ResultOf } -> => { - const { envUrl, checkoutId } = props; - - try { +export const getPaymentGateways = actionClient + .schema( + z.object({ + checkoutId: z.string(), + envUrl: envUrlSchema, + }), + ) + .metadata({ actionName: "getPaymentGateways" }) + .action(async ({ parsedInput: { envUrl, checkoutId } }) => { const response = await request(envUrl, GetPaymentGatewaysQuery, { checkoutId, + }).catch((error) => { + throw BaseError.normalize(error, UnknownError); }); - return { type: "success", value: response }; - } catch (error) { - logger.error("Failed to get payment gateways", { error }); - return { - type: "error", - name: "GetPaymentGatewaysError", - message: "Failed to get payment gateways", - }; - } -}; + return response; + }); diff --git a/src/modules/summary/actions/complete-checkout.ts b/src/modules/summary/actions/complete-checkout.ts index 6f04fa0..801ce54 100644 --- a/src/modules/summary/actions/complete-checkout.ts +++ b/src/modules/summary/actions/complete-checkout.ts @@ -1,13 +1,11 @@ "use server"; -import { graphql, ResultOf } from "gql.tada"; +import { graphql } from "gql.tada"; import request from "graphql-request"; +import { z } from "zod"; -import { BaseError } from "@/lib/errors"; -import { createLogger } from "@/lib/logger"; - -const CompleteCheckoutError = BaseError.subclass("CompleteCheckoutError"); - -const logger = createLogger("completeCheckout"); +import { envUrlSchema } from "@/lib/env-url"; +import { BaseError, UnknownError } from "@/lib/errors"; +import { actionClient } from "@/lib/safe-action"; const CompleteCheckoutMutation = graphql(` mutation CompleteCheckout($checkoutId: ID!) { @@ -24,39 +22,35 @@ const CompleteCheckoutMutation = graphql(` } `); -export const completeCheckout = async (props: { - envUrl: string; - checkoutId: string; -}): Promise< - | { type: "error"; name: string; message: string } - | { type: "success"; value: ResultOf } -> => { - const { envUrl, checkoutId } = props; - try { +const CreateCheckoutMutationError = BaseError.subclass( + "CreateCheckoutMutationError", +); + +export const completeCheckout = actionClient + .schema( + z.object({ + envUrl: envUrlSchema, + checkoutId: z.string(), + }), + ) + .metadata({ actionName: "completeCheckout" }) + .action(async ({ parsedInput: { envUrl, checkoutId } }) => { const response = await request(envUrl, CompleteCheckoutMutation, { checkoutId, + }).catch((error) => { + throw BaseError.normalize(error, UnknownError); }); if ((response.checkoutComplete?.errors ?? []).length > 0) { - logger.error("Failed to complete checkout", { - errors: response.checkoutComplete?.errors, - }); - return { - type: "error", - name: "CompleteCheckoutError", - message: "Failed to complete checkout - errors in mutation response", - }; + throw new CreateCheckoutMutationError( + "Failed to create checkout - errors in completeCheckout mutation.", + { + errors: response.checkoutComplete?.errors.map((e) => + CreateCheckoutMutationError.normalize(e), + ), + }, + ); } - return { type: "success", value: response }; - } catch (error) { - logger.error("Failed to complete checkout", { - error, - }); - return { - type: "error", - name: "CompleteCheckoutError", - message: "Failed to complete checkout", - }; - } -}; + return response; + }); diff --git a/src/modules/summary/actions/get-checkout-summary.ts b/src/modules/summary/actions/get-checkout-summary.ts index ce79f86..c82772c 100644 --- a/src/modules/summary/actions/get-checkout-summary.ts +++ b/src/modules/summary/actions/get-checkout-summary.ts @@ -1,8 +1,12 @@ "use server"; -import { graphql, ResultOf } from "gql.tada"; +import { graphql } from "gql.tada"; import request from "graphql-request"; +import { z } from "zod"; +import { envUrlSchema } from "@/lib/env-url"; +import { BaseError, UnknownError } from "@/lib/errors"; import { createLogger } from "@/lib/logger"; +import { actionClient } from "@/lib/safe-action"; import { CheckoutFragment } from "../fragments"; @@ -19,28 +23,20 @@ const GetCheckoutSummaryQuery = graphql( [CheckoutFragment], ); -export const getCheckoutSummary = async (props: { - envUrl: string; - checkoutId: string; -}): Promise< - | { type: "error"; name: string; message: string } - | { type: "success"; value: ResultOf } -> => { - const { envUrl, checkoutId } = props; - try { +export const getCheckoutSummary = actionClient + .schema( + z.object({ + envUrl: envUrlSchema, + checkoutId: z.string(), + }), + ) + .metadata({ actionName: "getCheckoutSummary" }) + .action(async ({ parsedInput: { envUrl, checkoutId } }) => { const response = await request(envUrl, GetCheckoutSummaryQuery, { checkoutId, + }).catch((error) => { + throw BaseError.normalize(error, UnknownError); }); - return { type: "success", value: response }; - } catch (error) { - logger.error("Failed to get checkout summary", { - error, - }); - return { - type: "error", - name: "GetCheckoutSummaryError", - message: "Failed to get checkout summary", - }; - } -}; + return response; + }); diff --git a/src/modules/summary/components/summary.tsx b/src/modules/summary/components/summary.tsx index b1c2d1c..8eb26fd 100644 --- a/src/modules/summary/components/summary.tsx +++ b/src/modules/summary/components/summary.tsx @@ -47,15 +47,15 @@ export const Summary = (props: { checkoutId: checkout.id, }); - if (response.type === "error") { + if (response?.serverError) { toast({ - title: response.name, + title: response.serverError.name, variant: "destructive", - description: response.message, + description: response.serverError.message, }); } - if (response.type === "success") { + if (response?.data) { toast({ title: "Successfully completed checkout", description: ( @@ -63,7 +63,7 @@ export const Summary = (props: {