From c3bf5c8873c2ac4051dcce6f9dcb0b2a5c890176 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 11 Jul 2023 10:01:45 +0100 Subject: [PATCH 01/35] feat: subscriptions with stripe initial --- subscriptions-with-stripe/.gitignore | 130 ++++++++++ subscriptions-with-stripe/README.md | 65 +++++ subscriptions-with-stripe/env.d.ts | 15 ++ subscriptions-with-stripe/package-lock.json | 241 +++++++++++++++++++ subscriptions-with-stripe/package.json | 17 ++ subscriptions-with-stripe/src/appwrite.js | 107 ++++++++ subscriptions-with-stripe/src/environment.js | 53 ++++ subscriptions-with-stripe/src/main.js | 65 +++++ subscriptions-with-stripe/src/setup.js | 17 ++ subscriptions-with-stripe/src/stripe.js | 66 +++++ 10 files changed, 776 insertions(+) create mode 100644 subscriptions-with-stripe/.gitignore create mode 100644 subscriptions-with-stripe/README.md create mode 100644 subscriptions-with-stripe/env.d.ts create mode 100644 subscriptions-with-stripe/package-lock.json create mode 100644 subscriptions-with-stripe/package.json create mode 100644 subscriptions-with-stripe/src/appwrite.js create mode 100644 subscriptions-with-stripe/src/environment.js create mode 100644 subscriptions-with-stripe/src/main.js create mode 100644 subscriptions-with-stripe/src/setup.js create mode 100644 subscriptions-with-stripe/src/stripe.js diff --git a/subscriptions-with-stripe/.gitignore b/subscriptions-with-stripe/.gitignore new file mode 100644 index 00000000..6a7d6d8e --- /dev/null +++ b/subscriptions-with-stripe/.gitignore @@ -0,0 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* \ No newline at end of file diff --git a/subscriptions-with-stripe/README.md b/subscriptions-with-stripe/README.md new file mode 100644 index 00000000..6a60798c --- /dev/null +++ b/subscriptions-with-stripe/README.md @@ -0,0 +1,65 @@ +# Stripe Subscriptions Function + +This function helps to handle Stripe subscriptions and track them in an Appwrite database. It consists of handling Stripe checkout sessions, webhooks, and managing user subscriptions in a database. + +## Setup + +### Stripe API + +Setting up the Stripe API involves generating the required API keys and setting up the necessary webhook. + +1. **API Keys** + - Log in to your Stripe dashboard. + - Navigate to Developers > API keys. + - Here, you can find your publishable key and your secret key. You will need the secret key for this function (i.e., `STRIPE_SECRET_KEY`). Be sure not to share or expose this key as it could allow others to make API requests on behalf of your account. + +2. **Webhooks** + - In your Stripe dashboard, navigate to Developers > Webhooks. + - Click "+ Add endpoint" and set the URL to where you've hosted this function with the /webhook path, and select the "customer.subscription.created" and "customer.subscription.deleted" events. + - Once you've created the webhook, you'll be able to view and copy the signing secret (i.e., `STRIPE_WEBHOOK_SECRET`). + +### Environment Variables + +To ensure the function operates as intended, ensure the following variables are set: + +- **APPWRITE_API_KEY**: This is your Appwrite project's API key. +- **APPWRITE_ENDPOINT**: This is the endpoint where your Appwrite server is located. +- **APPWRITE_PROJECT_ID**: This refers to the specific ID of your Appwrite project. +- **STRIPE_SECRET_KEY**: This is your Stripe Secret key. +- **STRIPE_WEBHOOK_SECRET**: The secret used to validate the Stripe Webhook signature. +- **SUCCESS_URL**: The URL users are redirected to after a successful payment. +- **CANCEL_URL**: The URL users are redirected to after a cancelled payment attempt. + +Additionally, the function has the following optional variables: + +- **DATABASE_ID**: This is the ID for the database where subscriptions will be stored. If not provided, it defaults to "stripe-subscriptions". +- **COLLECTION_ID**: This is the ID for the collection within the database. If not provided, it defaults to "subscriptions". + +### Database Setup + +To setup the database, run `npm run setup`. +If the specified database doesn't exist, the script will automatically create it. It will also create a collection within the database, adding the necessary attributes to the collection. + +## Usage + +This function supports two primary request paths: + +1. **Checkout Session Creation** + + - **Request Path:** /checkout + - **Request Type:** GET + - **Response:** + - On success, the function will redirect to the Stripe Checkout session URL. + - If the request fails, the user is redirected to the specified `CANCEL_URL`. + +2. **Webhook Handling** + + - **Request Path:** /webhook + - **Request Type:** POST + - **Content Type:** application/json + - **Response:** + - The function will respond with an empty response after processing the webhook events. + - It handles two events: + 1. `customer.subscription.created` - The user is upgraded to a premium subscription. + 2. `customer.subscription.deleted` - The user is downgraded from their premium subscription. + diff --git a/subscriptions-with-stripe/env.d.ts b/subscriptions-with-stripe/env.d.ts new file mode 100644 index 00000000..55743a80 --- /dev/null +++ b/subscriptions-with-stripe/env.d.ts @@ -0,0 +1,15 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + APPWRITE_ENDPOINT?: string; + APPWRITE_PROJECT_ID?: string; + APPWRITE_API_KEY?: string; + STRIPE_SECRET_KEY?: string; + STRIPE_WEBHOOK_SECRET?: string; + SUCCESS_URL?: string; + CANCEL_URL?: string; + } + } +} + +export {}; diff --git a/subscriptions-with-stripe/package-lock.json b/subscriptions-with-stripe/package-lock.json new file mode 100644 index 00000000..4bf1c46a --- /dev/null +++ b/subscriptions-with-stripe/package-lock.json @@ -0,0 +1,241 @@ +{ + "name": "subscriptions-with-stripe", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "subscriptions-with-stripe", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "node-appwrite": "^9.0.0", + "stripe": "^12.12.0", + "stripe-event-types": "^2.3.0" + } + }, + "node_modules/@types/node": { + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.1.tgz", + "integrity": "sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", + "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-appwrite": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-9.0.0.tgz", + "integrity": "sha512-iTcHbuaJfr6bP/HFkRVV+FcaumKkbINqZyypQdl+tYxv6Dx0bkB/YKUXGYfTkgP18TLPWQQB++OGQhi98dlo2w==", + "dependencies": { + "axios": "^1.3.6", + "form-data": "^4.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/qs": { + "version": "6.11.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.2.tgz", + "integrity": "sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stripe": { + "version": "12.12.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-12.12.0.tgz", + "integrity": "sha512-aM9xfyDryiaf/qSWMtJaTMrlc/he3qyx3aVHMqOZqUiMdgTV6lt7tLpFrU0pG+QURm1LAP9GYZ+EcA17446YoQ==", + "dependencies": { + "@types/node": ">=8.1.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=12.*" + } + }, + "node_modules/stripe-event-types": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/stripe-event-types/-/stripe-event-types-2.3.0.tgz", + "integrity": "sha512-4qh0wblfZBbdHn3IH9bbRrGZw+TJuIern6UjoG1+17grSyhIJFvrrqTn2ACE/wx3PE7ZLXNG5Qzo7BEXXkuyoA==", + "peerDependencies": { + "stripe": ">=10.0.0" + } + } + } +} diff --git a/subscriptions-with-stripe/package.json b/subscriptions-with-stripe/package.json new file mode 100644 index 00000000..34372ef9 --- /dev/null +++ b/subscriptions-with-stripe/package.json @@ -0,0 +1,17 @@ +{ + "name": "subscriptions-with-stripe", + "version": "1.0.0", + "description": "", + "main": "src/main.js", + "scripts": { + "start": "node src/main.js", + "setup": "node src/setup.js" + }, + "author": "", + "license": "MIT", + "dependencies": { + "node-appwrite": "^9.0.0", + "stripe": "^12.12.0", + "stripe-event-types": "^2.3.0" + } +} diff --git a/subscriptions-with-stripe/src/appwrite.js b/subscriptions-with-stripe/src/appwrite.js new file mode 100644 index 00000000..3ce17b9a --- /dev/null +++ b/subscriptions-with-stripe/src/appwrite.js @@ -0,0 +1,107 @@ +const { Client, Databases } = require("node-appwrite"); +const getEnvironment = require("./environment"); + +const Subscriptions = { + PREMIUM: "premium", +}; + +module.exports = function AppwriteService() { + const { + APPWRITE_ENDPOINT, + APPWRITE_PROJECT_ID, + APPWRITE_API_KEY, + DATABASE_ID, + DATABASE_NAME, + COLLECTION_ID, + COLLECTION_NAME, + } = getEnvironment(); + + const client = new Client(); + client + .setEndpoint(APPWRITE_ENDPOINT) + .setProject(APPWRITE_PROJECT_ID) + .setKey(APPWRITE_API_KEY); + + const databases = new Databases(client); + + return { + /** + * @returns {Promise} + */ + doesSubscribersDatabaseExist: async function () { + try { + await databases.get(DATABASE_ID); + return true; + } catch (err) { + if (err.code === 404) return false; + throw err; + } + }, + setupSubscribersDatabase: async function () { + try { + await databases.create(DATABASE_ID, DATABASE_NAME); + await databases.createCollection( + DATABASE_ID, + COLLECTION_ID, + COLLECTION_NAME + ); + await databases.createStringAttribute( + DATABASE_ID, + COLLECTION_ID, + "userId", + 255, + true + ); + await databases.createStringAttribute( + DATABASE_ID, + COLLECTION_ID, + "subscriptionType", + 255, + true + ); + } catch (err) { + // If resource already exists, we can ignore the error + if (err.code !== 409) throw err; + } + }, + /** + * @param {string} userId + * @returns {Promise} + */ + hasSubscription: async function (userId) { + try { + await databases.getDocument(DATABASE_ID, COLLECTION_ID, userId); + return true; + } catch (err) { + if (err.code !== 404) throw err; + return false; + } + }, + /** + * @param {string} userId + * @returns {Promise} + */ + deleteSubscription: async function (userId) { + try { + await databases.deleteDocument(DATABASE_ID, COLLECTION_ID, userId); + return true; + } catch (err) { + return false; + } + }, + /** + * @param {string} userId + * @returns {Promise} + */ + createSubscription: async function (userId) { + try { + await databases.createDocument(DATABASE_ID, COLLECTION_ID, userId, { + subscriptionType: Subscriptions.PREMIUM, + }); + return true; + } catch (err) { + return false; + } + }, + }; +}; diff --git a/subscriptions-with-stripe/src/environment.js b/subscriptions-with-stripe/src/environment.js new file mode 100644 index 00000000..5c616035 --- /dev/null +++ b/subscriptions-with-stripe/src/environment.js @@ -0,0 +1,53 @@ +/** + * @param {string} key + * @return {string} + */ +function getRequiredEnv(key) { + const value = process.env[key]; + if (value === undefined) { + throw new Error(`Environment variable ${key} is not set`); + } + return value; +} + +/** + * @param {string} key + * @return {string} + */ +function getRequiredUrlEnv(key) { + const value = getRequiredEnv(key); + if (!isValidUrl(value)) { + throw new Error(`Environment variable ${key} is a not valid URL`); + } + return value; +} + +/** + * @param {string | undefined} url + * @returns {boolean} + */ +function isValidUrl(url) { + if (!url) return false; + try { + new URL(url); + return true; + } catch (err) { + return false; + } +} + +module.exports = function getEnvironment() { + return { + APPWRITE_ENDPOINT: getRequiredUrlEnv("APPWRITE_ENDPOINT"), + APPWRITE_PROJECT_ID: getRequiredEnv("APPWRITE_PROJECT_ID"), + APPWRITE_API_KEY: getRequiredEnv("APPWRITE_API_KEY"), + STRIPE_WEBHOOK_SECRET: getRequiredEnv("STRIPE_WEBHOOK_SECRET"), + STRIPE_SECRET_KEY: getRequiredEnv("STRIPE_SECRET_KEY"), + SUCCESS_URL: getRequiredUrlEnv("SUCCESS_URL"), + CANCEL_URL: getRequiredUrlEnv("CANCEL_URL"), + DATABASE_ID: process.env.DATABASE_ID ?? "stripe-subscriptions", + DATABASE_NAME: "Stripe Subscriptions", + COLLECTION_ID: process.env.COLLECTION_ID ?? "subscriptions", + COLLECTION_NAME: "Subscriptions", + }; +}; diff --git a/subscriptions-with-stripe/src/main.js b/subscriptions-with-stripe/src/main.js new file mode 100644 index 00000000..ed4a64b5 --- /dev/null +++ b/subscriptions-with-stripe/src/main.js @@ -0,0 +1,65 @@ +/// + +const StripeService = require("./stripe"); +const AppwriteService = require("./appwrite"); +const getEnvironment = require("./environment"); + +module.exports = async ({ req, res, log, error }) => { + const { CANCEL_URL } = getEnvironment(); + + const appwrite = AppwriteService(); + const stripe = StripeService(); + + switch (req.path) { + case "/checkout": + const userId = req.headers["x-appwrite-user-id"]; + if (!userId) { + error("User ID not found in request."); + return res.redirect(CANCEL_URL, 303); + } + + const session = await stripe.checkoutSubscription(userId); + if (!session) { + error("Failed to create Stripe checkout session."); + return res.redirect(CANCEL_URL, 303); + } + + log(`Created Stripe checkout session for user ${userId}.`); + return res.redirect(session.url, 303); + + case "/webhook": + const event = stripe.validateWebhook(req); + if (!event) return res.json({ success: false }, 401); + + if (event.type === "customer.subscription.created") { + const session = event.data.object; + const userId = session.metadata.userId; + + if (await appwrite.hasSubscription(userId)) { + error(`Subscription already exists - skipping`); + return res.json({ success: true }); + } + + await appwrite.createSubscription(userId); + log(`Created subscription for user ${userId}`); + } + + if (event.type === "customer.subscription.deleted") { + const session = event.data.object; + const userId = session.metadata.userId; + + if (!(await appwrite.hasSubscription(userId))) { + error(`Subscription does not exist - skipping`); + return res.json({ success: true }); + } + + await appwrite.deleteSubscription(userId); + log(`Deleted subscription for user ${userId}`); + } + + return res.json({ success: true }); + + default: + return res.send("Not Found", 404); + } +}; diff --git a/subscriptions-with-stripe/src/setup.js b/subscriptions-with-stripe/src/setup.js new file mode 100644 index 00000000..f3183ff5 --- /dev/null +++ b/subscriptions-with-stripe/src/setup.js @@ -0,0 +1,17 @@ +const AppwriteService = require("./appwrite"); + +async function setup() { + console.log("Executing setup script..."); + + const appwrite = AppwriteService(); + + if (await appwrite.doesSubscribersDatabaseExist()) { + console.log(`Database exists.`); + return; + } + + await appwrite.setupSubscribersDatabase(); + console.log(`Database created.`); +} + +setup(); diff --git a/subscriptions-with-stripe/src/stripe.js b/subscriptions-with-stripe/src/stripe.js new file mode 100644 index 00000000..446ea2cd --- /dev/null +++ b/subscriptions-with-stripe/src/stripe.js @@ -0,0 +1,66 @@ +const getEnvironment = require("./environment"); +const stripe = require("stripe"); + +module.exports = function StripeService() { + const { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, SUCCESS_URL, CANCEL_URL } = + getEnvironment(); + + // Note: stripe cjs API types are faulty + /** @type {import('stripe').Stripe} */ + // @ts-ignore + const stripeClient = stripe(STRIPE_SECRET_KEY); + + return { + /** + * @param {string} userId + */ + checkoutSubscription: async function (userId) { + try { + return await stripeClient.checkout.sessions.create({ + payment_method_types: ["card"], + line_items: [ + { + price_data: { + currency: "usd", + product_data: { + name: "Premium Subscription", + }, + unit_amount: 1000, + recurring: { + interval: "year", + }, + }, + quantity: 1, + }, + ], + success_url: SUCCESS_URL, + cancel_url: CANCEL_URL, + client_reference_id: userId, + metadata: { + userId, + }, + mode: "subscription", + }); + } catch (err) { + return null; + } + }, + /** + * @returns {import("stripe").Stripe.DiscriminatedEvent | null} + */ + validateWebhook: function (req) { + try { + const event = stripeClient.webhooks.constructEvent( + req.body, + req.headers["stripe-signature"], + STRIPE_WEBHOOK_SECRET + ); + return /** @type {import("stripe").Stripe.DiscriminatedEvent} */ ( + event + ); + } catch (err) { + return null; + } + }, + }; +}; From 01988836b52c415a29fff1973bc80794a16476ac Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 11 Jul 2023 10:10:17 +0100 Subject: [PATCH 02/35] chore: add prettierrc --- subscriptions-with-stripe/.prettierrc.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 subscriptions-with-stripe/.prettierrc.json diff --git a/subscriptions-with-stripe/.prettierrc.json b/subscriptions-with-stripe/.prettierrc.json new file mode 100644 index 00000000..e74ed9ff --- /dev/null +++ b/subscriptions-with-stripe/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 4, + "semi": false, + "singleQuote": true +} From b2cf96565156e06d3f9cab0316c708b8bf13ab29 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 11 Jul 2023 10:24:30 +0100 Subject: [PATCH 03/35] chore: add prettier --- subscriptions-with-stripe/.prettierrc.json | 2 +- subscriptions-with-stripe/package-lock.json | 21 +++++- subscriptions-with-stripe/package.json | 8 ++- subscriptions-with-stripe/src/appwrite.js | 62 ++++++++--------- subscriptions-with-stripe/src/environment.js | 46 ++++++------- subscriptions-with-stripe/src/main.js | 72 ++++++++++---------- subscriptions-with-stripe/src/setup.js | 20 +++--- subscriptions-with-stripe/src/stripe.js | 38 +++++------ 8 files changed, 147 insertions(+), 122 deletions(-) diff --git a/subscriptions-with-stripe/.prettierrc.json b/subscriptions-with-stripe/.prettierrc.json index e74ed9ff..b7858424 100644 --- a/subscriptions-with-stripe/.prettierrc.json +++ b/subscriptions-with-stripe/.prettierrc.json @@ -1,6 +1,6 @@ { "trailingComma": "es5", - "tabWidth": 4, + "tabWidth": 2, "semi": false, "singleQuote": true } diff --git a/subscriptions-with-stripe/package-lock.json b/subscriptions-with-stripe/package-lock.json index 4bf1c46a..a9ca97dd 100644 --- a/subscriptions-with-stripe/package-lock.json +++ b/subscriptions-with-stripe/package-lock.json @@ -10,7 +10,10 @@ "license": "MIT", "dependencies": { "node-appwrite": "^9.0.0", - "stripe": "^12.12.0", + "stripe": "^12.12.0" + }, + "devDependencies": { + "prettier": "^3.0.0", "stripe-event-types": "^2.3.0" } }, @@ -185,6 +188,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/prettier": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -233,6 +251,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/stripe-event-types/-/stripe-event-types-2.3.0.tgz", "integrity": "sha512-4qh0wblfZBbdHn3IH9bbRrGZw+TJuIern6UjoG1+17grSyhIJFvrrqTn2ACE/wx3PE7ZLXNG5Qzo7BEXXkuyoA==", + "dev": true, "peerDependencies": { "stripe": ">=10.0.0" } diff --git a/subscriptions-with-stripe/package.json b/subscriptions-with-stripe/package.json index 34372ef9..7c153e24 100644 --- a/subscriptions-with-stripe/package.json +++ b/subscriptions-with-stripe/package.json @@ -5,13 +5,17 @@ "main": "src/main.js", "scripts": { "start": "node src/main.js", - "setup": "node src/setup.js" + "setup": "node src/setup.js", + "format": "prettier --write src/**/*.js" }, "author": "", "license": "MIT", "dependencies": { "node-appwrite": "^9.0.0", - "stripe": "^12.12.0", + "stripe": "^12.12.0" + }, + "devDependencies": { + "prettier": "^3.0.0", "stripe-event-types": "^2.3.0" } } diff --git a/subscriptions-with-stripe/src/appwrite.js b/subscriptions-with-stripe/src/appwrite.js index 3ce17b9a..b14acb19 100644 --- a/subscriptions-with-stripe/src/appwrite.js +++ b/subscriptions-with-stripe/src/appwrite.js @@ -1,9 +1,9 @@ -const { Client, Databases } = require("node-appwrite"); -const getEnvironment = require("./environment"); +const { Client, Databases } = require('node-appwrite') +const getEnvironment = require('./environment') const Subscriptions = { - PREMIUM: "premium", -}; + PREMIUM: 'premium', +} module.exports = function AppwriteService() { const { @@ -14,15 +14,15 @@ module.exports = function AppwriteService() { DATABASE_NAME, COLLECTION_ID, COLLECTION_NAME, - } = getEnvironment(); + } = getEnvironment() - const client = new Client(); + const client = new Client() client .setEndpoint(APPWRITE_ENDPOINT) .setProject(APPWRITE_PROJECT_ID) - .setKey(APPWRITE_API_KEY); + .setKey(APPWRITE_API_KEY) - const databases = new Databases(client); + const databases = new Databases(client) return { /** @@ -30,38 +30,38 @@ module.exports = function AppwriteService() { */ doesSubscribersDatabaseExist: async function () { try { - await databases.get(DATABASE_ID); - return true; + await databases.get(DATABASE_ID) + return true } catch (err) { - if (err.code === 404) return false; - throw err; + if (err.code === 404) return false + throw err } }, setupSubscribersDatabase: async function () { try { - await databases.create(DATABASE_ID, DATABASE_NAME); + await databases.create(DATABASE_ID, DATABASE_NAME) await databases.createCollection( DATABASE_ID, COLLECTION_ID, COLLECTION_NAME - ); + ) await databases.createStringAttribute( DATABASE_ID, COLLECTION_ID, - "userId", + 'userId', 255, true - ); + ) await databases.createStringAttribute( DATABASE_ID, COLLECTION_ID, - "subscriptionType", + 'subscriptionType', 255, true - ); + ) } catch (err) { // If resource already exists, we can ignore the error - if (err.code !== 409) throw err; + if (err.code !== 409) throw err } }, /** @@ -70,11 +70,11 @@ module.exports = function AppwriteService() { */ hasSubscription: async function (userId) { try { - await databases.getDocument(DATABASE_ID, COLLECTION_ID, userId); - return true; + await databases.getDocument(DATABASE_ID, COLLECTION_ID, userId) + return true } catch (err) { - if (err.code !== 404) throw err; - return false; + if (err.code !== 404) throw err + return false } }, /** @@ -83,10 +83,10 @@ module.exports = function AppwriteService() { */ deleteSubscription: async function (userId) { try { - await databases.deleteDocument(DATABASE_ID, COLLECTION_ID, userId); - return true; + await databases.deleteDocument(DATABASE_ID, COLLECTION_ID, userId) + return true } catch (err) { - return false; + return false } }, /** @@ -97,11 +97,11 @@ module.exports = function AppwriteService() { try { await databases.createDocument(DATABASE_ID, COLLECTION_ID, userId, { subscriptionType: Subscriptions.PREMIUM, - }); - return true; + }) + return true } catch (err) { - return false; + return false } }, - }; -}; + } +} diff --git a/subscriptions-with-stripe/src/environment.js b/subscriptions-with-stripe/src/environment.js index 5c616035..d0a9f060 100644 --- a/subscriptions-with-stripe/src/environment.js +++ b/subscriptions-with-stripe/src/environment.js @@ -3,11 +3,11 @@ * @return {string} */ function getRequiredEnv(key) { - const value = process.env[key]; + const value = process.env[key] if (value === undefined) { - throw new Error(`Environment variable ${key} is not set`); + throw new Error(`Environment variable ${key} is not set`) } - return value; + return value } /** @@ -15,11 +15,11 @@ function getRequiredEnv(key) { * @return {string} */ function getRequiredUrlEnv(key) { - const value = getRequiredEnv(key); + const value = getRequiredEnv(key) if (!isValidUrl(value)) { - throw new Error(`Environment variable ${key} is a not valid URL`); + throw new Error(`Environment variable ${key} is a not valid URL`) } - return value; + return value } /** @@ -27,27 +27,27 @@ function getRequiredUrlEnv(key) { * @returns {boolean} */ function isValidUrl(url) { - if (!url) return false; + if (!url) return false try { - new URL(url); - return true; + new URL(url) + return true } catch (err) { - return false; + return false } } module.exports = function getEnvironment() { return { - APPWRITE_ENDPOINT: getRequiredUrlEnv("APPWRITE_ENDPOINT"), - APPWRITE_PROJECT_ID: getRequiredEnv("APPWRITE_PROJECT_ID"), - APPWRITE_API_KEY: getRequiredEnv("APPWRITE_API_KEY"), - STRIPE_WEBHOOK_SECRET: getRequiredEnv("STRIPE_WEBHOOK_SECRET"), - STRIPE_SECRET_KEY: getRequiredEnv("STRIPE_SECRET_KEY"), - SUCCESS_URL: getRequiredUrlEnv("SUCCESS_URL"), - CANCEL_URL: getRequiredUrlEnv("CANCEL_URL"), - DATABASE_ID: process.env.DATABASE_ID ?? "stripe-subscriptions", - DATABASE_NAME: "Stripe Subscriptions", - COLLECTION_ID: process.env.COLLECTION_ID ?? "subscriptions", - COLLECTION_NAME: "Subscriptions", - }; -}; + APPWRITE_ENDPOINT: getRequiredUrlEnv('APPWRITE_ENDPOINT'), + APPWRITE_PROJECT_ID: getRequiredEnv('APPWRITE_PROJECT_ID'), + APPWRITE_API_KEY: getRequiredEnv('APPWRITE_API_KEY'), + STRIPE_WEBHOOK_SECRET: getRequiredEnv('STRIPE_WEBHOOK_SECRET'), + STRIPE_SECRET_KEY: getRequiredEnv('STRIPE_SECRET_KEY'), + SUCCESS_URL: getRequiredUrlEnv('SUCCESS_URL'), + CANCEL_URL: getRequiredUrlEnv('CANCEL_URL'), + DATABASE_ID: process.env.DATABASE_ID ?? 'stripe-subscriptions', + DATABASE_NAME: 'Stripe Subscriptions', + COLLECTION_ID: process.env.COLLECTION_ID ?? 'subscriptions', + COLLECTION_NAME: 'Subscriptions', + } +} diff --git a/subscriptions-with-stripe/src/main.js b/subscriptions-with-stripe/src/main.js index ed4a64b5..f38d032d 100644 --- a/subscriptions-with-stripe/src/main.js +++ b/subscriptions-with-stripe/src/main.js @@ -1,65 +1,63 @@ -/// - -const StripeService = require("./stripe"); -const AppwriteService = require("./appwrite"); -const getEnvironment = require("./environment"); +const StripeService = require('./stripe') +const AppwriteService = require('./appwrite') +const getEnvironment = require('./environment') module.exports = async ({ req, res, log, error }) => { - const { CANCEL_URL } = getEnvironment(); + const { CANCEL_URL } = getEnvironment() - const appwrite = AppwriteService(); - const stripe = StripeService(); + const appwrite = AppwriteService() + const stripe = StripeService() switch (req.path) { - case "/checkout": - const userId = req.headers["x-appwrite-user-id"]; + case '/checkout': + const userId = req.headers['x-appwrite-user-id'] if (!userId) { - error("User ID not found in request."); - return res.redirect(CANCEL_URL, 303); + error('User ID not found in request.') + return res.redirect(CANCEL_URL, 303) } - const session = await stripe.checkoutSubscription(userId); + const session = await stripe.checkoutSubscription(userId) if (!session) { - error("Failed to create Stripe checkout session."); - return res.redirect(CANCEL_URL, 303); + error('Failed to create Stripe checkout session.') + return res.redirect(CANCEL_URL, 303) } - log(`Created Stripe checkout session for user ${userId}.`); - return res.redirect(session.url, 303); + log(`Created Stripe checkout session for user ${userId}.`) + return res.redirect(session.url, 303) - case "/webhook": - const event = stripe.validateWebhook(req); - if (!event) return res.json({ success: false }, 401); + case '/webhook': + const event = stripe.validateWebhook(req) + if (!event) return res.json({ success: false }, 401) - if (event.type === "customer.subscription.created") { - const session = event.data.object; - const userId = session.metadata.userId; + if (event.type === 'customer.subscription.created') { + const session = event.data.object + const userId = session.metadata.userId if (await appwrite.hasSubscription(userId)) { - error(`Subscription already exists - skipping`); - return res.json({ success: true }); + error(`Subscription already exists - skipping`) + return res.json({ success: true }) } - await appwrite.createSubscription(userId); - log(`Created subscription for user ${userId}`); + await appwrite.createSubscription(userId) + log(`Created subscription for user ${userId}`) } - if (event.type === "customer.subscription.deleted") { - const session = event.data.object; - const userId = session.metadata.userId; + if (event.type === 'customer.subscription.deleted') { + const session = event.data.object + const userId = session.metadata.userId if (!(await appwrite.hasSubscription(userId))) { - error(`Subscription does not exist - skipping`); - return res.json({ success: true }); + error(`Subscription does not exist - skipping`) + return res.json({ success: true }) } - await appwrite.deleteSubscription(userId); - log(`Deleted subscription for user ${userId}`); + await appwrite.deleteSubscription(userId) + log(`Deleted subscription for user ${userId}`) } - return res.json({ success: true }); + return res.json({ success: true }) default: - return res.send("Not Found", 404); + return res.send('Not Found', 404) } -}; +} diff --git a/subscriptions-with-stripe/src/setup.js b/subscriptions-with-stripe/src/setup.js index f3183ff5..82979af9 100644 --- a/subscriptions-with-stripe/src/setup.js +++ b/subscriptions-with-stripe/src/setup.js @@ -1,17 +1,21 @@ -const AppwriteService = require("./appwrite"); +const AppwriteService = require('./appwrite') +/** + * Setup script for the subscribers database. + * If the database already exists, this script will do nothing. + */ async function setup() { - console.log("Executing setup script..."); + console.log('Executing setup script...') - const appwrite = AppwriteService(); + const appwrite = AppwriteService() if (await appwrite.doesSubscribersDatabaseExist()) { - console.log(`Database exists.`); - return; + console.log(`Database exists.`) + return } - await appwrite.setupSubscribersDatabase(); - console.log(`Database created.`); + await appwrite.setupSubscribersDatabase() + console.log(`Database created.`) } -setup(); +setup() diff --git a/subscriptions-with-stripe/src/stripe.js b/subscriptions-with-stripe/src/stripe.js index 446ea2cd..cdefab5d 100644 --- a/subscriptions-with-stripe/src/stripe.js +++ b/subscriptions-with-stripe/src/stripe.js @@ -1,14 +1,16 @@ -const getEnvironment = require("./environment"); -const stripe = require("stripe"); +/// + +const getEnvironment = require('./environment') +const stripe = require('stripe') module.exports = function StripeService() { const { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, SUCCESS_URL, CANCEL_URL } = - getEnvironment(); + getEnvironment() // Note: stripe cjs API types are faulty /** @type {import('stripe').Stripe} */ // @ts-ignore - const stripeClient = stripe(STRIPE_SECRET_KEY); + const stripeClient = stripe(STRIPE_SECRET_KEY) return { /** @@ -17,17 +19,17 @@ module.exports = function StripeService() { checkoutSubscription: async function (userId) { try { return await stripeClient.checkout.sessions.create({ - payment_method_types: ["card"], + payment_method_types: ['card'], line_items: [ { price_data: { - currency: "usd", + currency: 'usd', product_data: { - name: "Premium Subscription", + name: 'Premium Subscription', }, unit_amount: 1000, recurring: { - interval: "year", + interval: 'year', }, }, quantity: 1, @@ -39,10 +41,10 @@ module.exports = function StripeService() { metadata: { userId, }, - mode: "subscription", - }); + mode: 'subscription', + }) } catch (err) { - return null; + return null } }, /** @@ -52,15 +54,13 @@ module.exports = function StripeService() { try { const event = stripeClient.webhooks.constructEvent( req.body, - req.headers["stripe-signature"], + req.headers['stripe-signature'], STRIPE_WEBHOOK_SECRET - ); - return /** @type {import("stripe").Stripe.DiscriminatedEvent} */ ( - event - ); + ) + return /** @type {import("stripe").Stripe.DiscriminatedEvent} */ (event) } catch (err) { - return null; + return null } }, - }; -}; + } +} From 5fe47c5b3723e53dd45bff8b18e2159f7e155403 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 12 Jul 2023 12:57:52 +0100 Subject: [PATCH 04/35] feat: migrate to esm --- subscriptions-with-stripe/package.json | 1 + subscriptions-with-stripe/src/appwrite.js | 6 +++--- subscriptions-with-stripe/src/environment.js | 2 +- subscriptions-with-stripe/src/main.js | 8 ++++---- subscriptions-with-stripe/src/setup.js | 2 +- subscriptions-with-stripe/src/stripe.js | 6 +++--- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/subscriptions-with-stripe/package.json b/subscriptions-with-stripe/package.json index 7c153e24..fc4a88f5 100644 --- a/subscriptions-with-stripe/package.json +++ b/subscriptions-with-stripe/package.json @@ -3,6 +3,7 @@ "version": "1.0.0", "description": "", "main": "src/main.js", + "type": "module", "scripts": { "start": "node src/main.js", "setup": "node src/setup.js", diff --git a/subscriptions-with-stripe/src/appwrite.js b/subscriptions-with-stripe/src/appwrite.js index b14acb19..07286366 100644 --- a/subscriptions-with-stripe/src/appwrite.js +++ b/subscriptions-with-stripe/src/appwrite.js @@ -1,11 +1,11 @@ -const { Client, Databases } = require('node-appwrite') -const getEnvironment = require('./environment') +import { Client, Databases } from 'node-appwrite' +import getEnvironment from './environment' const Subscriptions = { PREMIUM: 'premium', } -module.exports = function AppwriteService() { +export default function AppwriteService() { const { APPWRITE_ENDPOINT, APPWRITE_PROJECT_ID, diff --git a/subscriptions-with-stripe/src/environment.js b/subscriptions-with-stripe/src/environment.js index d0a9f060..6564d6cf 100644 --- a/subscriptions-with-stripe/src/environment.js +++ b/subscriptions-with-stripe/src/environment.js @@ -36,7 +36,7 @@ function isValidUrl(url) { } } -module.exports = function getEnvironment() { +export default function getEnvironment() { return { APPWRITE_ENDPOINT: getRequiredUrlEnv('APPWRITE_ENDPOINT'), APPWRITE_PROJECT_ID: getRequiredEnv('APPWRITE_PROJECT_ID'), diff --git a/subscriptions-with-stripe/src/main.js b/subscriptions-with-stripe/src/main.js index f38d032d..a713f9e0 100644 --- a/subscriptions-with-stripe/src/main.js +++ b/subscriptions-with-stripe/src/main.js @@ -1,8 +1,8 @@ -const StripeService = require('./stripe') -const AppwriteService = require('./appwrite') -const getEnvironment = require('./environment') +import StripeService from './stripe' +import AppwriteService from './appwrite' +import getEnvironment from './environment' -module.exports = async ({ req, res, log, error }) => { +export default async ({ req, res, log, error }) => { const { CANCEL_URL } = getEnvironment() const appwrite = AppwriteService() diff --git a/subscriptions-with-stripe/src/setup.js b/subscriptions-with-stripe/src/setup.js index 82979af9..fad4cae3 100644 --- a/subscriptions-with-stripe/src/setup.js +++ b/subscriptions-with-stripe/src/setup.js @@ -1,4 +1,4 @@ -const AppwriteService = require('./appwrite') +import AppwriteService from './appwrite' /** * Setup script for the subscribers database. diff --git a/subscriptions-with-stripe/src/stripe.js b/subscriptions-with-stripe/src/stripe.js index cdefab5d..44a026f5 100644 --- a/subscriptions-with-stripe/src/stripe.js +++ b/subscriptions-with-stripe/src/stripe.js @@ -1,9 +1,9 @@ /// -const getEnvironment = require('./environment') -const stripe = require('stripe') +import getEnvironment from './environment' +import stripe from 'stripe' -module.exports = function StripeService() { +export default function StripeService() { const { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, SUCCESS_URL, CANCEL_URL } = getEnvironment() From f92ff3a0aa876df6ff2a4fef56ad2f53ad045653 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 17 Jul 2023 11:29:55 +0100 Subject: [PATCH 05/35] fix: esm migration --- subscriptions-with-stripe/src/appwrite.js | 7 ++++--- subscriptions-with-stripe/src/environment.js | 4 +++- subscriptions-with-stripe/src/main.js | 13 +++++++------ subscriptions-with-stripe/src/setup.js | 6 ++++-- subscriptions-with-stripe/src/stripe.js | 7 ++++--- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/subscriptions-with-stripe/src/appwrite.js b/subscriptions-with-stripe/src/appwrite.js index 07286366..823ed65f 100644 --- a/subscriptions-with-stripe/src/appwrite.js +++ b/subscriptions-with-stripe/src/appwrite.js @@ -1,11 +1,10 @@ import { Client, Databases } from 'node-appwrite' -import getEnvironment from './environment' const Subscriptions = { PREMIUM: 'premium', } -export default function AppwriteService() { +function AppwriteService(environment) { const { APPWRITE_ENDPOINT, APPWRITE_PROJECT_ID, @@ -14,7 +13,7 @@ export default function AppwriteService() { DATABASE_NAME, COLLECTION_ID, COLLECTION_NAME, - } = getEnvironment() + } = environment const client = new Client() client @@ -105,3 +104,5 @@ export default function AppwriteService() { }, } } + +export default AppwriteService diff --git a/subscriptions-with-stripe/src/environment.js b/subscriptions-with-stripe/src/environment.js index 6564d6cf..5ed4ba31 100644 --- a/subscriptions-with-stripe/src/environment.js +++ b/subscriptions-with-stripe/src/environment.js @@ -36,7 +36,7 @@ function isValidUrl(url) { } } -export default function getEnvironment() { +function EnvironmentService() { return { APPWRITE_ENDPOINT: getRequiredUrlEnv('APPWRITE_ENDPOINT'), APPWRITE_PROJECT_ID: getRequiredEnv('APPWRITE_PROJECT_ID'), @@ -51,3 +51,5 @@ export default function getEnvironment() { COLLECTION_NAME: 'Subscriptions', } } + +export default EnvironmentService diff --git a/subscriptions-with-stripe/src/main.js b/subscriptions-with-stripe/src/main.js index a713f9e0..0949d147 100644 --- a/subscriptions-with-stripe/src/main.js +++ b/subscriptions-with-stripe/src/main.js @@ -1,12 +1,13 @@ -import StripeService from './stripe' -import AppwriteService from './appwrite' -import getEnvironment from './environment' +import StripeService from './stripe.js' +import AppwriteService from './appwrite.js' +import EnvironmentService from './environment.js' export default async ({ req, res, log, error }) => { - const { CANCEL_URL } = getEnvironment() + const environment = EnvironmentService() + const appwrite = AppwriteService(environment) + const stripe = StripeService(environment) - const appwrite = AppwriteService() - const stripe = StripeService() + const { CANCEL_URL } = environment switch (req.path) { case '/checkout': diff --git a/subscriptions-with-stripe/src/setup.js b/subscriptions-with-stripe/src/setup.js index fad4cae3..9889e6e8 100644 --- a/subscriptions-with-stripe/src/setup.js +++ b/subscriptions-with-stripe/src/setup.js @@ -1,4 +1,5 @@ -import AppwriteService from './appwrite' +import AppwriteService from './appwrite.js' +import EnvironmentService from './environment.js' /** * Setup script for the subscribers database. @@ -7,7 +8,8 @@ import AppwriteService from './appwrite' async function setup() { console.log('Executing setup script...') - const appwrite = AppwriteService() + const environment = EnvironmentService() + const appwrite = AppwriteService(environment) if (await appwrite.doesSubscribersDatabaseExist()) { console.log(`Database exists.`) diff --git a/subscriptions-with-stripe/src/stripe.js b/subscriptions-with-stripe/src/stripe.js index 44a026f5..e9ff2222 100644 --- a/subscriptions-with-stripe/src/stripe.js +++ b/subscriptions-with-stripe/src/stripe.js @@ -1,11 +1,10 @@ /// -import getEnvironment from './environment' import stripe from 'stripe' -export default function StripeService() { +function StripeService(environment) { const { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, SUCCESS_URL, CANCEL_URL } = - getEnvironment() + environment // Note: stripe cjs API types are faulty /** @type {import('stripe').Stripe} */ @@ -64,3 +63,5 @@ export default function StripeService() { }, } } + +export default StripeService From de28d65bc84b39963b1356b8c95de322d5cb4320 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 18 Jul 2023 15:13:14 +0100 Subject: [PATCH 06/35] docs: update readme --- subscriptions-with-stripe/README.md | 47 ++++++++++------------------- 1 file changed, 16 insertions(+), 31 deletions(-) diff --git a/subscriptions-with-stripe/README.md b/subscriptions-with-stripe/README.md index 6a60798c..7440195d 100644 --- a/subscriptions-with-stripe/README.md +++ b/subscriptions-with-stripe/README.md @@ -1,22 +1,19 @@ # Stripe Subscriptions Function -This function helps to handle Stripe subscriptions and track them in an Appwrite database. It consists of handling Stripe checkout sessions, webhooks, and managing user subscriptions in a database. +Integrates Stripe subscriptions into your Appwrite project. Collect payments using the `/checkout` endpoint and check the status of a user subscription using the `Subscriptions` collection. ## Setup - ### Stripe API -Setting up the Stripe API involves generating the required API keys and setting up the necessary webhook. - -1. **API Keys** - - Log in to your Stripe dashboard. - - Navigate to Developers > API keys. - - Here, you can find your publishable key and your secret key. You will need the secret key for this function (i.e., `STRIPE_SECRET_KEY`). Be sure not to share or expose this key as it could allow others to make API requests on behalf of your account. +**Stripe Key** + - Log in to your Stripe dashboard. + - Navigate to Developers > API keys. + - Here, you can find your publishable key and your secret key. You will need the secret key for this function (i.e., `STRIPE_SECRET_KEY`). Be sure not to share or expose this key as it could allow others to make API requests on behalf of your account. -2. **Webhooks** - - In your Stripe dashboard, navigate to Developers > Webhooks. - - Click "+ Add endpoint" and set the URL to where you've hosted this function with the /webhook path, and select the "customer.subscription.created" and "customer.subscription.deleted" events. - - Once you've created the webhook, you'll be able to view and copy the signing secret (i.e., `STRIPE_WEBHOOK_SECRET`). +**Stripe Webhook Secret** + - In your Stripe dashboard, navigate to Developers > Webhooks. + - Click "+ Add endpoint" and set the URL to where you've hosted this function with the /webhook path, and select the "customer.subscription.created" and "customer.subscription.deleted" events. + - Once you've created the webhook, you'll be able to view and copy the signing secret (i.e., `STRIPE_WEBHOOK_SECRET`). ### Environment Variables @@ -37,29 +34,17 @@ Additionally, the function has the following optional variables: ### Database Setup -To setup the database, run `npm run setup`. -If the specified database doesn't exist, the script will automatically create it. It will also create a collection within the database, adding the necessary attributes to the collection. +A setup script is included in `src/setup.js`. If the `Subscriptions` database doesn't exist, the setup script will automatically create it. It will also create a collection within the database, adding the necessary attributes to the collection. The setup script will run automatically when the function is deployed. -## Usage +## Function API -This function supports two primary request paths: +- `GET /checkout` - Creates a Stripe Checkout session and redirects the user to the Stripe Checkout page. If the user successfully completes the payment, they are redirected to the `SUCCESS_URL`. If the user cancels the payment, they are redirected to the `CANCEL_URL`. -1. **Checkout Session Creation** +- `POST /webhook` - Handles Stripe webhook events. This function handles two events: +`customer.subscription.created` and `customer.subscription.deleted` - The function validates the webhook event and upgrades or downgrades the user's subscription in the database based on the event type. - - **Request Path:** /checkout - - **Request Type:** GET - - **Response:** - - On success, the function will redirect to the Stripe Checkout session URL. - - If the request fails, the user is redirected to the specified `CANCEL_URL`. -2. **Webhook Handling** +## Advanced Usage - - **Request Path:** /webhook - - **Request Type:** POST - - **Content Type:** application/json - - **Response:** - - The function will respond with an empty response after processing the webhook events. - - It handles two events: - 1. `customer.subscription.created` - The user is upgraded to a premium subscription. - 2. `customer.subscription.deleted` - The user is downgraded from their premium subscription. +The checkout page may be customised by editing `src/stripe.js` to include your desired product data, prices, and styling. From 457a42cacb20f250aebfdec14625416d8b887343 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 19 Jul 2023 10:36:11 +0100 Subject: [PATCH 07/35] chore: add semis, remove pjson extras --- subscriptions-with-stripe/.prettierrc.json | 8 +-- subscriptions-with-stripe/package.json | 3 - subscriptions-with-stripe/src/appwrite.js | 54 ++++++++--------- subscriptions-with-stripe/src/environment.js | 24 ++++---- subscriptions-with-stripe/src/main.js | 64 ++++++++++---------- subscriptions-with-stripe/src/setup.js | 20 +++--- subscriptions-with-stripe/src/stripe.js | 22 ++++--- 7 files changed, 97 insertions(+), 98 deletions(-) diff --git a/subscriptions-with-stripe/.prettierrc.json b/subscriptions-with-stripe/.prettierrc.json index b7858424..0a725205 100644 --- a/subscriptions-with-stripe/.prettierrc.json +++ b/subscriptions-with-stripe/.prettierrc.json @@ -1,6 +1,6 @@ { - "trailingComma": "es5", - "tabWidth": 2, - "semi": false, - "singleQuote": true + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true } diff --git a/subscriptions-with-stripe/package.json b/subscriptions-with-stripe/package.json index fc4a88f5..b49a168b 100644 --- a/subscriptions-with-stripe/package.json +++ b/subscriptions-with-stripe/package.json @@ -5,12 +5,9 @@ "main": "src/main.js", "type": "module", "scripts": { - "start": "node src/main.js", "setup": "node src/setup.js", "format": "prettier --write src/**/*.js" }, - "author": "", - "license": "MIT", "dependencies": { "node-appwrite": "^9.0.0", "stripe": "^12.12.0" diff --git a/subscriptions-with-stripe/src/appwrite.js b/subscriptions-with-stripe/src/appwrite.js index 823ed65f..2b1b966b 100644 --- a/subscriptions-with-stripe/src/appwrite.js +++ b/subscriptions-with-stripe/src/appwrite.js @@ -1,8 +1,8 @@ -import { Client, Databases } from 'node-appwrite' +import { Client, Databases } from 'node-appwrite'; const Subscriptions = { PREMIUM: 'premium', -} +}; function AppwriteService(environment) { const { @@ -13,15 +13,15 @@ function AppwriteService(environment) { DATABASE_NAME, COLLECTION_ID, COLLECTION_NAME, - } = environment + } = environment; - const client = new Client() + const client = new Client(); client .setEndpoint(APPWRITE_ENDPOINT) .setProject(APPWRITE_PROJECT_ID) - .setKey(APPWRITE_API_KEY) + .setKey(APPWRITE_API_KEY); - const databases = new Databases(client) + const databases = new Databases(client); return { /** @@ -29,38 +29,38 @@ function AppwriteService(environment) { */ doesSubscribersDatabaseExist: async function () { try { - await databases.get(DATABASE_ID) - return true + await databases.get(DATABASE_ID); + return true; } catch (err) { - if (err.code === 404) return false - throw err + if (err.code === 404) return false; + throw err; } }, setupSubscribersDatabase: async function () { try { - await databases.create(DATABASE_ID, DATABASE_NAME) + await databases.create(DATABASE_ID, DATABASE_NAME); await databases.createCollection( DATABASE_ID, COLLECTION_ID, COLLECTION_NAME - ) + ); await databases.createStringAttribute( DATABASE_ID, COLLECTION_ID, 'userId', 255, true - ) + ); await databases.createStringAttribute( DATABASE_ID, COLLECTION_ID, 'subscriptionType', 255, true - ) + ); } catch (err) { // If resource already exists, we can ignore the error - if (err.code !== 409) throw err + if (err.code !== 409) throw err; } }, /** @@ -69,11 +69,11 @@ function AppwriteService(environment) { */ hasSubscription: async function (userId) { try { - await databases.getDocument(DATABASE_ID, COLLECTION_ID, userId) - return true + await databases.getDocument(DATABASE_ID, COLLECTION_ID, userId); + return true; } catch (err) { - if (err.code !== 404) throw err - return false + if (err.code !== 404) throw err; + return false; } }, /** @@ -82,10 +82,10 @@ function AppwriteService(environment) { */ deleteSubscription: async function (userId) { try { - await databases.deleteDocument(DATABASE_ID, COLLECTION_ID, userId) - return true + await databases.deleteDocument(DATABASE_ID, COLLECTION_ID, userId); + return true; } catch (err) { - return false + return false; } }, /** @@ -96,13 +96,13 @@ function AppwriteService(environment) { try { await databases.createDocument(DATABASE_ID, COLLECTION_ID, userId, { subscriptionType: Subscriptions.PREMIUM, - }) - return true + }); + return true; } catch (err) { - return false + return false; } }, - } + }; } -export default AppwriteService +export default AppwriteService; diff --git a/subscriptions-with-stripe/src/environment.js b/subscriptions-with-stripe/src/environment.js index 5ed4ba31..0f73fb5e 100644 --- a/subscriptions-with-stripe/src/environment.js +++ b/subscriptions-with-stripe/src/environment.js @@ -3,11 +3,11 @@ * @return {string} */ function getRequiredEnv(key) { - const value = process.env[key] + const value = process.env[key]; if (value === undefined) { - throw new Error(`Environment variable ${key} is not set`) + throw new Error(`Environment variable ${key} is not set`); } - return value + return value; } /** @@ -15,11 +15,11 @@ function getRequiredEnv(key) { * @return {string} */ function getRequiredUrlEnv(key) { - const value = getRequiredEnv(key) + const value = getRequiredEnv(key); if (!isValidUrl(value)) { - throw new Error(`Environment variable ${key} is a not valid URL`) + throw new Error(`Environment variable ${key} is a not valid URL`); } - return value + return value; } /** @@ -27,12 +27,12 @@ function getRequiredUrlEnv(key) { * @returns {boolean} */ function isValidUrl(url) { - if (!url) return false + if (!url) return false; try { - new URL(url) - return true + new URL(url); + return true; } catch (err) { - return false + return false; } } @@ -49,7 +49,7 @@ function EnvironmentService() { DATABASE_NAME: 'Stripe Subscriptions', COLLECTION_ID: process.env.COLLECTION_ID ?? 'subscriptions', COLLECTION_NAME: 'Subscriptions', - } + }; } -export default EnvironmentService +export default EnvironmentService; diff --git a/subscriptions-with-stripe/src/main.js b/subscriptions-with-stripe/src/main.js index 0949d147..27ba7f42 100644 --- a/subscriptions-with-stripe/src/main.js +++ b/subscriptions-with-stripe/src/main.js @@ -1,64 +1,64 @@ -import StripeService from './stripe.js' -import AppwriteService from './appwrite.js' -import EnvironmentService from './environment.js' +import StripeService from './stripe.js'; +import AppwriteService from './appwrite.js'; +import EnvironmentService from './environment.js'; export default async ({ req, res, log, error }) => { - const environment = EnvironmentService() - const appwrite = AppwriteService(environment) - const stripe = StripeService(environment) + const environment = EnvironmentService(); + const appwrite = AppwriteService(environment); + const stripe = StripeService(environment); - const { CANCEL_URL } = environment + const { CANCEL_URL } = environment; switch (req.path) { case '/checkout': - const userId = req.headers['x-appwrite-user-id'] + const userId = req.headers['x-appwrite-user-id']; if (!userId) { - error('User ID not found in request.') - return res.redirect(CANCEL_URL, 303) + error('User ID not found in request.'); + return res.redirect(CANCEL_URL, 303); } - const session = await stripe.checkoutSubscription(userId) + const session = await stripe.checkoutSubscription(userId); if (!session) { - error('Failed to create Stripe checkout session.') - return res.redirect(CANCEL_URL, 303) + error('Failed to create Stripe checkout session.'); + return res.redirect(CANCEL_URL, 303); } - log(`Created Stripe checkout session for user ${userId}.`) - return res.redirect(session.url, 303) + log(`Created Stripe checkout session for user ${userId}.`); + return res.redirect(session.url, 303); case '/webhook': - const event = stripe.validateWebhook(req) - if (!event) return res.json({ success: false }, 401) + const event = stripe.validateWebhook(req); + if (!event) return res.json({ success: false }, 401); if (event.type === 'customer.subscription.created') { - const session = event.data.object - const userId = session.metadata.userId + const session = event.data.object; + const userId = session.metadata.userId; if (await appwrite.hasSubscription(userId)) { - error(`Subscription already exists - skipping`) - return res.json({ success: true }) + error(`Subscription already exists - skipping`); + return res.json({ success: true }); } - await appwrite.createSubscription(userId) - log(`Created subscription for user ${userId}`) + await appwrite.createSubscription(userId); + log(`Created subscription for user ${userId}`); } if (event.type === 'customer.subscription.deleted') { - const session = event.data.object - const userId = session.metadata.userId + const session = event.data.object; + const userId = session.metadata.userId; if (!(await appwrite.hasSubscription(userId))) { - error(`Subscription does not exist - skipping`) - return res.json({ success: true }) + error(`Subscription does not exist - skipping`); + return res.json({ success: true }); } - await appwrite.deleteSubscription(userId) - log(`Deleted subscription for user ${userId}`) + await appwrite.deleteSubscription(userId); + log(`Deleted subscription for user ${userId}`); } - return res.json({ success: true }) + return res.json({ success: true }); default: - return res.send('Not Found', 404) + return res.send('Not Found', 404); } -} +}; diff --git a/subscriptions-with-stripe/src/setup.js b/subscriptions-with-stripe/src/setup.js index 9889e6e8..21a160f0 100644 --- a/subscriptions-with-stripe/src/setup.js +++ b/subscriptions-with-stripe/src/setup.js @@ -1,23 +1,23 @@ -import AppwriteService from './appwrite.js' -import EnvironmentService from './environment.js' +import AppwriteService from './appwrite.js'; +import EnvironmentService from './environment.js'; /** * Setup script for the subscribers database. * If the database already exists, this script will do nothing. */ async function setup() { - console.log('Executing setup script...') + console.log('Executing setup script...'); - const environment = EnvironmentService() - const appwrite = AppwriteService(environment) + const environment = EnvironmentService(); + const appwrite = AppwriteService(environment); if (await appwrite.doesSubscribersDatabaseExist()) { - console.log(`Database exists.`) - return + console.log(`Database exists.`); + return; } - await appwrite.setupSubscribersDatabase() - console.log(`Database created.`) + await appwrite.setupSubscribersDatabase(); + console.log(`Database created.`); } -setup() +setup(); diff --git a/subscriptions-with-stripe/src/stripe.js b/subscriptions-with-stripe/src/stripe.js index e9ff2222..b08e32e1 100644 --- a/subscriptions-with-stripe/src/stripe.js +++ b/subscriptions-with-stripe/src/stripe.js @@ -1,15 +1,15 @@ /// -import stripe from 'stripe' +import stripe from 'stripe'; function StripeService(environment) { const { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, SUCCESS_URL, CANCEL_URL } = - environment + environment; // Note: stripe cjs API types are faulty /** @type {import('stripe').Stripe} */ // @ts-ignore - const stripeClient = stripe(STRIPE_SECRET_KEY) + const stripeClient = stripe(STRIPE_SECRET_KEY); return { /** @@ -41,9 +41,9 @@ function StripeService(environment) { userId, }, mode: 'subscription', - }) + }); } catch (err) { - return null + return null; } }, /** @@ -55,13 +55,15 @@ function StripeService(environment) { req.body, req.headers['stripe-signature'], STRIPE_WEBHOOK_SECRET - ) - return /** @type {import("stripe").Stripe.DiscriminatedEvent} */ (event) + ); + return /** @type {import("stripe").Stripe.DiscriminatedEvent} */ ( + event + ); } catch (err) { - return null + return null; } }, - } + }; } -export default StripeService +export default StripeService; From e237cd2ece6f9eea2192d1909b93c3fc7cb13a15 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 19 Jul 2023 20:46:51 +0100 Subject: [PATCH 08/35] chore: use classes over function pattern --- subscriptions-with-stripe/src/appwrite.js | 203 ++++++++++--------- subscriptions-with-stripe/src/environment.js | 26 ++- subscriptions-with-stripe/src/main.js | 12 +- subscriptions-with-stripe/src/setup.js | 4 +- subscriptions-with-stripe/src/stripe.js | 115 +++++------ 5 files changed, 185 insertions(+), 175 deletions(-) diff --git a/subscriptions-with-stripe/src/appwrite.js b/subscriptions-with-stripe/src/appwrite.js index 2b1b966b..9f89bee8 100644 --- a/subscriptions-with-stripe/src/appwrite.js +++ b/subscriptions-with-stripe/src/appwrite.js @@ -4,105 +4,118 @@ const Subscriptions = { PREMIUM: 'premium', }; -function AppwriteService(environment) { - const { - APPWRITE_ENDPOINT, - APPWRITE_PROJECT_ID, - APPWRITE_API_KEY, - DATABASE_ID, - DATABASE_NAME, - COLLECTION_ID, - COLLECTION_NAME, - } = environment; +class AppwriteService { + /** + * @param {import('./environment').default} env + */ + constructor(env) { + this.env = env; - const client = new Client(); - client - .setEndpoint(APPWRITE_ENDPOINT) - .setProject(APPWRITE_PROJECT_ID) - .setKey(APPWRITE_API_KEY); + const client = new Client(); + client + .setEndpoint(env.APPWRITE_ENDPOINT) + .setProject(env.APPWRITE_PROJECT_ID) + .setKey(env.APPWRITE_API_KEY); - const databases = new Databases(client); + const databases = new Databases(client); + this.databases = databases; + } - return { - /** - * @returns {Promise} - */ - doesSubscribersDatabaseExist: async function () { - try { - await databases.get(DATABASE_ID); - return true; - } catch (err) { - if (err.code === 404) return false; - throw err; - } - }, - setupSubscribersDatabase: async function () { - try { - await databases.create(DATABASE_ID, DATABASE_NAME); - await databases.createCollection( - DATABASE_ID, - COLLECTION_ID, - COLLECTION_NAME - ); - await databases.createStringAttribute( - DATABASE_ID, - COLLECTION_ID, - 'userId', - 255, - true - ); - await databases.createStringAttribute( - DATABASE_ID, - COLLECTION_ID, - 'subscriptionType', - 255, - true - ); - } catch (err) { - // If resource already exists, we can ignore the error - if (err.code !== 409) throw err; - } - }, - /** - * @param {string} userId - * @returns {Promise} - */ - hasSubscription: async function (userId) { - try { - await databases.getDocument(DATABASE_ID, COLLECTION_ID, userId); - return true; - } catch (err) { - if (err.code !== 404) throw err; - return false; - } - }, - /** - * @param {string} userId - * @returns {Promise} - */ - deleteSubscription: async function (userId) { - try { - await databases.deleteDocument(DATABASE_ID, COLLECTION_ID, userId); - return true; - } catch (err) { - return false; - } - }, - /** - * @param {string} userId - * @returns {Promise} - */ - createSubscription: async function (userId) { - try { - await databases.createDocument(DATABASE_ID, COLLECTION_ID, userId, { + /** + * @returns {Promise} + */ + async doesSubscribersDatabaseExist() { + try { + await this.databases.get(this.env.DATABASE_ID); + return true; + } catch (err) { + if (err.code === 404) return false; + throw err; + } + } + + async setupSubscribersDatabase() { + try { + await this.databases.create(this.env.DATABASE_ID, this.env.DATABASE_NAME); + await this.databases.createCollection( + this.env.DATABASE_ID, + this.env.COLLECTION_ID, + this.env.COLLECTION_NAME + ); + await this.databases.createStringAttribute( + this.env.DATABASE_ID, + this.env.COLLECTION_ID, + 'userId', + 255, + true + ); + await this.databases.createStringAttribute( + this.env.DATABASE_ID, + this.env.COLLECTION_ID, + 'subscriptionType', + 255, + true + ); + } catch (err) { + // If resource already exists, we can ignore the error + if (err.code !== 409) throw err; + } + } + + /** + * @param {string} userId + * @returns {Promise} + */ + async hasSubscription(userId) { + try { + await this.databases.getDocument( + this.env.DATABASE_ID, + this.env.COLLECTION_ID, + userId + ); + return true; + } catch (err) { + if (err.code !== 404) throw err; + return false; + } + } + + /** + * @param {string} userId + * @returns {Promise} + */ + async deleteSubscription(userId) { + try { + await this.databases.deleteDocument( + this.env.DATABASE_ID, + this.env.COLLECTION_ID, + userId + ); + return true; + } catch (err) { + return false; + } + } + + /** + * @param {string} userId + * @returns {Promise} + */ + async createSubscription(userId) { + try { + await this.databases.createDocument( + this.env.DATABASE_ID, + this.env.COLLECTION_ID, + userId, + { subscriptionType: Subscriptions.PREMIUM, - }); - return true; - } catch (err) { - return false; - } - }, - }; + } + ); + return true; + } catch (err) { + return false; + } + } } export default AppwriteService; diff --git a/subscriptions-with-stripe/src/environment.js b/subscriptions-with-stripe/src/environment.js index 0f73fb5e..ab4ce4a2 100644 --- a/subscriptions-with-stripe/src/environment.js +++ b/subscriptions-with-stripe/src/environment.js @@ -36,20 +36,18 @@ function isValidUrl(url) { } } -function EnvironmentService() { - return { - APPWRITE_ENDPOINT: getRequiredUrlEnv('APPWRITE_ENDPOINT'), - APPWRITE_PROJECT_ID: getRequiredEnv('APPWRITE_PROJECT_ID'), - APPWRITE_API_KEY: getRequiredEnv('APPWRITE_API_KEY'), - STRIPE_WEBHOOK_SECRET: getRequiredEnv('STRIPE_WEBHOOK_SECRET'), - STRIPE_SECRET_KEY: getRequiredEnv('STRIPE_SECRET_KEY'), - SUCCESS_URL: getRequiredUrlEnv('SUCCESS_URL'), - CANCEL_URL: getRequiredUrlEnv('CANCEL_URL'), - DATABASE_ID: process.env.DATABASE_ID ?? 'stripe-subscriptions', - DATABASE_NAME: 'Stripe Subscriptions', - COLLECTION_ID: process.env.COLLECTION_ID ?? 'subscriptions', - COLLECTION_NAME: 'Subscriptions', - }; +class EnvironmentService { + APPWRITE_ENDPOINT = getRequiredUrlEnv('APPWRITE_ENDPOINT'); + APPWRITE_PROJECT_ID = getRequiredEnv('APPWRITE_PROJECT_ID'); + APPWRITE_API_KEY = getRequiredEnv('APPWRITE_API_KEY'); + STRIPE_WEBHOOK_SECRET = getRequiredEnv('STRIPE_WEBHOOK_SECRET'); + STRIPE_SECRET_KEY = getRequiredEnv('STRIPE_SECRET_KEY'); + SUCCESS_URL = getRequiredUrlEnv('SUCCESS_URL'); + CANCEL_URL = getRequiredUrlEnv('CANCEL_URL'); + DATABASE_ID = process.env.DATABASE_ID ?? 'stripe-subscriptions'; + DATABASE_NAME = 'Stripe Subscriptions'; + COLLECTION_ID = process.env.COLLECTION_ID ?? 'subscriptions'; + COLLECTION_NAME = 'Subscriptions'; } export default EnvironmentService; diff --git a/subscriptions-with-stripe/src/main.js b/subscriptions-with-stripe/src/main.js index 27ba7f42..bbcb6c28 100644 --- a/subscriptions-with-stripe/src/main.js +++ b/subscriptions-with-stripe/src/main.js @@ -3,24 +3,22 @@ import AppwriteService from './appwrite.js'; import EnvironmentService from './environment.js'; export default async ({ req, res, log, error }) => { - const environment = EnvironmentService(); - const appwrite = AppwriteService(environment); - const stripe = StripeService(environment); - - const { CANCEL_URL } = environment; + const env = new EnvironmentService(); + const appwrite = new AppwriteService(env); + const stripe = new StripeService(env); switch (req.path) { case '/checkout': const userId = req.headers['x-appwrite-user-id']; if (!userId) { error('User ID not found in request.'); - return res.redirect(CANCEL_URL, 303); + return res.redirect(env.CANCEL_URL, 303); } const session = await stripe.checkoutSubscription(userId); if (!session) { error('Failed to create Stripe checkout session.'); - return res.redirect(CANCEL_URL, 303); + return res.redirect(env.CANCEL_URL, 303); } log(`Created Stripe checkout session for user ${userId}.`); diff --git a/subscriptions-with-stripe/src/setup.js b/subscriptions-with-stripe/src/setup.js index 21a160f0..5aef9af1 100644 --- a/subscriptions-with-stripe/src/setup.js +++ b/subscriptions-with-stripe/src/setup.js @@ -8,8 +8,8 @@ import EnvironmentService from './environment.js'; async function setup() { console.log('Executing setup script...'); - const environment = EnvironmentService(); - const appwrite = AppwriteService(environment); + const env = new EnvironmentService(); + const appwrite = new AppwriteService(env); if (await appwrite.doesSubscribersDatabaseExist()) { console.log(`Database exists.`); diff --git a/subscriptions-with-stripe/src/stripe.js b/subscriptions-with-stripe/src/stripe.js index b08e32e1..24814c08 100644 --- a/subscriptions-with-stripe/src/stripe.js +++ b/subscriptions-with-stripe/src/stripe.js @@ -2,68 +2,69 @@ import stripe from 'stripe'; -function StripeService(environment) { - const { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, SUCCESS_URL, CANCEL_URL } = - environment; +class StripeService { + /** + * @param {import('./environment.js').default} env + */ + constructor(env) { + this.env = env; - // Note: stripe cjs API types are faulty - /** @type {import('stripe').Stripe} */ - // @ts-ignore - const stripeClient = stripe(STRIPE_SECRET_KEY); + // Note: stripe cjs API types are faulty + /** @type {import('stripe').Stripe} */ + // @ts-ignore + this.client = stripe(env.STRIPE_SECRET_KEY); + } - return { - /** - * @param {string} userId - */ - checkoutSubscription: async function (userId) { - try { - return await stripeClient.checkout.sessions.create({ - payment_method_types: ['card'], - line_items: [ - { - price_data: { - currency: 'usd', - product_data: { - name: 'Premium Subscription', - }, - unit_amount: 1000, - recurring: { - interval: 'year', - }, + /** + * @param {string} userId + */ + async checkoutSubscription(userId) { + try { + return await this.client.checkout.sessions.create({ + payment_method_types: ['card'], + line_items: [ + { + price_data: { + currency: 'usd', + product_data: { + name: 'Premium Subscription', + }, + unit_amount: 1000, + recurring: { + interval: 'year', }, - quantity: 1, }, - ], - success_url: SUCCESS_URL, - cancel_url: CANCEL_URL, - client_reference_id: userId, - metadata: { - userId, + quantity: 1, }, - mode: 'subscription', - }); - } catch (err) { - return null; - } - }, - /** - * @returns {import("stripe").Stripe.DiscriminatedEvent | null} - */ - validateWebhook: function (req) { - try { - const event = stripeClient.webhooks.constructEvent( - req.body, - req.headers['stripe-signature'], - STRIPE_WEBHOOK_SECRET - ); - return /** @type {import("stripe").Stripe.DiscriminatedEvent} */ ( - event - ); - } catch (err) { - return null; - } - }, - }; + ], + success_url: this.env.SUCCESS_URL, + cancel_url: this.env.CANCEL_URL, + client_reference_id: userId, + metadata: { + userId, + }, + mode: 'subscription', + }); + } catch (err) { + return null; + } + } + + /** + * @returns {import("stripe").Stripe.DiscriminatedEvent | null} + */ + validateWebhook(req) { + try { + const event = this.client.webhooks.constructEvent( + req.body, + req.headers['stripe-signature'], + this.env.STRIPE_WEBHOOK_SECRET + ); + return /** @type {import("stripe").Stripe.DiscriminatedEvent} */ (event); + } catch (err) { + return null; + } + } } export default StripeService; From a34fee90997192eee66cce5208807a0fc03ef88c Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 20 Jul 2023 10:39:16 +0100 Subject: [PATCH 09/35] chore: new structure --- .../subscriptions-with-stripe}/.gitignore | 0 .../subscriptions-with-stripe}/.prettierrc.json | 0 .../subscriptions-with-stripe}/README.md | 0 .../subscriptions-with-stripe}/env.d.ts | 0 .../subscriptions-with-stripe}/package-lock.json | 0 .../subscriptions-with-stripe}/package.json | 0 .../subscriptions-with-stripe}/src/appwrite.js | 0 .../subscriptions-with-stripe}/src/environment.js | 0 .../subscriptions-with-stripe}/src/main.js | 0 .../subscriptions-with-stripe}/src/setup.js | 0 .../subscriptions-with-stripe}/src/stripe.js | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename {subscriptions-with-stripe => node/subscriptions-with-stripe}/.gitignore (100%) rename {subscriptions-with-stripe => node/subscriptions-with-stripe}/.prettierrc.json (100%) rename {subscriptions-with-stripe => node/subscriptions-with-stripe}/README.md (100%) rename {subscriptions-with-stripe => node/subscriptions-with-stripe}/env.d.ts (100%) rename {subscriptions-with-stripe => node/subscriptions-with-stripe}/package-lock.json (100%) rename {subscriptions-with-stripe => node/subscriptions-with-stripe}/package.json (100%) rename {subscriptions-with-stripe => node/subscriptions-with-stripe}/src/appwrite.js (100%) rename {subscriptions-with-stripe => node/subscriptions-with-stripe}/src/environment.js (100%) rename {subscriptions-with-stripe => node/subscriptions-with-stripe}/src/main.js (100%) rename {subscriptions-with-stripe => node/subscriptions-with-stripe}/src/setup.js (100%) rename {subscriptions-with-stripe => node/subscriptions-with-stripe}/src/stripe.js (100%) diff --git a/subscriptions-with-stripe/.gitignore b/node/subscriptions-with-stripe/.gitignore similarity index 100% rename from subscriptions-with-stripe/.gitignore rename to node/subscriptions-with-stripe/.gitignore diff --git a/subscriptions-with-stripe/.prettierrc.json b/node/subscriptions-with-stripe/.prettierrc.json similarity index 100% rename from subscriptions-with-stripe/.prettierrc.json rename to node/subscriptions-with-stripe/.prettierrc.json diff --git a/subscriptions-with-stripe/README.md b/node/subscriptions-with-stripe/README.md similarity index 100% rename from subscriptions-with-stripe/README.md rename to node/subscriptions-with-stripe/README.md diff --git a/subscriptions-with-stripe/env.d.ts b/node/subscriptions-with-stripe/env.d.ts similarity index 100% rename from subscriptions-with-stripe/env.d.ts rename to node/subscriptions-with-stripe/env.d.ts diff --git a/subscriptions-with-stripe/package-lock.json b/node/subscriptions-with-stripe/package-lock.json similarity index 100% rename from subscriptions-with-stripe/package-lock.json rename to node/subscriptions-with-stripe/package-lock.json diff --git a/subscriptions-with-stripe/package.json b/node/subscriptions-with-stripe/package.json similarity index 100% rename from subscriptions-with-stripe/package.json rename to node/subscriptions-with-stripe/package.json diff --git a/subscriptions-with-stripe/src/appwrite.js b/node/subscriptions-with-stripe/src/appwrite.js similarity index 100% rename from subscriptions-with-stripe/src/appwrite.js rename to node/subscriptions-with-stripe/src/appwrite.js diff --git a/subscriptions-with-stripe/src/environment.js b/node/subscriptions-with-stripe/src/environment.js similarity index 100% rename from subscriptions-with-stripe/src/environment.js rename to node/subscriptions-with-stripe/src/environment.js diff --git a/subscriptions-with-stripe/src/main.js b/node/subscriptions-with-stripe/src/main.js similarity index 100% rename from subscriptions-with-stripe/src/main.js rename to node/subscriptions-with-stripe/src/main.js diff --git a/subscriptions-with-stripe/src/setup.js b/node/subscriptions-with-stripe/src/setup.js similarity index 100% rename from subscriptions-with-stripe/src/setup.js rename to node/subscriptions-with-stripe/src/setup.js diff --git a/subscriptions-with-stripe/src/stripe.js b/node/subscriptions-with-stripe/src/stripe.js similarity index 100% rename from subscriptions-with-stripe/src/stripe.js rename to node/subscriptions-with-stripe/src/stripe.js From 29aef16e1d8597af4d33864fe555428aa81fdb02 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Thu, 20 Jul 2023 12:09:24 +0100 Subject: [PATCH 10/35] chore: prettier script --- node/subscriptions-with-stripe/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/subscriptions-with-stripe/package.json b/node/subscriptions-with-stripe/package.json index b49a168b..04b2d839 100644 --- a/node/subscriptions-with-stripe/package.json +++ b/node/subscriptions-with-stripe/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "setup": "node src/setup.js", - "format": "prettier --write src/**/*.js" + "format": "prettier --write ." }, "dependencies": { "node-appwrite": "^9.0.0", From 33c582add3a2eb80aa3be59562f1961c71002245 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Tue, 25 Jul 2023 12:53:38 +0100 Subject: [PATCH 11/35] docs: update to new template --- node/subscriptions-with-stripe/README.md | 172 ++++++++++++++---- .../src/environment.js | 4 +- 2 files changed, 143 insertions(+), 33 deletions(-) diff --git a/node/subscriptions-with-stripe/README.md b/node/subscriptions-with-stripe/README.md index 7440195d..830520b0 100644 --- a/node/subscriptions-with-stripe/README.md +++ b/node/subscriptions-with-stripe/README.md @@ -1,50 +1,158 @@ -# Stripe Subscriptions Function +# ⚡ Stripe Subscriptions Function -Integrates Stripe subscriptions into your Appwrite project. Collect payments using the `/checkout` endpoint and check the status of a user subscription using the `Subscriptions` collection. +Integrates Stripe subscriptions into your Appwrite project. Collect card payment with the `/checkout` endpoint and check the status of a user subscription using the `Subscriptions` collection. -## Setup -### Stripe API +## 🧰 Usage -**Stripe Key** - - Log in to your Stripe dashboard. - - Navigate to Developers > API keys. - - Here, you can find your publishable key and your secret key. You will need the secret key for this function (i.e., `STRIPE_SECRET_KEY`). Be sure not to share or expose this key as it could allow others to make API requests on behalf of your account. +### `POST /checkout` -**Stripe Webhook Secret** - - In your Stripe dashboard, navigate to Developers > Webhooks. - - Click "+ Add endpoint" and set the URL to where you've hosted this function with the /webhook path, and select the "customer.subscription.created" and "customer.subscription.deleted" events. - - Once you've created the webhook, you'll be able to view and copy the signing secret (i.e., `STRIPE_WEBHOOK_SECRET`). +This endpoint initiates a Stripe checkout session for a subscription. The user ID is fetched from the headers of the request. If the user ID is not found or a Stripe checkout session could not be created, the request will be redirected to a cancel URL. -### Environment Variables +**Parameters** -To ensure the function operates as intended, ensure the following variables are set: +| Name | Description | Location | Type | Sample Value | +| ------------------ | ---------------------- | -------- | ------ | ------------ | +| x-appwrite-user-id | User ID from Appwrite. | Header | String | 642...7cd | -- **APPWRITE_API_KEY**: This is your Appwrite project's API key. -- **APPWRITE_ENDPOINT**: This is the endpoint where your Appwrite server is located. -- **APPWRITE_PROJECT_ID**: This refers to the specific ID of your Appwrite project. -- **STRIPE_SECRET_KEY**: This is your Stripe Secret key. -- **STRIPE_WEBHOOK_SECRET**: The secret used to validate the Stripe Webhook signature. -- **SUCCESS_URL**: The URL users are redirected to after a successful payment. -- **CANCEL_URL**: The URL users are redirected to after a cancelled payment attempt. +**Response** -Additionally, the function has the following optional variables: +Sample `303` Response: -- **DATABASE_ID**: This is the ID for the database where subscriptions will be stored. If not provided, it defaults to "stripe-subscriptions". -- **COLLECTION_ID**: This is the ID for the collection within the database. If not provided, it defaults to "subscriptions". +The response is a redirect to the Stripe checkout session URL or to the cancel URL if an error occurs -### Database Setup +```text +Location: https://checkout.stripe.com/pay/cs_test_...#fidkdWxOYHwnP +``` -A setup script is included in `src/setup.js`. If the `Subscriptions` database doesn't exist, the setup script will automatically create it. It will also create a collection within the database, adding the necessary attributes to the collection. The setup script will run automatically when the function is deployed. +```text +Location: https://mywebapp.com/cancel +``` -## Function API +### `POST /webhook` -- `GET /checkout` - Creates a Stripe Checkout session and redirects the user to the Stripe Checkout page. If the user successfully completes the payment, they are redirected to the `SUCCESS_URL`. If the user cancels the payment, they are redirected to the `CANCEL_URL`. +This endpoint is a webhook that handles two types of events from Stripe: `customer.subscription.created` and `customer.subscription.deleted`. It validates the incoming request using the Stripe's validateWebhook method. If the validation fails, a `401` response is sent. -- `POST /webhook` - Handles Stripe webhook events. This function handles two events: -`customer.subscription.created` and `customer.subscription.deleted` - The function validates the webhook event and upgrades or downgrades the user's subscription in the database based on the event type. +**Parameters** +| Name | Description | Location | Type | Sample Value | +| ---- | ---------------------------- | -------- | ------ | --------------------------------------------------------------------- | +| None | Webhook payload from Stripe. | Body | Object | [See Stripe documentation](https://stripe.com/docs/api/events/object) | -## Advanced Usage +**Response** -The checkout page may be customised by editing `src/stripe.js` to include your desired product data, prices, and styling. +Sample `200` Response: +In case of `customer.subscription.created` event, it creates a new subscription for the user. +In case of `customer.subscription.deleted` event, it deletes the subscription for the user. + +```json +{ "success": true } +``` + +Sample `401` Response: + +```json +{ "success": false } +``` + +## ⚙️ Configuration + +| Setting | Value | +| ----------------- | --------------- | +| Runtime | Node (18.0) | +| Entrypoint | `src/main.js` | +| Build Commands | `npm install` | +| | `npm run setup` | +| Permissions | `any` | +| Timeout (Seconds) | 15 | + +## 🔒 Environment Variables + +### APPWRITE_API_KEY + +Your Appwrite project's API key. + +| Question | Answer | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `083d341ee48...` | +| Documentation | [Appwrite: Create an API key](https://appwrite.io/docs/keys#:~:text=To%20create%20a%20new%20API,scope%20to%20grant%20your%20application.) | + +### APPWRITE_ENDPOINT + +The endpoint where your Appwrite server is located. If not provided, it defaults to the Appwrite Cloud server: `https://cloud.appwrite.io/v1`. + +| Question | Answer | +| ------------ | ------------------------------ | +| Required | No | +| Sample Value | `https://cloud.appwrite.io/v1` | + +### APPWRITE_PROJECT_ID + +The ID of your Appwrite project. + +| Question | Answer | +| ------------- | ----------------------------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `builtWithAppwrite` | +| Documentation | [Appwrite: Getting Started](https://appwrite.io/docs/getting-started-for-web#addPlatform) | + +### STRIPE_SECRET_KEY + +Secret for sending requests to the Stripe API. + +| Question | Answer | +| ------------- | ------------------------------------------------ | +| Required | Yes | +| Sample Value | `sk_test_51J...` | +| Documentation | [Stripe: API Keys](https://stripe.com/docs/keys) | + +### STRIPE_WEBHOOK_SECRET + +Secret used to validate the Stripe Webhook signature. + +| Question | Answer | +| ------------- | ---------------------------------------------------- | +| Required | Yes | +| Sample Value | `whsec_...` | +| Documentation | [Stripe: Webhooks](https://stripe.com/docs/webhooks) | + +### SUCCESS_URL + +The URL to redirect to after a successful payment. + +| Question | Answer | +| ------------- | ----------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `https://example.com/success` | +| Documentation | [Stripe: Redirects](https://stripe.com/docs/payments/checkout/redirect) | + +### CANCEL_URL + +The URL to redirect to after a cancelled payment attempt. + +| Question | Answer | +| ------------- | ----------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `https://example.com/cancel` | +| Documentation | [Stripe: Redirects](https://stripe.com/docs/payments/checkout/redirect) | + +### DATABASE_ID + +The ID for the database where subscriptions will be stored. If not provided, it defaults to "stripe-subscriptions". + +| Question | Answer | +| ------------- | --------------------------------------------------------- | +| Required | No | +| Sample Value | `stripe-subscriptions` | +| Documentation | [Appwrite: Databases](https://appwrite.io/docs/databases) | + +### COLLECTION_ID + +The ID for the collection within the database. If not provided, it defaults to "subscriptions". + +| Question | Answer | +| ------------- | ------------------------------------------------------------- | +| Required | No | +| Sample Value | `subscriptions` | +| Documentation | [Appwrite: Collections](https://appwrite.io/docs/collections) | diff --git a/node/subscriptions-with-stripe/src/environment.js b/node/subscriptions-with-stripe/src/environment.js index ab4ce4a2..4b2585b4 100644 --- a/node/subscriptions-with-stripe/src/environment.js +++ b/node/subscriptions-with-stripe/src/environment.js @@ -37,7 +37,9 @@ function isValidUrl(url) { } class EnvironmentService { - APPWRITE_ENDPOINT = getRequiredUrlEnv('APPWRITE_ENDPOINT'); + APPWRITE_ENDPOINT = isValidUrl(process.env.APPWRITE_ENDPOINT) + ? process.env.APPWRITE_ENDPOINT + : 'https://cloud.appwrite.io/v1'; APPWRITE_PROJECT_ID = getRequiredEnv('APPWRITE_PROJECT_ID'); APPWRITE_API_KEY = getRequiredEnv('APPWRITE_API_KEY'); STRIPE_WEBHOOK_SECRET = getRequiredEnv('STRIPE_WEBHOOK_SECRET'); From d540ce27ef9e55f40b9043522ab07e690fafc231 Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Mon, 31 Jul 2023 11:17:04 +0100 Subject: [PATCH 12/35] chore: new utils, env.d.ts --- node/subscriptions-with-stripe/env.d.ts | 11 ++-- .../subscriptions-with-stripe/src/appwrite.js | 53 ++++++++++-------- .../src/environment.js | 55 ------------------- node/subscriptions-with-stripe/src/main.js | 18 ++++-- node/subscriptions-with-stripe/src/setup.js | 4 +- node/subscriptions-with-stripe/src/stripe.js | 42 +++++++------- node/subscriptions-with-stripe/src/utils.js | 17 ++++++ 7 files changed, 84 insertions(+), 116 deletions(-) delete mode 100644 node/subscriptions-with-stripe/src/environment.js create mode 100644 node/subscriptions-with-stripe/src/utils.js diff --git a/node/subscriptions-with-stripe/env.d.ts b/node/subscriptions-with-stripe/env.d.ts index 55743a80..779a1c72 100644 --- a/node/subscriptions-with-stripe/env.d.ts +++ b/node/subscriptions-with-stripe/env.d.ts @@ -3,11 +3,12 @@ declare global { interface ProcessEnv { APPWRITE_ENDPOINT?: string; APPWRITE_PROJECT_ID?: string; - APPWRITE_API_KEY?: string; - STRIPE_SECRET_KEY?: string; - STRIPE_WEBHOOK_SECRET?: string; - SUCCESS_URL?: string; - CANCEL_URL?: string; + APPWRITE_FUNCTION_PROJECT_ID: string; + APPWRITE_API_KEY: string; + STRIPE_SECRET_KEY: string; + STRIPE_WEBHOOK_SECRET: string; + SUCCESS_URL: string; + CANCEL_URL: string; } } } diff --git a/node/subscriptions-with-stripe/src/appwrite.js b/node/subscriptions-with-stripe/src/appwrite.js index 9f89bee8..270d6b47 100644 --- a/node/subscriptions-with-stripe/src/appwrite.js +++ b/node/subscriptions-with-stripe/src/appwrite.js @@ -5,17 +5,17 @@ const Subscriptions = { }; class AppwriteService { - /** - * @param {import('./environment').default} env - */ - constructor(env) { - this.env = env; - + constructor() { const client = new Client(); client - .setEndpoint(env.APPWRITE_ENDPOINT) - .setProject(env.APPWRITE_PROJECT_ID) - .setKey(env.APPWRITE_API_KEY); + .setEndpoint( + process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1' + ) + .setProject( + process.env.APPWRITE_PROJECT_I ?? + process.env.APPWRITE_FUNCTION_PROJECT_ID + ) + .setKey(process.env.APPWRITE_API_KEY); const databases = new Databases(client); this.databases = databases; @@ -26,7 +26,9 @@ class AppwriteService { */ async doesSubscribersDatabaseExist() { try { - await this.databases.get(this.env.DATABASE_ID); + await this.databases.get( + process.env.DATABASE_ID ?? 'stripe-subscriptions' + ); return true; } catch (err) { if (err.code === 404) return false; @@ -36,22 +38,25 @@ class AppwriteService { async setupSubscribersDatabase() { try { - await this.databases.create(this.env.DATABASE_ID, this.env.DATABASE_NAME); + await this.databases.create( + process.env.DATABASE_ID ?? 'stripe-subscriptions', + 'Stripe Subscriptions' + ); await this.databases.createCollection( - this.env.DATABASE_ID, - this.env.COLLECTION_ID, - this.env.COLLECTION_NAME + process.env.DATABASE_ID ?? 'stripe-subscriptions', + process.env.COLLECTION_ID ?? 'subscriptions', + 'Subscriptions' ); await this.databases.createStringAttribute( - this.env.DATABASE_ID, - this.env.COLLECTION_ID, + process.env.DATABASE_ID ?? 'stripe-subscriptions', + process.env.COLLECTION_ID ?? 'subscriptions', 'userId', 255, true ); await this.databases.createStringAttribute( - this.env.DATABASE_ID, - this.env.COLLECTION_ID, + process.env.DATABASE_ID ?? 'stripe-subscriptions', + process.env.COLLECTION_ID ?? 'subscriptions', 'subscriptionType', 255, true @@ -69,8 +74,8 @@ class AppwriteService { async hasSubscription(userId) { try { await this.databases.getDocument( - this.env.DATABASE_ID, - this.env.COLLECTION_ID, + process.env.DATABASE_ID ?? 'stripe-subscriptions', + process.env.COLLECTION_ID ?? 'subscriptions', userId ); return true; @@ -87,8 +92,8 @@ class AppwriteService { async deleteSubscription(userId) { try { await this.databases.deleteDocument( - this.env.DATABASE_ID, - this.env.COLLECTION_ID, + process.env.DATABASE_ID ?? 'stripe-subscriptions', + process.env.COLLECTION_ID ?? 'subscriptions', userId ); return true; @@ -104,8 +109,8 @@ class AppwriteService { async createSubscription(userId) { try { await this.databases.createDocument( - this.env.DATABASE_ID, - this.env.COLLECTION_ID, + process.env.DATABASE_ID ?? 'stripe-subscriptions', + process.env.COLLECTION_ID ?? 'subscriptions', userId, { subscriptionType: Subscriptions.PREMIUM, diff --git a/node/subscriptions-with-stripe/src/environment.js b/node/subscriptions-with-stripe/src/environment.js deleted file mode 100644 index 4b2585b4..00000000 --- a/node/subscriptions-with-stripe/src/environment.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * @param {string} key - * @return {string} - */ -function getRequiredEnv(key) { - const value = process.env[key]; - if (value === undefined) { - throw new Error(`Environment variable ${key} is not set`); - } - return value; -} - -/** - * @param {string} key - * @return {string} - */ -function getRequiredUrlEnv(key) { - const value = getRequiredEnv(key); - if (!isValidUrl(value)) { - throw new Error(`Environment variable ${key} is a not valid URL`); - } - return value; -} - -/** - * @param {string | undefined} url - * @returns {boolean} - */ -function isValidUrl(url) { - if (!url) return false; - try { - new URL(url); - return true; - } catch (err) { - return false; - } -} - -class EnvironmentService { - APPWRITE_ENDPOINT = isValidUrl(process.env.APPWRITE_ENDPOINT) - ? process.env.APPWRITE_ENDPOINT - : 'https://cloud.appwrite.io/v1'; - APPWRITE_PROJECT_ID = getRequiredEnv('APPWRITE_PROJECT_ID'); - APPWRITE_API_KEY = getRequiredEnv('APPWRITE_API_KEY'); - STRIPE_WEBHOOK_SECRET = getRequiredEnv('STRIPE_WEBHOOK_SECRET'); - STRIPE_SECRET_KEY = getRequiredEnv('STRIPE_SECRET_KEY'); - SUCCESS_URL = getRequiredUrlEnv('SUCCESS_URL'); - CANCEL_URL = getRequiredUrlEnv('CANCEL_URL'); - DATABASE_ID = process.env.DATABASE_ID ?? 'stripe-subscriptions'; - DATABASE_NAME = 'Stripe Subscriptions'; - COLLECTION_ID = process.env.COLLECTION_ID ?? 'subscriptions'; - COLLECTION_NAME = 'Subscriptions'; -} - -export default EnvironmentService; diff --git a/node/subscriptions-with-stripe/src/main.js b/node/subscriptions-with-stripe/src/main.js index bbcb6c28..5327dc93 100644 --- a/node/subscriptions-with-stripe/src/main.js +++ b/node/subscriptions-with-stripe/src/main.js @@ -1,24 +1,30 @@ import StripeService from './stripe.js'; import AppwriteService from './appwrite.js'; -import EnvironmentService from './environment.js'; +import { throwIfMissing } from './utils.js'; export default async ({ req, res, log, error }) => { - const env = new EnvironmentService(); - const appwrite = new AppwriteService(env); - const stripe = new StripeService(env); + throwIfMissing(process.env, [ + 'STRIPE_SECRET_KEY', + 'STRIPE_WEBHOOK_SECRET', + 'SUCCESS_URL', + 'CANCEL_URL', + ]); + + const appwrite = new AppwriteService(); + const stripe = new StripeService(); switch (req.path) { case '/checkout': const userId = req.headers['x-appwrite-user-id']; if (!userId) { error('User ID not found in request.'); - return res.redirect(env.CANCEL_URL, 303); + return res.redirect(process.env.CANCEL_URL, 303); } const session = await stripe.checkoutSubscription(userId); if (!session) { error('Failed to create Stripe checkout session.'); - return res.redirect(env.CANCEL_URL, 303); + return res.redirect(process.env.CANCEL_URL, 303); } log(`Created Stripe checkout session for user ${userId}.`); diff --git a/node/subscriptions-with-stripe/src/setup.js b/node/subscriptions-with-stripe/src/setup.js index 5aef9af1..dc47977e 100644 --- a/node/subscriptions-with-stripe/src/setup.js +++ b/node/subscriptions-with-stripe/src/setup.js @@ -1,5 +1,4 @@ import AppwriteService from './appwrite.js'; -import EnvironmentService from './environment.js'; /** * Setup script for the subscribers database. @@ -8,8 +7,7 @@ import EnvironmentService from './environment.js'; async function setup() { console.log('Executing setup script...'); - const env = new EnvironmentService(); - const appwrite = new AppwriteService(env); + const appwrite = new AppwriteService(); if (await appwrite.doesSubscribersDatabaseExist()) { console.log(`Database exists.`); diff --git a/node/subscriptions-with-stripe/src/stripe.js b/node/subscriptions-with-stripe/src/stripe.js index 24814c08..043b216e 100644 --- a/node/subscriptions-with-stripe/src/stripe.js +++ b/node/subscriptions-with-stripe/src/stripe.js @@ -3,12 +3,7 @@ import stripe from 'stripe'; class StripeService { - /** - * @param {import('./environment.js').default} env - */ constructor(env) { - this.env = env; - // Note: stripe cjs API types are faulty /** @type {import('stripe').Stripe} */ // @ts-ignore @@ -19,26 +14,27 @@ class StripeService { * @param {string} userId */ async checkoutSubscription(userId) { + /** @type {import('stripe').Stripe.Checkout.SessionCreateParams.LineItem} */ + const lineItem = { + price_data: { + currency: 'usd', + product_data: { + name: 'Premium Subscription', + }, + unit_amount: 1000, + recurring: { + interval: 'year', + }, + }, + quantity: 1, + }; + try { return await this.client.checkout.sessions.create({ payment_method_types: ['card'], - line_items: [ - { - price_data: { - currency: 'usd', - product_data: { - name: 'Premium Subscription', - }, - unit_amount: 1000, - recurring: { - interval: 'year', - }, - }, - quantity: 1, - }, - ], - success_url: this.env.SUCCESS_URL, - cancel_url: this.env.CANCEL_URL, + line_items: [lineItem], + success_url: process.env.SUCCESS_URL, + cancel_url: process.env.CANCEL_URL, client_reference_id: userId, metadata: { userId, @@ -58,7 +54,7 @@ class StripeService { const event = this.client.webhooks.constructEvent( req.body, req.headers['stripe-signature'], - this.env.STRIPE_WEBHOOK_SECRET + process.env.STRIPE_WEBHOOK_SECRET ); return /** @type {import("stripe").Stripe.DiscriminatedEvent} */ (event); } catch (err) { diff --git a/node/subscriptions-with-stripe/src/utils.js b/node/subscriptions-with-stripe/src/utils.js new file mode 100644 index 00000000..dcca7015 --- /dev/null +++ b/node/subscriptions-with-stripe/src/utils.js @@ -0,0 +1,17 @@ +/** + * Throws an error if any of the keys are missing from the object + * @param {*} obj + * @param {string[]} keys + * @throws {Error} + */ +export function throwIfMissing(obj, keys) { + const missing = []; + for (let key of keys) { + if (!(key in obj) || !obj[key]) { + missing.push(key); + } + } + if (missing.length > 0) { + throw new Error(`Missing required fields: ${missing.join(', ')}`); + } +} From 83499081c5ffd4bb9b91d3c5c405e506b284406c Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 9 Aug 2023 10:38:02 +0100 Subject: [PATCH 13/35] feat: use appwrite_function_project_id --- node/subscriptions-with-stripe/README.md | 10 ---------- node/subscriptions-with-stripe/env.d.ts | 1 - node/subscriptions-with-stripe/src/appwrite.js | 5 +---- 3 files changed, 1 insertion(+), 15 deletions(-) diff --git a/node/subscriptions-with-stripe/README.md b/node/subscriptions-with-stripe/README.md index 830520b0..5d6b978b 100644 --- a/node/subscriptions-with-stripe/README.md +++ b/node/subscriptions-with-stripe/README.md @@ -87,16 +87,6 @@ The endpoint where your Appwrite server is located. If not provided, it defaults | Required | No | | Sample Value | `https://cloud.appwrite.io/v1` | -### APPWRITE_PROJECT_ID - -The ID of your Appwrite project. - -| Question | Answer | -| ------------- | ----------------------------------------------------------------------------------------- | -| Required | Yes | -| Sample Value | `builtWithAppwrite` | -| Documentation | [Appwrite: Getting Started](https://appwrite.io/docs/getting-started-for-web#addPlatform) | - ### STRIPE_SECRET_KEY Secret for sending requests to the Stripe API. diff --git a/node/subscriptions-with-stripe/env.d.ts b/node/subscriptions-with-stripe/env.d.ts index 779a1c72..8cfe2325 100644 --- a/node/subscriptions-with-stripe/env.d.ts +++ b/node/subscriptions-with-stripe/env.d.ts @@ -2,7 +2,6 @@ declare global { namespace NodeJS { interface ProcessEnv { APPWRITE_ENDPOINT?: string; - APPWRITE_PROJECT_ID?: string; APPWRITE_FUNCTION_PROJECT_ID: string; APPWRITE_API_KEY: string; STRIPE_SECRET_KEY: string; diff --git a/node/subscriptions-with-stripe/src/appwrite.js b/node/subscriptions-with-stripe/src/appwrite.js index 270d6b47..9d37f452 100644 --- a/node/subscriptions-with-stripe/src/appwrite.js +++ b/node/subscriptions-with-stripe/src/appwrite.js @@ -11,10 +11,7 @@ class AppwriteService { .setEndpoint( process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1' ) - .setProject( - process.env.APPWRITE_PROJECT_I ?? - process.env.APPWRITE_FUNCTION_PROJECT_ID - ) + .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID) .setKey(process.env.APPWRITE_API_KEY); const databases = new Databases(client); From 04329cca49c418b6e08435a67892c947b14ebedb Mon Sep 17 00:00:00 2001 From: loks0n <22452787+loks0n@users.noreply.github.com> Date: Wed, 9 Aug 2023 13:02:48 +0100 Subject: [PATCH 14/35] fix: more robust setup --- node/subscriptions-with-stripe/src/appwrite.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/node/subscriptions-with-stripe/src/appwrite.js b/node/subscriptions-with-stripe/src/appwrite.js index 9d37f452..24f5ea61 100644 --- a/node/subscriptions-with-stripe/src/appwrite.js +++ b/node/subscriptions-with-stripe/src/appwrite.js @@ -39,11 +39,21 @@ class AppwriteService { process.env.DATABASE_ID ?? 'stripe-subscriptions', 'Stripe Subscriptions' ); + } catch (err) { + // If resource already exists, we can ignore the error + if (err.code !== 409) throw err; + } + try { await this.databases.createCollection( process.env.DATABASE_ID ?? 'stripe-subscriptions', process.env.COLLECTION_ID ?? 'subscriptions', 'Subscriptions' ); + } catch (err) { + if (err.code !== 409) throw err; + } + + try { await this.databases.createStringAttribute( process.env.DATABASE_ID ?? 'stripe-subscriptions', process.env.COLLECTION_ID ?? 'subscriptions', @@ -51,6 +61,10 @@ class AppwriteService { 255, true ); + } catch (err) { + if (err.code !== 409) throw err; + } + try { await this.databases.createStringAttribute( process.env.DATABASE_ID ?? 'stripe-subscriptions', process.env.COLLECTION_ID ?? 'subscriptions', @@ -59,7 +73,6 @@ class AppwriteService { true ); } catch (err) { - // If resource already exists, we can ignore the error if (err.code !== 409) throw err; } } From 1d2513647d709f0661a91f7360176e04a56f52dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 Sep 2023 15:01:34 +0200 Subject: [PATCH 15/35] Migrate stripe sub to labels --- node/subscriptions-with-stripe/README.md | 9 +- node/subscriptions-with-stripe/env.d.ts | 2 +- .../package-lock.json | 11 +- node/subscriptions-with-stripe/package.json | 3 +- .../subscriptions-with-stripe/src/appwrite.js | 120 ++---------------- node/subscriptions-with-stripe/src/main.js | 24 ++-- node/subscriptions-with-stripe/src/setup.js | 21 --- node/subscriptions-with-stripe/src/stripe.js | 14 +- 8 files changed, 39 insertions(+), 165 deletions(-) delete mode 100644 node/subscriptions-with-stripe/src/setup.js diff --git a/node/subscriptions-with-stripe/README.md b/node/subscriptions-with-stripe/README.md index 5d6b978b..9e0b8f2b 100644 --- a/node/subscriptions-with-stripe/README.md +++ b/node/subscriptions-with-stripe/README.md @@ -1,10 +1,10 @@ -# ⚡ Stripe Subscriptions Function +# 💳 Node.js Stripe Subscriptions Function Integrates Stripe subscriptions into your Appwrite project. Collect card payment with the `/checkout` endpoint and check the status of a user subscription using the `Subscriptions` collection. ## 🧰 Usage -### `POST /checkout` +### `POST /subscribe` This endpoint initiates a Stripe checkout session for a subscription. The user ID is fetched from the headers of the request. If the user ID is not found or a Stripe checkout session could not be created, the request will be redirected to a cancel URL. @@ -62,7 +62,6 @@ Sample `401` Response: | Runtime | Node (18.0) | | Entrypoint | `src/main.js` | | Build Commands | `npm install` | -| | `npm run setup` | | Permissions | `any` | | Timeout (Seconds) | 15 | @@ -117,14 +116,14 @@ The URL to redirect to after a successful payment. | Sample Value | `https://example.com/success` | | Documentation | [Stripe: Redirects](https://stripe.com/docs/payments/checkout/redirect) | -### CANCEL_URL +### FAILURE_URL The URL to redirect to after a cancelled payment attempt. | Question | Answer | | ------------- | ----------------------------------------------------------------------- | | Required | Yes | -| Sample Value | `https://example.com/cancel` | +| Sample Value | `https://example.com/failure` | | Documentation | [Stripe: Redirects](https://stripe.com/docs/payments/checkout/redirect) | ### DATABASE_ID diff --git a/node/subscriptions-with-stripe/env.d.ts b/node/subscriptions-with-stripe/env.d.ts index 8cfe2325..a52f24ee 100644 --- a/node/subscriptions-with-stripe/env.d.ts +++ b/node/subscriptions-with-stripe/env.d.ts @@ -7,7 +7,7 @@ declare global { STRIPE_SECRET_KEY: string; STRIPE_WEBHOOK_SECRET: string; SUCCESS_URL: string; - CANCEL_URL: string; + FAILURE_URL: string; } } } diff --git a/node/subscriptions-with-stripe/package-lock.json b/node/subscriptions-with-stripe/package-lock.json index a9ca97dd..5f3e1c59 100644 --- a/node/subscriptions-with-stripe/package-lock.json +++ b/node/subscriptions-with-stripe/package-lock.json @@ -7,9 +7,8 @@ "": { "name": "subscriptions-with-stripe", "version": "1.0.0", - "license": "MIT", "dependencies": { - "node-appwrite": "^9.0.0", + "node-appwrite": "^10.0.0", "stripe": "^12.12.0" }, "devDependencies": { @@ -172,11 +171,11 @@ } }, "node_modules/node-appwrite": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-9.0.0.tgz", - "integrity": "sha512-iTcHbuaJfr6bP/HFkRVV+FcaumKkbINqZyypQdl+tYxv6Dx0bkB/YKUXGYfTkgP18TLPWQQB++OGQhi98dlo2w==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-10.0.1.tgz", + "integrity": "sha512-J+tccrkJ83D7s1pT3HoBCfNnlA/e/nbBVFuVCJUl0/6oYZndVi5ssHjDWtCiIEESNusVck/8dkpVldMzZzf7Lw==", "dependencies": { - "axios": "^1.3.6", + "axios": "^1.4.0", "form-data": "^4.0.0" } }, diff --git a/node/subscriptions-with-stripe/package.json b/node/subscriptions-with-stripe/package.json index 04b2d839..7570e6a6 100644 --- a/node/subscriptions-with-stripe/package.json +++ b/node/subscriptions-with-stripe/package.json @@ -5,11 +5,10 @@ "main": "src/main.js", "type": "module", "scripts": { - "setup": "node src/setup.js", "format": "prettier --write ." }, "dependencies": { - "node-appwrite": "^9.0.0", + "node-appwrite": "^10.0.0", "stripe": "^12.12.0" }, "devDependencies": { diff --git a/node/subscriptions-with-stripe/src/appwrite.js b/node/subscriptions-with-stripe/src/appwrite.js index 24f5ea61..8cba76f0 100644 --- a/node/subscriptions-with-stripe/src/appwrite.js +++ b/node/subscriptions-with-stripe/src/appwrite.js @@ -1,8 +1,6 @@ -import { Client, Databases } from 'node-appwrite'; +import { Client, Users } from 'node-appwrite'; -const Subscriptions = { - PREMIUM: 'premium', -}; +const LabelsSubscriber = 'subscriber'; class AppwriteService { constructor() { @@ -14,122 +12,28 @@ class AppwriteService { .setProject(process.env.APPWRITE_FUNCTION_PROJECT_ID) .setKey(process.env.APPWRITE_API_KEY); - const databases = new Databases(client); - this.databases = databases; - } - - /** - * @returns {Promise} - */ - async doesSubscribersDatabaseExist() { - try { - await this.databases.get( - process.env.DATABASE_ID ?? 'stripe-subscriptions' - ); - return true; - } catch (err) { - if (err.code === 404) return false; - throw err; - } - } - - async setupSubscribersDatabase() { - try { - await this.databases.create( - process.env.DATABASE_ID ?? 'stripe-subscriptions', - 'Stripe Subscriptions' - ); - } catch (err) { - // If resource already exists, we can ignore the error - if (err.code !== 409) throw err; - } - try { - await this.databases.createCollection( - process.env.DATABASE_ID ?? 'stripe-subscriptions', - process.env.COLLECTION_ID ?? 'subscriptions', - 'Subscriptions' - ); - } catch (err) { - if (err.code !== 409) throw err; - } - - try { - await this.databases.createStringAttribute( - process.env.DATABASE_ID ?? 'stripe-subscriptions', - process.env.COLLECTION_ID ?? 'subscriptions', - 'userId', - 255, - true - ); - } catch (err) { - if (err.code !== 409) throw err; - } - try { - await this.databases.createStringAttribute( - process.env.DATABASE_ID ?? 'stripe-subscriptions', - process.env.COLLECTION_ID ?? 'subscriptions', - 'subscriptionType', - 255, - true - ); - } catch (err) { - if (err.code !== 409) throw err; - } + this.users = new Users(client); } /** * @param {string} userId - * @returns {Promise} - */ - async hasSubscription(userId) { - try { - await this.databases.getDocument( - process.env.DATABASE_ID ?? 'stripe-subscriptions', - process.env.COLLECTION_ID ?? 'subscriptions', - userId - ); - return true; - } catch (err) { - if (err.code !== 404) throw err; - return false; - } - } - - /** - * @param {string} userId - * @returns {Promise} + * @returns {Promise} */ async deleteSubscription(userId) { - try { - await this.databases.deleteDocument( - process.env.DATABASE_ID ?? 'stripe-subscriptions', - process.env.COLLECTION_ID ?? 'subscriptions', - userId - ); - return true; - } catch (err) { - return false; - } + const labels = (await this.users.get(userId)).labels.filter((label) => label !== LabelsSubscriber); + + await this.users.updateLabels(userId, labels); } /** * @param {string} userId - * @returns {Promise} + * @returns {Promise} */ async createSubscription(userId) { - try { - await this.databases.createDocument( - process.env.DATABASE_ID ?? 'stripe-subscriptions', - process.env.COLLECTION_ID ?? 'subscriptions', - userId, - { - subscriptionType: Subscriptions.PREMIUM, - } - ); - return true; - } catch (err) { - return false; - } + const labels = (await this.users.get(userId)).labels; + labels.push(LabelsSubscriber); + + await this.users.updateLabels(userId, labels); } } diff --git a/node/subscriptions-with-stripe/src/main.js b/node/subscriptions-with-stripe/src/main.js index 5327dc93..088ab181 100644 --- a/node/subscriptions-with-stripe/src/main.js +++ b/node/subscriptions-with-stripe/src/main.js @@ -7,24 +7,24 @@ export default async ({ req, res, log, error }) => { 'STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET', 'SUCCESS_URL', - 'CANCEL_URL', + 'FAILURE_URL', ]); const appwrite = new AppwriteService(); const stripe = new StripeService(); switch (req.path) { - case '/checkout': + case '/subscribe': const userId = req.headers['x-appwrite-user-id']; if (!userId) { error('User ID not found in request.'); - return res.redirect(process.env.CANCEL_URL, 303); + return res.redirect(process.env.FAILURE_URL, 303); } const session = await stripe.checkoutSubscription(userId); if (!session) { error('Failed to create Stripe checkout session.'); - return res.redirect(process.env.CANCEL_URL, 303); + return res.redirect(process.env.FAILURE_URL, 303); } log(`Created Stripe checkout session for user ${userId}.`); @@ -32,32 +32,26 @@ export default async ({ req, res, log, error }) => { case '/webhook': const event = stripe.validateWebhook(req); - if (!event) return res.json({ success: false }, 401); + if (!event) { + return res.json({ success: false }, 401); + } if (event.type === 'customer.subscription.created') { const session = event.data.object; const userId = session.metadata.userId; - if (await appwrite.hasSubscription(userId)) { - error(`Subscription already exists - skipping`); - return res.json({ success: true }); - } - await appwrite.createSubscription(userId); log(`Created subscription for user ${userId}`); + return res.json({ success: true }); } if (event.type === 'customer.subscription.deleted') { const session = event.data.object; const userId = session.metadata.userId; - if (!(await appwrite.hasSubscription(userId))) { - error(`Subscription does not exist - skipping`); - return res.json({ success: true }); - } - await appwrite.deleteSubscription(userId); log(`Deleted subscription for user ${userId}`); + return res.json({ success: true }); } return res.json({ success: true }); diff --git a/node/subscriptions-with-stripe/src/setup.js b/node/subscriptions-with-stripe/src/setup.js deleted file mode 100644 index dc47977e..00000000 --- a/node/subscriptions-with-stripe/src/setup.js +++ /dev/null @@ -1,21 +0,0 @@ -import AppwriteService from './appwrite.js'; - -/** - * Setup script for the subscribers database. - * If the database already exists, this script will do nothing. - */ -async function setup() { - console.log('Executing setup script...'); - - const appwrite = new AppwriteService(); - - if (await appwrite.doesSubscribersDatabaseExist()) { - console.log(`Database exists.`); - return; - } - - await appwrite.setupSubscribersDatabase(); - console.log(`Database created.`); -} - -setup(); diff --git a/node/subscriptions-with-stripe/src/stripe.js b/node/subscriptions-with-stripe/src/stripe.js index 043b216e..37ae0206 100644 --- a/node/subscriptions-with-stripe/src/stripe.js +++ b/node/subscriptions-with-stripe/src/stripe.js @@ -3,11 +3,11 @@ import stripe from 'stripe'; class StripeService { - constructor(env) { + constructor() { // Note: stripe cjs API types are faulty /** @type {import('stripe').Stripe} */ // @ts-ignore - this.client = stripe(env.STRIPE_SECRET_KEY); + this.client = stripe(process.env.STRIPE_SECRET_KEY); } /** @@ -17,14 +17,14 @@ class StripeService { /** @type {import('stripe').Stripe.Checkout.SessionCreateParams.LineItem} */ const lineItem = { price_data: { + unit_amount: 1000, // $10.00 currency: 'usd', + recurring: { + interval: 'month', + }, product_data: { name: 'Premium Subscription', }, - unit_amount: 1000, - recurring: { - interval: 'year', - }, }, quantity: 1, }; @@ -52,7 +52,7 @@ class StripeService { validateWebhook(req) { try { const event = this.client.webhooks.constructEvent( - req.body, + req.bodyRaw, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET ); From 7ce17d890aab123e9ac4d2c58a969ccf51002fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 Sep 2023 15:06:44 +0200 Subject: [PATCH 16/35] URLs to be optional --- node/subscriptions-with-stripe/README.md | 24 ++------------------ node/subscriptions-with-stripe/src/main.js | 4 ++-- node/subscriptions-with-stripe/src/stripe.js | 4 ++-- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/node/subscriptions-with-stripe/README.md b/node/subscriptions-with-stripe/README.md index 9e0b8f2b..9870e62f 100644 --- a/node/subscriptions-with-stripe/README.md +++ b/node/subscriptions-with-stripe/README.md @@ -112,7 +112,7 @@ The URL to redirect to after a successful payment. | Question | Answer | | ------------- | ----------------------------------------------------------------------- | -| Required | Yes | +| Required | No | | Sample Value | `https://example.com/success` | | Documentation | [Stripe: Redirects](https://stripe.com/docs/payments/checkout/redirect) | @@ -122,26 +122,6 @@ The URL to redirect to after a cancelled payment attempt. | Question | Answer | | ------------- | ----------------------------------------------------------------------- | -| Required | Yes | +| Required | No | | Sample Value | `https://example.com/failure` | | Documentation | [Stripe: Redirects](https://stripe.com/docs/payments/checkout/redirect) | - -### DATABASE_ID - -The ID for the database where subscriptions will be stored. If not provided, it defaults to "stripe-subscriptions". - -| Question | Answer | -| ------------- | --------------------------------------------------------- | -| Required | No | -| Sample Value | `stripe-subscriptions` | -| Documentation | [Appwrite: Databases](https://appwrite.io/docs/databases) | - -### COLLECTION_ID - -The ID for the collection within the database. If not provided, it defaults to "subscriptions". - -| Question | Answer | -| ------------- | ------------------------------------------------------------- | -| Required | No | -| Sample Value | `subscriptions` | -| Documentation | [Appwrite: Collections](https://appwrite.io/docs/collections) | diff --git a/node/subscriptions-with-stripe/src/main.js b/node/subscriptions-with-stripe/src/main.js index 088ab181..bde8eca7 100644 --- a/node/subscriptions-with-stripe/src/main.js +++ b/node/subscriptions-with-stripe/src/main.js @@ -18,13 +18,13 @@ export default async ({ req, res, log, error }) => { const userId = req.headers['x-appwrite-user-id']; if (!userId) { error('User ID not found in request.'); - return res.redirect(process.env.FAILURE_URL, 303); + return res.redirect(process.env.FAILURE_URL ?? '/', 303); } const session = await stripe.checkoutSubscription(userId); if (!session) { error('Failed to create Stripe checkout session.'); - return res.redirect(process.env.FAILURE_URL, 303); + return res.redirect(process.env.FAILURE_URL ?? '/', 303); } log(`Created Stripe checkout session for user ${userId}.`); diff --git a/node/subscriptions-with-stripe/src/stripe.js b/node/subscriptions-with-stripe/src/stripe.js index 37ae0206..8cd74335 100644 --- a/node/subscriptions-with-stripe/src/stripe.js +++ b/node/subscriptions-with-stripe/src/stripe.js @@ -33,8 +33,8 @@ class StripeService { return await this.client.checkout.sessions.create({ payment_method_types: ['card'], line_items: [lineItem], - success_url: process.env.SUCCESS_URL, - cancel_url: process.env.CANCEL_URL, + success_url: process.env.SUCCESS_URL ?? '/', + cancel_url: process.env.FAILURE_URL ?? '/', client_reference_id: userId, metadata: { userId, From 9c1bead652d7a6b01dc7da9f1656aeb1d138834e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 Sep 2023 15:09:09 +0200 Subject: [PATCH 17/35] Fix required env vars --- node/subscriptions-with-stripe/src/main.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/node/subscriptions-with-stripe/src/main.js b/node/subscriptions-with-stripe/src/main.js index bde8eca7..608185f7 100644 --- a/node/subscriptions-with-stripe/src/main.js +++ b/node/subscriptions-with-stripe/src/main.js @@ -6,8 +6,7 @@ export default async ({ req, res, log, error }) => { throwIfMissing(process.env, [ 'STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET', - 'SUCCESS_URL', - 'FAILURE_URL', + 'APPWRITE_API_KEY' ]); const appwrite = new AppwriteService(); From be693933cffc83f431a0ad35f9fa81bfadf7b6a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 Sep 2023 15:15:03 +0200 Subject: [PATCH 18/35] Add basic HTML file --- node/subscriptions-with-stripe/src/main.js | 9 ++++ .../static/index.html | 47 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 node/subscriptions-with-stripe/static/index.html diff --git a/node/subscriptions-with-stripe/src/main.js b/node/subscriptions-with-stripe/src/main.js index 608185f7..0023a056 100644 --- a/node/subscriptions-with-stripe/src/main.js +++ b/node/subscriptions-with-stripe/src/main.js @@ -9,6 +9,15 @@ export default async ({ req, res, log, error }) => { 'APPWRITE_API_KEY' ]); + if (req.method === 'GET') { + const html = interpolate(getStaticFile('index.html'), { + APPWRITE_ENDPOINT: process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1', + APPWRITE_FUNCTION_PROJECT_ID: process.env.APPWRITE_FUNCTION_PROJECT_ID, + }); + + return res.send(html, 200, { 'Content-Type': 'text/html; charset=utf-8' }); + } + const appwrite = new AppwriteService(); const stripe = new StripeService(); diff --git a/node/subscriptions-with-stripe/static/index.html b/node/subscriptions-with-stripe/static/index.html new file mode 100644 index 00000000..900e4532 --- /dev/null +++ b/node/subscriptions-with-stripe/static/index.html @@ -0,0 +1,47 @@ + + + + + + + Stripe Subscriptions Demo + + + + + +
+
+
+
+

