Skip to content

Commit

Permalink
Add support for next safe action (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
krzysztofzuraw authored Sep 23, 2024
1 parent 0985c0d commit 0e6bc44
Show file tree
Hide file tree
Showing 37 changed files with 830 additions and 672 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
80 changes: 80 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 9 additions & 7 deletions src/app/env/[envUrl]/checkout/[checkoutId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<main className="mx-auto grid max-w-6xl items-start gap-6 px-4 py-6 md:grid-cols-2 lg:gap-12">
<Billing
data={checkoutDetailsResponse.value.checkout?.billingAddress}
data={checkoutDetailsResponse?.data?.checkout?.billingAddress}
envUrl={decodedEnvUrl}
checkoutId={checkoutId}
/>
<Shipping
data={checkoutDetailsResponse.value.checkout?.shippingAddress}
data={checkoutDetailsResponse?.data?.checkout?.shippingAddress}
envUrl={decodedEnvUrl}
checkoutId={checkoutId}
/>
{hasDeliveryMethodsToSelect ? (
<DeliveryMethod
deliveryMethodData={
checkoutDetailsResponse.value.checkout?.deliveryMethod
checkoutDetailsResponse?.data?.checkout?.deliveryMethod
}
shippingMethodData={
checkoutDetailsResponse.value.checkout?.shippingMethods
checkoutDetailsResponse?.data?.checkout?.shippingMethods
}
envUrl={decodedEnvUrl}
checkoutId={checkoutId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,43 @@ export default async function PaymentGatewayPage({
checkoutId,
});

if (checkoutTotalPriceDataResponse.type === "error") {
if (checkoutTotalPriceDataResponse?.serverError) {
// Sends the error to the error boundary
throw new PaymentGatewayError(checkoutTotalPriceDataResponse.message);
throw new PaymentGatewayError(
checkoutTotalPriceDataResponse.serverError.message,
);
}

const totalPrice = readFragment(
TotalPriceFragment,
checkoutTotalPriceDataResponse.value.checkout?.totalPrice,
checkoutTotalPriceDataResponse?.data?.checkout?.totalPrice,
);

const initalizePaymentGatewayDataResponse = await initalizePaymentGateway({
envUrl: decodedEnvUrl,
checkoutId,
paymentGatewayId: decodedPaymentGatewayId,
amount: totalPrice?.gross.amount ?? 0,
amount: totalPrice?.gross.amount,
});

if (initalizePaymentGatewayDataResponse.type === "error") {
if (initalizePaymentGatewayDataResponse?.serverError) {
// Sends the error to the error boundary
throw new PaymentGatewayError(initalizePaymentGatewayDataResponse.message);
throw new PaymentGatewayError(
initalizePaymentGatewayDataResponse.serverError.message,
);
}

if (!initalizePaymentGatewayDataResponse?.data) {
// Sends the error to the error boundary
throw new PaymentGatewayError("No data returned from the server");
}

return (
<AdyenDropin
initalizePaymentGatewayData={initalizePaymentGatewayDataResponse.value}
totalPriceData={checkoutTotalPriceDataResponse.value.checkout?.totalPrice}
initalizePaymentGatewayData={initalizePaymentGatewayDataResponse?.data}
totalPriceData={
checkoutTotalPriceDataResponse?.data?.checkout?.totalPrice
}
envUrl={decodedEnvUrl}
checkoutId={checkoutId}
paymentGatewayId={decodedPaymentGatewayId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,26 @@ export default async function CheckoutSummaryPage({
checkoutId,
});

if (checkoutSummaryDataResponse.type === "error") {
// Sends the error to the error boundary
throw new CheckoutSummaryPageError(checkoutSummaryDataResponse.message);
if (checkoutSummaryDataResponse?.serverError) {
throw new CheckoutSummaryPageError(
checkoutSummaryDataResponse.serverError.message,
);
}

if (!checkoutSummaryDataResponse?.data) {
throw new CheckoutSummaryPageError("No checkout data found");
}

const checkout = readFragment(
CheckoutFragment,
checkoutSummaryDataResponse.value.checkout,
checkoutSummaryDataResponse.data.checkout,
);

return (
<main className="mx-auto grid max-w-6xl items-start gap-6 px-4 py-6 md:grid-cols-2 lg:gap-12">
{checkout?.id ? (
<Summary
data={checkoutSummaryDataResponse.value.checkout}
data={checkoutSummaryDataResponse.data.checkout}
envUrl={decodedEnvUrl}
/>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<main className="mx-auto grid max-w-6xl items-start gap-6 px-4 py-6 md:grid-cols-2 lg:gap-12">
<PaymentGatewaySelect
data={paymentGatewaysResponse.value.checkout?.availablePaymentGateways}
data={paymentGatewaysResponse?.data?.checkout?.availablePaymentGateways}
/>
</main>
);
Expand Down
7 changes: 3 additions & 4 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down
4 changes: 2 additions & 2 deletions src/graphql-cache.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ":
Expand Down
6 changes: 6 additions & 0 deletions src/lib/env-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { z } from "zod";

export const envUrlSchema = z
.string()
.url()
.endsWith("/graphql/", "Must end with /graphql/");
7 changes: 6 additions & 1 deletion src/lib/errors.ts
Original file line number Diff line number Diff line change
@@ -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");
2 changes: 1 addition & 1 deletion src/lib/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) =>
Expand Down
36 changes: 36 additions & 0 deletions src/lib/safe-action.ts
Original file line number Diff line number Diff line change
@@ -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",
};
},
});
Loading

0 comments on commit 0e6bc44

Please sign in to comment.