diff --git a/db/schema.gql b/db/schema.gql new file mode 100644 index 0000000..15a9889 --- /dev/null +++ b/db/schema.gql @@ -0,0 +1,9 @@ +type User { + netlifyID: ID! + stripeID: ID! +} + +type Query { + getUserByNetlifyID(netlifyID: ID!): User! + getUserByStripeID(stripeID: ID!): User! +} diff --git a/functions/get-protected-content.js b/functions/get-protected-content.js new file mode 100644 index 0000000..fb36a2e --- /dev/null +++ b/functions/get-protected-content.js @@ -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]), + }; +}; diff --git a/functions/identity-signup.js b/functions/identity-signup.js new file mode 100644 index 0000000..364f476 --- /dev/null +++ b/functions/identity-signup.js @@ -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'], + }, + }), + }; +}; diff --git a/functions/utils/fauna.js b/functions/utils/fauna.js new file mode 100644 index 0000000..be9f97d --- /dev/null +++ b/functions/utils/fauna.js @@ -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))); +}; diff --git a/package-lock.json b/package-lock.json index e779b7b..e51e89a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2705,6 +2705,11 @@ "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", "dev": true }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" + }, "nopt": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", diff --git a/package.json b/package.json index bf0bc0d..98486a5 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/_data/layout.js b/src/_data/layout.js new file mode 100644 index 0000000..024b12f --- /dev/null +++ b/src/_data/layout.js @@ -0,0 +1 @@ +module.exports = 'default'; diff --git a/src/_data/meta.js b/src/_data/meta.js new file mode 100644 index 0000000..28195f8 --- /dev/null +++ b/src/_data/meta.js @@ -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', +}; diff --git a/src/_includes/default.liquid b/src/_includes/default.liquid index 0439a27..285b412 100644 --- a/src/_includes/default.liquid +++ b/src/_includes/default.liquid @@ -3,7 +3,7 @@
-