Stripe Subscriptions Demo

+ +
+

+ Use this demo to create Stripe subscription. Once subscribed, label + is added to an user. You can then use this to give permissions to + subscribers only. +

+
+
+
+
+

TODO

+
+
+
+ + + + + From 15e83317e6675d8b8c9329889bce761c06464251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 Sep 2023 15:16:32 +0200 Subject: [PATCH 19/35] add file utils --- node/subscriptions-with-stripe/src/main.js | 2 +- node/subscriptions-with-stripe/src/utils.js | 26 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/node/subscriptions-with-stripe/src/main.js b/node/subscriptions-with-stripe/src/main.js index 0023a056..74034a77 100644 --- a/node/subscriptions-with-stripe/src/main.js +++ b/node/subscriptions-with-stripe/src/main.js @@ -1,6 +1,6 @@ import StripeService from './stripe.js'; import AppwriteService from './appwrite.js'; -import { throwIfMissing } from './utils.js'; +import { getStaticFile, interpolate, throwIfMissing } from './utils.js'; export default async ({ req, res, log, error }) => { throwIfMissing(process.env, [ diff --git a/node/subscriptions-with-stripe/src/utils.js b/node/subscriptions-with-stripe/src/utils.js index dcca7015..5488c2eb 100644 --- a/node/subscriptions-with-stripe/src/utils.js +++ b/node/subscriptions-with-stripe/src/utils.js @@ -1,3 +1,7 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; +import fs from 'fs'; + /** * Throws an error if any of the keys are missing from the object * @param {*} obj @@ -15,3 +19,25 @@ export function throwIfMissing(obj, keys) { throw new Error(`Missing required fields: ${missing.join(', ')}`); } } + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const staticFolder = path.join(__dirname, '../static'); + +/** + * Returns the contents of a file in the static folder + * @param {string} fileName + * @returns {string} Contents of static/{fileName} + */ +export function getStaticFile(fileName) { + return fs.readFileSync(path.join(staticFolder, fileName)).toString(); +} + +/** + * @param {string} template + * @param {Record} values + * @returns {string} + */ +export function interpolate(template, values) { + return template.replace(/{{([^}]+)}}/g, (_, key) => values[key] || ''); +} \ No newline at end of file From 2c070ae5f1a2a5e920f1877689fb7d73d2d35f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 Sep 2023 15:45:24 +0200 Subject: [PATCH 20/35] Implement frontend --- node/subscriptions-with-stripe/src/main.js | 5 +- node/subscriptions-with-stripe/src/stripe.js | 6 +- .../static/index.html | 104 +++++++++++++++++- 3 files changed, 106 insertions(+), 9 deletions(-) diff --git a/node/subscriptions-with-stripe/src/main.js b/node/subscriptions-with-stripe/src/main.js index 74034a77..62905789 100644 --- a/node/subscriptions-with-stripe/src/main.js +++ b/node/subscriptions-with-stripe/src/main.js @@ -13,6 +13,7 @@ export default async ({ req, res, log, error }) => { const html = interpolate(getStaticFile('index.html'), { APPWRITE_ENDPOINT: process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1', APPWRITE_FUNCTION_PROJECT_ID: process.env.APPWRITE_FUNCTION_PROJECT_ID, + APPWRITE_FUNCTION_ID: process.env.APPWRITE_FUNCTION_ID }); return res.send(html, 200, { 'Content-Type': 'text/html; charset=utf-8' }); @@ -29,7 +30,7 @@ export default async ({ req, res, log, error }) => { return res.redirect(process.env.FAILURE_URL ?? '/', 303); } - const session = await stripe.checkoutSubscription(userId); + const session = await stripe.checkoutSubscription(context, userId); if (!session) { error('Failed to create Stripe checkout session.'); return res.redirect(process.env.FAILURE_URL ?? '/', 303); @@ -39,7 +40,7 @@ export default async ({ req, res, log, error }) => { return res.redirect(session.url, 303); case '/webhook': - const event = stripe.validateWebhook(req); + const event = stripe.validateWebhook(context, req); if (!event) { return res.json({ success: false }, 401); } diff --git a/node/subscriptions-with-stripe/src/stripe.js b/node/subscriptions-with-stripe/src/stripe.js index 8cd74335..232ce8e7 100644 --- a/node/subscriptions-with-stripe/src/stripe.js +++ b/node/subscriptions-with-stripe/src/stripe.js @@ -13,7 +13,7 @@ class StripeService { /** * @param {string} userId */ - async checkoutSubscription(userId) { + async checkoutSubscription(context, userId) { /** @type {import('stripe').Stripe.Checkout.SessionCreateParams.LineItem} */ const lineItem = { price_data: { @@ -42,6 +42,7 @@ class StripeService { mode: 'subscription', }); } catch (err) { + context.error(err); return null; } } @@ -49,7 +50,7 @@ class StripeService { /** * @returns {import("stripe").Stripe.DiscriminatedEvent | null} */ - validateWebhook(req) { + validateWebhook(context, req) { try { const event = this.client.webhooks.constructEvent( req.bodyRaw, @@ -58,6 +59,7 @@ class StripeService { ); return /** @type {import("stripe").Stripe.DiscriminatedEvent} */ (event); } catch (err) { + context.error(err); return null; } } diff --git a/node/subscriptions-with-stripe/static/index.html b/node/subscriptions-with-stripe/static/index.html index 900e4532..777d3095 100644 --- a/node/subscriptions-with-stripe/static/index.html +++ b/node/subscriptions-with-stripe/static/index.html @@ -8,6 +8,8 @@ + +
@@ -29,19 +31,111 @@

Stripe Subscriptions Demo

-
+
-

TODO

+ + +
From 3d07d9595f1137f0a87e1c15bd3526f9476c4e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 Sep 2023 15:46:22 +0200 Subject: [PATCH 21/35] Introduce context var --- node/subscriptions-with-stripe/src/main.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/node/subscriptions-with-stripe/src/main.js b/node/subscriptions-with-stripe/src/main.js index 62905789..b1826d1e 100644 --- a/node/subscriptions-with-stripe/src/main.js +++ b/node/subscriptions-with-stripe/src/main.js @@ -2,7 +2,9 @@ import StripeService from './stripe.js'; import AppwriteService from './appwrite.js'; import { getStaticFile, interpolate, throwIfMissing } from './utils.js'; -export default async ({ req, res, log, error }) => { +export default async (context) => { + const { req, res, log, error } = context; + throwIfMissing(process.env, [ 'STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET', From b6ca9d5969bae87dbc1cc07dfa59d518d8602e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 Sep 2023 15:52:09 +0200 Subject: [PATCH 22/35] Fix redirect URL --- node/subscriptions-with-stripe/src/main.js | 8 +++++--- node/subscriptions-with-stripe/src/stripe.js | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/node/subscriptions-with-stripe/src/main.js b/node/subscriptions-with-stripe/src/main.js index b1826d1e..401215f5 100644 --- a/node/subscriptions-with-stripe/src/main.js +++ b/node/subscriptions-with-stripe/src/main.js @@ -26,16 +26,18 @@ export default async (context) => { switch (req.path) { case '/subscribe': + const fallbackUrl = req.scheme + '://' + req.headers['host'] + '/'; + const userId = req.headers['x-appwrite-user-id']; if (!userId) { error('User ID not found in request.'); - return res.redirect(process.env.FAILURE_URL ?? '/', 303); + return res.redirect(process.env.FAILURE_URL ?? fallbackUrl, 303); } - const session = await stripe.checkoutSubscription(context, userId); + const session = await stripe.checkoutSubscription(context, userId, fallbackUrl); if (!session) { error('Failed to create Stripe checkout session.'); - return res.redirect(process.env.FAILURE_URL ?? '/', 303); + return res.redirect(process.env.FAILURE_URL ?? fallbackUrl, 303); } log(`Created Stripe checkout session for user ${userId}.`); diff --git a/node/subscriptions-with-stripe/src/stripe.js b/node/subscriptions-with-stripe/src/stripe.js index 232ce8e7..5389d192 100644 --- a/node/subscriptions-with-stripe/src/stripe.js +++ b/node/subscriptions-with-stripe/src/stripe.js @@ -12,8 +12,9 @@ class StripeService { /** * @param {string} userId + * @param {string} fallbackUrl */ - async checkoutSubscription(context, userId) { + async checkoutSubscription(context, userId, fallbackUrl) { /** @type {import('stripe').Stripe.Checkout.SessionCreateParams.LineItem} */ const lineItem = { price_data: { @@ -33,8 +34,8 @@ class StripeService { return await this.client.checkout.sessions.create({ payment_method_types: ['card'], line_items: [lineItem], - success_url: process.env.SUCCESS_URL ?? '/', - cancel_url: process.env.FAILURE_URL ?? '/', + success_url: process.env.SUCCESS_URL ?? fallbackUrl, + cancel_url: process.env.FAILURE_URL ?? fallbackUrl, client_reference_id: userId, metadata: { userId, From 2c88880e1e4c0cf744af723d9bfd0272853ecee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Wed, 6 Sep 2023 15:54:05 +0200 Subject: [PATCH 23/35] Remove dev changes --- node/subscriptions-with-stripe/static/index.html | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/node/subscriptions-with-stripe/static/index.html b/node/subscriptions-with-stripe/static/index.html index 777d3095..ee2fecea 100644 --- a/node/subscriptions-with-stripe/static/index.html +++ b/node/subscriptions-with-stripe/static/index.html @@ -100,17 +100,15 @@

Stripe Subscriptions Demo

+ + +
+
+
+
+

Stripe Subscriptions Demo

+ +
+

+ Use this demo to create Stripe subscription. Once subscribed, label + is added to an user. You can then use this to give permissions to + subscribers only. +

+
+
+
+
+ + + +
+
+
+ + + + + diff --git a/node/subscriptions-with-stripe/README.md b/node/subscriptions-with-stripe/README.md index 224dfd4d..0260c9fe 100644 --- a/node/subscriptions-with-stripe/README.md +++ b/node/subscriptions-with-stripe/README.md @@ -10,12 +10,12 @@ This endpoint initiates a Stripe checkout session for a subscription. The user I **Parameters** -| Name | Description | Location | Type | Sample Value | -| ------------------ | ---------------------- | -------- | ------ | ------------ | -| x-appwrite-user-id | User ID from Appwrite. | Header | String | 642...7cd | -| Content-Type | The content type of the request body | Header | `application/json` | N/A | -| successUrl | The URL to redirect to after a successful payment. | Body | String | https://example.com/success | -| failureUrl | The URL to redirect to after a cancelled payment attempt. | Body | String | https://example.com/failure | +| Name | Description | Location | Type | Sample Value | +| ------------------ | --------------------------------------------------------- | -------- | ------------------ | --------------------------- | +| x-appwrite-user-id | User ID from Appwrite. | Header | String | 642...7cd | +| Content-Type | The content type of the request body | Header | `application/json` | N/A | +| successUrl | The URL to redirect to after a successful payment. | Body | String | https://example.com/success | +| failureUrl | The URL to redirect to after a cancelled payment attempt. | Body | String | https://example.com/failure | **Response** @@ -60,13 +60,13 @@ Sample `401` Response: ## ⚙️ Configuration -| Setting | Value | -| ----------------- | --------------- | -| Runtime | Node (18.0) | -| Entrypoint | `src/main.js` | -| Build Commands | `npm install` | -| Permissions | `any` | -| Timeout (Seconds) | 15 | +| Setting | Value | +| ----------------- | ------------- | +| Runtime | Node (18.0) | +| Entrypoint | `src/main.js` | +| Build Commands | `npm install` | +| Permissions | `any` | +| Timeout (Seconds) | 15 | > If using a demo web app to subscribe, make sure to add your function domain as a web platform to your Appwrite project. Doing this fixes CORS errors and allows proper functionality. diff --git a/node/subscriptions-with-stripe/src/appwrite.js b/node/subscriptions-with-stripe/src/appwrite.js index 8cba76f0..23154805 100644 --- a/node/subscriptions-with-stripe/src/appwrite.js +++ b/node/subscriptions-with-stripe/src/appwrite.js @@ -20,7 +20,9 @@ class AppwriteService { * @returns {Promise} */ async deleteSubscription(userId) { - const labels = (await this.users.get(userId)).labels.filter((label) => label !== LabelsSubscriber); + const labels = (await this.users.get(userId)).labels.filter( + (label) => label !== LabelsSubscriber + ); await this.users.updateLabels(userId, labels); } diff --git a/node/subscriptions-with-stripe/src/main.js b/node/subscriptions-with-stripe/src/main.js index 9bacd20c..623c3939 100644 --- a/node/subscriptions-with-stripe/src/main.js +++ b/node/subscriptions-with-stripe/src/main.js @@ -8,14 +8,15 @@ export default async (context) => { throwIfMissing(process.env, [ 'STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET', - 'APPWRITE_API_KEY' + 'APPWRITE_API_KEY', ]); if (req.method === 'GET') { const html = interpolate(getStaticFile('index.html'), { - APPWRITE_ENDPOINT: process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1', + APPWRITE_ENDPOINT: + process.env.APPWRITE_ENDPOINT ?? 'https://cloud.appwrite.io/v1', APPWRITE_FUNCTION_PROJECT_ID: process.env.APPWRITE_FUNCTION_PROJECT_ID, - APPWRITE_FUNCTION_ID: process.env.APPWRITE_FUNCTION_ID + APPWRITE_FUNCTION_ID: process.env.APPWRITE_FUNCTION_ID, }); return res.send(html, 200, { 'Content-Type': 'text/html; charset=utf-8' }); @@ -37,13 +38,18 @@ export default async (context) => { return res.redirect(failureUrl, 303); } - const session = await stripe.checkoutSubscription(context, userId, successUrl, failureUrl); + const session = await stripe.checkoutSubscription( + context, + userId, + successUrl, + failureUrl + ); if (!session) { error('Failed to create Stripe checkout session.'); return res.redirect(failureUrl, 303); } - - context.log("Session:"); + + context.log('Session:'); context.log(session); log(`Created Stripe checkout session for user ${userId}.`); @@ -55,7 +61,7 @@ export default async (context) => { return res.json({ success: false }, 401); } - context.log("Event:"); + context.log('Event:'); context.log(event); if (event.type === 'customer.subscription.created') { diff --git a/node/subscriptions-with-stripe/src/utils.js b/node/subscriptions-with-stripe/src/utils.js index 5488c2eb..1a8951bd 100644 --- a/node/subscriptions-with-stripe/src/utils.js +++ b/node/subscriptions-with-stripe/src/utils.js @@ -40,4 +40,4 @@ export function getStaticFile(fileName) { */ export function interpolate(template, values) { return template.replace(/{{([^}]+)}}/g, (_, key) => values[key] || ''); -} \ No newline at end of file +} From cd9a7830f70bbd6914464bd7b20f403da5c186a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 8 Sep 2023 19:18:01 +0200 Subject: [PATCH 30/35] Fix HTML page for payments --- node/payments-with-stripe/static/index.html | 71 +++++---------------- 1 file changed, 17 insertions(+), 54 deletions(-) diff --git a/node/payments-with-stripe/static/index.html b/node/payments-with-stripe/static/index.html index a065afc6..f019a930 100644 --- a/node/payments-with-stripe/static/index.html +++ b/node/payments-with-stripe/static/index.html @@ -4,7 +4,7 @@ - Stripe Subscriptions Demo + Stripe Payments Demo @@ -18,16 +18,17 @@
-

Stripe Subscriptions Demo

+

Stripe Payments Demo

- Use this demo to create Stripe subscription. Once subscribed, label - is added to an user. You can then use this to give permissions to - subscribers only. + Use this demo to create Stripe payment. Once paid, document is + created in Appwrite Database. You can then use this to ship + packages, send PDF books, give access to video courses, or anything + else your business needs.

@@ -38,9 +39,9 @@

Stripe Subscriptions Demo