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.
+