Chuyển đến nội dung chính

Cài đặt

1

Cài đặt gói

Chạy lệnh sau trong thư mục gốc của dự án bạn:
npm install @dodopayments/convex
2

Thêm Thành phần vào Cấu hình Convex

Thêm thành phần Dodo Payments vào cấu hình Convex của bạn:
// convex/convex.config.ts
import { defineApp } from "convex/server";
import dodopayments from "@dodopayments/convex/convex.config";

const app = defineApp();
app.use(dodopayments);
export default app;
Sau khi chỉnh sửa convex.config.ts, chạy npx convex dev một lần để tạo các kiểu cần thiết.
3

Thiết lập biến môi trường

Thiết lập biến môi trường trong bảng điều khiển Convex của bạn (Cài đặtBiến Môi Trường). Bạn có thể truy cập bảng điều khiển bằng cách chạy:
npx convex dashboard
Thêm các biến môi trường sau:
  • DODO_PAYMENTS_API_KEY - Khóa API Dodo Payments của bạn
  • DODO_PAYMENTS_ENVIRONMENT - Đặt thành test_mode hoặc live_mode
  • DODO_PAYMENTS_WEBHOOK_SECRET - Bí mật webhook của bạn (cần thiết cho việc xử lý webhook)
Luôn sử dụng biến môi trường Convex cho thông tin nhạy cảm. Không bao giờ cam kết bí mật vào kiểm soát phiên bản.

Ví dụ Thiết lập Thành phần

1

Tạo Truy vấn Nội bộ

Đầu tiên, tạo một truy vấn nội bộ để lấy khách hàng từ cơ sở dữ liệu của bạn. Điều này sẽ được sử dụng trong các chức năng thanh toán để xác định khách hàng.
Trước khi sử dụng truy vấn này, hãy đảm bảo định nghĩa đúng schema trong tệp convex/schema.ts của bạn hoặc thay đổi truy vấn để phù hợp với schema hiện có của bạn.
// convex/customers.ts
import { internalQuery } from "./_generated/server";
import { v } from "convex/values";

// Internal query to fetch customer by auth ID
export const getByAuthId = internalQuery({
  args: { authId: v.string() },
  handler: async (ctx, { authId }) => {
    return await ctx.db
      .query("customers")
      .withIndex("by_auth_id", (q) => q.eq("authId", authId))
      .first();
  },
});
2

Cấu hình Thành phần DodoPayments

// convex/dodo.ts
import { DodoPayments, DodoPaymentsClientConfig } from "@dodopayments/convex";
import { components } from "./_generated/api";
import { internal } from "./_generated/api";

export const dodo = new DodoPayments(components.dodopayments, {
// This function maps your Convex user to a Dodo Payments customer
// Customize it based on your authentication provider and database
identify: async (ctx) => {
  const identity = await ctx.auth.getUserIdentity();
  if (!identity) {
    return null; // User is not logged in
  }
  
  // Use ctx.runQuery() to lookup customer from your database
  const customer = await ctx.runQuery(internal.customers.getByAuthId, {
    authId: identity.subject,
  });
  
  if (!customer) {
    return null; // Customer not found in database
  }
  
  return {
    dodoCustomerId: customer.dodoCustomerId, // Replace customer.dodoCustomerId with your field storing Dodo Payments customer ID
  };
},
apiKey: process.env.DODO_PAYMENTS_API_KEY!,
environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode",
} as DodoPaymentsClientConfig);

// Export the API methods for use in your app
export const { checkout, customerPortal } = dodo.api();
Sử dụng chức năng này để tích hợp thanh toán Dodo vào ứng dụng Convex của bạn. Sử dụng thanh toán dựa trên phiên với hỗ trợ đầy đủ các tính năng.
// convex/payments.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
import { checkout } from "./dodo";

export const createCheckout = action({
  args: { 
    product_cart: v.array(v.object({
      product_id: v.string(),
      quantity: v.number(),
    })),
    returnUrl: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    try {
      const session = await checkout(ctx, {
        payload: {
          product_cart: args.product_cart,
          return_url: args.returnUrl,
          billing_currency: "USD",
          feature_flags: {
            allow_discount_code: true,
          },
        },
      });
      if (!session?.checkout_url) {
        throw new Error("Checkout session did not return a checkout_url");
      }
      return session;
    } catch (error) {
      console.error("Failed to create checkout session", error);
      throw new Error("Unable to create checkout session. Please try again.");
    }
  },
});

Chức năng Thanh toán

