├── .gitignore ├── netlify.toml ├── db └── schema.gql ├── package.json ├── functions ├── package.json ├── get-premium-content.js ├── utils │ └── fauna.js ├── create-manage-link.js ├── package-lock.json ├── identity-signup.js └── handle-webhooks.js └── src └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public 3 | .cache 4 | 5 | # Local Netlify folder 6 | .netlify -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | command = "npm i --prefix=functions" 3 | publish = "src" 4 | functions = "functions" 5 | -------------------------------------------------------------------------------- /db/schema.gql: -------------------------------------------------------------------------------- 1 | type User { 2 | netlifyID: ID! 3 | stripeID: ID! 4 | } 5 | 6 | type Query { 7 | getUserByNetlifyID(netlifyID: ID!): User! 8 | getUserByStripeID(stripeID: ID!): User! 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jamstack-subscriptions", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "author": "Jason Lengstorf (https://lengstorf.com)", 6 | "license": "MIT", 7 | "scripts": {}, 8 | "dependencies": {} 9 | } 10 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "author": "Jason Lengstorf (https://lengstorf.com)", 6 | "license": "MIT", 7 | "dependencies": { 8 | "node-fetch": "^2.6.0", 9 | "stripe": "^8.52.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /functions/get-premium-content.js: -------------------------------------------------------------------------------- 1 | exports.handler = async (event, context) => { 2 | const { user } = context.clientContext; 3 | 4 | if (!user || !user.app_metadata.roles.includes('sub:premium')) { 5 | return { 6 | statusCode: 402, 7 | body: 'You need a premium subscription for corgi content this good!', 8 | }; 9 | } 10 | 11 | return { 12 | statusCode: 200, 13 | body: 'This is super secret corgi content!', 14 | }; 15 | }; 16 | -------------------------------------------------------------------------------- /functions/utils/fauna.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | 3 | exports.faunaFetch = async ({ query, variables }) => { 4 | return await fetch('https://graphql.fauna.com/graphql', { 5 | method: 'POST', 6 | headers: { 7 | Authorization: `Bearer ${process.env.FAUNA_SERVER_KEY}`, 8 | }, 9 | body: JSON.stringify({ 10 | query, 11 | variables, 12 | }), 13 | }) 14 | .then((res) => res.json()) 15 | .catch((err) => console.error(JSON.stringify(err, null, 2))); 16 | }; 17 | -------------------------------------------------------------------------------- /functions/create-manage-link.js: -------------------------------------------------------------------------------- 1 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); 2 | const { faunaFetch } = require('./utils/fauna'); 3 | 4 | exports.handler = async (event, context) => { 5 | const { user } = context.clientContext; 6 | 7 | console.log(user); 8 | 9 | const query = ` 10 | query ($netlifyID: ID!) { 11 | getUserByNetlifyID(netlifyID: $netlifyID){ 12 | stripeID 13 | netlifyID 14 | } 15 | } 16 | `; 17 | const variables = { netlifyID: user.sub }; 18 | 19 | const result = await faunaFetch({ query, variables }); 20 | 21 | const stripeID = result.data.getUserByNetlifyID.stripeID; 22 | const link = await stripe.billingPortal.sessions.create({ 23 | customer: stripeID, 24 | return_url: process.env.URL, 25 | }); 26 | 27 | return { 28 | statusCode: 200, 29 | body: JSON.stringify(link.url), 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /functions/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "version": "0.0.1", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "14.0.1", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.1.tgz", 10 | "integrity": "sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA==" 11 | }, 12 | "node-fetch": { 13 | "version": "2.6.0", 14 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", 15 | "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==" 16 | }, 17 | "qs": { 18 | "version": "6.9.4", 19 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", 20 | "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" 21 | }, 22 | "stripe": { 23 | "version": "8.52.0", 24 | "resolved": "https://registry.npmjs.org/stripe/-/stripe-8.52.0.tgz", 25 | "integrity": "sha512-3Olq0EYmNU2aQ5j6rinWxfoRzaYW83k8wgNImmF9HgKi+SR85lplncO8sEq1jju5AnHzx2DO0/tJSic8jQ9PCQ==", 26 | "requires": { 27 | "@types/node": ">=8.1.0", 28 | "qs": "^6.6.0" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /functions/identity-signup.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); 3 | 4 | exports.handler = async (event) => { 5 | const { user } = JSON.parse(event.body); 6 | 7 | const customer = await stripe.customers.create({ email: user.email }); 8 | 9 | await stripe.subscriptions.create({ 10 | customer: customer.id, 11 | items: [{ plan: 'plan_HGv85ROASAWfwe' }], 12 | }); 13 | 14 | const netlifyID = user.id; 15 | const stripeID = customer.id; 16 | 17 | // TODO create a customer record in Fauna 18 | const response = await fetch('https://graphql.fauna.com/graphql', { 19 | method: 'POST', 20 | headers: { 21 | Authorization: `Bearer ${process.env.FAUNA_SERVER_KEY}`, 22 | }, 23 | body: JSON.stringify({ 24 | query: ` 25 | mutation ($netlifyID: ID! $stripeID: ID!) { 26 | createUser(data: {netlifyID: $netlifyID, stripeID: $stripeID}) { 27 | netlifyID 28 | stripeID 29 | } 30 | } 31 | `, 32 | variables: { 33 | netlifyID, 34 | stripeID, 35 | }, 36 | }), 37 | }) 38 | .then((res) => res.json()) 39 | .catch((err) => console.error(JSON.stringify(err, null, 2))); 40 | 41 | console.log({ response }); 42 | 43 | return { 44 | statusCode: 200, 45 | body: JSON.stringify({ app_metadata: { roles: ['sub:free'] } }), 46 | }; 47 | }; 48 | -------------------------------------------------------------------------------- /functions/handle-webhooks.js: -------------------------------------------------------------------------------- 1 | const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); 2 | const fetch = require('node-fetch'); 3 | const { faunaFetch } = require('./utils/fauna'); 4 | 5 | exports.handler = async ({ body, headers }, context) => { 6 | console.log('boop'); 7 | try { 8 | const stripeEvent = stripe.webhooks.constructEvent( 9 | body, 10 | headers['stripe-signature'], 11 | process.env.STRIPE_WEBHOOK_SECRET 12 | ); 13 | 14 | if (stripeEvent.type === 'customer.subscription.updated') { 15 | const subscription = stripeEvent.data.object; 16 | 17 | const stripeID = subscription.customer; 18 | const plan = subscription.items.data[0].plan.nickname; 19 | 20 | const role = `sub:${plan.split(' ')[0].toLowerCase()}`; 21 | 22 | const query = ` 23 | query ($stripeID: ID!) { 24 | getUserByStripeID(stripeID: $stripeID){ 25 | netlifyID 26 | } 27 | } 28 | `; 29 | const variables = { stripeID }; 30 | 31 | const result = await faunaFetch({ query, variables }); 32 | const netlifyID = result.data.getUserByStripeID.netlifyID; 33 | 34 | const { identity } = context.clientContext; 35 | const response = await fetch(`${identity.url}/admin/users/${netlifyID}`, { 36 | method: 'PUT', 37 | headers: { 38 | Authorization: `Bearer ${identity.token}`, 39 | }, 40 | body: JSON.stringify({ 41 | app_metadata: { 42 | roles: [role], 43 | }, 44 | }), 45 | }) 46 | .then((res) => res.json()) 47 | .catch((err) => console.error(err)); 48 | 49 | console.log(response); 50 | } 51 | 52 | return { 53 | statusCode: 200, 54 | body: JSON.stringify({ received: true }), 55 | }; 56 | } catch (err) { 57 | console.log(`Stripe webhook failed with ${err}`); 58 | 59 | return { 60 | statusCode: 400, 61 | body: `Webhook Error: ${err.message}`, 62 | }; 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Log In 7 | 11 | 12 | 13 |

Sign Up for Premium Corgi Content

14 |

15 | Get your subscription today to access the goodest corgos on the internet. 16 |

17 |
Login with Netlify Identity
18 | 19 |
20 | 21 |

22 | Manage Your Subscription 23 |

24 | 25 |

26 | 
27 |     
81 |   
82 | 
83 | 


--------------------------------------------------------------------------------