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
139 changes: 49 additions & 90 deletions docs/earn-money/payments/payments_add.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
<ProductButton
showIcon
product={product}
onPress={(p) => 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'` &#124; `'detailed'` &#124; `'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

Expand All @@ -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: <string>}` with a reason for the order rejection.
Return `{ success: true }` to accept the order, or `{ success: false, reason: "<string>" }` 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<Order>();
if (!order.products.some((p) => p.sku === GOD_MODE_SKU)) {
return c.json<PaymentHandlerResponse>({ success: false, reason: "Unable to fulfill order: sku not found" });
}
if (order.status !== "PAID") {
return c.json<PaymentHandlerResponse>({ 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<PaymentHandlerResponse>({ success: true });
});
```

Expand All @@ -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 (
<vstack>
{products.map((product) => (
<hstack>
<text>{product.name}</text>
<text>{product.price}</text>
</hstack>
))}
</vstack>
);
}
```
```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:
<button onPress{payments.purchase(sku)}>Buy a {sku}</button>
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)
}
}
```
65 changes: 28 additions & 37 deletions docs/earn-money/payments/payments_manage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <text>Purchased!</text>;
} else {
return <button onPress={/* Trigger purchase */}>Buy Cosmic Sword</button>;
}
}
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.
Expand All @@ -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<Order>();
if (order.products.some((p) => p.sku === GOD_MODE_SKU)) {
const livesKey = `${order.userId}:lives`;
await redis.incrBy(livesKey, -1);
}
return c.json<PaymentHandlerResponse>({ success: true });
});
```

Expand Down
92 changes: 40 additions & 52 deletions docs/earn-money/payments/support_this_app.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Order>();
const username = order.userId; // or the username field on the order
if (!username) {
return c.json<PaymentHandlerResponse>({ 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<PaymentHandlerResponse>({ 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 (
<ProductButton
product={supportProduct}
onPress={(p) => 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
Expand Down
Loading
Loading