Thành phần Convex Dodo Payments sử dụng thanh toán dựa trên phiên, cung cấp trải nghiệm thanh toán an toàn, tùy chỉnh với giỏ hàng sản phẩm được cấu hình sẵn và thông tin khách hàng. Đây là cách tiếp cận được khuyến nghị cho tất cả các quy trình thanh toán.

Cách sử dụng

const result = await checkout(ctx, {
  payload: {
    product_cart: [{ product_id: "prod_123", quantity: 1 }],
    customer: { email: "[email protected]" },
    return_url: "https://example.com/success"
  }
});
Tham khảo Phiên Thanh toán để biết thêm chi tiết và danh sách đầy đủ các trường được hỗ trợ.

Định dạng Phản hồi

Chức năng thanh toán trả về một phản hồi JSON với URL thanh toán:
{
  "checkout_url": "https://checkout.dodopayments.com/session/..."
}

Chức năng Cổng thông tin Khách hàng

Chức năng Cổng thông tin Khách hàng cho phép bạn tích hợp liền mạch cổng thông tin khách hàng Dodo Payments vào ứng dụng Convex của bạn.

Cách sử dụng

const result = await customerPortal(ctx, {
  send_email: false
});

Tham số

send_email
boolean
Nếu được đặt thành true, sẽ gửi email cho khách hàng với liên kết cổng thông tin.
Khách hàng sẽ được xác định tự động bằng cách sử dụng chức năng identify được cấu hình trong thiết lập DodoPayments của bạn. Chức năng này nên trả về dodoCustomerId của khách hàng.

Xử lý Webhook

  • Phương thức: Chỉ hỗ trợ yêu cầu POST. Các phương thức khác trả về 405.
  • Xác minh Chữ ký: Xác minh chữ ký webhook bằng cách sử dụng DODO_PAYMENTS_WEBHOOK_SECRET. Trả về 401 nếu xác minh thất bại.
  • Xác thực Tải trọng: Được xác thực bằng Zod. Trả về 400 cho các tải trọng không hợp lệ.
  • Xử lý Lỗi:
    • 401: Chữ ký không hợp lệ
    • 400: Tải trọng không hợp lệ
    • 500: Lỗi nội bộ trong quá trình xác minh
  • Định tuyến Sự kiện: Gọi trình xử lý sự kiện thích hợp dựa trên loại tải trọng.

Các Trình xử lý Sự kiện Webhook Hỗ trợ

onPayload?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onPaymentSucceeded?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onPaymentFailed?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onPaymentProcessing?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onPaymentCancelled?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onRefundSucceeded?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onRefundFailed?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onDisputeOpened?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onDisputeExpired?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onDisputeAccepted?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onDisputeCancelled?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onDisputeChallenged?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onDisputeWon?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onDisputeLost?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onSubscriptionActive?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onSubscriptionOnHold?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onSubscriptionRenewed?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onSubscriptionPlanChanged?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onSubscriptionCancelled?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onSubscriptionFailed?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onSubscriptionExpired?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;
onLicenseKeyCreated?: (ctx: GenericActionCtx, payload: WebhookPayload) => Promise<void>;

Cách sử dụng Frontend

Sử dụng chức năng thanh toán từ các thành phần React của bạn với các hook Convex.
import { useAction } from "convex/react";
import { api } from "../convex/_generated/api";

export function CheckoutButton() {
  const createCheckout = useAction(api.payments.createCheckout);

  const handleCheckout = async () => {
    try {
      const { checkout_url } = await createCheckout({
        product_cart: [{ product_id: "prod_123", quantity: 1 }],
        returnUrl: "https://example.com/success"
      });
      if (!checkout_url) {
        throw new Error("Missing checkout_url in response");
      }
      window.location.href = checkout_url;
    } catch (error) {
      console.error("Failed to create checkout", error);
      throw new Error("Unable to create checkout. Please try again.");
    }
  };

  return <button onClick={handleCheckout}>Buy Now</button>;
}
import { useAction } from "convex/react";
import { api } from "../convex/_generated/api";

export function CustomerPortalButton() {
  const getPortal = useAction(api.payments.getCustomerPortal);

  const handlePortal = async () => {
    try {
      const { portal_url } = await getPortal({ send_email: false });
      if (!portal_url) {
        throw new Error("Missing portal_url in response");
      }
      window.location.href = portal_url;
    } catch (error) {
      console.error("Unable to open customer portal", error);
      alert("We couldn't open the customer portal. Please try again.");
    }
  };

  return <button onClick={handlePortal}>Manage Subscription</button>;
}

Lời nhắc cho LLM

You are an expert Convex developer assistant. Your task is to guide a user through integrating the @dodopayments/convex component into their existing Convex application.

The @dodopayments/convex adapter provides a Convex component for Dodo Payments' Checkout, Customer Portal, and Webhook functionalities, built using the official Convex component architecture pattern.

