WIP: feat(providers): add dodopayments provider - #158
Conversation
|
@ayushsharma74 is attempting to deploy a commit to the maxktz Team on Vercel. A member of the Team first needs to authorize it. |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughIntroduces the Changes
Sequence DiagramssequenceDiagram
participant App as PayKit Application
participant Provider as DodoPayments Provider
participant DodoAPI as Dodo Payments API
App->>Provider: createCustomer(email, name, metadata)
Provider->>DodoAPI: POST /customers (create)
DodoAPI-->>Provider: Customer created or ConflictError
alt Conflict - Email exists
Provider->>DodoAPI: GET /customers (list by email)
DodoAPI-->>Provider: Existing customer
Provider->>DodoAPI: PATCH /customers (relink/update)
DodoAPI-->>Provider: Customer updated
end
Provider-->>App: Customer ID
sequenceDiagram
participant App as PayKit Application
participant Provider as DodoPayments Provider
participant DodoAPI as Dodo Payments API
App->>Provider: createSubscriptionCheckout(customerId, items)
Provider->>DodoAPI: POST /checkout/sessions (create with items)
DodoAPI-->>Provider: Checkout session with URL
Provider-->>App: Checkout URL
Note over App,DodoAPI: User completes checkout at Dodo
Note over Provider,DodoAPI: Webhook event fires
sequenceDiagram
participant DodoAPI as Dodo Payments API
participant Provider as DodoPayments Provider
participant App as PayKit Application
DodoAPI->>Provider: POST /webhooks (signed event)
Provider->>Provider: Extract headers & signature
Provider->>DodoAPI: client.webhooks.unwrap(event)
DodoAPI-->>Provider: Unwrapped event or error
alt Signature valid
Provider->>Provider: Map event type (subscription.active, etc.)
Provider-->>App: Handled events array
else Signature invalid
Provider-->>DodoAPI: PayKitError (signature failure)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (1)
packages/dodopayments/src/dodopayments-provider.ts (1)
3-9: Splitpaykitjsvalue imports from type-only imports.This import mixes runtime and type-only symbols. Please separate it to match the repo TS rules.
♻️ Suggested import split
-import { - PAYKIT_ERROR_CODES, - PayKitError, - type NormalizedWebhookEvent, - type PayKitProviderConfig, - type PaymentProvider, -} from "paykitjs"; +import { PAYKIT_ERROR_CODES, PayKitError } from "paykitjs"; +import type { + NormalizedWebhookEvent, + PayKitProviderConfig, + PaymentProvider, +} from "paykitjs";As per coding guidelines "Use
import typewith separated style and Node.js protocol (node:fs,node:path) for built-in modules".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/dodopayments/src/dodopayments-provider.ts` around lines 3 - 9, Split the mixed runtime and type-only imports from "paykitjs": import the runtime values PAYKIT_ERROR_CODES and PayKitError with a normal import (e.g., import { PAYKIT_ERROR_CODES, PayKitError } from "paykitjs";) and import the types NormalizedWebhookEvent, PayKitProviderConfig, and PaymentProvider using an import type (e.g., import type { NormalizedWebhookEvent, PayKitProviderConfig, PaymentProvider } from "paykitjs";) so runtime symbols and type-only symbols are separated per the repo TS rules.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/dodopayments/src/dodopayments-provider.ts`:
- Around line 113-117: The code uses non-null assertions on
providerProduct.productId (e.g., in createSubscriptionCheckout and
scheduleSubscriptionChange) which can cause opaque runtime failures; replace the
exclamation marks with explicit validation: check that data.providerProduct and
data.providerProduct.productId are present (and non-empty) before using them,
and if missing throw or return a clear provider error (with a descriptive
message and appropriate error type/return shape) instead of proceeding; apply
the same validation and error handling for the other occurrences referenced (the
product_cart construction and the lines around scheduleSubscriptionChange) so no
code assumes productId exists.
- Around line 277-279: The syncProducts method currently returns an empty
success ({ results: [] }) which silently hides unsupported behavior and leads to
unmapped products; update async syncProducts(data) in dodopayments-provider.ts
to either delegate to a standardized fallback (e.g., call a
notSupported('syncProducts') helper or throw a clear NotSupportedError) or
implement a minimal product sync that returns mapped results with
providerProduct identifiers; ensure the returned shape matches the caller's
expectations (include errors or a non-empty results array with providerProduct)
so downstream code can handle unsupported providers explicitly.
- Around line 282-320: The switch in handleWebhook currently falls through so
"subscription.active" and "subscription.renewed" hit default and return [],
dropping all events; update the switch in handleWebhook to handle those cases
explicitly (e.g., create and return the appropriate NormalizedWebhookEvent array
for subscription activation/renewal) instead of falling through to default —
locate the switch on the local variable event in handleWebhook and implement
case "subscription.active" and case "subscription.renewed" to build/return the
normalized payload (or call an existing normalizer helper) with relevant fields
(subscription id, customer id, status/timestamps) so webhook-driven subscription
state is processed.
- Around line 89-94: updateCustomer currently overwrites provider metadata with
data.metadata ?? {} and can drop the stored paykitCustomerId used by check();
before calling client.customers.update(data.providerCustomerId, ...), fetch the
existing customer (e.g., via client.customers.retrieve(data.providerCustomerId))
read its metadata.paykitCustomerId, and merge it into the metadata you send so
the resulting metadata is something like {...existing.metadata,
...data.metadata} (ensuring paykitCustomerId from existing.metadata is preserved
if present) when calling updateCustomer.
- Around line 282-304: The DodoPayments client is not given the webhook secret
so signature verification in handleWebhook (which calls
client.webhooks.unwrap(...)) will fail; update the DodoPayments constructor
usage to accept and forward the webhookSecret from DodopaymentsOptions into the
underlying client setup and ensure that when calling client.webhooks.unwrap in
handleWebhook you pass the webhookKey/webhookSecret parameter (use the same
secret field name expected by the client) so the HMAC SHA256 verification can
succeed; locate the DodoPayments constructor and the handleWebhook method to add
the missing option and pass it through to the client and unwrap call.
---
Nitpick comments:
In `@packages/dodopayments/src/dodopayments-provider.ts`:
- Around line 3-9: Split the mixed runtime and type-only imports from
"paykitjs": import the runtime values PAYKIT_ERROR_CODES and PayKitError with a
normal import (e.g., import { PAYKIT_ERROR_CODES, PayKitError } from
"paykitjs";) and import the types NormalizedWebhookEvent, PayKitProviderConfig,
and PaymentProvider using an import type (e.g., import type {
NormalizedWebhookEvent, PayKitProviderConfig, PaymentProvider } from
"paykitjs";) so runtime symbols and type-only symbols are separated per the repo
TS rules.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: a6b47ed4-4d14-4006-9e54-62324a17b8fa
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (4)
packages/dodopayments/package.jsonpackages/dodopayments/src/dodopayments-provider.tspackages/dodopayments/src/index.tspackages/dodopayments/tsdown.config.ts
| async updateCustomer(data) { | ||
| await client.customers.update(data.providerCustomerId, { | ||
| email: data.email, | ||
| name: data.name, | ||
| metadata: data.metadata ?? {}, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate and read the dodopayments-provider.ts file
find . -name "dodopayments-provider.ts" -type fRepository: getpaykit/paykit
Length of output: 113
🏁 Script executed:
# Get the file structure to understand the context
fd -t f "dodopayments" | head -20Repository: getpaykit/paykit
Length of output: 111
🏁 Script executed:
# Search for the file more broadly
find . -path "*dodopayments*" -name "*.ts" -type fRepository: getpaykit/paykit
Length of output: 191
🏁 Script executed:
cat -n ./packages/dodopayments/src/dodopayments-provider.tsRepository: getpaykit/paykit
Length of output: 13970
Preserve paykitCustomerId in customer updates.
updateCustomer() sends metadata: data.metadata ?? {}, which will overwrite the existing metadata and lose the paykitCustomerId stored during customer creation. This breaks the reconciliation logic in check() (line 337) that relies on reading this mapping. Update the metadata to preserve the existing paykitCustomerId, similar to how the conflict-resolution code handles it at lines 78-81.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/dodopayments/src/dodopayments-provider.ts` around lines 89 - 94,
updateCustomer currently overwrites provider metadata with data.metadata ?? {}
and can drop the stored paykitCustomerId used by check(); before calling
client.customers.update(data.providerCustomerId, ...), fetch the existing
customer (e.g., via client.customers.retrieve(data.providerCustomerId)) read its
metadata.paykitCustomerId, and merge it into the metadata you send so the
resulting metadata is something like {...existing.metadata, ...data.metadata}
(ensuring paykitCustomerId from existing.metadata is preserved if present) when
calling updateCustomer.
| async createSubscriptionCheckout(data) { | ||
| const checkout = await client.checkoutSessions.create({ | ||
| // Our core type do not allow product quantity, but dodo requires it | ||
| // Need to decide wether to change the core type or default product to 1 | ||
| product_cart: [{ product_id: data.providerProduct.productId!, quantity: 1 }], |
There was a problem hiding this comment.
Validate providerProduct.productId instead of asserting it.
These paths all rely on productId!, but the adapter contract only gives you Record<string, string>, and scheduleSubscriptionChange() even permits providerProduct?: ... | null. Bad or missing mappings will surface as opaque runtime failures instead of a provider error.
Also applies to: 140-141, 176-177
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/dodopayments/src/dodopayments-provider.ts` around lines 113 - 117,
The code uses non-null assertions on providerProduct.productId (e.g., in
createSubscriptionCheckout and scheduleSubscriptionChange) which can cause
opaque runtime failures; replace the exclamation marks with explicit validation:
check that data.providerProduct and data.providerProduct.productId are present
(and non-empty) before using them, and if missing throw or return a clear
provider error (with a descriptive message and appropriate error type/return
shape) instead of proceeding; apply the same validation and error handling for
the other occurrences referenced (the product_cart construction and the lines
around scheduleSubscriptionChange) so no code assumes productId exists.
| async syncProducts(data) { | ||
| return { results: [] }; | ||
| }, |
There was a problem hiding this comment.
Don't silently noop syncProducts().
Returning { results: [] } makes sync look successful while leaving every product unmapped. Downstream code will only discover the problem later when providerProduct is missing.
Do you want me to sketch a minimal notSupported(...) fallback or a first-pass Dodo product sync implementation?
🧰 Tools
🪛 GitHub Check: lint
[warning] 277-277: eslint(no-unused-vars)
Parameter 'data' is declared but never used. Unused parameters should start with a '_'.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/dodopayments/src/dodopayments-provider.ts` around lines 277 - 279,
The syncProducts method currently returns an empty success ({ results: [] })
which silently hides unsupported behavior and leads to unmapped products; update
async syncProducts(data) in dodopayments-provider.ts to either delegate to a
standardized fallback (e.g., call a notSupported('syncProducts') helper or throw
a clear NotSupportedError) or implement a minimal product sync that returns
mapped results with providerProduct identifiers; ensure the returned shape
matches the caller's expectations (include errors or a non-empty results array
with providerProduct) so downstream code can handle unsupported providers
explicitly.
| async handleWebhook(data): Promise<NormalizedWebhookEvent[]> { | ||
| const headers = data.headers; | ||
|
|
||
| const getHeader = (name: string) => | ||
| Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase()) | ||
| ? headers[Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase())!] | ||
| : undefined; | ||
|
|
||
| const webhookId = getHeader("webhook-id") ?? ""; | ||
| const webhookSignature = getHeader("webhook-signature"); | ||
| const webhookTimestamp = getHeader("webhook-timestamp"); | ||
|
|
||
| let event: WebhookEventType; | ||
|
|
||
| const webhookHeaders = { | ||
| "webhook-id": webhookId as string, | ||
| "webhook-signature": webhookSignature as string, | ||
| "webhook-timestamp": webhookTimestamp as string, | ||
| }; | ||
|
|
||
| try { | ||
| const wh = client.webhooks.unwrap(data.body.toString(), { headers: webhookHeaders }); | ||
| event = wh.type; | ||
| } catch (error) { | ||
| // HANDLE OTHER ERRORS HERE | ||
| throw PayKitError.from( | ||
| "BAD_REQUEST", | ||
| PAYKIT_ERROR_CODES.PROVIDER_SIGNATURE_MISSING, | ||
| "Invalid DodoPayments webhook signature", | ||
| ); | ||
| } | ||
|
|
||
| switch (event) { | ||
| case "subscription.active": | ||
| case "subscription.renewed": | ||
|
|
||
| default: | ||
| return []; | ||
| } |
There was a problem hiding this comment.
handleWebhook() currently drops every event.
"subscription.active" and "subscription.renewed" both fall through to default, so this always returns []. That means webhook-driven subscription state never gets normalized or synced.
🧰 Tools
🪛 GitHub Check: lint
[warning] 305-305: eslint(no-unused-vars)
Catch parameter 'error' is caught but never used.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/dodopayments/src/dodopayments-provider.ts` around lines 282 - 320,
The switch in handleWebhook currently falls through so "subscription.active" and
"subscription.renewed" hit default and return [], dropping all events; update
the switch in handleWebhook to handle those cases explicitly (e.g., create and
return the appropriate NormalizedWebhookEvent array for subscription
activation/renewal) instead of falling through to default — locate the switch on
the local variable event in handleWebhook and implement case
"subscription.active" and case "subscription.renewed" to build/return the
normalized payload (or call an existing normalizer helper) with relevant fields
(subscription id, customer id, status/timestamps) so webhook-driven subscription
state is processed.
| async handleWebhook(data): Promise<NormalizedWebhookEvent[]> { | ||
| const headers = data.headers; | ||
|
|
||
| const getHeader = (name: string) => | ||
| Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase()) | ||
| ? headers[Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase())!] | ||
| : undefined; | ||
|
|
||
| const webhookId = getHeader("webhook-id") ?? ""; | ||
| const webhookSignature = getHeader("webhook-signature"); | ||
| const webhookTimestamp = getHeader("webhook-timestamp"); | ||
|
|
||
| let event: WebhookEventType; | ||
|
|
||
| const webhookHeaders = { | ||
| "webhook-id": webhookId as string, | ||
| "webhook-signature": webhookSignature as string, | ||
| "webhook-timestamp": webhookTimestamp as string, | ||
| }; | ||
|
|
||
| try { | ||
| const wh = client.webhooks.unwrap(data.body.toString(), { headers: webhookHeaders }); | ||
| event = wh.type; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "dodopayments-provider.ts" -type fRepository: getpaykit/paykit
Length of output: 113
🏁 Script executed:
wc -l packages/dodopayments/src/dodopayments-provider.tsRepository: getpaykit/paykit
Length of output: 115
🏁 Script executed:
cat -n packages/dodopayments/src/dodopayments-provider.ts | head -100Repository: getpaykit/paykit
Length of output: 3470
🏁 Script executed:
cat -n packages/dodopayments/src/dodopayments-provider.ts | sed -n '280,310p'Repository: getpaykit/paykit
Length of output: 1409
🏁 Script executed:
cat -n packages/dodopayments/src/dodopayments-provider.ts | sed -n '360,375p'Repository: getpaykit/paykit
Length of output: 611
🏁 Script executed:
cat -n packages/dodopayments/package.json | grep -A 5 -B 5 dodopaymentsRepository: getpaykit/paykit
Length of output: 634
🏁 Script executed:
find . -path "*/node_modules/dodopayments" -name "*.d.ts" -o -name "*.ts" 2>/dev/null | head -20Repository: getpaykit/paykit
Length of output: 847
🏁 Script executed:
rg "webhookKey|webhook-key" --type ts --type tsx -iRepository: getpaykit/paykit
Length of output: 88
🏁 Script executed:
find . -name "*test*" -o -name "*spec*" | grep -i dodo | head -20Repository: getpaykit/paykit
Length of output: 42
🏁 Script executed:
rg "webhookKey|webhook-key" -iRepository: getpaykit/paykit
Length of output: 42
🏁 Script executed:
rg "DodoPayments" -A 5 | head -50Repository: getpaykit/paykit
Length of output: 2396
🏁 Script executed:
find . -name "*.test.*" -o -name "*.spec.*" | grep -i dodoRepository: getpaykit/paykit
Length of output: 42
🏁 Script executed:
cat packages/dodopayments/src/dodopayments-provider.ts | sed -n '1,50p'Repository: getpaykit/paykit
Length of output: 1462
🌐 Web query:
DodoPayments SDK webhookKey webhook configuration initialization
💡 Result:
To configure and initialize the DodoPayments SDK with the webhookKey for webhook handling: 1. Obtain the webhook secret key from the Dodo Payments dashboard settings page. Keep it secure and use environment variables (e.g., DODO_PAYMENTS_WEBHOOK_KEY). 2. Initialize the SDK client by passing the webhookKey in the constructor options, along with bearerToken and environment. Examples from official docs: TypeScript/Node.js: import DodoPayments from 'dodopayments'; const client = new DodoPayments({ bearerToken: process.env.DODO_PAYMENTS_API_KEY, environment: process.env.DODO_PAYMENTS_ENVIRONMENT, webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY, }); Python: from dodopayments import DodoPayments import os client = DodoPayments( bearer_token=os.getenv("DODO_PAYMENTS_API_KEY"), environment=os.getenv("DODO_PAYMENTS_ENVIRONMENT"), webhook_key=os.getenv("DODO_PAYMENTS_WEBHOOK_KEY"), ) Go: client := dodopayments.NewClient( option.WithBearerToken(os.Getenv("DODO_PAYMENTS_API_KEY")), option.WithEnvironment(os.Getenv("DODO_PAYMENTS_ENVIRONMENT")), option.WithWebhookKey(os.Getenv("DODO_PAYMENTS_WEBHOOK_KEY")), ) 3. Handle incoming webhooks by using client.webhooks.unwrap to verify the signature with the required headers (webhook-id, webhook-signature, webhook-timestamp) and raw body. Example (TypeScript): app.post('/webhook', async (req, res) => { try { const unwrapped = client.webhooks.unwrap(req.body.toString, { headers: { 'webhook-id': req.headers['webhook-id'] as string, 'webhook-signature': req.headers['webhook-signature'] as string, 'webhook-timestamp': req.headers['webhook-timestamp'] as string, }, }); // Process unwrapped event res.json({ received: true }); } catch (error) { res.status(401).json({ error: 'Invalid signature' }); } }); The webhookKey enables built-in signature verification following the Standard Webhooks spec (HMAC SHA256 of webhook-id.webhook-timestamp.payload using the secret).
Citations:
- 1: https://docs.dodopayments.com/developer-resources/webhooks
- 2: https://github.com/sancho1952007/dodopayments-webhooks
- 3: https://better-auth.com/docs/plugins/dodopayments
- 4: https://docs.dodopayments.com/api-reference/webhooks/create-webhook
🌐 Web query:
DodoPayments "webhooks.unwrap" signature verification
💡 Result:
DodoPayments webhooks use HMAC SHA256 signatures for verification, following the Standard Webhooks specification. Each webhook includes headers: webhook-id, webhook-timestamp, and webhook-signature. Official SDKs provide client.webhooks.unwrap(rawBody, { headers }) to verify the signature using your webhook secret key (set via DODO_PAYMENTS_WEBHOOK_KEY) and parse the payload. If verification fails, it throws an error. Example (Node.js): app.post('/webhook', async (req, res) => { try { const unwrapped = client.webhooks.unwrap(req.body.toString, { headers: { 'webhook-id': req.headers['webhook-id'], 'webhook-signature': req.headers['webhook-signature'], 'webhook-timestamp': req.headers['webhook-timestamp'], }, }); // Process unwrapped event res.json({ received: true }); } catch (error) { res.status(401).json({ error: 'Invalid signature' }); } }); Similar examples exist for Python, Go, etc. Manual verification: Concatenate webhook-id.webhook-timestamp.payload (raw string), compute HMAC SHA256 with secret, compare to webhook-signature using constant-time comparison. Use unsafe_unwrap only for testing without signatures. Get secret via Dashboard or API.
Citations:
- 1: https://docs.dodopayments.com/developer-resources/webhooks
- 2: https://github.com/sancho1952007/dodopayments-webhooks
- 3: https://docs.dodopayments.com/developer-resources/sdks/cli
🏁 Script executed:
rg "new DodoPayments" -A 10Repository: getpaykit/paykit
Length of output: 777
🏁 Script executed:
cat packages/dodopayments/src/dodopayments-provider.ts | sed -n '300,320p'Repository: getpaykit/paykit
Length of output: 590
Pass the webhook secret into the Dodo client.
handleWebhook() calls client.webhooks.unwrap(...), which requires the webhookKey parameter to verify signatures using HMAC SHA256. The DodoPayments constructor at lines 365-368 only accepts bearerToken and environment—the webhookSecret from DodopaymentsOptions is never passed. Signature validation will fail without this configuration.
Fix
createAdapter(): PaymentProvider {
const client = new DodoPayments({
bearerToken: dodopaymentsOptions.bearerToken,
environment: dodopaymentsOptions.environment ?? "live_mode",
+ webhookKey: dodopaymentsOptions.webhookSecret,
});
return createDodopaymentsProvider(client, dodopaymentsOptions);
},🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/dodopayments/src/dodopayments-provider.ts` around lines 282 - 304,
The DodoPayments client is not given the webhook secret so signature
verification in handleWebhook (which calls client.webhooks.unwrap(...)) will
fail; update the DodoPayments constructor usage to accept and forward the
webhookSecret from DodopaymentsOptions into the underlying client setup and
ensure that when calling client.webhooks.unwrap in handleWebhook you pass the
webhookKey/webhookSecret parameter (use the same secret field name expected by
the client) so the HMAC SHA256 verification can succeed; locate the DodoPayments
constructor and the handleWebhook method to add the missing option and pass it
through to the client and unwrap call.
00d682a to
79b2a78
Compare
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Summary
fixes #104
This PR will add support for dodopayments in paykit
Summary by CodeRabbit