Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions node/payments-with-dodo-payments/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# 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.*

# Directory used by Appwrite CLI for local development
.appwrite
6 changes: 6 additions & 0 deletions node/payments-with-dodo-payments/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": true,
"singleQuote": true
}
151 changes: 151 additions & 0 deletions node/payments-with-dodo-payments/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# 馃挸 Node.js Payments with Dodo Payments Function

Receive one-time payments with Dodo Payments and store paid orders in an Appwrite table.

## 馃О Usage

### `GET /`

Serves a demo page where a user can register, create a checkout and see their paid orders.

### `POST /checkout`

This endpoint creates a [Dodo Payments checkout session](https://docs.dodopayments.com/developer-resources/checkout-session) for the product in `DODO_PAYMENTS_PRODUCT_ID`. The user ID is fetched from the headers of the request and stored in the checkout metadata. If the user ID is not found or the checkout session can't be created, the request is redirected to the failure URL.

**Parameters**

| Name | Description | Location | Type | Sample Value |
| ------------------ | ------------------------------------------------------------------------------------------------- | -------- | ------------------ | --------------------------- |
| x-appwrite-user-id | User ID from Appwrite. | Header | String | 642...7cd |
| Content-Type | The content type of the request body | Header | `application/json` | N/A |
| successUrl | The URL to return to after checkout. Defaults to the function domain. | Body | String | https://example.com/success |
| verifyUrl | Where the provider returns the user to verify the purchase. Defaults to `/success` on the `successUrl` origin. | Body | String | https://<function-domain>/success |
| failureUrl | The URL to redirect to after a cancelled or failed checkout. Defaults to the function domain. | Body | String | https://example.com/failure |
| email | Optional. Prefills the customer's email address in the checkout. Test mode sends real emails too. | Body | String | jane@example.com |
| name | Optional. Prefills the customer's name in the checkout when `email` is also set. | Body | String | Jane Doe |

**Response**

Sample `303` Response:

The response is a redirect to the Dodo Payments checkout URL, or to the failure URL if an error occurs.

```text
Location: https://test.checkout.dodopayments.com/session/cks_Gi6KGJ2zFJo9rq9Ukifwa
```

```text
Location: https://example.com/failure
```

### `GET /success`

Dodo Payments redirects the user here after checkout. Dodo Payments adds `payment_id` to the redirect URL, and this endpoint fetches the payment from Dodo Payments with it. It provisions the user the same way the webhook does, then redirects to the `successUrl` given to the checkout endpoint.

The webhook and this endpoint both provision the user, so either one works if the other fails. Running twice is safe: orders use the provider ID as the row ID, so the database rejects a second write.

### `POST /webhook`

This endpoint receives Dodo Payments webhooks and handles the `payment.succeeded` event. It verifies the [Standard Webhooks](https://www.standardwebhooks.com/) signature in the `webhook-id`, `webhook-signature` and `webhook-timestamp` headers. If the verification fails, a `401` response is sent. The webhook payload isn't trusted: the function fetches the payment from Dodo Payments and stores the order only if its status is `succeeded`.

On `payment.succeeded`, the function creates the orders database and table if they don't exist yet, then creates a row for the order. The row ID is the Dodo Payments payment ID, so a repeated delivery of the same event doesn't store the order twice. Only the user who paid can read the row.

**Parameters**

| Name | Description | Location | Type | Sample Value |
| ----------------- | ----------------------------------- | -------- | ------ | ------------------------------------------------------------------------------------ |
| None | Webhook payload from Dodo Payments. | Body | Object | [See Dodo Payments docs](https://docs.dodopayments.com/developer-resources/webhooks) |
| webhook-id | Unique ID of the webhook message. | Header | String | `msg_2Lh9...` |
| webhook-signature | Signature from Dodo Payments. | Header | String | [See Dodo Payments docs](https://docs.dodopayments.com/developer-resources/webhooks) |
| webhook-timestamp | Unix timestamp of the delivery. | Header | String | `1759938600` |

**Response**

Sample `200` Response:

In case of the `payment.succeeded` event, a row for the order is created in the Appwrite table. Other events are acknowledged and ignored.

```json
{ "success": true }
```

Sample `401` Response:

```json
{ "success": false }
```

## 鈿欙笍 Configuration

| Setting | Value |
| ----------------- | -------------------------------------------------------------- |
| Runtime | Node (22) |
| Entrypoint | `src/main.js` |
| Build Commands | `npm install` |
| Permissions | `any` |
| Timeout (Seconds) | 15 |
| Scopes | `databases.write`, `tables.read`, `tables.write`, `rows.write` |

> If using a demo web app to create order, make sure to add your function domain as a web platform to your Appwrite project. Doing this fixes CORS errors and allows proper functionality.

> After deploying, add `https://<function-domain>/webhook` as an endpoint in the Dodo Payments dashboard under **Developer > Webhooks** and select the `payment.succeeded` event. An endpoint with no events selected receives every event type. Copy the endpoint's signing secret into `DODO_PAYMENTS_WEBHOOK_KEY`.

## 馃敀 Environment Variables

### DODO_PAYMENTS_API_KEY

API key for sending requests to the Dodo Payments API. The key needs write access to create checkout sessions.

| Question | Answer |
| ------------- | -------------------------------------------------------------------------------------------------- |
| Required | Yes |
| Sample Value | `abcd...` |
| Documentation | [Dodo Payments: API keys](https://docs.dodopayments.com/api-reference/introduction#authentication) |

### DODO_PAYMENTS_WEBHOOK_KEY

Signing secret of the webhook endpoint, used to verify that webhooks come from Dodo Payments.

| Question | Answer |
| ------------- | ------------------------------------------------------------------------------------- |
| Required | Yes |
| Sample Value | `whsec_MfKQ...` |
| Documentation | [Dodo Payments: Webhooks](https://docs.dodopayments.com/developer-resources/webhooks) |

### DODO_PAYMENTS_PRODUCT_ID

ID of the one-time product to sell.

| Question | Answer |
| ------------- | -------------------------------------------------------------------------- |
| Required | Yes |
| Sample Value | `pdt_0NWa...` |
| Documentation | [Dodo Payments: Products](https://docs.dodopayments.com/features/products) |

### DODO_PAYMENTS_ENVIRONMENT

Dodo Payments environment to use, either `test_mode` or `live_mode`. Defaults to `test_mode`. Test mode and live mode have separate API keys, products and webhooks.

| Question | Answer |
| ------------- | ----------------------------------------------------------------------------------------------------------- |
| Required | No |
| Sample Value | `test_mode` |
| Documentation | [Dodo Payments: Test mode vs live mode](https://docs.dodopayments.com/miscellaneous/test-mode-vs-live-mode) |

### APPWRITE_DATABASE_ID

The ID of the database to store the orders. Defaults to `orders`.

| Question | Answer |
| ------------ | -------- |
| Required | No |
| Sample Value | `orders` |

### APPWRITE_TABLE_ID

The ID of the table to store the orders. Defaults to `orders`.

| Question | Answer |
| ------------ | -------- |
| Required | No |
| Sample Value | `orders` |
Loading
Loading