From bc9babeeac6758bee5abb96c6ec0069979dd15fb Mon Sep 17 00:00:00 2001 From: Spencer Lepine <60903378+spencerlepine@users.noreply.github.com> Date: Wed, 23 Oct 2024 22:44:02 +0300 Subject: [PATCH] feat: e2e stripe checkout functionality --- .env.template | 14 +- README.md | 10 + package.json | 1 + public/icons/check-mark.png | Bin 0 -> 42357 bytes src/app/{ => (orders)}/account/page.tsx | 0 src/app/{ => (orders)}/cart/page.tsx | 47 ++--- src/app/(orders)/order-confirmation/page.tsx | 20 ++ src/app/api/v1/checkout/route.ts | 96 ++++++++++ src/app/api/v1/webhook/checkout/route.ts | 56 ++++++ src/components/Header.tsx | 4 +- src/components/OrderConfirmation.tsx | 29 +++ src/components/ProductSizeSelector.tsx | 2 +- src/components/RelatedProducts.tsx | 2 +- src/lib/products.ts | 2 +- src/lib/stripe.ts | 124 ++++++++++++- src/utils/validateCartItems.ts | 115 ++++++++++++ yarn.lock | 184 ++++++++++++++++++- 17 files changed, 654 insertions(+), 52 deletions(-) create mode 100644 public/icons/check-mark.png rename src/app/{ => (orders)}/account/page.tsx (100%) rename src/app/{ => (orders)}/cart/page.tsx (65%) create mode 100644 src/app/(orders)/order-confirmation/page.tsx create mode 100644 src/app/api/v1/checkout/route.ts create mode 100644 src/app/api/v1/webhook/checkout/route.ts create mode 100644 src/components/OrderConfirmation.tsx create mode 100644 src/utils/validateCartItems.ts diff --git a/.env.template b/.env.template index c349cb1..6170c7c 100644 --- a/.env.template +++ b/.env.template @@ -1,10 +1,4 @@ -MY_SECRET_VALUE="foobar" - -# Auth0 - https://auth0.com/docs/quickstart/webapp/nextjs/interactive -# Allowed callback URLs: https://my-app.vercel.app/api/auth/callback, http://localhost:3000/api/auth/callback -# Allowed logout URLs: https://my-app.vercel.app, http://localhost:3000 -AUTH0_SECRET='use [openssl rand -hex 32] to generate a 32 bytes value' -AUTH0_BASE_URL='http://localhost:3000' -AUTH0_ISSUER_BASE_URL='https://{yourDomain}' -AUTH0_CLIENT_ID='{yourClientId}' -AUTH0_CLIENT_SECRET='{yourClientSecret}' \ No newline at end of file +NEXT_PUBLIC_URL="http://localhost:3000" +NEXT_PUBLIC_STRIPE_KEY="pk_asdfasdfasdfasdfasdfasdfasdfasdfasdfasdf" +STRIPE_SECRET_KEY="sk_asdfasdfasdfasdfasdfasdfasdfasdfasdfasdf" +STRIPE_WEBHOOK_SECRET="whsec_asdfasdfasdfasdfasdfa" \ No newline at end of file diff --git a/README.md b/README.md index 6bde1b4..4693c73 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,16 @@ docker-compose -f ./docker/development docker-compose.yml up -d # visit http://locahost:3001 ``` +### Local Stripe Webhook Testing + +```sh +brew install stripe/stripe-cli/stripe +stripe login +stripe listen --forward-to localhost:3000/api/v1/webhook/checkout +# *open separate terminal* +stripe trigger checkout.session.completed --add checkout_session:metadata.printifyOrderId=123 +``` + ## License GNU General Public License v3.0 or later diff --git a/package.json b/package.json index 9a26b0e..04ad5dd 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "next": "14.2.11", "react": "^18", "react-dom": "^18", + "sharp": "^0.33.5", "stripe": "^16.9.0", "use-shopping-cart": "^3.2.0" }, diff --git a/public/icons/check-mark.png b/public/icons/check-mark.png new file mode 100644 index 0000000000000000000000000000000000000000..546e7c1016e02da61cdd9a7e9cd0256cf12e0145 GIT binary patch literal 42357 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 literal 0 HcmV?d00001 diff --git a/src/app/account/page.tsx b/src/app/(orders)/account/page.tsx similarity index 100% rename from src/app/account/page.tsx rename to src/app/(orders)/account/page.tsx diff --git a/src/app/cart/page.tsx b/src/app/(orders)/cart/page.tsx similarity index 65% rename from src/app/cart/page.tsx rename to src/app/(orders)/cart/page.tsx index ae87e33..3816ecc 100644 --- a/src/app/cart/page.tsx +++ b/src/app/(orders)/cart/page.tsx @@ -1,42 +1,33 @@ 'use client'; +import { useRouter } from 'next/navigation'; import CartItemCard from '@/components/CartItemCard'; import { formatPriceForDisplay } from '@/lib/stripe'; import { useShoppingCart } from 'use-shopping-cart'; import { CartItem } from '@/types'; export default function CartPage() { - const { - cartCount, - cartDetails, - removeItem, - totalPrice, - // redirectToCheckout, // don't use this out-of-box, redirect with POST request from my route endpoint - addItem, - decrementItem, - } = useShoppingCart(); + const router = useRouter(); + + const { cartCount, cartDetails, removeItem, totalPrice, addItem, decrementItem } = useShoppingCart(); const cartItems = Object.values(cartDetails ?? {}); async function handleCheckoutClick() { - alert('feature: work-in-progress'); - // TODO_STRIPE - // if (cartCount && cartCount > 0) { - // try { - // const res = await fetch('/api/v1/checkout', { - // method: 'POST', - // body: JSON.stringify(cartDetails), - // }); - // const data = await res.json(); - // // TODO_STRIPE - checkout redirect - // // instead of useShoppingCart stripe, use our own stripe-js - // const result = await redirectToCheckout(data.sessionId); - // if (result?.error) { - // console.error(result); - // } - // } catch (error) { - // console.error(error); - // } - // } + if (!cartCount || cartCount === 0) { + return; + } + + try { + const res = await fetch('/api/v1/checkout', { + method: 'POST', + body: JSON.stringify({ cartItems }), + }); + const { checkoutUrl } = await res.json(); + if (!checkoutUrl) return alert('Unable to checkout at this time. Please try again later.'); + router.push(checkoutUrl); + } catch (error) { + console.error(error); + } } const handleRemove = (cartItem: CartItem) => { diff --git a/src/app/(orders)/order-confirmation/page.tsx b/src/app/(orders)/order-confirmation/page.tsx new file mode 100644 index 0000000..4c64e6f --- /dev/null +++ b/src/app/(orders)/order-confirmation/page.tsx @@ -0,0 +1,20 @@ +import { memo } from 'react'; +import OrderConfirmation from '@/components/OrderConfirmation'; +import { validateStripeSession } from '@/lib/stripe'; +import { notFound } from 'next/navigation'; + +// Redirect page after successful checkout +// /order-confirmation?session_id=cs_test_b1FKoQomBOaQFgMqW1lU6oYXRwIruD6AbGV804gMZRrptJr1bF91sDmK5T +const OrderConfirmationPage: React.FC<{ searchParams: { [key: string]: string | undefined } }> = async ({ searchParams }) => { + const sessionId = searchParams['session_id']; + const { validSession } = await validateStripeSession(sessionId); + if (!validSession) return notFound(); + + return ( +
+ +
+ ); +}; + +export default memo(OrderConfirmationPage); diff --git a/src/app/api/v1/checkout/route.ts b/src/app/api/v1/checkout/route.ts new file mode 100644 index 0000000..5c2e935 --- /dev/null +++ b/src/app/api/v1/checkout/route.ts @@ -0,0 +1,96 @@ +import { createCheckoutSession, formatCartItemsForStripe } from '@/lib/stripe'; +import validateCartItems from '@/utils/validateCartItems'; +import { NextRequest, NextResponse } from 'next/server'; + +/** + * @openapi + * /v1/checkout: + * post: + * summary: Creates a Stripe checkout session. + * tags: + * - Checkout + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * cartItems: + * type: array + * items: + * type: object + * properties: + * id: + * type: string + * name: + * type: string + * description: + * type: string + * quantity: + * type: integer + * price: + * type: number + * image: + * type: string + * currency: + * type: string + * price_data: + * type: object + * product_data: + * type: object + * properties: + * size: + * type: string + * productId: + * type: string + * category: + * type: string + * type: string + * responses: + * 200: + * description: Checkout session created successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * checkoutUrl: + * type: string + * 400: + * description: Error creating checkout session. + * content: + * application/json: + * schema: + * type: object + * properties: + * message: + * type: string + */ +export const POST = async (request: NextRequest) => { + try { + const body = await request.json(); + const { cartItems: clientCartItems } = body; + + const cartItems = validateCartItems(clientCartItems); + if (!cartItems) { + console.error('[Checkout] invalid client cart', clientCartItems); + return NextResponse.json({ message: 'Unable to checkout cart items' }, { status: 400 }); + } + + // TODO_PRINTIFY + const printifyOrderId = 'asdf1234'; + const stripeLineItems = formatCartItemsForStripe(cartItems); + const session = await createCheckoutSession(stripeLineItems, { printifyOrderId }); + + if (!session || !session.url) { + console.error('[Stripe] error creating checkout session:', session); + return NextResponse.json({ message: 'Unable to create Stripe checkout session' }, { status: 400 }); + } + + return NextResponse.json({ checkoutUrl: session.url }); + } catch (error) { + console.error('Error processing request:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +}; diff --git a/src/app/api/v1/webhook/checkout/route.ts b/src/app/api/v1/webhook/checkout/route.ts new file mode 100644 index 0000000..c327a09 --- /dev/null +++ b/src/app/api/v1/webhook/checkout/route.ts @@ -0,0 +1,56 @@ +import type { Stripe as StripeType } from 'stripe'; +import { stripe } from '@/lib/stripe'; +import { NextRequest, NextResponse } from 'next/server'; + +async function fulfillCheckout(printifyOrderId: string) { + console.log('Fulfilling Checkout Session - printifyOrderId:' + printifyOrderId); + // TODO_PRINTIFY + return; + + // // TODO: Make this function safe to run multiple times, + // // even concurrently, with the same session ID + + // // TODO: Make sure fulfillment hasn't already been + // // peformed for this Checkout Session + + // const checkoutSession = await retrieveCheckoutSession(sessionId) + // const { shipping_details, line_items, metadata } = checkoutSession + // const { printifyOrderId } = metadata + + // // Check the Checkout Session's payment_status property + // // to determine if fulfillment should be peformed + // if (checkoutSession.payment_status !== 'unpaid') { + // // TODO: Perform fulfillment of the line items + + // // TODO: Record/save fulfillment status for this + // // Checkout Session + // await sendOrderToProduction(printifyOrderId) +} + +export const POST = async (request: NextRequest) => { + try { + const secret = process.env.STRIPE_WEBHOOK_SECRET || ''; + if (!secret) { + throw new Error('Missing STRIPE_WEBHOOK_SECRET environment variable'); + } + + const body = await (await request.blob()).text(); + const signature = request.headers.get('stripe-signature') as string; + const event: StripeType.Event = stripe.webhooks.constructEvent(body, signature, secret); + + if (['checkout.session.completed', 'checkout.session.async_payment_succeeded'].includes(event.type)) { + // @ts-expect-error - acceptable error + const printifyOrderId = event.data.object?.metadata?.printifyOrderId; + if (!printifyOrderId) { + throw new Error(`missing printifyOrderId on metadata, ${event.id}`); + } + + await fulfillCheckout(printifyOrderId); + } + + return NextResponse.json({ result: event, ok: true }); + } catch (error) { + console.error('Error processing request:', error); + return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); + } +}; diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 5e44515..92c4733 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,12 +1,12 @@ 'use client'; -import SearchBar from './SearchBar'; +import SearchBar from '@/components/SearchBar'; import { useShoppingCart } from 'use-shopping-cart'; const Header: React.FC = () => { const { cartCount } = useShoppingCart(); - // TODO_AUTH_ACCOUNT + // TODO_AUTH_ORDERS return (
diff --git a/src/components/OrderConfirmation.tsx b/src/components/OrderConfirmation.tsx new file mode 100644 index 0000000..497b364 --- /dev/null +++ b/src/components/OrderConfirmation.tsx @@ -0,0 +1,29 @@ +'use client'; + +import Image from 'next/image'; +import Link from 'next/link'; +import React, { useEffect } from 'react'; +import { useShoppingCart } from 'use-shopping-cart'; + +const OrderConfirmation = () => { + const { clearCart } = useShoppingCart(); + + useEffect(() => { + clearCart(); + }, [clearCart]); + + return ( +
+
+ Check Mark Icon +

Thank you for your order!

+ {/* TODO_AUTH_ORDER */} +

+ You'll receive an email receipt shortly. You can checkout the status of your order anytime by visiting the "Account" page +

+
+
+ ); +}; + +export default OrderConfirmation; diff --git a/src/components/ProductSizeSelector.tsx b/src/components/ProductSizeSelector.tsx index 4916139..6ae6482 100644 --- a/src/components/ProductSizeSelector.tsx +++ b/src/components/ProductSizeSelector.tsx @@ -1,7 +1,7 @@ 'use client'; import { Product, Size } from '@/types'; -import AddToCartBtn from './AddToCartBtn'; +import AddToCartBtn from '@/components/AddToCartBtn'; import { formatPriceForDisplay } from '@/lib/stripe'; import { useState } from 'react'; import { DEFAULT_STICKER_SIZES } from '@/lib/products'; diff --git a/src/components/RelatedProducts.tsx b/src/components/RelatedProducts.tsx index cf99678..2ef3dbd 100644 --- a/src/components/RelatedProducts.tsx +++ b/src/components/RelatedProducts.tsx @@ -1,5 +1,5 @@ import { getRelatedProductsByCategory } from '@/lib/catalog'; -import ProductCard from './ProductCard'; +import ProductCard from '@/components/ProductCard'; import { Category } from '@/types'; const RelatedProducts: React.FC<{ productId: string; category: Category }> = ({ productId, category }) => { diff --git a/src/lib/products.ts b/src/lib/products.ts index 7b0fe8b..9ae0be6 100644 --- a/src/lib/products.ts +++ b/src/lib/products.ts @@ -1,6 +1,6 @@ import { Product, Size } from '@/types'; -const STICKER_SIZES = { +export const STICKER_SIZES = { TWO_BY_TWO_IN: '2x2in', THREE_BY_THREE_IN: '3x3in', FOUR_BY_FOUR_IN: '4x4in', diff --git a/src/lib/stripe.ts b/src/lib/stripe.ts index 6b55635..01c2580 100644 --- a/src/lib/stripe.ts +++ b/src/lib/stripe.ts @@ -1,11 +1,8 @@ import Stripe from 'stripe'; import { PRODUCT_CONFIG } from '@/lib/products'; +import { CartItem } from '@/types'; -// .env.template -// NEXT_PUBLIC_STRIPE_KEY="pk_asdfasdfasdfasdfasdf" -// STRIPE_SECRET_KEY="sk_asdfasdfasdfasdfasdf" -// NEXT_PUBLIC_URL=http://localhost:3000 - +// See your keys here: https://dashboard.stripe.com/apikeys export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, { // https://github.com/stripe/stripe-node#configuration apiVersion: '2024-06-20', @@ -24,7 +21,118 @@ export const formatPriceForDisplay = (amount: number = 0): string => { return numberFormat.format(amount / 100); }; -// TODO_STRIPE -export const formatCartItemsForStripe = () => {}; +export const formatCartItemsForStripe = (cartItems: CartItem[]): Stripe.Checkout.SessionCreateParams.LineItem[] => { + return cartItems.map(cartItem => { + const lineItem = { + price_data: { + currency: cartItem.currency, + product_data: { + name: cartItem.name, + description: cartItem.description, + images: [`${process.env.NEXT_PUBLIC_URL}${cartItem.image}`], // up to 8 images + metadata: { + // pass metadata to stripe, (productId, size, category, ...) + ...(cartItem.product_data || {}), + }, + }, + unit_amount: Math.round(cartItem.price), // Convert price to cents for Stripe + }, + quantity: cartItem.quantity, + // (optional) Do not allow adjustable quantity + adjustable_quantity: { + enabled: false, + }, + }; + + return lineItem; + }); +}; + +// docs: https://docs.stripe.com/api/checkout/sessions/create +export async function createCheckoutSession( + lineItems: Stripe.Checkout.SessionCreateParams.LineItem[], + metadata: { [key: string]: string } = {} +): Promise> { + return stripe.checkout.sessions.create({ + line_items: lineItems, + metadata: metadata, + mode: 'payment', + success_url: `${process.env.NEXT_PUBLIC_URL}/order-confirmation?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${process.env.NEXT_PUBLIC_URL}/cart`, + // TODO_AUTH_ORDERS + // customer: 'customerId', + // customer_email: 'customer@gmail.com', + shipping_address_collection: { + allowed_countries: PRODUCT_CONFIG.allowCountries as Stripe.Checkout.SessionCreateParams.ShippingAddressCollection.AllowedCountry[], + }, + // TODO_PRINTIFY - calculate this dynamically with Printify request + shipping_options: [ + { + shipping_rate_data: { + type: 'fixed_amount', + fixed_amount: { + amount: 459, + currency: PRODUCT_CONFIG.currency, + }, + display_name: 'Standard', + delivery_estimate: { + minimum: { + unit: 'business_day', + value: 2, + }, + maximum: { + unit: 'business_day', + value: 5, + }, + }, + }, + }, + { + shipping_rate_data: { + type: 'fixed_amount', + fixed_amount: { + amount: 429, + currency: 'usd', + }, + display_name: 'Economy', + delivery_estimate: { + minimum: { + unit: 'business_day', + value: 4, + }, + maximum: { + unit: 'business_day', + value: 8, + }, + }, + }, + }, + ], + automatic_tax: { + enabled: true, // Enable tax based on location + }, + }); +} + +// TODO_AUTH_ORDERS +export async function retrieveCheckoutSession(sessionId: string) { + return await stripe.checkout.sessions.retrieve(sessionId, { + expand: ['line_items'], + }); +} + +export async function validateStripeSession(sessionId?: string) { + if (!sessionId) return { validSession: false }; + + try { + const session = await retrieveCheckoutSession(sessionId); + + if (session.object !== 'checkout.session') return { validSession: false }; + + // TODO_AUTH_ORDERS - only this users' orders -// TODO_STRIPE (see notion) + return { validSession: true }; + } catch (error) { + return { validSession: false }; + } +} diff --git a/src/utils/validateCartItems.ts b/src/utils/validateCartItems.ts new file mode 100644 index 0000000..5c83dfc --- /dev/null +++ b/src/utils/validateCartItems.ts @@ -0,0 +1,115 @@ +import { CartItem } from '@/types'; +import { PRODUCT_CONFIG, STICKER_PRICES, STICKER_SIZES } from '@/lib/products'; + +export const validateCartItems = (cartItems: CartItem[]): CartItem[] => { + const ALLOWED_SIZES = Object.values(STICKER_SIZES); + const MAX_ITEMS_ALLOWED = 50; + const MAX_ITEM_QUANTITY = 10; + const MAX_STRING_LENGTH = 500; + const VALID_CURRENCY = PRODUCT_CONFIG.currency; + + try { + if (!Array.isArray(cartItems) || cartItems.length > MAX_ITEMS_ALLOWED || cartItems.length === 0) { + return []; + } + + const validCartItems: CartItem[] = []; + + for (const item of cartItems) { + // Basic type checks + if (typeof item !== 'object' || !item.product_data || typeof item.product_data !== 'object') { + return []; + } + + // Validate strings and their lengths + if ( + !item.name || + !item.description || + !item.image || + !item.currency || + !item.product_data.productId || + !item.product_data.size || + !item.product_data.type || + typeof item.name !== 'string' || + typeof item.description !== 'string' || + typeof item.image !== 'string' || + typeof item.currency !== 'string' || + typeof item.product_data.productId !== 'string' || + typeof item.product_data.size !== 'string' || + typeof item.product_data.type !== 'string' || + item.name.length > MAX_STRING_LENGTH || + item.description.length > MAX_STRING_LENGTH || + item.image.length > MAX_STRING_LENGTH || + item.product_data.productId.length > MAX_STRING_LENGTH + ) { + console.warn('Cart validation failed: Invalid string properties', { + name: item.name, + description: item.description, + image: item.image, + currency: item.currency, + productId: item.product_data.productId, + size: item.product_data.size, + type: item.product_data.type, + }); + return []; + } + + // Validate size + if (!ALLOWED_SIZES.includes(item.product_data.size)) { + console.warn('Cart validation failed: Invalid size', { + size: item.product_data.size, + allowedSizes: ALLOWED_SIZES, + }); + return []; + } + + // Validate currency + if (item.currency !== VALID_CURRENCY) { + return []; + } + + // Validate quantity + const quantity = Number(item.quantity) || 1; + if (!Number.isInteger(quantity) || quantity < 1 || quantity > MAX_ITEM_QUANTITY) { + return []; + } + + // Get price from server-side configuration + const price = STICKER_PRICES[item.product_data.size]; + if (!price) { + console.warn('Cart validation failed: Price not found for size', { + size: item.product_data.size, + }); + return []; + } + + // Construct validated item + const validCartItem: CartItem = { + id: item.id, + price, + currency: VALID_CURRENCY, + quantity, + description: item.description.trim(), + name: item.name.trim(), + image: item.image.trim(), + product_data: { + size: item.product_data.size, + productId: item.product_data.productId.trim(), + type: item.product_data.type.trim(), + ...(item.product_data.category && { + category: item.product_data.category.trim(), + }), + }, + }; + + validCartItems.push(validCartItem); + } + + return validCartItems; + } catch (error) { + console.error('Cart validation error:', error); + return []; + } +}; + +export default validateCartItems; diff --git a/yarn.lock b/yarn.lock index a6895df..e23683e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -37,6 +37,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@emnapi/runtime@^1.2.0": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@emnapi/runtime/-/runtime-1.3.1.tgz#0fcaa575afc31f455fd33534c19381cfce6c6f60" + integrity sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw== + dependencies: + tslib "^2.4.0" + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -88,6 +95,119 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@img/sharp-darwin-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz#ef5b5a07862805f1e8145a377c8ba6e98813ca08" + integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ== + optionalDependencies: + "@img/sharp-libvips-darwin-arm64" "1.0.4" + +"@img/sharp-darwin-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz#e03d3451cd9e664faa72948cc70a403ea4063d61" + integrity sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q== + optionalDependencies: + "@img/sharp-libvips-darwin-x64" "1.0.4" + +"@img/sharp-libvips-darwin-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz#447c5026700c01a993c7804eb8af5f6e9868c07f" + integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg== + +"@img/sharp-libvips-darwin-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz#e0456f8f7c623f9dbfbdc77383caa72281d86062" + integrity sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ== + +"@img/sharp-libvips-linux-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz#979b1c66c9a91f7ff2893556ef267f90ebe51704" + integrity sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA== + +"@img/sharp-libvips-linux-arm@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz#99f922d4e15216ec205dcb6891b721bfd2884197" + integrity sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g== + +"@img/sharp-libvips-linux-s390x@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz#f8a5eb1f374a082f72b3f45e2fb25b8118a8a5ce" + integrity sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA== + +"@img/sharp-libvips-linux-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz#d4c4619cdd157774906e15770ee119931c7ef5e0" + integrity sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw== + +"@img/sharp-libvips-linuxmusl-arm64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz#166778da0f48dd2bded1fa3033cee6b588f0d5d5" + integrity sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA== + +"@img/sharp-libvips-linuxmusl-x64@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz#93794e4d7720b077fcad3e02982f2f1c246751ff" + integrity sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw== + +"@img/sharp-linux-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz#edb0697e7a8279c9fc829a60fc35644c4839bb22" + integrity sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA== + optionalDependencies: + "@img/sharp-libvips-linux-arm64" "1.0.4" + +"@img/sharp-linux-arm@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz#422c1a352e7b5832842577dc51602bcd5b6f5eff" + integrity sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ== + optionalDependencies: + "@img/sharp-libvips-linux-arm" "1.0.5" + +"@img/sharp-linux-s390x@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz#f5c077926b48e97e4a04d004dfaf175972059667" + integrity sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q== + optionalDependencies: + "@img/sharp-libvips-linux-s390x" "1.0.4" + +"@img/sharp-linux-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz#d806e0afd71ae6775cc87f0da8f2d03a7c2209cb" + integrity sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA== + optionalDependencies: + "@img/sharp-libvips-linux-x64" "1.0.4" + +"@img/sharp-linuxmusl-arm64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz#252975b915894fb315af5deea174651e208d3d6b" + integrity sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + +"@img/sharp-linuxmusl-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz#3f4609ac5d8ef8ec7dadee80b560961a60fd4f48" + integrity sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw== + optionalDependencies: + "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + +"@img/sharp-wasm32@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz#6f44f3283069d935bb5ca5813153572f3e6f61a1" + integrity sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg== + dependencies: + "@emnapi/runtime" "^1.2.0" + +"@img/sharp-win32-ia32@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz#1a0c839a40c5351e9885628c85f2e5dfd02b52a9" + integrity sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ== + +"@img/sharp-win32-x64@0.33.5": + version "0.33.5" + resolved "https://registry.yarnpkg.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz#56f00962ff0c4e0eb93d34a047d29fa995e3e342" + integrity sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg== + "@isaacs/cliui@^8.0.2": version "8.0.2" resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" @@ -787,11 +907,27 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4" + integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/color/-/color-4.2.3.tgz#d781ecb5e57224ee43ea9627560107c0e0c6463a" + integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A== + dependencies: + color-convert "^2.0.1" + color-string "^1.9.0" + colorette@^2.0.16: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" @@ -952,6 +1088,11 @@ define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" +detect-libc@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" @@ -1798,6 +1939,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-async-function@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" @@ -2904,6 +3050,35 @@ set-function-name@^2.0.1, set-function-name@^2.0.2: functions-have-names "^1.2.3" has-property-descriptors "^1.0.2" +sharp@^0.33.5: + version "0.33.5" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.33.5.tgz#13e0e4130cc309d6a9497596715240b2ec0c594e" + integrity sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw== + dependencies: + color "^4.2.3" + detect-libc "^2.0.3" + semver "^7.6.3" + optionalDependencies: + "@img/sharp-darwin-arm64" "0.33.5" + "@img/sharp-darwin-x64" "0.33.5" + "@img/sharp-libvips-darwin-arm64" "1.0.4" + "@img/sharp-libvips-darwin-x64" "1.0.4" + "@img/sharp-libvips-linux-arm" "1.0.5" + "@img/sharp-libvips-linux-arm64" "1.0.4" + "@img/sharp-libvips-linux-s390x" "1.0.4" + "@img/sharp-libvips-linux-x64" "1.0.4" + "@img/sharp-libvips-linuxmusl-arm64" "1.0.4" + "@img/sharp-libvips-linuxmusl-x64" "1.0.4" + "@img/sharp-linux-arm" "0.33.5" + "@img/sharp-linux-arm64" "0.33.5" + "@img/sharp-linux-s390x" "0.33.5" + "@img/sharp-linux-x64" "0.33.5" + "@img/sharp-linuxmusl-arm64" "0.33.5" + "@img/sharp-linuxmusl-x64" "0.33.5" + "@img/sharp-wasm32" "0.33.5" + "@img/sharp-win32-ia32" "0.33.5" + "@img/sharp-win32-x64" "0.33.5" + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -2936,6 +3111,13 @@ signal-exit@^4.0.1: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg== + dependencies: + is-arrayish "^0.3.1" + slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"