Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for next safe action #20

Merged
merged 2 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

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

is this commited on purpose?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes - https://gql-tada.0no.co/get-started/workflows#uncommitted-output-files:

As you've seen on this page, there are two different output files we're concerned with when running inside an continuous integration environment.

Checking these files into your repository makes sure that your codebase is less reliant on running gql.tada, and that anyone who clones your code does not have to even know how to use gql.tada.

Copy link
Member

Choose a reason for hiding this comment

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

makes sense, the same approach we should take on graphql-codegen in other places

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