First, install the necessary package:

npm install @dodopayments/convex

Here's how you should structure your response:

1. Ask the user which functionalities they want to integrate.

"Which parts of the @dodopayments/convex component would you like to integrate into your project? You can choose one or more of the following:

- Checkout Function (for handling product checkouts)
- Customer Portal Function (for managing customer subscriptions/details)
- Webhook Handler (for receiving Dodo Payments webhook events)
- All (integrate all three)"

2. Based on the user's selection, provide detailed integration steps for each chosen functionality.

If Checkout Function is selected:

Purpose: This function handles session-based checkout flows and returns checkout URLs for programmatic handling.

Integration Steps:

Step 1: Add the component to your Convex configuration.

// convex/convex.config.ts
import { defineApp } from "convex/server";
import dodopayments from "@dodopayments/convex/convex.config";

const app = defineApp();
app.use(dodopayments);
export default app;

Step 2: Guide the user to set up environment variables in the Convex dashboard. Instruct them to open the dashboard by running:

npx convex dashboard

Then add the required environment variables (e.g., DODO_PAYMENTS_API_KEY, DODO_PAYMENTS_ENVIRONMENT, DODO_PAYMENTS_WEBHOOK_SECRET) in **Settings → Environment Variables**. Do not use .env files for backend functions.

Step 3: Create an internal query to fetch customers from your database.
Note: Ensure the user has appropriate schema defined in their convex/schema.ts file or modify the query to match their existing schema.

// convex/customers.ts
import { internalQuery } from "./_generated/server";
import { v } from "convex/values";

// Internal query to fetch customer by auth ID
export const getByAuthId = internalQuery({
  args: { authId: v.string() },
  handler: async (ctx, { authId }) => {
    return await ctx.db
      .query("customers")
      .withIndex("by_auth_id", (q) => q.eq("authId", authId))
      .first();
  },
});

Step 4: Create your payment functions file.

// convex/dodo.ts
import { DodoPayments, DodoPaymentsClientConfig } from "@dodopayments/convex";
import { components } from "./_generated/api";
import { internal } from "./_generated/api";

export const dodo = new DodoPayments(components.dodopayments, {
  // This function maps your Convex user to a Dodo Payments customer
  // Customize it based on your authentication provider and user database
  identify: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) {
      return null; // User is not logged in
    }
    
    // Use ctx.runQuery() to lookup customer from your database
    const customer = await ctx.runQuery(internal.customers.getByAuthId, {
      authId: identity.subject,
    });
    
    if (!customer) {
      return null; // Customer not found in database
    }
    
    return {
      dodoCustomerId: customer.dodoCustomerId, // Replace customer.dodoCustomerId with your field storing Dodo Payments customer ID
    };
  },
  apiKey: process.env.DODO_PAYMENTS_API_KEY!,
  environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode",
} as DodoPaymentsClientConfig);

// Export the API methods for use in your app
export const { checkout, customerPortal } = dodo.api();

Step 5: Create actions that use the checkout function.

// convex/payments.ts
import { action } from "./_generated/server";
import { v } from "convex/values";
import { checkout } from "./dodo";

// Checkout session with full feature support
export const createCheckout = action({
  args: { 
    product_cart: v.array(v.object({
      product_id: v.string(),
      quantity: v.number(),
    })),
    returnUrl: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    return await checkout(ctx, {
      payload: {
        product_cart: args.product_cart,
        return_url: args.returnUrl,
        billing_currency: "USD",
        feature_flags: {
          allow_discount_code: true,
        },
      },
    });
  },
});

Step 6: Use in your frontend.

// Your frontend component
import { useAction } from "convex/react";
import { api } from "../convex/_generated/api";

export function CheckoutButton() {
  const createCheckout = useAction(api.payments.createCheckout);

  const handleCheckout = async () => {
    const { checkout_url } = await createCheckout({
      product_cart: [{ product_id: "prod_123", quantity: 1 }],
    });
    window.location.href = checkout_url;
  };

  return <button onClick={handleCheckout}>Buy Now</button>;
}

Configuration Details:

- `checkout()`: Checkout session with full feature support using session checkout.
- Returns: `{"checkout_url": "https://checkout.dodopayments.com/..."}`

For complete API documentation, refer to:
- Checkout Sessions: https://docs.dodopayments.com/developer-resources/checkout-session
- One-time Payments: https://docs.dodopayments.com/api-reference/payments/post-payments
- Subscriptions: https://docs.dodopayments.com/api-reference/subscriptions/post-subscriptions

If Customer Portal Function is selected:

