diff --git a/docs/earn-money/payments/payments_add.mdx b/docs/earn-money/payments/payments_add.mdx index 73c6839..5071336 100644 --- a/docs/earn-money/payments/payments_add.mdx +++ b/docs/earn-money/payments/payments_add.mdx @@ -255,31 +255,13 @@ If you don’t provide an image, the default Reddit product image is used. ### Purchase buttons (required) -#### Blocks +#### Devvit Web -The `ProductButton` is a Devvit blocks component designed to render a product with a purchase button. It can be customized to match your app's look and feel. +In Devvit Web, use your own UI (e.g. a button or product card) and call `purchase(sku)` from `@devvit/web/client` when the user chooses a product. Follow the [design guidelines](#design-guidelines) (e.g. gold icon, clear labeling). -**Usage:** +#### Blocks (legacy) -```tsx - payments.purchase(p.sku)} - appearance="tile" -/> -``` - -##### `ProductButtonProps` - -| **Prop Name** | **Type** | **Description** | -| ------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------------ | -| `product` | `Product` | The product object containing details such as `sku`, `price`, and `metadata`. | -| `onPress` | `(product: Product) => void` | Callback function triggered when the button is pressed. | -| `showIcon` | `boolean` | Determines whether the product icon is displayed on the button. Defaults to `false`. | -| `appearance` | `'compact'` | `'detailed'` | `'tile'` | Defines the visual style of the button. Defaults to `compact`. | -| `buttonAppearance` | `string` | Optional [button appearance](../../blocks/button.mdx#appearance). | -| `textColor` | `string` | Optional [text color](../../blocks/text.mdx#color). | +If your app still uses Devvit Blocks, you can use the `ProductButton` component and [migrate to Devvit Web](./payments_migrate.mdx) when ready. The `ProductButton` renders a product with a purchase button; use `payments.purchase(p.sku)` in the `onPress` callback (from `@devvit/payments`). #### Webviews @@ -297,36 +279,30 @@ Use a consistent and clear product component to display paid goods or services t ## Complete the payment flow -Use `addPaymentHandler` to specify the function that is called during the order flow. This customizes how your app fulfills product orders and provides the ability for you to reject an order. +Your **fulfill** endpoint (configured in `devvit.json` and implemented in the server) is called during the order flow. It customizes how your app fulfills product orders and lets you reject an order. -Errors thrown within the payment handler automatically reject the order. To provide a custom error message to the frontend of your application, you can return `{success: false, reason: }` with a reason for the order rejection. +Return `{ success: true }` to accept the order, or `{ success: false, reason: "" }` to reject it and send a message to the client. Throwing an error in the handler also rejects the order. -This example shows how to issue an "extra life" to a user when they purchase the "extra_life" product. +This example shows how to grant an "extra life" in your fulfill endpoint when the user purchases the "god_mode" product (using Redis from `@devvit/web/server`): -```ts -import { type Context } from "@devvit/public-api"; -import { addPaymentHandler } from "@devvit/payments"; -import { Devvit, useState } from "@devvit/public-api"; - -Devvit.configure({ - redis: true, - redditAPI: true, -}); +```tsx title="server/index.ts" +import type { PaymentHandlerResponse, Order } from "@devvit/web/server"; +import { redis } from "@devvit/web/server"; const GOD_MODE_SKU = "god_mode"; -addPaymentHandler({ - fulfillOrder: async (order, ctx) => { - if (!order.products.some(({ sku }) => sku === GOD_MODE_SKU)) { - throw new Error("Unable to fulfill order: sku not found"); - } - if (order.status !== "PAID") { - throw new Error("Becoming a god has a cost (in Reddit Gold)"); - } +app.post("/internal/payments/fulfill", async (c) => { + const order = await c.req.json(); + if (!order.products.some((p) => p.sku === GOD_MODE_SKU)) { + return c.json({ success: false, reason: "Unable to fulfill order: sku not found" }); + } + if (order.status !== "PAID") { + return c.json({ success: false, reason: "Becoming a god has a cost (in Reddit Gold)" }); + } - const redisKey = godModeRedisKey(ctx.postId, ctx.userId); - await ctx.redis.set(redisKey, "true"); - }, + const redisKey = `post:${order.postId}:user:${order.userId}:god_mode`; + await redis.set(redisKey, "true"); + return c.json({ success: true }); }); ``` @@ -336,61 +312,44 @@ The frontend and backend of your app coordinate order processing. ![Order workflow diagram](../../assets/payments_order_flow_diagram.png) -To launch the payment flow, create a hook with `usePayments()` followed by `hook.purchase()` to initiate the purchase from the frontend. +To launch the payment flow, call `purchase(sku)` from `@devvit/web/client`. That triggers the native payment flow on all platforms (web, iOS, Android); Reddit then calls your server's **fulfill** endpoint. Your app can acknowledge or reject the order (for example, reject once a limited product is sold out). -This triggers a native payment flow on all platforms (web, iOS, Android) that works with the Reddit backend to process the order. The `fulfillOrder()` hook calls your app during this process. +### Get your product details -Your app can acknowledge or reject the order. For example, for goods with limited quantities, your app may reject an order once the product is sold out. +**Server:** Use `payments.getProducts()` in your server (see [Server: Fetch products](#server-fetch-products)) and expose products via your own `/api/products` (or similar) endpoint if the client needs them. -### Get your product details +**Client:** Fetch product metadata from your API and use it to display products and call `purchase(sku)`: -Use the `useProducts` hook or `getProducts` function to fetch details about products. - -```tsx -import { useProducts } from "@devvit/payments"; - -export function ProductsList(context: Devvit.Context): JSX.Element { - // Only query for products with the metadata "category" of value "powerup". - // The metadata field can be empty - if it is, useProducts will not filter on metadata. - const { products } = useProducts(context, { - metadata: { - category: "powerup", - }, - }); - - return ( - - {products.map((product) => ( - - {product.name} - {product.price} - - ))} - - ); -} -``` +```tsx title="client/index.ts" +import { purchase, OrderResultStatus } from "@devvit/web/client"; -You can also fetch all products using custom-defined metadata or by an array of skus. Only one is required; if you provide both then they will be AND’d. +// Fetch products from your server endpoint +const products = await fetch("/api/products").then((r) => r.json()); -```tsx -import { getProducts } from '@devvit/payments'; -const products = await getProducts({, -}); +// Render your UI; when user chooses a product: +async function handleBuy(sku: string) { + const result = await purchase(sku); + if (result.status === OrderResultStatus.STATUS_SUCCESS) { + // show success + } else { + // show error or retry (result.errorMessage may be set) + } +} ``` ### Initiate orders -Provide the product sku to trigger a purchase. This automatically populates the most recently-approved product metadata for that product id. - -**Example** - -```tsx -import { usePayments } from '@devvit/payments'; +Provide the product SKU to trigger a purchase. Use `purchase(sku)` from `@devvit/web/client`; the result indicates success or failure. -// handles purchase results -const payments = usePayments((result: OnPurchaseResult) => { console.log('Tried to buy:', result.sku, '; result:', result.status); }); +```tsx title="client/index.ts" +import { purchase, OrderResultStatus } from "@devvit/web/client"; -// for each sku in products: - +export async function buy(sku: string) { + const result = await purchase(sku); + if (result.status === OrderResultStatus.STATUS_SUCCESS) { + // show success + } else { + // show error or retry (result.errorMessage may be set) + } +} ``` diff --git a/docs/earn-money/payments/payments_manage.md b/docs/earn-money/payments/payments_manage.md index 16404da..4c3f341 100644 --- a/docs/earn-money/payments/payments_manage.md +++ b/docs/earn-money/payments/payments_manage.md @@ -4,30 +4,23 @@ Once your app and products have been approved, you’re ready to use Reddit’s ## Check orders -Reddit keeps track of historical purchases and lets you query user purchases. +Reddit keeps track of historical purchases and lets you query orders. -Orders are returned in reverse chronological order and can be filtered based on user, product, success state, or other attributes. +In Devvit Web, use **server-side** `payments.getOrders()` from `@devvit/web/server`. Orders are returned in reverse chronological order and can be filtered by user, product, success state, or other attributes. Expose the data to your client via your own API (e.g. `/api/orders`) if the client needs it. -**Example** +**Example (server):** expose orders for the current user so the client can show "Purchased!" or a purchase button. -```tsx -import { useOrders, OrderStatus } from '@devvit/payments'; +```tsx title="server/index.ts" +import { payments } from "@devvit/web/server"; -export function CosmicSwordShop(context: Devvit.Context): JSX.Element { - const { orders } = useOrders(context, { - sku: 'cosmic_sword', - }); - - // if the user hasn’t already bought the cosmic sword - // then show them the purchase button - if (orders.length > 0) { - return Purchased!; - } else { - return ; - } -} +app.get("/api/orders", async (c) => { + const orders = await payments.getOrders({ sku: "cosmic_sword" }); + return c.json(orders); +}); ``` +**Client:** call your `/api/orders` endpoint; if the user has already bought the product, show "Purchased!"; otherwise show a button that calls `purchase("cosmic_sword")` from `@devvit/web/client`. + ## Update products Once your app is in production, existing installations will need to be manually updated via the admin tool if you release a new version. Contact the Developer Platform team if you need to update your app installation versions. @@ -38,25 +31,23 @@ Automatic updates will be supported in a future release. Reddit may reverse transactions under certain circumstances, such as card disputes, policy violations, or technical issues. If there’s a problem with a digital good, a user can submit a request for a refund via [Reddit Help](https://support.reddithelp.com/hc/en-us/requests/new?ticket_form_id=29770197409428). -When a transaction is reversed for any reason, you may optionally revoke product functionality from the user by adding a `refundOrder` handler. - -**Example** - -```tsx -addPaymentHandler({ - fulfillOrder: async (order: Order, ctx: Context) => { - // Snip order fulfillment - }, - refundOrder: async (order: Order, ctx: Context) => { - // check if the order contains an extra life - if (order.products.some(({ sku }) => sku === GOD_MODE_SKU)) { - // redis key for storing number of lives user has left - const livesKey = `${ctx.userId}:lives`; - - // if so, decrement the number of lives - await ctx.redis.incrBy(livesKey, -1); - } - }, +When a transaction is reversed for any reason, you may optionally revoke product functionality from the user by implementing the **refund** endpoint (configured in `devvit.json` under `payments.endpoints.refundOrder`). + +**Example (Devvit Web):** in your server’s refund endpoint, revoke the entitlement (e.g. decrement lives in Redis). + +```tsx title="server/index.ts" +import type { PaymentHandlerResponse, Order } from "@devvit/web/server"; +import { redis } from "@devvit/web/server"; + +const GOD_MODE_SKU = "god_mode"; + +app.post("/internal/payments/refund", async (c) => { + const order = await c.req.json(); + if (order.products.some((p) => p.sku === GOD_MODE_SKU)) { + const livesKey = `${order.userId}:lives`; + await redis.incrBy(livesKey, -1); + } + return c.json({ success: true }); }); ``` diff --git a/docs/earn-money/payments/support_this_app.md b/docs/earn-money/payments/support_this_app.md index e17dc83..cc26a0a 100644 --- a/docs/earn-money/payments/support_this_app.md +++ b/docs/earn-money/payments/support_this_app.md @@ -19,69 +19,57 @@ devvit products add support-app ### Add a payment handler -The [payment handler](./payments_add.mdx#complete-the-payment-flow) is where you award the promised incentive to your supporters. For example, this is how you can award custom user flair: +In Devvit Web, the [payment handler](./payments_add.mdx#complete-the-payment-flow) is your server’s **fulfill** endpoint. That’s where you award the promised incentive (e.g. custom user flair). Implement it in your server and reference it in `devvit.json` under `payments.endpoints.fulfillOrder`. -```tsx -addPaymentHandler({ - fulfillOrder: async (order, context) => { - const username = await context.reddit.getCurrentUsername(); - if (!username) { - throw new Error("User not found"); - } - - const subredditName = await context.reddit.getCurrentSubredditName(); - - await context.reddit.setUserFlair({ - text: "Super Duper User", - subredditName, - username, - backgroundColor: "#ffbea6", - textColor: "dark", - }); - }, +Example: award custom user flair when a user completes a support purchase: + +```tsx title="server/index.ts" +import type { PaymentHandlerResponse, Order } from "@devvit/web/server"; +import { reddit } from "@devvit/web/server"; + +app.post("/internal/payments/fulfill", async (c) => { + const order = await c.req.json(); + const username = order.userId; // or the username field on the order + if (!username) { + return c.json({ success: false, reason: "User not found" }); + } + + const subredditName = order.subredditName ?? order.subredditId; + + await reddit.setUserFlair({ + text: "Super Duper User", + subredditName, + username, + backgroundColor: "#ffbea6", + textColor: "dark", + }); + + return c.json({ success: true }); }); ``` ### Initiate purchases -Next you need to provide a way for users to support your app: +Provide a way for users to support your app from your client: -- If you use Devvit blocks, you can use the ProductButton helper to render a purchase button. -- If you use webviews, make sure that your design follows the [design guidelines](./payments_add.mdx#design-guidelines) to [initiate purchases](./payments_add.mdx#initiate-orders). +- **Devvit Web:** Add a button or link that calls `purchase("support-app")` from `@devvit/web/client`. Handle the result (e.g. show a toast on success). Optionally fetch product info from your `/api/products` endpoint to display the support option. +- Follow the [design guidelines](./payments_add.mdx#design-guidelines) when [initiating purchases](./payments_add.mdx#initiate-orders). ![Support App Example](../../assets/support_this_app.png) -Here's how you create a ProductButton in blocks: +Example client code: -```tsx -import { usePayments, useProducts } from '@devvit/payments'; -import { ProductButton } from '@devvit/payments/helpers/ProductButton'; -import { Devvit } from '@devvit/public-api'; - -// addCustomPostType() is deprecated and will be unsupported. It will not work after June 30. -Devvit.addCustomPostType({ - render: (context) => { - const { products } = useProducts(context); - const payments = usePayments((result: OnPurchaseResult) => { - if (result.status === OrderResultStatus.Success) { - context.ui.showToast({ - appearance: 'success', - text: 'Thanks for your support!', - }); - } else { - context.ui.showToast( - `Purchase failed! Please try again.` - ); - } - }); - const supportProduct = products.find(products.find((p) => p.sku === 'support-app'); - return ( - payments.purchase(p.sku)} - /> - ); -}) +```tsx title="client/index.ts" +import { purchase, OrderResultStatus } from "@devvit/web/client"; + +async function handleSupportApp() { + const result = await purchase("support-app"); + if (result.status === OrderResultStatus.STATUS_SUCCESS) { + // show success, e.g. toast: "Thanks for your support!" + } else { + // show error or retry (result.errorMessage may be set) + } +} ``` ## Example diff --git a/versioned_docs/version-0.12/earn-money/payments/payments_add.mdx b/versioned_docs/version-0.12/earn-money/payments/payments_add.mdx index 73c6839..20973a7 100644 --- a/versioned_docs/version-0.12/earn-money/payments/payments_add.mdx +++ b/versioned_docs/version-0.12/earn-money/payments/payments_add.mdx @@ -22,7 +22,7 @@ npm install @devvit/payments ``` :::note -Make sure you’re on Devvit 0.11.3 or higher. See the [quickstart](https://developers.reddit.com/docs/next/quickstart) to get up and running. +Make sure you're on Devvit 0.11.3 or higher. See the [quickstart](https://developers.reddit.com/docs/next/quickstart) to get up and running. ::: ## Implement Devvit Web payments @@ -212,10 +212,10 @@ Actual payments will not be processed until your products are approved. While yo ## Design guidelines -You’ll need to clearly identify paid products or services. Here are some best practices to follow: +You'll need to clearly identify paid products or services. Here are some best practices to follow: - Use a short name, description, and image for each product. -- Don’t overwhelm users with too many items. +- Don't overwhelm users with too many items. - Try to keep purchases in a consistent location or use a consistent visual pattern. - Only use the gold icon to indicate purchases for Reddit Gold. @@ -226,7 +226,7 @@ Product images need to meet the following requirements: - Minimum size: 256x256 - Supported file type: .png -If you don’t provide an image, the default Reddit product image is used. +If you don't provide an image, the default Reddit product image is used. ![default image](../../assets/default_product_image.png) @@ -255,35 +255,17 @@ If you don’t provide an image, the default Reddit product image is used. ### Purchase buttons (required) -#### Blocks +#### Devvit Web -The `ProductButton` is a Devvit blocks component designed to render a product with a purchase button. It can be customized to match your app's look and feel. +In Devvit Web, use your own UI (e.g. a button or product card) and call `purchase(sku)` from `@devvit/web/client` when the user chooses a product. Follow the [design guidelines](#design-guidelines) (e.g. gold icon, clear labeling). -**Usage:** +#### Blocks (legacy) -```tsx - payments.purchase(p.sku)} - appearance="tile" -/> -``` - -##### `ProductButtonProps` - -| **Prop Name** | **Type** | **Description** | -| ------------------ | ----------------------------------------------- | ------------------------------------------------------------------------------------ | -| `product` | `Product` | The product object containing details such as `sku`, `price`, and `metadata`. | -| `onPress` | `(product: Product) => void` | Callback function triggered when the button is pressed. | -| `showIcon` | `boolean` | Determines whether the product icon is displayed on the button. Defaults to `false`. | -| `appearance` | `'compact'` | `'detailed'` | `'tile'` | Defines the visual style of the button. Defaults to `compact`. | -| `buttonAppearance` | `string` | Optional [button appearance](../../blocks/button.mdx#appearance). | -| `textColor` | `string` | Optional [text color](../../blocks/text.mdx#color). | +If your app still uses Devvit Blocks, you can use the `ProductButton` component and [migrate to Devvit Web](./payments_migrate.mdx) when ready. The `ProductButton` renders a product with a purchase button; use `payments.purchase(p.sku)` in the `onPress` callback (from `@devvit/payments`). #### Webviews -Use Reddit’s primary, secondary, or bordered button component and gold icon in one of the following formats: +Use Reddit's primary, secondary, or bordered button component and gold icon in one of the following formats: ![default image](../../assets/payments_button_purchase.png) @@ -297,36 +279,30 @@ Use a consistent and clear product component to display paid goods or services t ## Complete the payment flow -Use `addPaymentHandler` to specify the function that is called during the order flow. This customizes how your app fulfills product orders and provides the ability for you to reject an order. +Your **fulfill** endpoint (configured in `devvit.json` and implemented in the server) is called during the order flow. It customizes how your app fulfills product orders and lets you reject an order. -Errors thrown within the payment handler automatically reject the order. To provide a custom error message to the frontend of your application, you can return `{success: false, reason: }` with a reason for the order rejection. +Return `{ success: true }` to accept the order, or `{ success: false, reason: "" }` to reject it and send a message to the client. Throwing an error in the handler also rejects the order. -This example shows how to issue an "extra life" to a user when they purchase the "extra_life" product. +This example shows how to grant an "extra life" in your fulfill endpoint when the user purchases the "god_mode" product (using Redis from `@devvit/web/server`): -```ts -import { type Context } from "@devvit/public-api"; -import { addPaymentHandler } from "@devvit/payments"; -import { Devvit, useState } from "@devvit/public-api"; - -Devvit.configure({ - redis: true, - redditAPI: true, -}); +```tsx title="server/index.ts" +import type { PaymentHandlerResponse, Order } from "@devvit/web/server"; +import { redis } from "@devvit/web/server"; const GOD_MODE_SKU = "god_mode"; -addPaymentHandler({ - fulfillOrder: async (order, ctx) => { - if (!order.products.some(({ sku }) => sku === GOD_MODE_SKU)) { - throw new Error("Unable to fulfill order: sku not found"); - } - if (order.status !== "PAID") { - throw new Error("Becoming a god has a cost (in Reddit Gold)"); - } +app.post("/internal/payments/fulfill", async (c) => { + const order = await c.req.json(); + if (!order.products.some((p) => p.sku === GOD_MODE_SKU)) { + return c.json({ success: false, reason: "Unable to fulfill order: sku not found" }); + } + if (order.status !== "PAID") { + return c.json({ success: false, reason: "Becoming a god has a cost (in Reddit Gold)" }); + } - const redisKey = godModeRedisKey(ctx.postId, ctx.userId); - await ctx.redis.set(redisKey, "true"); - }, + const redisKey = `post:${order.postId}:user:${order.userId}:god_mode`; + await redis.set(redisKey, "true"); + return c.json({ success: true }); }); ``` @@ -336,61 +312,44 @@ The frontend and backend of your app coordinate order processing. ![Order workflow diagram](../../assets/payments_order_flow_diagram.png) -To launch the payment flow, create a hook with `usePayments()` followed by `hook.purchase()` to initiate the purchase from the frontend. +To launch the payment flow, call `purchase(sku)` from `@devvit/web/client`. That triggers the native payment flow on all platforms (web, iOS, Android); Reddit then calls your server's **fulfill** endpoint. Your app can acknowledge or reject the order (for example, reject once a limited product is sold out). -This triggers a native payment flow on all platforms (web, iOS, Android) that works with the Reddit backend to process the order. The `fulfillOrder()` hook calls your app during this process. +### Get your product details -Your app can acknowledge or reject the order. For example, for goods with limited quantities, your app may reject an order once the product is sold out. +**Server:** Use `payments.getProducts()` in your server (see [Server: Fetch products](#server-fetch-products)) and expose products via your own `/api/products` (or similar) endpoint if the client needs them. -### Get your product details +**Client:** Fetch product metadata from your API and use it to display products and call `purchase(sku)`: -Use the `useProducts` hook or `getProducts` function to fetch details about products. - -```tsx -import { useProducts } from "@devvit/payments"; - -export function ProductsList(context: Devvit.Context): JSX.Element { - // Only query for products with the metadata "category" of value "powerup". - // The metadata field can be empty - if it is, useProducts will not filter on metadata. - const { products } = useProducts(context, { - metadata: { - category: "powerup", - }, - }); - - return ( - - {products.map((product) => ( - - {product.name} - {product.price} - - ))} - - ); -} -``` +```tsx title="client/index.ts" +import { purchase, OrderResultStatus } from "@devvit/web/client"; -You can also fetch all products using custom-defined metadata or by an array of skus. Only one is required; if you provide both then they will be AND’d. +// Fetch products from your server endpoint +const products = await fetch("/api/products").then((r) => r.json()); -```tsx -import { getProducts } from '@devvit/payments'; -const products = await getProducts({, -}); +// Render your UI; when user chooses a product: +async function handleBuy(sku: string) { + const result = await purchase(sku); + if (result.status === OrderResultStatus.STATUS_SUCCESS) { + // show success + } else { + // show error or retry (result.errorMessage may be set) + } +} ``` ### Initiate orders -Provide the product sku to trigger a purchase. This automatically populates the most recently-approved product metadata for that product id. - -**Example** - -```tsx -import { usePayments } from '@devvit/payments'; +Provide the product SKU to trigger a purchase. Use `purchase(sku)` from `@devvit/web/client`; the result indicates success or failure. -// handles purchase results -const payments = usePayments((result: OnPurchaseResult) => { console.log('Tried to buy:', result.sku, '; result:', result.status); }); +```tsx title="client/index.ts" +import { purchase, OrderResultStatus } from "@devvit/web/client"; -// for each sku in products: - +export async function buy(sku: string) { + const result = await purchase(sku); + if (result.status === OrderResultStatus.STATUS_SUCCESS) { + // show success + } else { + // show error or retry (result.errorMessage may be set) + } +} ``` diff --git a/versioned_docs/version-0.12/earn-money/payments/payments_manage.md b/versioned_docs/version-0.12/earn-money/payments/payments_manage.md index 16404da..6b54051 100644 --- a/versioned_docs/version-0.12/earn-money/payments/payments_manage.md +++ b/versioned_docs/version-0.12/earn-money/payments/payments_manage.md @@ -1,33 +1,26 @@ # Manage Payments -Once your app and products have been approved, you’re ready to use Reddit’s production payments system. Real payments will be triggered automatically when invoked from approved app versions. No code changes are required. +Once your app and products have been approved, you're ready to use Reddit's production payments system. Real payments will be triggered automatically when invoked from approved app versions. No code changes are required. ## Check orders -Reddit keeps track of historical purchases and lets you query user purchases. +Reddit keeps track of historical purchases and lets you query orders. -Orders are returned in reverse chronological order and can be filtered based on user, product, success state, or other attributes. +In Devvit Web, use **server-side** `payments.getOrders()` from `@devvit/web/server`. Orders are returned in reverse chronological order and can be filtered by user, product, success state, or other attributes. Expose the data to your client via your own API (e.g. `/api/orders`) if the client needs it. -**Example** +**Example (server):** expose orders for the current user so the client can show "Purchased!" or a purchase button. -```tsx -import { useOrders, OrderStatus } from '@devvit/payments'; +```tsx title="server/index.ts" +import { payments } from "@devvit/web/server"; -export function CosmicSwordShop(context: Devvit.Context): JSX.Element { - const { orders } = useOrders(context, { - sku: 'cosmic_sword', - }); - - // if the user hasn’t already bought the cosmic sword - // then show them the purchase button - if (orders.length > 0) { - return Purchased!; - } else { - return ; - } -} +app.get("/api/orders", async (c) => { + const orders = await payments.getOrders({ sku: "cosmic_sword" }); + return c.json(orders); +}); ``` +**Client:** call your `/api/orders` endpoint; if the user has already bought the product, show "Purchased!"; otherwise show a button that calls `purchase("cosmic_sword")` from `@devvit/web/client`. + ## Update products Once your app is in production, existing installations will need to be manually updated via the admin tool if you release a new version. Contact the Developer Platform team if you need to update your app installation versions. @@ -36,27 +29,25 @@ Automatic updates will be supported in a future release. ## Issue a refund -Reddit may reverse transactions under certain circumstances, such as card disputes, policy violations, or technical issues. If there’s a problem with a digital good, a user can submit a request for a refund via [Reddit Help](https://support.reddithelp.com/hc/en-us/requests/new?ticket_form_id=29770197409428). +Reddit may reverse transactions under certain circumstances, such as card disputes, policy violations, or technical issues. If there's a problem with a digital good, a user can submit a request for a refund via [Reddit Help](https://support.reddithelp.com/hc/en-us/requests/new?ticket_form_id=29770197409428). + +When a transaction is reversed for any reason, you may optionally revoke product functionality from the user by implementing the **refund** endpoint (configured in `devvit.json` under `payments.endpoints.refundOrder`). -When a transaction is reversed for any reason, you may optionally revoke product functionality from the user by adding a `refundOrder` handler. +**Example (Devvit Web):** in your server's refund endpoint, revoke the entitlement (e.g. decrement lives in Redis). -**Example** +```tsx title="server/index.ts" +import type { PaymentHandlerResponse, Order } from "@devvit/web/server"; +import { redis } from "@devvit/web/server"; -```tsx -addPaymentHandler({ - fulfillOrder: async (order: Order, ctx: Context) => { - // Snip order fulfillment - }, - refundOrder: async (order: Order, ctx: Context) => { - // check if the order contains an extra life - if (order.products.some(({ sku }) => sku === GOD_MODE_SKU)) { - // redis key for storing number of lives user has left - const livesKey = `${ctx.userId}:lives`; +const GOD_MODE_SKU = "god_mode"; - // if so, decrement the number of lives - await ctx.redis.incrBy(livesKey, -1); - } - }, +app.post("/internal/payments/refund", async (c) => { + const order = await c.req.json(); + if (order.products.some((p) => p.sku === GOD_MODE_SKU)) { + const livesKey = `${order.userId}:lives`; + await redis.incrBy(livesKey, -1); + } + return c.json({ success: true }); }); ``` diff --git a/versioned_docs/version-0.12/earn-money/payments/support_this_app.md b/versioned_docs/version-0.12/earn-money/payments/support_this_app.md index e17dc83..dae1456 100644 --- a/versioned_docs/version-0.12/earn-money/payments/support_this_app.md +++ b/versioned_docs/version-0.12/earn-money/payments/support_this_app.md @@ -1,11 +1,11 @@ # Support this app -You can ask users to contribute to your app’s development by adding the “support this app” feature. This allows users to support your app with Reddit Gold in exchange for some kind of award or recognition. +You can ask users to contribute to your app's development by adding the "support this app" feature. This allows users to support your app with Reddit Gold in exchange for some kind of award or recognition. ## Requirements 1. You must give something in return to users who support your app. This could be unique custom user flair, an honorable mention in a thank you post, or another creative way to show your appreciation. -2. The “Support this App” purchase button must meet the Developer Platform’s [design guidelines](./payments_add.mdx#design-guidelines). +2. The "Support this App" purchase button must meet the Developer Platform's [design guidelines](./payments_add.mdx#design-guidelines). ## How to integrate app support @@ -19,69 +19,57 @@ devvit products add support-app ### Add a payment handler -The [payment handler](./payments_add.mdx#complete-the-payment-flow) is where you award the promised incentive to your supporters. For example, this is how you can award custom user flair: +In Devvit Web, the [payment handler](./payments_add.mdx#complete-the-payment-flow) is your server's **fulfill** endpoint. That's where you award the promised incentive (e.g. custom user flair). Implement it in your server and reference it in `devvit.json` under `payments.endpoints.fulfillOrder`. -```tsx -addPaymentHandler({ - fulfillOrder: async (order, context) => { - const username = await context.reddit.getCurrentUsername(); - if (!username) { - throw new Error("User not found"); - } - - const subredditName = await context.reddit.getCurrentSubredditName(); - - await context.reddit.setUserFlair({ - text: "Super Duper User", - subredditName, - username, - backgroundColor: "#ffbea6", - textColor: "dark", - }); - }, +Example: award custom user flair when a user completes a support purchase: + +```tsx title="server/index.ts" +import type { PaymentHandlerResponse, Order } from "@devvit/web/server"; +import { reddit } from "@devvit/web/server"; + +app.post("/internal/payments/fulfill", async (c) => { + const order = await c.req.json(); + const username = order.userId; // or the username field on the order + if (!username) { + return c.json({ success: false, reason: "User not found" }); + } + + const subredditName = order.subredditName ?? order.subredditId; + + await reddit.setUserFlair({ + text: "Super Duper User", + subredditName, + username, + backgroundColor: "#ffbea6", + textColor: "dark", + }); + + return c.json({ success: true }); }); ``` ### Initiate purchases -Next you need to provide a way for users to support your app: +Provide a way for users to support your app from your client: -- If you use Devvit blocks, you can use the ProductButton helper to render a purchase button. -- If you use webviews, make sure that your design follows the [design guidelines](./payments_add.mdx#design-guidelines) to [initiate purchases](./payments_add.mdx#initiate-orders). +- **Devvit Web:** Add a button or link that calls `purchase("support-app")` from `@devvit/web/client`. Handle the result (e.g. show a toast on success). Optionally fetch product info from your `/api/products` endpoint to display the support option. +- Follow the [design guidelines](./payments_add.mdx#design-guidelines) when [initiating purchases](./payments_add.mdx#initiate-orders). ![Support App Example](../../assets/support_this_app.png) -Here's how you create a ProductButton in blocks: +Example client code: -```tsx -import { usePayments, useProducts } from '@devvit/payments'; -import { ProductButton } from '@devvit/payments/helpers/ProductButton'; -import { Devvit } from '@devvit/public-api'; - -// addCustomPostType() is deprecated and will be unsupported. It will not work after June 30. -Devvit.addCustomPostType({ - render: (context) => { - const { products } = useProducts(context); - const payments = usePayments((result: OnPurchaseResult) => { - if (result.status === OrderResultStatus.Success) { - context.ui.showToast({ - appearance: 'success', - text: 'Thanks for your support!', - }); - } else { - context.ui.showToast( - `Purchase failed! Please try again.` - ); - } - }); - const supportProduct = products.find(products.find((p) => p.sku === 'support-app'); - return ( - payments.purchase(p.sku)} - /> - ); -}) +```tsx title="client/index.ts" +import { purchase, OrderResultStatus } from "@devvit/web/client"; + +async function handleSupportApp() { + const result = await purchase("support-app"); + if (result.status === OrderResultStatus.STATUS_SUCCESS) { + // show success, e.g. toast: "Thanks for your support!" + } else { + // show error or retry (result.errorMessage may be set) + } +} ``` ## Example