Skip to content

Commit

Permalink
wip: identity hook
Browse files Browse the repository at this point in the history
  • Loading branch information
jlengstorf committed Jul 8, 2020
1 parent 93004a4 commit 234e51d
Show file tree
Hide file tree
Showing 12 changed files with 334 additions and 64 deletions.
9 changes: 9 additions & 0 deletions db/schema.gql
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!
}
57 changes: 57 additions & 0 deletions functions/get-protected-content.js
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]),
};
};
40 changes: 40 additions & 0 deletions functions/identity-signup.js
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'],
},
}),
};
};
16 changes: 16 additions & 0 deletions functions/utils/fauna.js
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)));
};
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@
"homepage": "https://github.com/jlengstorf/lwj-demo-layout#readme",
"devDependencies": {
"@11ty/eleventy": "^0.11.0"
},
"dependencies": {
"node-fetch": "^2.6.0"
}
}
1 change: 1 addition & 0 deletions src/_data/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = 'default';
10 changes: 10 additions & 0 deletions src/_data/meta.js
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',
};
2 changes: 1 addition & 1 deletion src/_includes/default.liquid
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ title }} · Learn With Jason</title>
<title>{{ meta.title }} · Learn With Jason</title>
<link rel="stylesheet" href="https://use.typekit.net/jwa3agu.css" />
<link rel="stylesheet" href="/css/global.css" />
</head>
Expand Down
15 changes: 8 additions & 7 deletions src/_includes/footer.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@
background-image: var(--lwj-gradient);
background-size: 200%;
border-bottom: 0.5px solid var(--light-gray);
height: 5px;
position: absolute;
bottom: 100%;
height: 6px;
left: 0;
position: absolute;
width: 100vw;
z-index: 5;
}
Expand All @@ -39,7 +40,7 @@
.lwj-demo-footer .bottom-bar::after {
content: '';
position: absolute;
left: -5vw;
left: 0;
width: 100vw;
}

Expand Down Expand Up @@ -71,13 +72,13 @@
<a href="https://www.learnwithjason.dev"
>Created on <em>Learn With Jason</em></a
>
{% if episode %}<a href="{{ episode }}">Watch It Get Built</a>{% endif %}{% if
tutorial %}<a href="{{ tutorial }}">Tutorial</a>{% endif %}{% if repo %}<a
href="https://github.com/{{ repo }}"
{% if meta.episode %}<a href="{{ meta.episode }}">Watch It Get Built</a>{%
endif %}{% if meta.tutorial %}<a href="{{ meta.tutorial }}">Tutorial</a>{%
endif %}{% if meta.repo %}<a href="https://github.com/{{ meta.repo }}"
>Source Code</a
>
<a
href="http://app.netlify.com/start/deploy?repository=https://github.com/{{ repo }}?utm_source=learnwithjason&utm_medium=lwj-demo-footer-jl&utm_campaign=devex"
href="http://app.netlify.com/start/deploy?repository=https://github.com/{{ meta.repo }}?utm_source=learnwithjason&utm_medium=lwj-demo-footer-jl&utm_campaign=devex"
>Deploy Your Own Copy</a
>{% endif %}

Expand Down
184 changes: 184 additions & 0 deletions src/index.html
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>
Loading

0 comments on commit 234e51d

Please sign in to comment.