Purpose: This function allows customers to manage their subscriptions and payment methods. The customer is automatically identified via the `identify` function.

Integration Steps:

Follow Steps 1-4 from the Checkout Function section, then:

Step 5: Create a customer portal action.

// convex/payments.ts (add to existing file)
import { action } from "./_generated/server";
import { v } from "convex/values";
import { customerPortal } from "./dodo";

export const getCustomerPortal = action({
  args: {
    send_email: v.optional(v.boolean()),
  },
  handler: async (ctx, args) => {
    try {
      const portal = await customerPortal(ctx, args);
      if (!portal?.portal_url) {
        throw new Error("Customer portal did not return a portal_url");
      }
      return portal;
    } catch (error) {
      console.error("Failed to generate customer portal link", error);
      throw new Error("Unable to generate customer portal link. Please retry.");
    }
  },
});

Step 6: Use in your frontend.

// Your frontend component
import { useAction } from "convex/react";
import { api } from "../convex/_generated/api";

export function CustomerPortalButton() {
  const getPortal = useAction(api.payments.getCustomerPortal);

  const handlePortal = async () => {
    const { portal_url } = await getPortal({ send_email: false });
    window.location.href = portal_url;
  };

  return <button onClick={handlePortal}>Manage Subscription</button>;
}

Configuration Details:
- Requires authenticated user (via `identify` function).
- Customer identification is handled automatically by the `identify` function.
- `send_email`: Optional boolean to send portal link via email.

If Webhook Handler is selected:

Purpose: This handler processes incoming webhook events from Dodo Payments, allowing your application to react to events like successful payments or subscription changes.

Integration Steps:

Step 1: Add the webhook secret to your environment variables in the Convex dashboard. You can open the dashboard by running:

Guide the user to open the Convex dashboard by running:

npx convex dashboard

In the dashboard, go to **Settings → Environment Variables** and add:

- `DODO_PAYMENTS_WEBHOOK_SECRET=whsec_...`

Do not use .env files for backend functions; always set secrets in the Convex dashboard.

Step 2: Create a file `convex/http.ts`:

// convex/http.ts
import { createDodoWebhookHandler } from "@dodopayments/convex";
import { httpRouter } from "convex/server";
import { internal } from "./_generated/api";

const http = httpRouter();

http.route({
  path: "/dodopayments-webhook",
  method: "POST",
  handler: createDodoWebhookHandler({
    // Handle successful payments
    onPaymentSucceeded: async (ctx, payload) => {
      console.log("🎉 Payment Succeeded!");

      // Use Convex context to persist payment data
      await ctx.runMutation(internal.webhooks.createPayment, {
        paymentId: payload.data.payment_id,
        businessId: payload.business_id,
        customerEmail: payload.data.customer.email,
        amount: payload.data.total_amount,
        currency: payload.data.currency,
        status: payload.data.status,
        webhookPayload: JSON.stringify(payload),
      });
    },

    // Handle subscription activation
    onSubscriptionActive: async (ctx, payload) => {
      console.log("🎉 Subscription Activated!");
      // Use Convex context to persist subscription data
      await ctx.runMutation(internal.webhooks.createSubscription, {
        subscriptionId: payload.data.subscription_id,
        businessId: payload.business_id,
        customerEmail: payload.data.customer.email,
        status: payload.data.status,
        webhookPayload: JSON.stringify(payload),
      });
    },
    // Add other event handlers as needed
  }),
});

export default http;

Note: Make sure to define the corresponding database mutations in your Convex backend for each webhook event you want to handle. For example, create a `createPayment` mutation to record successful payments or a `createSubscription` mutation to manage subscription state.

Now, you can set the webhook endpoint URL in your Dodo Payments dashboard to `https://<your-convex-deployment-url>/dodopayments-webhook`.

Environment Variable Setup:

Set up the following environment variables in your Convex dashboard if you haven't already (Settings → Environment Variables):

- `DODO_PAYMENTS_API_KEY` - Your Dodo Payments API key
- `DODO_PAYMENTS_ENVIRONMENT` - Set to `test_mode` or `live_mode`
- `DODO_PAYMENTS_WEBHOOK_SECRET` - Your webhook secret (required for webhook handling)

Usage in your component configuration:

apiKey: process.env.DODO_PAYMENTS_API_KEY
environment: process.env.DODO_PAYMENTS_ENVIRONMENT as "test_mode" | "live_mode"

Important: Never commit sensitive environment variables directly into your code. Always use Convex environment variables for all sensitive information.

If the user needs assistance setting up environment variables or deployment, ask them about their specific setup and provide guidance accordingly.

Run `npx convex dev` after setting up the component to generate the necessary types.