Skip to content

Instantly share code, notes, and snippets.

@frankdilo
Last active November 2, 2022 20:45
Show Gist options
  • Save frankdilo/fc90829bc17066d103ea966ae050b39b to your computer and use it in GitHub Desktop.
Save frankdilo/fc90829bc17066d103ea966ae050b39b to your computer and use it in GitHub Desktop.
Editable Stripe Invoice Page for NextJS https://twitter.com/frankdilo/status/1549318147265929216
import { GetServerSideProps } from "next";
import Head from "next/head";
import { useEffect, useRef, useState } from "react";
// I suggest to create a read-only Stripe key with access to invoices + customers for this)
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY_INVOICE_GENERATOR);
export default function InvoicePage(props) {
const invoice = props.invoice;
const [billTo, setBillTo] = useState(
[invoice.customer_name, invoice.customer_email, invoice.customer_address].filter((e) => !!e).join("\n")
);
const textAreaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
setTimeout(() => {
textAreaRef.current?.focus();
textAreaRef.current?.setSelectionRange(0, 100000);
}, 1000);
}, []);
return (
<>
<HeadComponent title={`Invoice #${invoice.number}`} />
<div className="invoice-box">
<table cellPadding="0" cellSpacing="0">
<tr className="top">
<td colSpan={2}>
<table>
<tr>
<td className="title">
<img
alt=""
src="https://typefully.com/icon/apple-icon.png"
style={{ width: "100%", maxWidth: "50px", borderRadius: "5px" }}
/>
</td>
<td>
<strong>Invoice #{invoice.number}</strong>
<br />
Paid: {formatDate(invoice.created)}
</td>
</tr>
</table>
</td>
</tr>
<tr className="information">
<td colSpan={2}>
<table>
<tr>
<td>
<strong>Typefully</strong>
<br />
TODO
</td>
<td>
<strong>Bill to:</strong>
<br />
<textarea
style={{ width: 300, minHeight: 100, textAlign: "right" }}
value={billTo}
onChange={(e) => setBillTo(e.target.value)}
ref={textAreaRef}
/>
</td>
</tr>
</table>
</td>
</tr>
<tr className="heading">
<td>Description</td>
<td>Amount</td>
</tr>
{invoice.lines.data.map((line) => {
return (
<tr className="item">
<td>{line.description}</td>
<td>{formatStripeAmount(line.amount)}</td>
</tr>
);
})}
{invoice.total_discount_amounts.map((discountAmount) => {
return (
<tr className="item">
<td>Discount</td>
<td>{formatStripeAmount(discountAmount.amount)}</td>
</tr>
);
})}
<tr className="total">
<td></td>
<td>Total: {formatStripeAmount(invoice.total)}</td>
</tr>
</table>
</div>
<button onClick={() => window.print()}>Print (and save as PDF)</Button>
</>
);
}
function formatDate(utcSeconds: number) {
return new Date(utcSeconds * 1000).toLocaleDateString("en-US", { dateStyle: "medium" });
}
function formatStripeAmount(amount: number) {
// format to two decimal places with dollar
return `$${(amount / 100).toFixed(2)}`;
}
const HeadComponent = ({ title }: { title: string }) => {
return (
<Head>
<title>{title}</title>
<style>{`
@media print {
textarea {
border-style: none;
border-color: Transparent;
overflow: auto;
resize: none;
}
.no-print {
display: none;
opacity: 0;
}
}
.invoice-box {
max-width: 800px;
margin: auto;
padding: 30px;
border: 1px solid #eee;
box-shadow: 0 0 4px rgba(0, 0, 0, 0.15);
font-size: 16px;
line-height: 24px;
font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;
color: #555;
}
textarea {
font-size: 16px;
line-height: 24px;
font-family: 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;
color: #555;
}
.invoice-box table {
width: 100%;
line-height: inherit;
text-align: left;
}
.invoice-box table td {
padding: 5px;
vertical-align: top;
}
.invoice-box table tr td:nth-child(2) {
text-align: right;
}
.invoice-box table tr.top table td {
padding-bottom: 20px;
}
.invoice-box table tr.top table td.title {
font-size: 45px;
line-height: 45px;
color: #333;
}
.invoice-box table tr.information table td {
padding-bottom: 40px;
}
.invoice-box table tr.heading td {
background: #eee;
border-bottom: 1px solid #ddd;
font-weight: bold;
}
.invoice-box table tr.details td {
padding-bottom: 20px;
}
.invoice-box table tr.item td {
border-bottom: 1px solid #eee;
}
.invoice-box table tr.item.last td {
border-bottom: none;
}
.invoice-box table tr.total td:nth-child(2) {
border-top: 2px solid #eee;
font-weight: bold;
}
@media only screen and (max-width: 600px) {
.invoice-box table tr.top table td {
width: 100%;
display: block;
text-align: center;
}
.invoice-box table tr.information table td {
width: 100%;
display: block;
text-align: center;
}
}
/** RTL **/
.invoice-box.rtl {
direction: rtl;
font-family: Tahoma, 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;
}
.invoice-box.rtl table {
text-align: right;
}
.invoice-box.rtl table tr td:nth-child(2) {
text-align: left;
}
`}</style>
</Head>
);
};
export const getServerSideProps: GetServerSideProps = async (context) => {
const invoiceID = typeof context.params?.invoice_id === "string" && context.params?.invoice_id;
if (!invoiceID) {
return { notFound: true };
}
try {
const invoice = await stripe.invoices.retrieve(invoiceID);
return { props: { invoice } };
} catch (e) {
console.error("error getting stripe invoice", e);
return { notFound: true };
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment