forked from stripe-samples/netlify-stripe-subscriptions
-
Notifications
You must be signed in to change notification settings - Fork 0
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
1 parent
93004a4
commit 234e51d
Showing
12 changed files
with
334 additions
and
64 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
type User { | ||
netlifyID: ID! | ||
stripeID: ID! | ||
} | ||
|
||
type Query { | ||
getUserByNetlifyID(netlifyID: ID!): User! | ||
getUserByStripeID(stripeID: ID!): User! | ||
} |
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,57 @@ | ||
const content = { | ||
free: { | ||
src: | ||
'https://images.unsplash.com/photo-1550159930-40066082a4fc?auto=format&fit=crop&w=600&h=600&q=80', | ||
alt: 'corgi in the park with a sunset in the background', | ||
credit: 'Jacob Van Blarcom', | ||
creditLink: 'https://unsplash.com/photos/lkzjENdWgd8', | ||
message: 'To view this content, you need to create an account!', | ||
allowedRoles: ['free', 'pro', 'premium'], | ||
}, | ||
pro: { | ||
src: | ||
'https://images.unsplash.com/photo-1519098901909-b1553a1190af?auto=format&fit=crop&w=600&h=600&q=80', | ||
alt: 'close-up of a corgi with its tongue hanging out', | ||
credit: 'Florencia Potter', | ||
creditLink: 'https://unsplash.com/photos/yxmNWxi3wCo', | ||
message: | ||
'This is protected content! It’s only available if you have a pro plan or higher.', | ||
allowedRoles: ['pro', 'premium'], | ||
}, | ||
premium: { | ||
src: | ||
'https://images.unsplash.com/photo-1546975490-e8b92a360b24?auto=format&fit=crop&w=600&h=600&q=80', | ||
alt: 'corgi in a tent with string lights in the foreground', | ||
credit: 'Cole Keister', | ||
creditLink: 'https://unsplash.com/photos/cX-KEISwDIw', | ||
message: | ||
'This is protected content! It’s only available if you have the premium plan.', | ||
allowedRoles: ['premium'], | ||
}, | ||
}; | ||
|
||
exports.handler = async (event, context) => { | ||
const { type } = JSON.parse(event.body); | ||
const { user } = context.clientContext; | ||
const roles = user ? user.app_metadata.roles : false; | ||
const { allowedRoles } = content[type]; | ||
|
||
if (!roles || !roles.some((role) => allowedRoles.includes(role))) { | ||
return { | ||
statusCode: 402, | ||
body: JSON.stringify({ | ||
src: | ||
'https://res.cloudinary.com/jlengstorf/image/upload/q_auto,f_auto/v1592618179/stripe-subscription/subscription-required.jpg', | ||
alt: 'corgi in a crossed circle with the text “subscription required”', | ||
credit: 'Jason Lengstorf', | ||
creditLink: 'https://dribbble.com/jlengstorf', | ||
message: `This content requires a ${type} subscription.`, | ||
}), | ||
}; | ||
} | ||
|
||
return { | ||
statusCode: 200, | ||
body: JSON.stringify(content[type]), | ||
}; | ||
}; |
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,40 @@ | ||
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); | ||
const { faunaFetch } = require('./utils/fauna'); | ||
|
||
exports.handler = async (event) => { | ||
const { user } = JSON.parse(event.body); | ||
|
||
// create a new customer in Stripe | ||
const customer = await stripe.customers.create({ email: user.email }); | ||
|
||
// subscribe the new customer to the free plan | ||
await stripe.subscriptions.create({ | ||
customer: customer.id, | ||
items: [{ price: process.env.STRIPE_DEFAULT_PRICE_PLAN }], | ||
}); | ||
|
||
// store the Netlify and Stripe IDs in Fauna | ||
await faunaFetch({ | ||
query: ` | ||
mutation ($netlifyID: ID!, $stripeID: ID!) { | ||
createUser(data: { netlifyID: $netlifyID, stripeID: $stripeID }) { | ||
netlifyID | ||
stripeID | ||
} | ||
} | ||
`, | ||
variables: { | ||
netlifyID: user.id, | ||
stripeID: customer.id, | ||
}, | ||
}); | ||
|
||
return { | ||
statusCode: 200, | ||
body: JSON.stringify({ | ||
app_metadata: { | ||
roles: ['free'], | ||
}, | ||
}), | ||
}; | ||
}; |
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,16 @@ | ||
const fetch = require('node-fetch'); | ||
|
||
exports.faunaFetch = async ({ query, variables }) => { | ||
return await fetch('https://graphql.fauna.com/graphql', { | ||
method: 'POST', | ||
headers: { | ||
Authorization: `Bearer ${process.env.FAUNA_SERVER_KEY}`, | ||
}, | ||
body: JSON.stringify({ | ||
query, | ||
variables, | ||
}), | ||
}) | ||
.then((res) => res.json()) | ||
.catch((err) => console.error(JSON.stringify(err, null, 2))); | ||
}; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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 @@ | ||
module.exports = 'default'; |
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,10 @@ | ||
module.exports = { | ||
// keep it short! shown in the header | ||
title: 'Stripe Subscriptions', | ||
|
||
// these are all optional and add links to the footer | ||
// repo: 'learnwithjason/demo-base', | ||
// episode: | ||
// 'https://www.learnwithjason.dev/creating-css-variable-font-text-effects', | ||
// tutorial: 'https://codepen.io/jlengstorf/pen/QWbdLjb', | ||
}; |
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,184 @@ | ||
<style> | ||
h1 { | ||
text-align: center; | ||
} | ||
|
||
.user-info { | ||
align-items: center; | ||
display: grid; | ||
gap: 1rem; | ||
grid-template-columns: repeat(2, 1fr); | ||
list-style: none; | ||
padding: 0; | ||
} | ||
|
||
.user-info button { | ||
background: var(--dark-gray); | ||
border: 0; | ||
border-radius: 0.5rem; | ||
color: var(--white); | ||
display: block; | ||
font-family: var(--font-family); | ||
font-size: 1.5rem; | ||
font-weight: 900; | ||
padding: 1rem; | ||
text-align: center; | ||
text-decoration: none; | ||
} | ||
|
||
.corgi-content { | ||
display: grid; | ||
gap: 1rem; | ||
grid-template-columns: repeat(3, 1fr); | ||
} | ||
|
||
.content h2 { | ||
font-size: 1.25rem; | ||
text-align: center; | ||
} | ||
|
||
.content-display { | ||
margin: 0; | ||
} | ||
|
||
.credit { | ||
display: block; | ||
font-size: 0.75rem; | ||
} | ||
|
||
.content img { | ||
width: 100%; | ||
} | ||
</style> | ||
|
||
<h1>Sign Up for Premium Corgi Content</h1> | ||
|
||
<div class="user-info"> | ||
<button id="left">Log In</button> | ||
<button id="right">Sign Up</button> | ||
</div> | ||
|
||
<div class="corgi-content"> | ||
<div class="content"> | ||
<h2>Free Content</h2> | ||
<div class="free"></div> | ||
</div> | ||
<div class="content"> | ||
<h2>Pro Content</h2> | ||
<div class="pro"></div> | ||
</div> | ||
<div class="content"> | ||
<h2>Premium Content</h2> | ||
<div class="premium"></div> | ||
</div> | ||
</div> | ||
|
||
<template id="content"> | ||
<figure class="content-display"> | ||
<img /> | ||
<figcaption> | ||
<a class="credit"></a> | ||
</figcaption> | ||
</figure> | ||
</template> | ||
|
||
<script | ||
type="text/javascript" | ||
src="https://unpkg.com/[email protected]/build/netlify-identity-widget.js" | ||
></script> | ||
|
||
<script> | ||
const button1 = document.getElementById('left'); | ||
const button2 = document.getElementById('right'); | ||
|
||
const login = () => netlifyIdentity.open('login'); | ||
const signup = () => netlifyIdentity.open('signup'); | ||
|
||
// by default, we want to add login and signup functionality | ||
button1.addEventListener('click', login); | ||
button2.addEventListener('click', signup); | ||
|
||
const updateUserInfo = (user) => { | ||
const container = document.querySelector('.user-info'); | ||
|
||
// cloning the buttons removes existing event listeners | ||
const b1 = button1.cloneNode(true); | ||
const b2 = button2.cloneNode(true); | ||
|
||
// empty the user info div | ||
container.innerHTML = ''; | ||
|
||
if (user) { | ||
b1.innerText = 'Log Out'; | ||
b1.addEventListener('click', () => { | ||
netlifyIdentity.logout(); | ||
}); | ||
|
||
b2.innerText = 'Manage Subscription'; | ||
b2.addEventListener('click', () => { | ||
// TODO handle subscription management | ||
}); | ||
} else { | ||
// if no one is logged in, show login/signup options | ||
b1.innerText = 'Log In'; | ||
b1.addEventListener('click', login); | ||
|
||
b2.innerText = 'Sign Up'; | ||
b2.addEventListener('click', signup); | ||
} | ||
|
||
// add the updated buttons back to the user info div | ||
container.appendChild(b1); | ||
container.appendChild(b2); | ||
}; | ||
|
||
const loadSubscriptionContent = async (user) => { | ||
const token = user ? await netlifyIdentity.currentUser().jwt(true) : false; | ||
|
||
['free', 'pro', 'premium'].forEach((type) => { | ||
fetch('/.netlify/functions/get-protected-content', { | ||
method: 'POST', | ||
headers: { | ||
Authorization: `Bearer ${token}`, | ||
}, | ||
body: JSON.stringify({ type }), | ||
}) | ||
.then((res) => res.json()) | ||
.then((data) => { | ||
const template = document.querySelector('#content'); | ||
const container = document.querySelector(`.${type}`); | ||
|
||
// remove any existing content from the content containers | ||
const oldContent = container.querySelector('.content-display'); | ||
if (oldContent) { | ||
container.removeChild(oldContent); | ||
} | ||
|
||
const content = template.content.cloneNode(true); | ||
|
||
const img = content.querySelector('img'); | ||
img.src = data.src; | ||
img.alt = data.alt; | ||
|
||
const credit = content.querySelector('.credit'); | ||
credit.href = data.creditLink; | ||
credit.innerText = `Credit: ${data.credit}`; | ||
|
||
const caption = content.querySelector('figcaption'); | ||
caption.innerText = data.message; | ||
caption.appendChild(credit); | ||
|
||
container.appendChild(content); | ||
}); | ||
}); | ||
}; | ||
|
||
const handleUserStateChange = (user) => { | ||
updateUserInfo(user); | ||
loadSubscriptionContent(user); | ||
}; | ||
|
||
netlifyIdentity.on('init', handleUserStateChange); | ||
netlifyIdentity.on('login', handleUserStateChange); | ||
netlifyIdentity.on('logout', handleUserStateChange); | ||
</script> |
Oops, something went wrong.