Skip to content

Commit

Permalink
refactor: send sticker png to printify
Browse files Browse the repository at this point in the history
  • Loading branch information
spencerlepine committed Oct 25, 2024
1 parent 6549e7d commit 2b4ee1f
Show file tree
Hide file tree
Showing 13 changed files with 98 additions and 71 deletions.
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ https://github.com/jadiaheno/vention-machine-cloud-test/assets/13062328/a42d55bb

- **Node.js** installed on your machine (download [here](https://nodejs.org/en/download))
- Stripe developer account (+ API keys)
- `stripe-cli` (`$ brew install stripe/stripe-cli/stripe`)
- Printify developer account (+ API keys)

#### Installation
Expand All @@ -75,33 +76,33 @@ npm install
#### Run Locally

```sh
stripe login
stripe listen --forward-to localhost:3000/api/v1/webhook/checkout

# *open separate terminal*

npm run dev
# visit http://locahost:3000
```

#### Production Build
#### Local Docker Container

```sh
cp .env.template .env.production
npm run build
```
stripe login
stripe listen --forward-to localhost:3000/api/v1/webhook/checkout

#### Local Docker Container
# *open separate terminal*

```sh
cp .env.template .env.development
docker-compose -f ./docker/development docker-compose.yml up -d
# visit http://locahost:3001
docker-compose -f ./docker/development/docker-compose.yml --env-file .env.development up -d
# visit http://locahost:3000
```

### Local Stripe Webhook Testing
#### Production Build

```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
cp .env.template .env.production
npm run build
```

## License
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
"pre-commit": "npm run build && lint-staged"
}
},
"lint-staged": {
Expand Down
8 changes: 0 additions & 8 deletions src/app/(orders)/account/page.tsx

This file was deleted.

1 change: 1 addition & 0 deletions src/app/api/v1/checkout/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export const POST = async (request: NextRequest) => {
return NextResponse.json({ message: 'Unable to checkout cart items' }, { status: 400 });
}

// TODO_PRINTIFY (move this to final webhook)
const printifyLineItems = formatCartItemsForPrintify(cartItems);
const { printifyOrderId } = await createDraftOrder(printifyLineItems);
if (!printifyOrderId) {
Expand Down
28 changes: 2 additions & 26 deletions src/app/(orders)/cart/page.tsx → src/app/cart/page.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,15 @@
'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';
import CheckoutButton from '@/components/CheckoutBtn';

export default function CartPage() {
const router = useRouter();

const { cartCount, cartDetails, removeItem, totalPrice, addItem, decrementItem } = useShoppingCart();
const cartItems = Object.values(cartDetails ?? {});

async function handleCheckoutClick() {
// TODO_CHECKOUT - debounce click
// TODO_CHECKOUT - error message popup
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('Checkout request threw an error', error);
}
}

const handleRemove = (cartItem: CartItem) => {
if (cartItem.quantity === 1) {
removeItem(cartItem.id);
Expand All @@ -45,9 +23,7 @@ export default function CartPage() {
<div className="my-8 mx-20">
<div className="flex justify-between items-center p-4">
<span className="text-lg font-semibold">Subtotal: {subtotal}</span>
<button disabled={cartCount === 0} className="bg-green-500 text-white px-4 py-2 rounded-md focus:outline-none" onClick={handleCheckoutClick}>
Checkout
</button>
<CheckoutButton cartCount={cartCount} cartItems={cartItems} />
</div>

{!cartItems ||
Expand Down
24 changes: 24 additions & 0 deletions src/app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';

import { useEffect } from 'react';

export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);

return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
);
}
File renamed without changes.
46 changes: 46 additions & 0 deletions src/components/CheckoutBtn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'use client';

import { useRouter } from 'next/navigation';
import { useRef, useState } from 'react';
import { CartEntry } from 'use-shopping-cart/core';

const CheckoutButton: React.FC<{ cartCount?: number; cartItems: CartEntry[] }> = ({ cartCount, cartItems }) => {
const router = useRouter();
const isProcessing = useRef(false);
const [error, setError] = useState('');

const handleCheckoutClick = async () => {
if (!cartCount || cartCount === 0 || isProcessing.current) {
return;
}

isProcessing.current = true;

try {
const res = await fetch('/api/v1/checkout', {
method: 'POST',
body: JSON.stringify({ cartItems }),
});

const { checkoutUrl } = await res.json();
if (!checkoutUrl) {
setError('Unable to checkout at this time. Please try again later.');
return;
}
router.push(checkoutUrl);
} catch (err) {
console.error('Checkout request threw an error', err, error);
setError('An error occurred during checkout. Please try again later.');
} finally {
isProcessing.current = false;
}
};

return (
<button disabled={cartCount === 0 || isProcessing.current} className="bg-green-500 text-white px-4 py-2 rounded-md focus:outline-none" onClick={handleCheckoutClick}>
Checkout
</button>
);
};

export default CheckoutButton;
10 changes: 0 additions & 10 deletions src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { useShoppingCart } from 'use-shopping-cart';
const Header: React.FC = () => {
const { cartCount } = useShoppingCart();

// TODO_AUTH_ORDERS
return (
<header className="flex justify-between items-center px-4 py-2 bg-white shadow-md">
<div className="container mx-auto flex justify-between items-center">
Expand All @@ -15,15 +14,6 @@ const Header: React.FC = () => {
</a>
<SearchBar />
<div className="flex space-x-4">
{false ? (
<a href="/account" className="text-blue-500 hover:underline">
Account
</a>
) : (
<a href="/api/auth/login" className="text-blue-500 hover:underline">
Sign in
</a>
)}
<a href="/cart" className="text-blue-500 hover:underline">
Cart ({cartCount})
</a>
Expand Down
6 changes: 1 addition & 5 deletions src/components/OrderConfirmation.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import Image from 'next/image';
import Link from 'next/link';
import React, { useEffect } from 'react';
import { useShoppingCart } from 'use-shopping-cart';

Expand All @@ -17,10 +16,7 @@ const OrderConfirmation = () => {
<div className="text-center">
<Image width={100} height={100} src="/icons/check-mark.png" alt="Check Mark Icon" className="w-32 h-32 object-cover mx-auto mb-4" />
<h3 className="text-2xl font-bold mb-2">Thank you for your order!</h3>
{/* TODO_AUTH_ORDER */}
<p className="text max-w-md text-gray-500">
You&apos;ll receive an email receipt shortly. You can checkout the status of your order anytime by visiting the <Link href="/account">&quot;Account&quot;</Link> page
</p>
<p className="text max-w-md text-gray-500">An order confirmation email is on its way. You&apos;ll also receive a shipping tracking number via email.</p>
</div>
</div>
);
Expand Down
7 changes: 4 additions & 3 deletions src/lib/printify.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Printify from 'printify-sdk-js';
import { STICKER_SIZES } from '@/lib/products';
import { retrieveStickerPNGFileUrl, STICKER_SIZES } from '@/lib/products';
import { CartItem, PrintifyLineItem } from '@/types';

// docs: https://developers.printify.com/
Expand All @@ -26,7 +26,7 @@ export const formatCartItemsForPrintify = (cartItems: CartItem[]): PrintifyLineI
blueprint_id: PRINTIFY_BLUEPRINT_ID,
variant_id: PRINTIFY_VARIANT_IDS[cartItem.product_data.size],
print_areas: {
front: `${process.env.NEXT_PUBLIC_URL}${cartItem.image}`,
front: retrieveStickerPNGFileUrl(cartItem.product_data.productId),
},
quantity: cartItem.quantity,
}));
Expand All @@ -42,12 +42,13 @@ export async function createDraftOrder(lineItems: PrintifyLineItem[]): Promise<{
const orderData = {
external_id: randomId,
label: `shipment_${randomLabel}`,
// TODO_PRINTIFY (pull/format from stripe)
line_items: lineItems,
shipping_method: 1,
is_printify_express: false,
is_economy_shipping: false,
send_shipping_notification: false,
// TODO_AUTH_ORDER
// TODO_PRINTIFY (pull address from stripe)
address_to: {
first_name: 'John',
last_name: 'Doe',
Expand Down
4 changes: 4 additions & 0 deletions src/lib/products.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ const stickerDescription = `
- Items are typically shipped within 2-5 business days.
`;

// source: https://github.com/spencerlepine/swagsticker.com-prod/tree/assets
const STICKER_PNG_ASSETS_BASE_URL = 'https://github.com/spencerlepine/swagsticker.com/raw/refs/heads/assets/pngs';
export const retrieveStickerPNGFileUrl = (productSlug: string) => `${STICKER_PNG_ASSETS_BASE_URL}/${productSlug}.png`;

export const STICKER_PRODUCTS: Product[] = [
{
id: 'airtable',
Expand Down
4 changes: 0 additions & 4 deletions src/lib/stripe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ export async function createCheckoutSession(
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: '[email protected]',
shipping_address_collection: {
Expand Down Expand Up @@ -115,7 +114,6 @@ export async function createCheckoutSession(
});
}

// TODO_AUTH_ORDERS
export async function retrieveCheckoutSession(sessionId: string) {
return await stripe.checkout.sessions.retrieve(sessionId, {
expand: ['line_items'],
Expand All @@ -130,8 +128,6 @@ export async function validateStripeSession(sessionId?: string) {

if (session.object !== 'checkout.session') return { validSession: false };

// TODO_AUTH_ORDERS - only this users' orders

return { validSession: true };
} catch (error) {
return { validSession: false };
Expand Down

0 comments on commit 2b4ee1f

Please sign in to comment.