-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
wzulfikar
committed
Mar 29, 2021
1 parent
2027c38
commit 11589d9
Showing
14 changed files
with
541 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import initStripe from '@src/lib/stripe/init'; | ||
|
||
import StripePreview from '@src/components/_preview/containers/StripePreview'; | ||
|
||
export default StripePreview; | ||
|
||
export async function getStaticProps() { | ||
const stripe = initStripe(); | ||
const prices = await stripe.prices.list({ | ||
active: true, | ||
limit: 10, | ||
|
||
// Inline product info in response | ||
expand: ['data.product'], | ||
|
||
// Only return prices of type `recurring` or `one_time`. | ||
// Leave it empty to inlude both types. | ||
// type: 'recurring', | ||
}); | ||
|
||
return { props: { prices: prices.data } }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import NextStripe from 'next-stripe'; | ||
|
||
export default NextStripe({ | ||
stripe_key: process.env.STRIPE_RESTRICTED_KEY, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
/** | ||
* Adapted from https://github.com/vercel/nextjs-subscription-payments/blob/main/pages/api/webhooks.js | ||
*/ | ||
|
||
import initStripe from '@src/lib/stripe/init'; | ||
import { | ||
upsertProductRecord, | ||
upsertPriceRecord, | ||
manageSubscriptionStatusChange, | ||
} from '@src/services/stripe-webhook'; | ||
|
||
export const config = { | ||
api: { | ||
bodyParser: false, // Stripe needs to use the raw body to construct the event. | ||
}, | ||
}; | ||
|
||
async function buffer(readable) { | ||
const chunks = []; | ||
for await (const chunk of readable) { | ||
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk); | ||
} | ||
return Buffer.concat(chunks); | ||
} | ||
|
||
const relevantEvents = new Set([ | ||
'product.created', | ||
'product.updated', | ||
'price.created', | ||
'price.updated', | ||
'checkout.session.completed', | ||
'customer.subscription.created', | ||
'customer.subscription.updated', | ||
'customer.subscription.deleted', | ||
]); | ||
|
||
const webhookHandler = async (req, res) => { | ||
if (req.method === 'POST') { | ||
const buf = await buffer(req); | ||
const sig = req.headers['stripe-signature']; | ||
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; | ||
|
||
let event; | ||
|
||
try { | ||
const stripe = initStripe(); | ||
event = stripe.webhooks.constructEvent(buf, sig, webhookSecret); | ||
} catch (err) { | ||
console.error(`[ERROR][stripe] webhook error: ${err.message}`); | ||
return res.status(400).send(`Webhook Error: ${err.message}`); | ||
} | ||
|
||
console.info('[INFO][stripe] new webhook event:', { | ||
type: event.type, | ||
relevant: relevantEvents.has(event.type), | ||
}); | ||
|
||
if (relevantEvents.has(event.type)) { | ||
try { | ||
switch (event.type) { | ||
case 'product.created': | ||
case 'product.updated': | ||
await upsertProductRecord(event.data.object); | ||
break; | ||
case 'price.created': | ||
case 'price.updated': | ||
await upsertPriceRecord(event.data.object); | ||
break; | ||
case 'customer.subscription.created': | ||
case 'customer.subscription.updated': | ||
case 'customer.subscription.deleted': | ||
await manageSubscriptionStatusChange( | ||
event.data.object.id, | ||
event.data.object.customer, | ||
event.type === 'customer.subscription.created' | ||
); | ||
break; | ||
case 'checkout.session.completed': | ||
const checkoutSession = event.data.object; | ||
if (checkoutSession.mode === 'subscription') { | ||
const subscriptionId = checkoutSession.subscription; | ||
await manageSubscriptionStatusChange( | ||
subscriptionId, | ||
checkoutSession.customer, | ||
true | ||
); | ||
} | ||
break; | ||
default: | ||
throw new Error('Unhandled relevant event!'); | ||
} | ||
} catch (error) { | ||
console.log(error); | ||
return res.json({ error: 'Webhook handler failed. View logs.' }); | ||
} | ||
} | ||
|
||
res.json({ received: true }); | ||
} else { | ||
res.setHeader('Allow', 'POST'); | ||
res.status(405).end('Method Not Allowed'); | ||
} | ||
}; | ||
|
||
export default webhookHandler; |
30 changes: 30 additions & 0 deletions
30
src/components/_preview/containers/StripePreview/controller.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import { useState } from 'react'; | ||
|
||
import { loadStripe } from '@stripe/stripe-js'; | ||
import { createCheckoutSession } from 'next-stripe/client'; | ||
|
||
import StripePreview from './elements'; | ||
|
||
export default function StripePreviewController({ prices }) { | ||
async function onCheckout(priceId, mode: 'subscription' | 'payment') { | ||
// Create stripe checkout session | ||
const qty = 1; // Assume user will buy one item | ||
const session = await createCheckoutSession({ | ||
success_url: window.location.href, | ||
cancel_url: window.location.href, | ||
line_items: [{ price: priceId, quantity: qty }], | ||
payment_method_types: ['card'], | ||
|
||
// Use `payment` mode if the `priceId` is one-time purchase | ||
// or `subscription` if it's a recurring payment. | ||
mode: mode, | ||
}); | ||
|
||
const stripe = await loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY); | ||
if (stripe) { | ||
stripe.redirectToCheckout({ sessionId: session.id }); | ||
} | ||
} | ||
|
||
return <StripePreview prices={prices} onCheckout={onCheckout} />; | ||
} |
92 changes: 92 additions & 0 deletions
92
src/components/_preview/containers/StripePreview/elements.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { useState } from 'react'; | ||
import { AiFillInfoCircle } from 'react-icons/ai'; | ||
|
||
function PriceList({ prices, onCheckout, recurringSuffix = null }) { | ||
const [isCheckingOut, setIsCheckingOut] = useState(null); | ||
|
||
async function _onCheckout(priceId, mode) { | ||
setIsCheckingOut(priceId); | ||
await onCheckout(priceId, mode); | ||
setIsCheckingOut(null); | ||
} | ||
|
||
return ( | ||
<ul className="grid sm:grid-cols-3 gap-4"> | ||
{prices.map((price) => ( | ||
<li | ||
className="grid grid-cols-2 border border-gray-800 dark:border-white p-4 cursor-pointer hover:text-blue-600 dark:hover:border-blue-600" | ||
key={price.id} | ||
onClick={() => | ||
_onCheckout( | ||
price.id, | ||
Boolean(recurringSuffix) ? 'subscription' : 'payment' | ||
) | ||
} | ||
> | ||
<div> | ||
<h2 className="font-bold">{price.product.name}</h2> | ||
<img src={price.product.images[0]} /> | ||
<p> | ||
{(price.unit_amount / 100).toFixed(2)}{' '} | ||
{price.currency.toUpperCase()} {recurringSuffix} | ||
</p> | ||
</div> | ||
<button className="ml-auto font-medium"> | ||
{price.id === isCheckingOut ? 'Processing..' : 'Checkout'} → | ||
</button> | ||
</li> | ||
))} | ||
</ul> | ||
); | ||
} | ||
|
||
export default function StripePreview({ prices, onCheckout }) { | ||
const pricesByType = { one_time: [], recurring: [] }; | ||
prices.forEach((price) => pricesByType[price.type].push(price)); | ||
|
||
return ( | ||
<div className="bg-gray-200 dark:bg-black dark:text-white min-h-screen px-4 py-8"> | ||
<h1 className="text-2xl font-bold text-center bg-clip-text text-transparent bg-gradient-to-r from-green-400 to-blue-500"> | ||
Stripe Checkout | ||
</h1> | ||
<div className="flex flex-col mx-auto max-w-4xl"> | ||
<p className="flex items-center mt-6 text-sm text-gray-600 dark:text-gray-400"> | ||
<AiFillInfoCircle className="mr-1" /> | ||
You can use Stripe test card during checkout:{' '} | ||
<code className="ml-1">4242 4242 4242 4242</code> | ||
</p> | ||
|
||
{/* One-time products */} | ||
<div> | ||
<h3 className="text-lg mb-8 pb-4 pt-4 font-semibold text-green-600 border-b border-green-600"> | ||
One-Time Payments | ||
</h3> | ||
<PriceList prices={pricesByType.one_time} onCheckout={onCheckout} /> | ||
</div> | ||
|
||
{/* Recurring products */} | ||
<div> | ||
<h3 className="text-lg mb-6 pb-4 pt-8 font-semibold text-green-600 border-b border-green-600"> | ||
Subscriptions | ||
</h3> | ||
<h4 className="pb-4">Pay monthly:</h4> | ||
<PriceList | ||
prices={pricesByType.recurring.filter( | ||
(price) => price.recurring.interval === 'month' | ||
)} | ||
onCheckout={onCheckout} | ||
recurringSuffix="/ mth" | ||
/> | ||
<h4 className="pb-4 pt-6">Pay yearly:</h4> | ||
<PriceList | ||
prices={pricesByType.recurring.filter( | ||
(price) => price.recurring.interval === 'year' | ||
)} | ||
onCheckout={onCheckout} | ||
recurringSuffix="/ yr" | ||
/> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import controller from './controller'; | ||
|
||
export default controller; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
export default function dateFromSecond(secs: number): Date { | ||
const t = new Date('1970-01-01T00:30:00Z'); // Unix epoch start. | ||
t.setSeconds(secs); | ||
return t; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import Stripe from 'stripe'; | ||
|
||
/** | ||
* @description | ||
* Create stripe instance (server-side) using `STRIPE_RESTRICTED_KEY` environment variable. | ||
* | ||
* @example | ||
* import initStripe from '@src/lib/stripe/init | ||
* const stripe = initStripe() | ||
*/ | ||
const initStripe = () => | ||
new Stripe(process.env.STRIPE_RESTRICTED_KEY, { | ||
// https://github.com/stripe/stripe-node#configuration | ||
apiVersion: '2020-08-27', | ||
}); | ||
|
||
export default initStripe; |
Oops, something went wrong.